Files
site-survey-ai/index.html
T

1602 lines
63 KiB
HTML
Raw 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.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IT Site Survey AI | Network Infrastructure Planning</title>
<style>
:root {
--bg: #0d1117;
--bg-2: #161b22;
--bg-3: #21262d;
--border: #30363d;
--text: #c9d1d9;
--text-2: #8b949e;
--accent: #58a6ff;
--accent-2: #1f6feb;
--success: #3fb950;
--warning: #d29922;
--danger: #f85149;
--radius: 8px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
line-height: 1.5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 30px 20px;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 15px;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
flex-wrap: wrap;
}
header .logo-area { display: flex; align-items: center; gap: 15px; }
header .logo {
width: 48px; height: 48px;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
border-radius: 12px;
display: grid; place-items: center;
font-size: 24px;
}
header h1 { font-size: 1.5rem; font-weight: 600; }
header p { color: var(--text-2); font-size: 0.9rem; }
.tabs {
display: flex;
gap: 10px;
margin-bottom: 30px;
border-bottom: 1px solid var(--border);
padding-bottom: 15px;
}
.tab {
padding: 10px 20px;
background: transparent;
border: none;
color: var(--text-2);
cursor: pointer;
font-size: 0.95rem;
border-radius: var(--radius);
transition: all 0.15s;
}
.tab:hover { color: var(--text); background: var(--bg-3); }
.tab.active { color: var(--accent); background: rgba(88,166,255,0.1); border: 1px solid var(--accent); }
.tab-content { display: none; }
.tab-content.active { display: block; }
.panel {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 25px;
margin-bottom: 20px;
}
.panel h2 {
font-size: 1.1rem;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-size: 0.9rem;
color: var(--text);
}
.form-group label .required {
color: var(--danger);
margin-left: 4px;
}
input, select, textarea {
width: 100%;
padding: 12px;
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-size: 0.95rem;
font-family: inherit;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--accent);
}
textarea { min-height: 100px; resize: vertical; }
.btn {
padding: 12px 24px;
border: none;
border-radius: var(--radius);
color: #fff;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent), var(--accent-2));
}
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 15px rgba(88,166,255,0.3); }
.btn-success { background: var(--success); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover { background: #f85149; }
.btn-secondary { background: var(--bg-3); color: var(--text); border: 1px solid var(--border); }
.btn-secondary:hover { background: var(--border); }
.btn:disabled {
background: var(--bg-3) !important;
color: var(--text-2) !important;
cursor: not-allowed;
transform: none !important;
}
.btn-small { padding: 8px 16px; font-size: 0.85rem; }
.question-card {
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
margin-bottom: 15px;
}
.question-card .category {
font-size: 0.75rem;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.question-card .question-text {
font-size: 1rem;
margin-bottom: 12px;
}
.checkbox-group, .radio-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.checkbox-item, .radio-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border-radius: 4px;
cursor: pointer;
}
.checkbox-item:hover, .radio-item:hover {
background: rgba(88,166,255,0.05);
}
.checkbox-item input, .radio-item input {
width: auto;
margin: 0;
}
.survey-list {
display: grid;
gap: 15px;
}
.survey-item {
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.15s;
}
.survey-item:hover {
border-color: var(--accent);
}
.survey-item-info h3 {
margin: 0 0 5px 0;
font-size: 1.1rem;
}
.survey-item-info p {
margin: 0;
color: var(--text-2);
font-size: 0.85rem;
}
.survey-item-meta {
text-align: right;
color: var(--text-2);
font-size: 0.85rem;
}
.ai-response {
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
margin-top: 20px;
white-space: pre-wrap;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.6;
max-height: 600px;
overflow-y: auto;
}
.ai-response.streaming::after {
content: '▋';
animation: blink 1s infinite;
}
@keyframes blink { 50% { opacity: 0; } }
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid var(--text-2);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.alert {
padding: 12px 16px;
border-radius: var(--radius);
margin-bottom: 15px;
}
.alert.success { background: rgba(63,185,80,0.1); border: 1px solid var(--success); color: var(--success); }
.alert.error { background: rgba(248,81,73,0.1); border: 1px solid var(--danger); color: var(--danger); }
.alert.info { background: rgba(88,166,255,0.1); border: 1px solid var(--accent); color: var(--accent); }
/* Photo upload styles */
.photo-upload-area {
border: 2px dashed var(--border);
border-radius: var(--radius);
padding: 30px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 15px;
}
.photo-upload-area:hover {
border-color: var(--accent);
background: rgba(88,166,255,0.05);
}
.photo-upload-area.dragover {
border-color: var(--accent);
background: rgba(88,166,255,0.1);
}
.photo-upload-area input[type="file"] {
display: none;
}
.photo-upload-icon {
font-size: 40px;
margin-bottom: 10px;
}
.photo-upload-text {
color: var(--text-2);
font-size: 0.9rem;
}
.photo-upload-text strong {
color: var(--accent);
}
.photo-preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 12px;
margin-bottom: 15px;
}
.photo-preview-item {
position: relative;
border-radius: var(--radius);
overflow: hidden;
aspect-ratio: 1;
border: 1px solid var(--border);
}
.photo-preview-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.photo-preview-item .remove-btn {
position: absolute;
top: 4px;
right: 4px;
background: rgba(0,0,0,0.7);
border: none;
color: #fff;
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 14px;
line-height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.photo-preview-item .remove-btn:hover {
background: var(--danger);
}
.photo-count-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: 20px;
font-size: 0.85rem;
color: var(--text-2);
margin-bottom: 10px;
}
.photo-count-badge.has-photos {
border-color: var(--success);
color: var(--success);
}
/* Model selector */
.model-selector {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 20px;
}
.model-selector label {
margin: 0;
font-size: 0.9rem;
color: var(--text-2);
white-space: nowrap;
}
.model-selector select {
width: auto;
flex: 1;
min-width: 200px;
}
.model-selector .model-status {
font-size: 0.8rem;
color: var(--text-2);
display: flex;
align-items: center;
gap: 6px;
}
.model-selector .model-status.online {
color: var(--success);
}
.model-selector .model-status.offline {
color: var(--danger);
}
/* Photos display in analysis */
.attached-photos {
margin-bottom: 20px;
}
.attached-photos h3 {
font-size: 0.95rem;
color: var(--text-2);
margin-bottom: 12px;
}
.attached-photo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
}
.attached-photo-item {
border-radius: var(--radius);
overflow: hidden;
border: 1px solid var(--border);
cursor: pointer;
transition: all 0.2s;
}
.attached-photo-item:hover {
border-color: var(--accent);
transform: translateY(-2px);
}
.attached-photo-item img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
}
.attached-photo-item .photo-label {
padding: 6px 8px;
font-size: 0.75rem;
color: var(--text-2);
background: var(--bg-3);
}
/* Photo lightbox */
.lightbox {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.9);
z-index: 9999;
align-items: center;
justify-content: center;
}
.lightbox.active {
display: flex;
}
.lightbox img {
max-width: 90%;
max-height: 90%;
border-radius: var(--radius);
}
.lightbox-close {
position: absolute;
top: 20px;
right: 20px;
background: none;
border: none;
color: #fff;
font-size: 32px;
cursor: pointer;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
n .btn-secondary {
background: var(--bg-3);
color: var(--text);
border: 1px solid var(--border);
margin-right: 10px;
}
.btn-secondary:hover {
background: var(--border);
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo-area">
<div class="logo">🏢</div>
<div>
<h1>IT Site Survey AI</h1>
<p>AI-powered network infrastructure planning & recommendations</p>
</div>
</div>
</header>
<div class="tabs">
<button class="tab active" onclick="switchTab('surveys')">📋 Surveys</button>
<button class="tab" onclick="switchTab('create')"> Create Survey</button>
<!-- Take Survey tab removed - now combined with Create Survey -->
<button class="tab" onclick="switchTab('analyze')" id="analyzeTab" style="display:none;">🤖 AI Analysis</button>
</div>
<!-- Surveys List Tab -->
<div id="surveysTab" class="tab-content active">
<div class="panel">
<h2>📋 Site Surveys</h2>
<div id="surveyList" class="survey-list">
<div class="alert info">Loading surveys...</div>
</div>
</div>
</div>
<!-- Create Survey Tab -->
<div id="createTab" class="tab-content">
<div class="panel">
<h2> Create New Site Survey</h2>
<button class="btn" onclick="loadSampleData()" style="background: var(--success); color: #fff; margin-bottom: 15px;">📋 Load Sample Data</button>
<div class="form-group">
<label>Survey Name</label>
<input type="text" id="surveyName" placeholder="e.g., Acme Corp Headquarters Survey">
</div>
<div class="form-group">
<label>Client Name</label>
<input type="text" id="clientName" placeholder="e.g., Acme Corporation">
</div>
<div class="form-group">
<label>Site Name/Location</label>
<input type="text" id="siteName" placeholder="e.g., Main Office - Downtown">
</div>
<div class="form-group">
<label>Description</label>
<textarea id="surveyDescription" placeholder="Brief description of the survey purpose..."></textarea>
</div>
<div id="createSurveyQuestions"></div>
<button class="btn btn-primary" onclick="createSurvey()">Create Survey</button>
</div>
</div>
<!-- Take Survey Tab -->
<div id="takeTab" class="tab-content">
<div id="surveyForm">
<div class="panel">
<h2 id="currentSurveyName">Site Survey</h2>
<div id="surveyQuestions"></div>
<!-- Photo Upload Section -->
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border);">
<h3 style="font-size: 1rem; margin-bottom: 15px; display: flex; align-items: center; gap: 8px;">
📷 Site Photos
<span id="photoCountBadge" class="photo-count-badge">0 photos</span>
</h3>
<div class="photo-preview-grid" id="photoPreviewGrid"></div>
<div class="photo-upload-area" id="photoUploadArea" onclick="document.getElementById('photoInput').click()">
<input type="file" id="photoInput" accept="image/*" multiple onchange="handlePhotoSelect(this.files)">
<div class="photo-upload-icon">📷</div>
<div class="photo-upload-text">
<strong>Click to upload</strong> or drag and drop<br>
<small style="color: var(--text-2);">JPG, PNG, WebP • Multiple files allowed</small>
</div>
</div>
</div>
<div class="form-group" style="margin-top: 30px;">
<label>Your Name</label>
<input type="text" id="submitterName" placeholder="Enter your name">
</div>
<!-- Model selector for survey submission -->
<div class="model-selector">
<label>🤖 AI Model:</label>
<select id="submitModelSelect">
<option value="">Loading models...</option>
</select>
<span id="ollamaStatusBadge" class="model-status offline">⚫ Offline</span>
</div>
<button class="btn btn-success" onclick="submitSurvey()">Submit Survey</button>
</div>
</div>
</div>
<!-- Analysis Tab -->
<div id="analyzeContent" class="tab-content">
<div class="panel">
<h2>🤖 AI Analysis & Recommendations</h2>
<p style="color: var(--text-2); margin-bottom: 20px;">Generate AI-powered network infrastructure recommendations based on survey responses.</p>
<!-- Attached photos display -->
<div class="attached-photos" id="attachedPhotosSection" style="display: none;">
<h3>📷 Attached Photos (<span id="attachedPhotoCount">0</span>)</h3>
<div class="attached-photo-grid" id="attachedPhotoGrid"></div>
</div>
<!-- Model selector for analysis -->
<div class="model-selector">
<label>🤖 AI Model:</label>
<select id="analyzeModelSelect">
<option value="">Loading models...</option>
</select>
</div>
<div style="display: flex; gap: 10px; margin-bottom: 20px;">
<button class="btn btn-primary" onclick="runAIAnalysis()">
<span id="analyzeSpinner" style="display: none;" class="spinner"></span>
Generate Recommendations
</button>
</div>
<div id="aiResponse" class="ai-response" style="display: none;"></div>
<!-- Chat Interface for AI Questions -->
<div id="chatInterface" style="display: none; margin-top: 20px; padding: 15px; background: var(--bg-3); border-radius: var(--radius); border: 1px solid var(--border);">
<h4>💬 Ask AI Questions</h4>
<div id="chatMessages" style="max-height: 300px; overflow-y: auto; margin-bottom: 10px; padding: 10px; background: var(--bg-1); border-radius: var(--radius); min-height: 100px;">
<p style="color: var(--text-2); font-style: italic;">Ask questions about the recommendations or request refinements...</p>
</div>
<div style="display: flex; gap: 10px;">
<input type="text" id="chatInput" placeholder="Type your question..." style="flex: 1; padding: 10px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--bg-1); color: var(--text);">
<button class="btn btn-primary" onclick="sendChatMessage()">Send</button>
</div>
</div>
<!-- Export buttons -->
<div id="exportButtons" style="display: none; margin-top: 20px; padding: 20px; background: var(--bg-3); border-radius: var(--radius); border: 1px solid var(--border);">
<h3>📤 Export Recommendations</h3>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button class="btn btn-secondary" onclick="exportToPDF()" title="Download as PDF">
📄 PDF
</button>
<button class="btn btn-secondary" onclick="exportToText()" title="Copy as formatted text">
📝 Text
</button>
<a id="emailLink" href="#" class="btn" style="text-decoration: none; display: inline-flex; align-items: center; gap: 5px;">
📧 Email
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Photo lightbox -->
<div class="lightbox" id="photoLightbox" onclick="closeLightbox()">
<button class="lightbox-close">&times;</button>
<img id="lightboxImg" src="" alt="">
</div>
<script>
let surveys = [];
let currentSurvey = null;
let currentSurveyId = null;
let surveyResponses = {};
let selectedPhotos = []; // Array of base64 data URLs
let availableModels = [];
let defaultModel = 'kimi-k2.6:cloud';
let currentPhotos = []; // Photos from last submission (for analysis tab)
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadSurveys();
loadTemplate();
loadModels();
setupPhotoDropzone();
});
function switchTab(tabName) {
// Remove active from all tabs and content
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
// Add active to clicked tab button
document.querySelectorAll('.tab').forEach(btn => {
if (btn.getAttribute('onclick')?.includes(tabName)) {
btn.classList.add('active');
}
});
// Show the tab content
const contentEl = document.getElementById(tabName === 'analyze' ? 'analyzeContent' : tabName + 'Tab');
if (contentEl) {
contentEl.classList.add('active');
console.log('Switched to tab:', tabName);
} else {
console.error('Tab content not found:', tabName + 'Tab');
}
}
async function loadModels() {
try {
const resp = await fetch('/api/models');
const data = await resp.json();
availableModels = data.models || [];
defaultModel = data.default || 'llama3.2:3b';
const submitSelect = document.getElementById('submitModelSelect');
const analyzeSelect = document.getElementById('analyzeModelSelect');
const options = availableModels.map(m =>
`<option value="${m}" ${m === defaultModel ? 'selected' : ''}>${m}</option>`
).join('');
submitSelect.innerHTML = options;
analyzeSelect.innerHTML = options;
// Check Ollama status
const statusResp = await fetch('/api/ollama-status');
const statusData = await statusResp.json();
const badge = document.getElementById('ollamaStatusBadge');
if (statusData.online) {
badge.className = 'model-status online';
badge.textContent = '🟢 Online';
} else {
badge.className = 'model-status offline';
badge.textContent = '🔴 Offline';
}
} catch (e) {
console.error('Failed to load models:', e);
document.getElementById('submitModelSelect').innerHTML =
'<option value="llama3.2:3b">llama3.2:3b (default)</option>';
document.getElementById('analyzeModelSelect').innerHTML =
'<option value="llama3.2:3b">llama3.2:3b (default)</option>';
}
}
async function loadSurveyTemplate() {
try {
const response = await fetch('/api/surveys/template');
const data = await response.json();
if (data.template && data.template.questions) {
surveyTemplateQuestions = data.template.questions;
renderCreateSurveyQuestions(data.template.questions);
}
} catch (e) {
console.error('Failed to load survey template:', e);
}
}
function renderCreateSurveyQuestions(questions) {
const container = document.getElementById('createSurveyQuestions');
if (!container) return;
createSurveyResponses = {};
container.innerHTML = questions.map(q => {
let inputHtml = '';
switch(q.type) {
case 'text':
inputHtml = `<input type="text" id="q_${q.id}" placeholder="${q.placeholder || ''}" onchange="saveCreateResponse('${q.id}', this.value)">`;
break;
case 'number':
inputHtml = `<input type="number" id="q_${q.id}" placeholder="${q.placeholder || ''}" onchange="saveCreateResponse('${q.id}', this.value)">`;
break;
case 'select':
inputHtml = `
<select id="q_${q.id}" onchange="saveCreateResponse('${q.id}', this.value)">
<option value="">Select...</option>
${q.options.map(opt => `<option value="${opt}">${opt}</option>`).join('')}
</select>
`;
break;
case 'multiselect':
inputHtml = `
<div class="checkbox-group">
${q.options.map(opt => `
<label class="checkbox-item">
<input type="checkbox" value="${opt}" onchange="saveCreateMultiResponse('${q.id}', '${opt}', this.checked)">
${opt}
</label>
`).join('')}
</div>
`;
break;
case 'textarea':
inputHtml = `<textarea id="q_${q.id}" placeholder="${q.placeholder || ''}" onchange="saveCreateResponse('${q.id}', this.value)"></textarea>`;
break;
}
return `
<div class="question-card">
<div class="category">${q.category}</div>
<div class="question-text">${q.question}</div>
${inputHtml}
</div>
`;
}).join('');
}
function saveCreateResponse(questionId, value) {
createSurveyResponses[questionId] = value;
}
function saveCreateMultiResponse(questionId, option, checked) {
if (!createSurveyResponses[questionId]) {
createSurveyResponses[questionId] = [];
}
if (checked) {
if (!createSurveyResponses[questionId].includes(option)) {
createSurveyResponses[questionId].push(option);
}
} else {
createSurveyResponses[questionId] = createSurveyResponses[questionId].filter(o => o !== option);
}
}
async function loadSurveys() {
try {
const response = await fetch('/api/surveys');
const data = await response.json();
surveys = data.surveys;
renderSurveyList();
} catch (e) {
document.getElementById('surveyList').innerHTML = '<div class="alert error">Failed to load surveys</div>';
}
}
// UPDATED: renderSurveyList with delete functionality
function renderSurveyList() {
const container = document.getElementById('surveyList');
if (surveys.length === 0) {
container.innerHTML = `
<div class="alert info">No surveys yet. Create your first site survey!</div>
<button class="btn btn-primary" onclick="switchTab('create')" style="margin-top: 15px;">Create Survey</button>
`;
return;
}
// Add Clear All button at top
let html = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<span style="color: var(--text-2);">${surveys.length} survey${surveys.length !== 1 ? 's' : ''}</span>
<button class="btn btn-danger" onclick="confirmClearAll()" style="font-size: 0.85rem;">
🗑️ Clear All
</button>
</div>
`;
html += surveys.map(s => `
<div class="survey-item" style="position: relative; justify-content: space-between;">
<div onclick="loadSurvey('${s.id}')" style="flex: 1; cursor: pointer;">
<div class="survey-item-info">
<h3>${s.name}</h3>
<p>${s.client_name} - ${s.site_name}</p>
</div>
<div class="survey-item-meta">
<div>${s.responses?.length || 0} responses</div>
<div style="font-size: 0.75rem; color: var(--text-2);">${new Date(s.created_at).toLocaleDateString()}</div>
</div>
</div>
<button class="btn btn-danger" onclick="event.stopPropagation(); deleteSurvey('${s.id}')"
style="margin-left: 15px; font-size: 0.85rem; padding: 8px 12px;">
🗑️ Delete
</button>
</div>
`).join('');
container.innerHTML = html;
}
// Delete confirmation
async function deleteSurvey(surveyId) {
if (!confirm("Are you sure you want to delete this survey?\n\nThis action cannot be undone.")) return;
try {
const response = await fetch(`/api/surveys/${surveyId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
alert('Survey deleted successfully');
loadSurveys();
if (currentSurveyId === surveyId) {
currentSurveyId = null;
currentSurvey = null;
document.getElementById('takeSurveyTab').style.display = 'none';
document.getElementById('analyzeTab').style.display = 'none';
}
} else {
alert(data.error || 'Failed to delete survey');
}
} catch (e) {
console.error('Delete failed:', e);
alert('Failed to delete survey');
}
}
// Clear all surveys
async function confirmClearAll() {
if (!confirm(`Are you sure you want to delete ALL ${surveys.length} surveys?\n\nThis action cannot be undone.`)) return;
try {
const response = await fetch('/api/surveys', {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
alert(data.message);
currentSurveyId = null;
currentSurvey = null;
document.getElementById('takeSurveyTab').style.display = 'none';
document.getElementById('analyzeTab').style.display = 'none';
loadSurveys();
} else {
alert(data.error || 'Failed to clear surveys');
}
} catch (e) {
console.error('Clear all failed:', e);
alert('Failed to clear surveys');
}
}
async function loadTemplate() {
try {
const response = await fetch('/api/surveys/template');
const data = await response.json();
} catch (e) {
console.error('Failed to load template:', e);
}
}
async function createSurvey() {
const name = document.getElementById('surveyName').value;
const clientName = document.getElementById('clientName').value;
const siteName = document.getElementById('siteName').value;
const description = document.getElementById('surveyDescription').value;
if (!name || !clientName || !siteName) {
alert('Please fill in all required fields');
return;
}
// Check if we have question responses
const hasResponses = Object.keys(createSurveyResponses).length > 0;
try {
const response = await fetch('/api/surveys', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name,
client_name: clientName,
site_name: siteName,
description,
responses: hasResponses ? createSurveyResponses : undefined
})
});
const data = await response.json();
document.getElementById('surveyName').value = '';
document.getElementById('clientName').value = '';
document.getElementById('siteName').value = '';
document.getElementById('surveyDescription').value = '';
await loadSurveys();
loadSurvey(data.survey.id);
} catch (e) {
alert('Failed to create survey');
}
}
async function loadSurvey(surveyId) {
try {
const response = await fetch(`/api/surveys/${surveyId}`);
const data = await response.json();
currentSurvey = data.survey;
currentSurveyId = surveyId;
// Get latest response photos if any
if (currentSurvey.responses && currentSurvey.responses.length > 0) {
const lastResponse = currentSurvey.responses[currentSurvey.responses.length - 1];
currentPhotos = lastResponse.photos || [];
renderAttachedPhotos();
} else {
currentPhotos = [];
renderAttachedPhotos();
}
document.getElementById('currentSurveyName').textContent = currentSurvey.name;
renderQuestions(currentSurvey.questions);
// Clear photo state for new survey load
selectedPhotos = [];
renderPhotoPreviews();
// Switch to take survey tab
document.getElementById('takeSurveyTab').style.display = 'inline-block';
document.getElementById('analyzeTab').style.display = 'inline-block';
switchTab('take');
} catch (e) {
alert('Failed to load survey');
}
}
function renderQuestions(questions) {
const container = document.getElementById('surveyQuestions');
surveyResponses = {};
container.innerHTML = questions.map(q => {
let inputHtml = '';
switch(q.type) {
case 'text':
inputHtml = `<input type="text" id="q_${q.id}" placeholder="${q.placeholder || ''}" onchange="saveResponse('${q.id}', this.value)">`;
break;
case 'number':
inputHtml = `<input type="number" id="q_${q.id}" placeholder="${q.placeholder || ''}" onchange="saveResponse('${q.id}', this.value)">`;
break;
case 'select':
inputHtml = `
<select id="q_${q.id}" onchange="saveResponse('${q.id}', this.value)">
<option value="">Select...</option>
${q.options.map(opt => `<option value="${opt}">${opt}</option>`).join('')}
</select>
`;
break;
case 'multiselect':
inputHtml = `
<div class="checkbox-group">
${q.options.map(opt => `
<label class="checkbox-item">
<input type="checkbox" value="${opt}" onchange="saveMultiResponse('${q.id}', '${opt}', this.checked)">
${opt}
</label>
`).join('')}
</div>
`;
break;
case 'textarea':
inputHtml = `<textarea id="q_${q.id}" placeholder="${q.placeholder || ''}" onchange="saveResponse('${q.id}', this.value)"></textarea>`;
break;
}
return `
<div class="question-card">
<div class="category">${q.category}</div>
<div class="question-text">${q.question}</div>
${inputHtml}
</div>
`;
}).join('');
}
function saveResponse(questionId, value) {
surveyResponses[questionId] = value;
}
function saveMultiResponse(questionId, option, checked) {
if (!surveyResponses[questionId]) {
surveyResponses[questionId] = [];
}
if (checked) {
if (!surveyResponses[questionId].includes(option)) {
surveyResponses[questionId].push(option);
}
} else {
surveyResponses[questionId] = surveyResponses[questionId].filter(o => o !== option);
}
}
// ===== PHOTO HANDLING =====
function setupPhotoDropzone() {
const area = document.getElementById('photoUploadArea');
area.addEventListener('dragover', (e) => {
e.preventDefault();
area.classList.add('dragover');
});
area.addEventListener('dragleave', () => {
area.classList.remove('dragover');
});
area.addEventListener('drop', (e) => {
e.preventDefault();
area.classList.remove('dragover');
handlePhotoSelect(e.dataTransfer.files);
});
}
function handlePhotoSelect(files) {
if (!files || files.length === 0) return;
Array.from(files).forEach(file => {
if (!file.type.startsWith('image/')) return;
if (selectedPhotos.length >= 20) {
alert('Maximum 20 photos allowed');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
selectedPhotos.push(e.target.result);
renderPhotoPreviews();
};
reader.readAsDataURL(file);
});
// Reset input
document.getElementById('photoInput').value = '';
}
function removePhoto(index) {
selectedPhotos.splice(index, 1);
renderPhotoPreviews();
}
function renderPhotoPreviews() {
const grid = document.getElementById('photoPreviewGrid');
const badge = document.getElementById('photoCountBadge');
if (selectedPhotos.length === 0) {
grid.innerHTML = '';
badge.textContent = '0 photos';
badge.classList.remove('has-photos');
return;
}
badge.textContent = `${selectedPhotos.length} photo${selectedPhotos.length !== 1 ? 's' : ''}`;
badge.classList.add('has-photos');
grid.innerHTML = selectedPhotos.map((data, i) => `
<div class="photo-preview-item">
<img src="${data}" alt="Photo ${i + 1}">
<button class="remove-btn" onclick="removePhoto(${i})">&times;</button>
</div>
`).join('');
}
function renderAttachedPhotos() {
const section = document.getElementById('attachedPhotosSection');
const grid = document.getElementById('attachedPhotoGrid');
const countSpan = document.getElementById('attachedPhotoCount');
if (!currentPhotos || currentPhotos.length === 0) {
section.style.display = 'none';
return;
}
countSpan.textContent = currentPhotos.length;
section.style.display = 'block';
grid.innerHTML = currentPhotos.map((photo, i) => `
<div class="attached-photo-item" onclick="openLightbox('${currentSurveyId}', '${photo}')">
<img src="/api/photos/${currentSurveyId}/${photo}" alt="Photo ${i + 1}" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect fill=%22%2321262d%22 width=%22100%22 height=%22100%22/><text x=%2250%22 y=%2255%22 text-anchor=%22middle%22 fill=%22%2364748b%22 font-size=%2240%22>📷</text></svg>'">
<div class="photo-label">Photo ${i + 1}</div>
</div>
`).join('');
}
function openLightbox(surveyId, filename) {
const img = document.getElementById('lightboxImg');
img.src = `/api/photos/${surveyId}/${filename}`;
document.getElementById('photoLightbox').classList.add('active');
}
function closeLightbox() {
document.getElementById('photoLightbox').classList.remove('active');
}
// ===== SUBMIT SURVEY =====
async function submitSurvey() {
const submitterName = document.getElementById('submitterName').value;
const selectedModel = document.getElementById('submitModelSelect').value || defaultModel;
if (!submitterName) {
alert('Please enter your name');
return;
}
if (Object.keys(surveyResponses).length === 0 && selectedPhotos.length === 0) {
alert('Please answer at least some questions or attach photos');
return;
}
try {
const response = await fetch(`/api/surveys/${currentSurveyId}/responses`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
submitted_by: submitterName,
responses: surveyResponses,
model: selectedModel,
photos: selectedPhotos
})
});
const data = await response.json();
// Update currentPhotos for display in analysis tab
currentPhotos = data.response.photos || [];
alert('Survey submitted successfully!');
renderAttachedPhotos();
document.getElementById('analyzeTab').style.display = 'inline-block';
switchTab('analyze');
} catch (e) {
alert('Failed to submit survey: ' + e.message);
}
}
// ===== AI ANALYSIS =====
async function runAIAnalysis() {
const btn = document.querySelector('#analyzeContent .btn-primary');
const spinner = document.getElementById('analyzeSpinner');
const responseEl = document.getElementById('aiResponse');
const selectedModel = document.getElementById('analyzeModelSelect').value || defaultModel;
btn.disabled = true;
spinner.style.display = 'inline-block';
responseEl.style.display = 'block';
responseEl.className = 'ai-response streaming';
responseEl.textContent = '';
try {
const response = await fetch(`/api/surveys/${currentSurveyId}/analyze`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
model: selectedModel
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const {done, value} = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(l => l.trim());
for (const line of lines) {
try {
const data = JSON.parse(line);
if (data.response) {
responseEl.textContent += data.response;
responseEl.scrollTop = responseEl.scrollHeight;
}
} catch (e) {}
}
}
responseEl.classList.remove('streaming');
document.getElementById('exportButtons').style.display = 'block';
document.getElementById('chatInterface').style.display = 'block';
updateEmailLink();
} catch (e) {
responseEl.textContent = 'Error: ' + e.message;
responseEl.classList.remove('streaming');
} finally {
btn.disabled = false;
spinner.style.display = 'none';
}
}
// ===== EXPORT FUNCTIONS =====
async function exportToPDF() {
if (!currentSurveyId) {
alert('Please generate recommendations first');
return;
}
try {
const response = await fetch(`/api/surveys/${currentSurveyId}/export/pdf`, {
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `Site_Survey_Report_${new Date().toISOString().slice(0,10)}.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
a.remove();
} else {
alert('Failed to generate PDF');
}
} catch (e) {
console.error('PDF export error:', e);
alert('Error generating PDF');
}
}
async function exportToText() {
const responseEl = document.getElementById('aiResponse');
if (!responseEl || !responseEl.textContent) {
alert('Please generate recommendations first');
return;
}
// Get the AI response text
const text = responseEl.textContent;
// Try to copy to clipboard
try {
await navigator.clipboard.writeText(text);
alert('Report copied to clipboard!');
} catch (clipboardErr) {
// Fallback: show in modal
showExportModal('Text Report', text);
}
}
async function exportToEmail() {
if (!currentSurveyId) {
alert('Please generate recommendations first');
return;
}
try {
const response = await fetch(`/api/surveys/${currentSurveyId}/export/email`, {
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
if (response.ok) {
const data = await response.json();
// Try to copy to clipboard, fallback to showing text
try {
await navigator.clipboard.writeText(data.email_body);
alert('Email body copied to clipboard!\n\nYou can paste it into your email client.');
} catch (clipboardErr) {
// Fallback: show in a textarea for manual copy
showExportModal('Email Format', data.email_body);
}
} else {
const errorData = await response.json();
alert('Failed to generate email: ' + (errorData.error || 'Unknown error'));
}
} catch (e) {
console.error('Email export error:', e);
alert('Error generating email format: ' + e.message);
}
}
// Helper function to show export in modal
function showExportModal(title, content) {
// Create modal if it doesn't exist
let modal = document.getElementById('exportModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'exportModal';
modal.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; display: flex; align-items: center; justify-content: center;';
modal.innerHTML = `
<div style="background: var(--bg-2); padding: 20px; border-radius: var(--radius); max-width: 90%; max-height: 90%; overflow: auto;">
<h3 id="exportModalTitle"></h3>
<textarea id="exportModalText" style="width: 100%; min-height: 300px; margin: 10px 0; font-family: monospace;"></textarea>
<div style="display: flex; gap: 10px; margin-top: 10px;">
<button class="btn" style="background: var(--danger); color: #fff;" onclick="document.getElementById('exportModal').style.display='none'">Close</button>
<button class="btn" style="background: var(--success); color: #fff;" onclick="copyExportText()">Copy</button>
</div>
</div>
`;
document.body.appendChild(modal);
}
document.getElementById('exportModalTitle').textContent = title;
document.getElementById('exportModalText').value = content;
modal.style.display = 'flex';
}
function copyExportText() {
const textarea = document.getElementById('exportModalText');
textarea.select();
document.execCommand('copy');
alert('Copied to clipboard!');
}
async function sendChatMessage() {
const input = document.getElementById('chatInput');
const messages = document.getElementById('chatMessages');
const question = input.value.trim();
if (!question) return;
// Add user message
messages.innerHTML += `<div style="margin: 5px 0; padding: 8px; background: var(--bg-2); border-radius: var(--radius);"><strong>You:</strong> ${question}</div>`;
input.value = '';
messages.scrollTop = messages.scrollHeight;
// Get AI response
try {
const response = await fetch(`/api/surveys/${currentSurveyId}/analyze`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
model: document.getElementById('analyzeModelSelect').value || defaultModel,
question: question
})
});
// Read streaming response
const reader = response.body.getReader();
const decoder = new TextDecoder();
let aiResponse = '';
while (true) {
const {done, value} = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(l => l.trim());
for (const line of lines) {
try {
const data = JSON.parse(line);
if (data.response) aiResponse += data.response;
} catch (e) {}
}
}
// Add AI response
messages.innerHTML += `<div style="margin: 5px 0; padding: 8px; background: var(--primary); color: white; border-radius: var(--radius);"><strong>AI:</strong> ${aiResponse || 'I need more context about your question.'}</div>`;
messages.scrollTop = messages.scrollHeight;
} catch (e) {
messages.innerHTML += `<div style="margin: 5px 0; padding: 8px; background: var(--bg-2); color: var(--text-2); border-radius: var(--radius);">Error: Could not get response</div>`;
}
}
// ===== SAMPLE DATA FUNCTION =====
// Global variable to store survey template questions
let surveyTemplateQuestions = [];
let createSurveyResponses = {};
function loadSampleData() {
if (!confirm('This will fill the survey with sample/test data. Any existing entries will be replaced. Continue?')) {
return;
}
console.log('Loading sample data...');
// Set survey metadata
document.getElementById('surveyName').value = 'Sample Corporate Office Survey';
document.getElementById('clientName').value = 'Acme Corporation';
document.getElementById('siteName').value = 'Main Office - Downtown';
document.getElementById('surveyDescription').value = 'Comprehensive network infrastructure assessment for downtown office location';
// Sample data for all questions (keyed by question number like 'q1', 'q2')
const sampleValues = {
'q1': '3500',
'q2': '2',
'q3': 'Single-story office building with open floor plan, 8 private offices, 2 conference rooms, break room, and reception area. Approximately 60 workstations in cubicle area.',
'q4': ['Concrete walls'],
'q5': '2',
'q6': 'MDF located in basement utility room. IDF on first floor in IT storage closet. Both have adequate ventilation but limited space.',
'q7': '2x 20A circuits per closet, UPS backup planned for MDF',
'q8': 'Yes, partial racks',
'q9': 'Cat5e (100Mb-1GbE)',
'q10': '120',
'q11': 'Average 150 feet, longest run approximately 280 feet to corner offices',
'q12': ['Plenum-rated cable required'],
'q13': ['Router/Firewall', 'Unmanaged switches', 'Wireless access points'],
'q14': 'Managed (Layer 2)',
'q15': '6',
'q16': '1800W estimated for all network equipment including PoE devices',
'q17': ['IP Phones', 'Wireless APs', 'IP Cameras'],
'q18': 'Limited backup (1-2 hours)',
'q19': '45',
'q20': '85',
'q21': ['Email/Web Browsing', 'Video conferencing', 'File sharing', 'Cloud applications', 'Remote desktop'],
'q22': ['SOC 2'],
'q23': ['Corporate network', 'Guest network', 'IoT network', 'Management network'],
'q24': ['Firewall', 'VPN access', 'Network access control (NAC)'],
'q25': ['Fiber', '5G/Cellular backup'],
'q26': '500Mbps download / 250Mbps upload',
'q27': 'No - Single site only',
'q28': 'Yes - Partial upgrade',
'q29': 'Current WiFi has dead zones in conference rooms. Existing switches are unmanaged and showing signs of age. Need better coverage and management capabilities.',
'q30': ['Slow internet', 'WiFi dead zones', 'Network congestion'],
'q31': '$25,000 - $50,000',
'q32': 'Within 3 months',
'q33': 'Internal IT team',
'q34': '',
'q35': 'Need outdoor WiFi coverage for patio area. Plan to add IP cameras in future. Prefer Cisco or Ubiquiti equipment. Require weekend installation to minimize downtime.'
};
let filled = 0;
let notFound = [];
// Fill all fields by looking for elements with IDs matching q_q1, q_q2, etc.
Object.keys(sampleValues).forEach(qNum => {
const fieldId = 'q_' + qNum; // e.g., q_q1
const field = document.getElementById(fieldId);
if (field) {
const value = sampleValues[qNum];
if (Array.isArray(value)) {
// For multiselect, find checkboxes for this question
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
let checkedCount = 0;
checkboxes.forEach(cb => {
const onchange = cb.getAttribute('onchange') || '';
if (onchange.includes("'" + qNum + "'") && value.includes(cb.value)) {
cb.checked = true;
// Call saveMultiResponse directly
saveMultiResponse(qNum, cb.value, true);
checkedCount++;
}
});
if (checkedCount > 0) filled++;
} else {
field.value = value;
// Call saveResponse directly
saveResponse(qNum, value);
filled++;
}
} else {
notFound.push(fieldId);
}
});
console.log('Filled ' + filled + ' fields');
console.log('Not found:', notFound);
alert('Sample data loaded! Filled ' + filled + ' fields. You can review and modify before submitting.');
}
function updateEmailLink() {
if (!currentSurvey || !currentSurveyId) return;
const survey = currentSurvey;
const subject = encodeURIComponent(`IT Site Survey: ${survey['name'] || 'Survey'} - ${survey['client_name'] || 'Client'}`);
let body = `Site Survey Results\n\n`;
body += `Client: ${survey['client_name'] || 'N/A'}\n`;
body += `Site: ${survey['site_name'] || 'N/A'}\n`;
body += `Date: ${new Date().toLocaleDateString()}\n\n`;
if (survey['responses'] && survey['responses'].length > 0) {
const lastResponse = survey['responses'][survey['responses'].length - 1];
body += `Survey Responses:\n`;
for (const q of survey['questions'] || []) {
const answer = (lastResponse['responses'] || {})[q['id']] || 'Not answered';
body += `\n${q['category']} - ${q['question']}\n`;
body += `Answer: ${answer}\n`;
}
}
const emailLink = document.getElementById('emailLink');
if (emailLink) {
emailLink.href = `mailto:?subject=${subject}&body=${encodeURIComponent(body)}`;
}
}
// Delete confirmation dialogs
function confirmDelete(surveyId, surveyName) {
if (confirm(`Are you sure you want to delete "${surveyName}"?\n\nThis action cannot be undone.`)) {
deleteSurvey(surveyId);
}
}
function confirmClearAll() {
if (confirm(`Are you sure you want to delete ALL ${surveys.length} surveys?\n\nThis action cannot be undone.`)) {
clearAllSurveys();
}
}
// Notification helper
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 8px;
color: white;
font-weight: 500;
z-index: 1000;
animation: slideIn 0.3s ease;
background: ${type === 'success' ? '#3fb950' : type === 'error' ? '#f85149' : '#58a6ff'};
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Initialize on page load
loadSurveyTemplate();
loadSurveys();
loadModels();
</script>
</body>
</html>