1130 lines
40 KiB
JavaScript
1130 lines
40 KiB
JavaScript
/**
|
||
* IT Site Survey AI - Frontend Application
|
||
* Single-page application with Surveys List, Create Survey, and AI Analysis tabs
|
||
*/
|
||
|
||
// ============================================
|
||
// State Management
|
||
// ============================================
|
||
const AppState = {
|
||
currentTab: 'surveys',
|
||
currentSurveyId: null,
|
||
surveys: [],
|
||
surveyTemplate: null,
|
||
uploadedPhotos: [],
|
||
analysisContent: '',
|
||
deleteTargetId: null,
|
||
isGenerating: false
|
||
};
|
||
|
||
// ============================================
|
||
// API Configuration
|
||
// ============================================
|
||
const API_BASE_URL = window.location.origin;
|
||
|
||
// ============================================
|
||
// Sample Data for Testing
|
||
// ============================================
|
||
const SAMPLE_DATA = {
|
||
name: "Acme Corporation Site Survey",
|
||
client: "Acme Corporation",
|
||
site: "Main Headquarters - San Francisco",
|
||
description: "Comprehensive IT infrastructure assessment for digital transformation initiative. Planning cloud migration and security modernization.",
|
||
model: "gpt-4o",
|
||
answers: {
|
||
"q1": "450 employees",
|
||
"q2": "3 locations (SF HQ, Denver Office, Austin Office)",
|
||
"q3": "1) Migrate 80% of workloads to cloud by end of 2025\n2) Implement Zero Trust security architecture\n3) Deploy AI-powered customer service platform",
|
||
"q4": "Hybrid topology with core-distribution-access layers at HQ",
|
||
"q5": "24 physical servers, 120 VMs",
|
||
"q6": "VMware vSphere 7.0 with vCenter",
|
||
"q7": "150TB usable, currently 78% utilized",
|
||
"q8": "Single internet circuit at Denver office, no redundant power in Austin server room",
|
||
"q9": "1Gbps symmetric at HQ, 500Mbps at Denver, 300Mbps at Austin",
|
||
"q10": "45 switches, 8 routers (Cisco Catalyst and ISR series)",
|
||
"q11": "Planning",
|
||
"q12": "Mix of Wi-Fi 5 (802.11ac) and Wi-Fi 6 (802.11ax)",
|
||
"q13": "12 VLANs - segmented by department (Finance, HR, Engineering, Sales, etc.)",
|
||
"q14": "Palo Alto Networks PA-5220",
|
||
"q15": "Yes",
|
||
"q16": "CrowdStrike Falcon",
|
||
"q17": "Yes - Critical Only",
|
||
"q18": "6 months ago - Medium risk findings related to outdated Java applications",
|
||
"q19": "Yes - Not Tested",
|
||
"q20": "Veeam Backup & Replication, RTO: 4 hours, RPO: 1 hour",
|
||
"q21": "AWS (primary), Azure (secondary for AD and O365)",
|
||
"q22": "30% cloud, 70% on-premises",
|
||
"q23": "Yes - Basic",
|
||
"q24": "~$45,000/month - Optimization in progress with Reserved Instances",
|
||
"q25": "1. SAP ERP\n2. Salesforce CRM\n3. Microsoft 365\n4. Custom e-commerce platform\n5. Workday HCM\n6. ServiceNow ITSM\n7. Confluence\n8. Jira\n9. Zoom\n10. Internal BI platform",
|
||
"q26": "Legacy AS/400 system for inventory, needs API layer or replacement",
|
||
"q27": "Microsoft Teams, Slack for engineering, Zoom for video",
|
||
"q28": "Yes - Partial",
|
||
"q29": "SOC2 Type II, ISO 27001, PCI-DSS (for e-commerce)",
|
||
"q30": "Last SOC2 audit completed 2 months ago with minor findings remediated",
|
||
"q31": "Yes - Implemented",
|
||
"q32": "ServiceNow ITSM, Jira Service Management for dev teams",
|
||
"q33": "1:75 ratio (6 IT staff for 450 users)",
|
||
"q34": "4.2 hours average for L2 tickets, 15 minutes for L1",
|
||
"q35": "1) Complete cloud migration\n2) Implement SD-WAN across all sites\n3) Deploy AI-driven security operations center"
|
||
},
|
||
photos: []
|
||
};
|
||
|
||
// ============================================
|
||
// DOM Elements Cache
|
||
// ============================================
|
||
const DOM = {};
|
||
|
||
function cacheDOMElements() {
|
||
// Tabs
|
||
DOM.tabButtons = document.querySelectorAll('.tab-btn');
|
||
DOM.tabContents = document.querySelectorAll('.tab-content');
|
||
|
||
// Navigation
|
||
DOM.navSurveys = document.getElementById('nav-surveys');
|
||
DOM.navCreate = document.getElementById('nav-create');
|
||
|
||
// Tab content sections
|
||
DOM.tabSurveys = document.getElementById('tab-surveys');
|
||
DOM.tabCreate = document.getElementById('tab-create');
|
||
DOM.tabAnalysis = document.getElementById('tab-analysis');
|
||
|
||
// Surveys List
|
||
DOM.surveysList = document.getElementById('surveys-list');
|
||
DOM.btnClearAll = document.getElementById('btn-clear-all');
|
||
|
||
// Create Survey Form
|
||
DOM.surveyForm = document.getElementById('survey-form');
|
||
DOM.surveyName = document.getElementById('survey-name');
|
||
DOM.surveyClient = document.getElementById('survey-client');
|
||
DOM.surveySite = document.getElementById('survey-site');
|
||
DOM.surveyDescription = document.getElementById('survey-description');
|
||
DOM.surveyModel = document.getElementById('survey-model');
|
||
DOM.questionsContainer = document.getElementById('questions-container');
|
||
DOM.questionCount = document.getElementById('question-count');
|
||
DOM.btnLoadSample = document.getElementById('btn-load-sample');
|
||
DOM.btnSubmitSurvey = document.getElementById('btn-submit-survey');
|
||
|
||
// Photo Upload
|
||
DOM.photoUploadArea = document.getElementById('photo-upload-area');
|
||
DOM.photoInput = document.getElementById('photo-input');
|
||
DOM.btnBrowsePhotos = document.getElementById('btn-browse-photos');
|
||
DOM.photoPreviewGrid = document.getElementById('photo-preview-grid');
|
||
|
||
// Analysis Tab
|
||
DOM.btnBackToSurveys = document.getElementById('btn-back-to-surveys');
|
||
DOM.analysisSurveyName = document.getElementById('analysis-survey-name');
|
||
DOM.analysisModel = document.getElementById('analysis-model');
|
||
DOM.btnGenerateAnalysis = document.getElementById('btn-generate-analysis');
|
||
DOM.analysisSurveyMeta = document.getElementById('analysis-survey-meta');
|
||
DOM.analysisQuestionSummary = document.getElementById('analysis-question-summary');
|
||
DOM.analysisOutput = document.getElementById('analysis-output');
|
||
DOM.analysisLoading = document.getElementById('analysis-loading');
|
||
DOM.exportButtons = document.getElementById('export-buttons');
|
||
DOM.btnExportText = document.getElementById('btn-export-text');
|
||
DOM.btnExportPdf = document.getElementById('btn-export-pdf');
|
||
DOM.btnEmail = document.getElementById('btn-email');
|
||
|
||
// Chat
|
||
DOM.chatMessages = document.getElementById('chat-messages');
|
||
DOM.chatInput = document.getElementById('chat-input');
|
||
DOM.btnSendChat = document.getElementById('btn-send-chat');
|
||
|
||
// Modals
|
||
DOM.deleteModal = document.getElementById('delete-modal');
|
||
DOM.btnCancelDelete = document.getElementById('btn-cancel-delete');
|
||
DOM.btnConfirmDelete = document.getElementById('btn-confirm-delete');
|
||
DOM.clearAllModal = document.getElementById('clear-all-modal');
|
||
DOM.btnCancelClearAll = document.getElementById('btn-cancel-clear-all');
|
||
DOM.btnConfirmClearAll = document.getElementById('btn-confirm-clear-all');
|
||
|
||
// Toast
|
||
DOM.toast = document.getElementById('toast');
|
||
DOM.toastMessage = document.getElementById('toast-message');
|
||
}
|
||
|
||
// ============================================
|
||
// API Functions
|
||
// ============================================
|
||
async function fetchAPI(endpoint, options = {}) {
|
||
const url = `${API_BASE_URL}${endpoint}`;
|
||
const response = await fetch(url, {
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...options.headers
|
||
},
|
||
...options
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json().catch(() => ({}));
|
||
throw new Error(error.error || `HTTP ${response.status}`);
|
||
}
|
||
|
||
return response.json();
|
||
}
|
||
|
||
async function loadSurveyTemplate() {
|
||
try {
|
||
const data = await fetchAPI('/api/surveys/template');
|
||
AppState.surveyTemplate = data;
|
||
renderQuestions();
|
||
} catch (error) {
|
||
showToast('Failed to load survey template', 'error');
|
||
console.error('Error loading template:', error);
|
||
}
|
||
}
|
||
|
||
async function loadSurveys() {
|
||
try {
|
||
const data = await fetchAPI('/api/surveys');
|
||
AppState.surveys = data.surveys;
|
||
renderSurveysList();
|
||
} catch (error) {
|
||
showToast('Failed to load surveys', 'error');
|
||
console.error('Error loading surveys:', error);
|
||
}
|
||
}
|
||
|
||
async function createSurvey(surveyData) {
|
||
try {
|
||
const data = await fetchAPI('/api/surveys', {
|
||
method: 'POST',
|
||
body: JSON.stringify(surveyData)
|
||
});
|
||
showToast('Survey created successfully', 'success');
|
||
return data.id;
|
||
} catch (error) {
|
||
showToast('Failed to create survey', 'error');
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async function deleteSurvey(surveyId) {
|
||
try {
|
||
await fetchAPI(`/api/surveys/${surveyId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
showToast('Survey deleted successfully', 'success');
|
||
} catch (error) {
|
||
showToast('Failed to delete survey', 'error');
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async function deleteAllSurveys() {
|
||
try {
|
||
await fetchAPI('/api/surveys', {
|
||
method: 'DELETE'
|
||
});
|
||
showToast('All surveys deleted', 'success');
|
||
} catch (error) {
|
||
showToast('Failed to delete surveys', 'error');
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async function getSurvey(surveyId) {
|
||
try {
|
||
return await fetchAPI(`/api/surveys/${surveyId}`);
|
||
} catch (error) {
|
||
showToast('Failed to load survey details', 'error');
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async function exportSurvey(surveyId, format, content) {
|
||
try {
|
||
const data = await fetchAPI(`/api/surveys/${surveyId}/export`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ format, content })
|
||
});
|
||
return data;
|
||
} catch (error) {
|
||
showToast('Failed to export survey', 'error');
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// UI Functions
|
||
// ============================================
|
||
function switchTab(tabName) {
|
||
// Update state
|
||
AppState.currentTab = tabName;
|
||
|
||
// Update tab buttons
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
||
});
|
||
|
||
// Update tab contents
|
||
document.querySelectorAll('.tab-content').forEach(content => {
|
||
content.classList.toggle('active', content.id === `tab-${tabName}`);
|
||
});
|
||
|
||
// Special handling for analysis tab visibility
|
||
if (tabName === 'analysis') {
|
||
document.getElementById('tab-analysis').classList.add('active');
|
||
}
|
||
|
||
// Update navigation
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
||
});
|
||
}
|
||
|
||
function showToast(message, type = 'success') {
|
||
DOM.toastMessage.textContent = message;
|
||
DOM.toast.className = `toast ${type}`;
|
||
DOM.toast.style.display = 'block';
|
||
|
||
setTimeout(() => {
|
||
DOM.toast.style.display = 'none';
|
||
}, 3000);
|
||
}
|
||
|
||
function showModal(modal) {
|
||
modal.style.display = 'flex';
|
||
}
|
||
|
||
function hideModal(modal) {
|
||
modal.style.display = 'none';
|
||
}
|
||
|
||
// ============================================
|
||
// Survey List Functions
|
||
// ============================================
|
||
function renderSurveysList() {
|
||
if (AppState.surveys.length === 0) {
|
||
DOM.surveysList.innerHTML = `
|
||
<div class="empty-state">
|
||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<path d="M9 12h.01M15 12h.01M10 16c.5.3 1.2.5 2 .5s1.5-.2 2-.5M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10z"/>
|
||
</svg>
|
||
<p>No surveys yet. Create your first survey!</p>
|
||
<button class="btn btn-primary" onclick="switchTab('create')">Create Survey</button>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
DOM.surveysList.innerHTML = AppState.surveys.map(survey => `
|
||
<div class="survey-card" data-id="${survey.id}">
|
||
<div class="survey-card-header">
|
||
<h3>${escapeHtml(survey.name)}</h3>
|
||
${survey.has_analysis ? '<span class="analysis-badge">📊 Analysis</span>' : ''}
|
||
</div>
|
||
<div class="survey-card-meta">
|
||
<span>
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
||
</svg>
|
||
${escapeHtml(survey.client)}
|
||
</span>
|
||
<span>
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
|
||
<path d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||
</svg>
|
||
${escapeHtml(survey.site)}
|
||
</span>
|
||
<span>
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||
</svg>
|
||
${formatDate(survey.created_at)}
|
||
</span>
|
||
</div>
|
||
<div class="survey-card-actions">
|
||
<button class="btn btn-secondary btn-small btn-edit-survey" data-id="${survey.id}">
|
||
Edit
|
||
</button>
|
||
<button class="btn btn-secondary btn-small btn-view-survey" data-id="${survey.id}">
|
||
View & Analyze
|
||
</button>
|
||
<button class="btn btn-delete-card btn-delete-survey" data-id="${survey.id}">
|
||
Delete
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
// Add event listeners
|
||
document.querySelectorAll('.survey-card').forEach(card => {
|
||
card.addEventListener('click', (e) => {
|
||
if (!e.target.closest('.btn-delete-survey')) {
|
||
const surveyId = card.dataset.id;
|
||
viewSurvey(surveyId);
|
||
}
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.btn-delete-survey').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
AppState.deleteTargetId = btn.dataset.id;
|
||
showModal(DOM.deleteModal);
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.btn-edit-survey').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const surveyId = btn.dataset.id;
|
||
editSurvey(surveyId);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ============================================
|
||
// Survey Form Functions
|
||
// ============================================
|
||
function renderQuestions() {
|
||
if (!AppState.surveyTemplate) return;
|
||
|
||
const questions = AppState.surveyTemplate.questions;
|
||
|
||
DOM.questionsContainer.innerHTML = questions.map((q, index) => {
|
||
const inputId = `question-${q.id}`;
|
||
let inputHtml = '';
|
||
|
||
if (q.type === 'textarea') {
|
||
inputHtml = `<textarea id="${inputId}" name="${q.id}" rows="3" placeholder="Enter your answer..."></textarea>`;
|
||
} else if (q.type === 'select' && q.options) {
|
||
const options = q.options.map(opt =>
|
||
`<option value="${escapeHtml(opt)}">${escapeHtml(opt)}</option>`
|
||
).join('');
|
||
inputHtml = `
|
||
<select id="${inputId}" name="${q.id}">
|
||
<option value="">Select an option...</option>
|
||
${options}
|
||
</select>
|
||
`;
|
||
} else {
|
||
inputHtml = `<input type="text" id="${inputId}" name="${q.id}" placeholder="Enter your answer...">`;
|
||
}
|
||
|
||
return `
|
||
<div class="question-item" data-question-id="${q.id}">
|
||
<div class="question-header">
|
||
<span class="question-number">Q${index + 1}</span>
|
||
<span class="question-category">${escapeHtml(q.category)}</span>
|
||
</div>
|
||
<div class="question-text">${escapeHtml(q.question)}</div>
|
||
<div class="question-input">
|
||
${inputHtml}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
updateQuestionCount();
|
||
}
|
||
|
||
function updateQuestionCount() {
|
||
if (!AppState.surveyTemplate) return;
|
||
DOM.questionCount.textContent = `(${AppState.surveyTemplate.questions.length} questions)`;
|
||
}
|
||
|
||
function loadSampleData() {
|
||
// Fill metadata
|
||
DOM.surveyName.value = SAMPLE_DATA.name;
|
||
DOM.surveyClient.value = SAMPLE_DATA.client;
|
||
DOM.surveySite.value = SAMPLE_DATA.site;
|
||
DOM.surveyDescription.value = SAMPLE_DATA.description;
|
||
DOM.surveyModel.value = SAMPLE_DATA.model;
|
||
|
||
// Fill answers
|
||
Object.entries(SAMPLE_DATA.answers).forEach(([qid, value]) => {
|
||
const input = document.getElementById(`question-${qid}`);
|
||
if (input) {
|
||
input.value = value;
|
||
}
|
||
});
|
||
|
||
showToast('Sample data loaded', 'success');
|
||
}
|
||
|
||
function getFormData() {
|
||
const answers = {};
|
||
|
||
if (AppState.surveyTemplate) {
|
||
AppState.surveyTemplate.questions.forEach(q => {
|
||
const input = document.getElementById(`question-${q.id}`);
|
||
if (input) {
|
||
answers[q.id] = input.value.trim();
|
||
}
|
||
});
|
||
}
|
||
|
||
return {
|
||
name: DOM.surveyName.value.trim(),
|
||
client: DOM.surveyClient.value.trim(),
|
||
site: DOM.surveySite.value.trim(),
|
||
description: DOM.surveyDescription.value.trim(),
|
||
model: DOM.surveyModel.value,
|
||
answers: answers,
|
||
photos: AppState.uploadedPhotos
|
||
};
|
||
}
|
||
|
||
async function handleFormSubmit(e) {
|
||
e.preventDefault();
|
||
|
||
// Validation
|
||
if (!DOM.surveyName.value.trim()) {
|
||
showToast('Please enter a survey name', 'error');
|
||
DOM.surveyName.focus();
|
||
return;
|
||
}
|
||
if (!DOM.surveyClient.value.trim()) {
|
||
showToast('Please enter a client name', 'error');
|
||
DOM.surveyClient.focus();
|
||
return;
|
||
}
|
||
if (!DOM.surveySite.value.trim()) {
|
||
showToast('Please enter a site location', 'error');
|
||
DOM.surveySite.focus();
|
||
return;
|
||
}
|
||
|
||
const formData = getFormData();
|
||
|
||
try {
|
||
if (AppState.currentSurveyId) {
|
||
// Update existing survey
|
||
await updateSurvey(AppState.currentSurveyId, formData);
|
||
await loadSurveys();
|
||
resetForm();
|
||
switchTab('surveys');
|
||
} else {
|
||
// Create new survey
|
||
const surveyId = await createSurvey(formData);
|
||
await loadSurveys();
|
||
resetForm();
|
||
switchTab('surveys');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving survey:', error);
|
||
}
|
||
}
|
||
|
||
function resetForm() {
|
||
DOM.surveyForm.reset();
|
||
AppState.uploadedPhotos = [];
|
||
AppState.currentSurveyId = null;
|
||
renderPhotoPreviews();
|
||
|
||
// Reset button text
|
||
const submitBtn = document.getElementById('btn-submit-survey');
|
||
if (submitBtn) {
|
||
submitBtn.textContent = 'Create Survey';
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// Photo Upload Functions
|
||
// ============================================
|
||
function handlePhotoUpload(files) {
|
||
Array.from(files).forEach(file => {
|
||
if (!file.type.startsWith('image/')) return;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
AppState.uploadedPhotos.push({
|
||
name: file.name,
|
||
data: e.target.result
|
||
});
|
||
renderPhotoPreviews();
|
||
};
|
||
reader.readAsDataURL(file);
|
||
});
|
||
}
|
||
|
||
function renderPhotoPreviews() {
|
||
if (AppState.uploadedPhotos.length === 0) {
|
||
DOM.photoPreviewGrid.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
DOM.photoPreviewGrid.innerHTML = AppState.uploadedPhotos.map((photo, index) => `
|
||
<div class="photo-preview-item">
|
||
<img src="${photo.data}" alt="${escapeHtml(photo.name)}">
|
||
<button type="button" class="photo-preview-remove" data-index="${index}">×</button>
|
||
</div>
|
||
`).join('');
|
||
|
||
document.querySelectorAll('.photo-preview-remove').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const index = parseInt(btn.dataset.index);
|
||
AppState.uploadedPhotos.splice(index, 1);
|
||
renderPhotoPreviews();
|
||
});
|
||
});
|
||
}
|
||
|
||
// ============================================
|
||
// Analysis Functions
|
||
// ============================================
|
||
async function viewSurvey(surveyId) {
|
||
try {
|
||
const survey = await getSurvey(surveyId);
|
||
AppState.currentSurveyId = surveyId;
|
||
|
||
// Populate analysis view
|
||
DOM.analysisSurveyName.textContent = survey.name;
|
||
DOM.analysisModel.value = survey.model;
|
||
|
||
// Populate survey meta
|
||
DOM.analysisSurveyMeta.innerHTML = `
|
||
<div class="meta-item">
|
||
<span class="meta-label">Client</span>
|
||
<span class="meta-value">${escapeHtml(survey.client)}</span>
|
||
</div>
|
||
<div class="meta-item">
|
||
<span class="meta-label">Site</span>
|
||
<span class="meta-value">${escapeHtml(survey.site)}</span>
|
||
</div>
|
||
<div class="meta-item">
|
||
<span class="meta-label">Created</span>
|
||
<span class="meta-value">${formatDate(survey.created_at)}</span>
|
||
</div>
|
||
<div class="meta-item">
|
||
<span class="meta-label">Description</span>
|
||
<span class="meta-value">${escapeHtml(survey.description || 'No description')}</span>
|
||
</div>
|
||
`;
|
||
|
||
// Populate question summary by category
|
||
const categories = {};
|
||
if (AppState.surveyTemplate) {
|
||
AppState.surveyTemplate.questions.forEach(q => {
|
||
const hasAnswer = survey.answers && survey.answers[q.id];
|
||
if (!categories[q.category]) {
|
||
categories[q.category] = { total: 0, answered: 0 };
|
||
}
|
||
categories[q.category].total++;
|
||
if (hasAnswer) categories[q.category].answered++;
|
||
});
|
||
}
|
||
|
||
DOM.analysisQuestionSummary.innerHTML = Object.entries(categories).map(([cat, stats]) => `
|
||
<div class="question-summary-item">
|
||
<span class="category">${escapeHtml(cat)}</span>
|
||
<span class="count">${stats.answered}/${stats.total}</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
// Check for saved analysis
|
||
if (survey.analysis) {
|
||
// Show saved analysis
|
||
DOM.analysisOutput.innerHTML = '<div class="analysis-content">' + formatMarkdown(survey.analysis) + '</div>';
|
||
AppState.analysisContent = survey.analysis;
|
||
DOM.exportButtons.style.display = 'flex';
|
||
|
||
// Add delete analysis button
|
||
DOM.analysisOutput.innerHTML += `
|
||
<div class="analysis-actions" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border-color);">
|
||
<button class="btn btn-danger" id="btn-delete-analysis">Delete Analysis</button>
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('btn-delete-analysis').addEventListener('click', deleteAnalysis);
|
||
} else {
|
||
// Show placeholder
|
||
DOM.analysisOutput.innerHTML = `
|
||
<div class="analysis-placeholder">
|
||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||
</svg>
|
||
<p>Click "Generate Recommendations" to get AI-powered insights</p>
|
||
</div>
|
||
`;
|
||
DOM.exportButtons.style.display = 'none';
|
||
AppState.analysisContent = '';
|
||
}
|
||
|
||
// Clear chat
|
||
DOM.chatMessages.innerHTML = `
|
||
<div class="chat-message system">
|
||
<p>Ask me anything about this survey - budget planning, security recommendations, timeline suggestions, or technical details!</p>
|
||
</div>
|
||
`;
|
||
DOM.chatInput.disabled = false;
|
||
DOM.btnSendChat.disabled = false;
|
||
|
||
switchTab('analysis');
|
||
} catch (error) {
|
||
console.error('Error viewing survey:', error);
|
||
}
|
||
}
|
||
|
||
async function editSurvey(surveyId) {
|
||
try {
|
||
const survey = await getSurvey(surveyId);
|
||
AppState.currentSurveyId = surveyId;
|
||
|
||
// Populate form with existing data
|
||
DOM.surveyName.value = survey.name || '';
|
||
DOM.surveyClient.value = survey.client || '';
|
||
DOM.surveySite.value = survey.site || '';
|
||
DOM.surveyDescription.value = survey.description || '';
|
||
|
||
// Set model
|
||
if (DOM.surveyModel) {
|
||
DOM.surveyModel.value = survey.model || '';
|
||
}
|
||
|
||
// Populate answers
|
||
if (survey.answers && AppState.surveyTemplate) {
|
||
AppState.surveyTemplate.questions.forEach(q => {
|
||
const inputId = `question-${q.id}`;
|
||
const element = document.getElementById(inputId);
|
||
if (element && survey.answers[q.id]) {
|
||
element.value = survey.answers[q.id];
|
||
}
|
||
});
|
||
}
|
||
|
||
// Update button text
|
||
const submitBtn = document.getElementById('btn-submit-survey');
|
||
if (submitBtn) {
|
||
submitBtn.textContent = 'Update Survey';
|
||
}
|
||
|
||
updateQuestionCount();
|
||
switchTab('create');
|
||
showToast('Survey loaded for editing', 'success');
|
||
} catch (error) {
|
||
console.error('Error loading survey for edit:', error);
|
||
showToast('Failed to load survey for editing', 'error');
|
||
}
|
||
}
|
||
|
||
async function updateSurvey(surveyId, surveyData) {
|
||
try {
|
||
const data = await fetchAPI(`/api/surveys/${surveyId}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(surveyData)
|
||
});
|
||
showToast('Survey updated successfully', 'success');
|
||
return data;
|
||
} catch (error) {
|
||
showToast('Failed to update survey', 'error');
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async function generateAnalysis() {
|
||
if (AppState.isGenerating || !AppState.currentSurveyId) return;
|
||
|
||
AppState.isGenerating = true;
|
||
DOM.analysisLoading.style.display = 'flex';
|
||
DOM.analysisOutput.innerHTML = '';
|
||
DOM.exportButtons.style.display = 'none';
|
||
DOM.btnGenerateAnalysis.disabled = true;
|
||
|
||
try {
|
||
const model = DOM.analysisModel.value;
|
||
|
||
// Use streaming fetch
|
||
const response = await fetch(`${API_BASE_URL}/api/surveys/${AppState.currentSurveyId}/analyze`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ model })
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Failed to generate analysis');
|
||
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let content = '';
|
||
|
||
// Create content container
|
||
const contentDiv = document.createElement('div');
|
||
contentDiv.className = 'analysis-content';
|
||
DOM.analysisOutput.appendChild(contentDiv);
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
const chunk = decoder.decode(value, { stream: true });
|
||
content += chunk;
|
||
contentDiv.innerHTML = formatMarkdown(content);
|
||
|
||
// Auto-scroll to bottom
|
||
DOM.analysisOutput.scrollTop = DOM.analysisOutput.scrollHeight;
|
||
}
|
||
|
||
AppState.analysisContent = content;
|
||
DOM.exportButtons.style.display = 'flex';
|
||
|
||
// Automatically save analysis
|
||
await saveAnalysis(AppState.currentSurveyId, content);
|
||
|
||
showToast('Analysis complete and saved', 'success');
|
||
|
||
} catch (error) {
|
||
console.error('Error generating analysis:', error);
|
||
showToast('Failed to generate analysis', 'error');
|
||
DOM.analysisOutput.innerHTML = `
|
||
<div class="analysis-placeholder">
|
||
<p style="color: var(--danger)">Failed to generate analysis. Please try again.</p>
|
||
</div>
|
||
`;
|
||
} finally {
|
||
AppState.isGenerating = false;
|
||
DOM.analysisLoading.style.display = 'none';
|
||
DOM.btnGenerateAnalysis.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function saveAnalysis(surveyId, analysisContent) {
|
||
try {
|
||
await fetchAPI(`/api/surveys/${surveyId}/analysis`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ analysis: analysisContent })
|
||
});
|
||
console.log('Analysis saved successfully');
|
||
} catch (error) {
|
||
console.error('Error saving analysis:', error);
|
||
}
|
||
}
|
||
|
||
async function deleteAnalysis() {
|
||
if (!AppState.currentSurveyId) return;
|
||
|
||
if (!confirm('Are you sure you want to delete this analysis? This action cannot be undone.')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await fetchAPI(`/api/surveys/${AppState.currentSurveyId}/analysis`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
// Clear the analysis display
|
||
AppState.analysisContent = '';
|
||
DOM.analysisOutput.innerHTML = `
|
||
<div class="analysis-placeholder">
|
||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||
</svg>
|
||
<p>Click "Generate Recommendations" to get AI-powered insights</p>
|
||
</div>
|
||
`;
|
||
DOM.exportButtons.style.display = 'none';
|
||
|
||
showToast('Analysis deleted', 'success');
|
||
} catch (error) {
|
||
console.error('Error deleting analysis:', error);
|
||
showToast('Failed to delete analysis', 'error');
|
||
}
|
||
}
|
||
|
||
function formatMarkdown(text) {
|
||
// Simple markdown formatting
|
||
return text
|
||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||
.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>')
|
||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||
.replace(/`(.*?)`/g, '<code>$1</code>')
|
||
.replace(/^\s*[-*+]\s+(.*$)/gim, '<li>$1</li>')
|
||
.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
||
.replace(/^\d+\.\s+(.*$)/gim, '<li>$1</li>')
|
||
.replace(/---+/g, '<hr>')
|
||
.replace(/\n/g, '<br>');
|
||
}
|
||
|
||
async function sendChatMessage() {
|
||
const message = DOM.chatInput.value.trim();
|
||
if (!message || !AppState.currentSurveyId) return;
|
||
|
||
// Add user message
|
||
addChatMessage('user', message);
|
||
DOM.chatInput.value = '';
|
||
|
||
// Show loading
|
||
DOM.btnSendChat.disabled = true;
|
||
|
||
try {
|
||
const response = await fetchAPI(`/api/surveys/${AppState.currentSurveyId}/chat`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
message,
|
||
model: DOM.analysisModel.value
|
||
})
|
||
});
|
||
|
||
addChatMessage('assistant', response.response);
|
||
} catch (error) {
|
||
showToast('Failed to get response', 'error');
|
||
addChatMessage('system', 'Sorry, I encountered an error. Please try again.');
|
||
} finally {
|
||
DOM.btnSendChat.disabled = false;
|
||
}
|
||
}
|
||
|
||
function addChatMessage(role, content) {
|
||
const messageDiv = document.createElement('div');
|
||
messageDiv.className = `chat-message ${role}`;
|
||
messageDiv.innerHTML = `<p>${escapeHtml(content)}</p>`;
|
||
DOM.chatMessages.appendChild(messageDiv);
|
||
DOM.chatMessages.scrollTop = DOM.chatMessages.scrollHeight;
|
||
}
|
||
|
||
// ============================================
|
||
// Export Functions
|
||
// ============================================
|
||
async function exportToText() {
|
||
if (!AppState.currentSurveyId) return;
|
||
|
||
try {
|
||
const data = await exportSurvey(AppState.currentSurveyId, 'text', AppState.analysisContent);
|
||
downloadFile(data.filename, data.content, 'text/plain');
|
||
showToast('Report exported as text', 'success');
|
||
} catch (error) {
|
||
console.error('Export error:', error);
|
||
}
|
||
}
|
||
|
||
async function exportToPDF() {
|
||
if (!AppState.currentSurveyId) return;
|
||
|
||
try {
|
||
const data = await exportSurvey(AppState.currentSurveyId, 'pdf', AppState.analysisContent);
|
||
|
||
// Decode base64 PDF content
|
||
const pdfContent = atob(data.content);
|
||
const bytes = new Uint8Array(pdfContent.length);
|
||
for (let i = 0; i < pdfContent.length; i++) {
|
||
bytes[i] = pdfContent.charCodeAt(i);
|
||
}
|
||
|
||
// Create blob and download
|
||
const blob = new Blob([bytes], { type: 'application/pdf' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = data.filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
|
||
showToast('PDF report downloaded', 'success');
|
||
} catch (error) {
|
||
console.error('PDF Export error:', error);
|
||
showToast('Failed to export PDF', 'error');
|
||
}
|
||
}
|
||
|
||
async function emailReport() {
|
||
if (!AppState.currentSurveyId) return;
|
||
|
||
try {
|
||
const data = await exportSurvey(AppState.currentSurveyId, 'email', AppState.analysisContent);
|
||
|
||
// Open email client with mailto link
|
||
if (data.mailto_link) {
|
||
window.open(data.mailto_link, '_blank');
|
||
showToast('Email client opened', 'success');
|
||
} else {
|
||
showToast('Failed to generate email link', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('Email error:', error);
|
||
showToast('Failed to open email', 'error');
|
||
}
|
||
}
|
||
|
||
function downloadFile(filename, content, mimeType) {
|
||
const blob = new Blob([content], { type: mimeType });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
// ============================================
|
||
// Utility Functions
|
||
// ============================================
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function formatDate(dateString) {
|
||
if (!dateString) return 'Unknown';
|
||
const date = new Date(dateString);
|
||
return date.toLocaleDateString('en-US', {
|
||
year: 'numeric',
|
||
month: 'short',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
|
||
// ============================================
|
||
// Event Listeners Setup
|
||
// ============================================
|
||
function setupEventListeners() {
|
||
// Tab navigation
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
switchTab(btn.dataset.tab);
|
||
});
|
||
});
|
||
|
||
// Create survey form
|
||
DOM.surveyForm.addEventListener('submit', handleFormSubmit);
|
||
DOM.btnLoadSample.addEventListener('click', loadSampleData);
|
||
|
||
// Photo upload
|
||
DOM.btnBrowsePhotos.addEventListener('click', () => DOM.photoInput.click());
|
||
DOM.photoInput.addEventListener('change', (e) => handlePhotoUpload(e.target.files));
|
||
|
||
DOM.photoUploadArea.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
DOM.photoUploadArea.classList.add('dragover');
|
||
});
|
||
|
||
DOM.photoUploadArea.addEventListener('dragleave', () => {
|
||
DOM.photoUploadArea.classList.remove('dragover');
|
||
});
|
||
|
||
DOM.photoUploadArea.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
DOM.photoUploadArea.classList.remove('dragover');
|
||
handlePhotoUpload(e.dataTransfer.files);
|
||
});
|
||
|
||
DOM.photoUploadArea.addEventListener('click', (e) => {
|
||
if (e.target === DOM.photoUploadArea || e.target.closest('svg') || e.target.tagName === 'P') {
|
||
DOM.photoInput.click();
|
||
}
|
||
});
|
||
|
||
// Clear all surveys
|
||
DOM.btnClearAll.addEventListener('click', () => {
|
||
showModal(DOM.clearAllModal);
|
||
});
|
||
|
||
DOM.btnCancelClearAll.addEventListener('click', () => {
|
||
hideModal(DOM.clearAllModal);
|
||
});
|
||
|
||
DOM.btnConfirmClearAll.addEventListener('click', async () => {
|
||
hideModal(DOM.clearAllModal);
|
||
try {
|
||
await deleteAllSurveys();
|
||
await loadSurveys();
|
||
} catch (error) {
|
||
console.error('Error clearing surveys:', error);
|
||
}
|
||
});
|
||
|
||
// Delete survey modal
|
||
DOM.btnCancelDelete.addEventListener('click', () => {
|
||
hideModal(DOM.deleteModal);
|
||
AppState.deleteTargetId = null;
|
||
});
|
||
|
||
DOM.btnConfirmDelete.addEventListener('click', async () => {
|
||
if (AppState.deleteTargetId) {
|
||
hideModal(DOM.deleteModal);
|
||
try {
|
||
await deleteSurvey(AppState.deleteTargetId);
|
||
await loadSurveys();
|
||
} catch (error) {
|
||
console.error('Error deleting survey:', error);
|
||
}
|
||
AppState.deleteTargetId = null;
|
||
}
|
||
});
|
||
|
||
// Analysis tab
|
||
DOM.btnBackToSurveys.addEventListener('click', () => {
|
||
switchTab('surveys');
|
||
});
|
||
|
||
DOM.btnGenerateAnalysis.addEventListener('click', generateAnalysis);
|
||
|
||
// Chat
|
||
DOM.btnSendChat.addEventListener('click', sendChatMessage);
|
||
DOM.chatInput.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
sendChatMessage();
|
||
}
|
||
});
|
||
|
||
// Export
|
||
DOM.btnExportText.addEventListener('click', exportToText);
|
||
DOM.btnExportPdf.addEventListener('click', exportToPDF);
|
||
DOM.btnEmail.addEventListener('click', emailReport);
|
||
}
|
||
|
||
// ============================================
|
||
// Initialization
|
||
// ============================================
|
||
// Load available AI models from API
|
||
async function loadModels() {
|
||
try {
|
||
const data = await fetchAPI('/api/models');
|
||
if (data.models && data.models.length > 0) {
|
||
const surveySelect = document.getElementById('survey-model');
|
||
const analysisSelect = document.getElementById('analysis-model');
|
||
|
||
// Clear loading option
|
||
surveySelect.innerHTML = '';
|
||
analysisSelect.innerHTML = '';
|
||
|
||
// Add models to both dropdowns
|
||
data.models.forEach(model => {
|
||
const option = document.createElement('option');
|
||
option.value = model.id;
|
||
option.textContent = model.name;
|
||
surveySelect.appendChild(option.cloneNode(true));
|
||
analysisSelect.appendChild(option);
|
||
});
|
||
|
||
// Set default
|
||
if (data.default) {
|
||
surveySelect.value = data.default;
|
||
analysisSelect.value = data.default;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load models:', error);
|
||
// Keep default options if API fails
|
||
}
|
||
}
|
||
|
||
function init() {
|
||
cacheDOMElements();
|
||
setupEventListeners();
|
||
loadModels(); // Load available AI models
|
||
loadSurveyTemplate();
|
||
loadSurveys();
|
||
}
|
||
|
||
// Start the app when DOM is ready
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|