Files

1130 lines
40 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();
}