1069 lines
42 KiB
HTML
1069 lines
42 KiB
HTML
<!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</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: 1.5rem;
|
||
}
|
||
header h1 { font-size: 1.25rem; font-weight: 600; }
|
||
header p { font-size: 0.85rem; color: var(--text-2); }
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 5px;
|
||
margin-bottom: 25px;
|
||
border-bottom: 1px solid var(--border);
|
||
padding-bottom: 10px;
|
||
overflow-x: auto;
|
||
}
|
||
.tab {
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--text-2);
|
||
padding: 10px 20px;
|
||
border-radius: var(--radius);
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
white-space: nowrap;
|
||
transition: all 0.15s;
|
||
}
|
||
.tab:hover { background: var(--bg-3); color: var(--text); }
|
||
.tab.active { background: var(--accent); color: #fff; font-weight: 500; }
|
||
|
||
.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;
|
||
}
|
||
.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;
|
||
font-weight: 500;
|
||
}
|
||
.form-group input,
|
||
.form-group select,
|
||
.form-group textarea {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
color: var(--text);
|
||
font-size: 0.9rem;
|
||
}
|
||
.form-group input:focus,
|
||
.form-group select:focus,
|
||
.form-group textarea:focus {
|
||
outline: none;
|
||
border-color: var(--accent);
|
||
}
|
||
.form-group textarea {
|
||
min-height: 80px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
color: #fff;
|
||
background: var(--accent);
|
||
}
|
||
.btn:hover { opacity: 0.9; }
|
||
.btn-success { background: var(--success); }
|
||
.btn-danger { background: var(--danger); }
|
||
.btn-primary { background: var(--accent); }
|
||
|
||
.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: 5px;
|
||
}
|
||
.question-card .question-text {
|
||
font-weight: 500;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.checkbox-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.checkbox-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
cursor: pointer;
|
||
}
|
||
.checkbox-item input[type="checkbox"] {
|
||
width: auto;
|
||
}
|
||
|
||
.photo-upload-area {
|
||
border: 2px dashed var(--border);
|
||
border-radius: var(--radius);
|
||
padding: 30px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
}
|
||
.photo-upload-area:hover {
|
||
border-color: var(--accent);
|
||
background: var(--bg-3);
|
||
}
|
||
.photo-upload-area input[type="file"] {
|
||
display: none;
|
||
}
|
||
.photo-preview-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||
gap: 10px;
|
||
margin-bottom: 15px;
|
||
}
|
||
.photo-preview {
|
||
aspect-ratio: 1;
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
.photo-preview img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.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;
|
||
margin-bottom: 10px;
|
||
}
|
||
.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;
|
||
}
|
||
|
||
.actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.alert {
|
||
padding: 15px 20px;
|
||
border-radius: var(--radius);
|
||
margin-bottom: 15px;
|
||
}
|
||
.alert.info {
|
||
background: rgba(88, 166, 255, 0.1);
|
||
border: 1px solid rgba(88, 166, 255, 0.3);
|
||
}
|
||
.alert.success {
|
||
background: rgba(63, 185, 80, 0.1);
|
||
border: 1px solid rgba(63, 185, 80, 0.3);
|
||
}
|
||
.alert.error {
|
||
background: rgba(248, 81, 73, 0.1);
|
||
border: 1px solid rgba(248, 81, 73, 0.3);
|
||
}
|
||
|
||
.spinner {
|
||
display: inline-block;
|
||
width: 16px;
|
||
height: 16px;
|
||
border: 2px solid rgba(255,255,255,0.3);
|
||
border-top-color: #fff;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<header>
|
||
<div class="logo-area">
|
||
<div class="logo">📊</div>
|
||
<div>
|
||
<h1>IT Site Survey AI</h1>
|
||
<p>Network Infrastructure Planning & Analysis</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>
|
||
<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">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<h2>📋 Site Surveys</h2>
|
||
<button class="btn btn-danger" onclick="confirmClearAll()">🗑️ Clear All</button>
|
||
</div>
|
||
<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 btn-success" onclick="loadSampleData()" style="margin-bottom: 20px;">📋 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 class="form-group">
|
||
<label>🤖 AI Model for Analysis</label>
|
||
<select id="createModelSelect">
|
||
<option value="">Loading models...</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div id="surveyQuestions"></div>
|
||
|
||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border);">
|
||
<h3 style="font-size: 1rem; margin-bottom: 15px;">📷 Site Photos</h3>
|
||
<div class="photo-preview-grid" id="photoPreviewGrid"></div>
|
||
<div class="photo-upload-area" onclick="document.getElementById('photoInput').click()">
|
||
<input type="file" id="photoInput" accept="image/*" multiple onchange="handlePhotoSelect(this.files)">
|
||
<div style="font-size: 2rem; margin-bottom: 10px;">📷</div>
|
||
<div><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: 20px;">
|
||
<label>Your Name</label>
|
||
<input type="text" id="submitterName" placeholder="Enter your name">
|
||
</div>
|
||
|
||
<div class="actions">
|
||
<button class="btn btn-success" onclick="submitSurvey()">Submit Survey</button>
|
||
<button class="btn btn-primary" onclick="switchTab('surveys')">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Analysis Tab -->
|
||
<div id="analyzeTab" 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>
|
||
|
||
<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="analysisResult" style="display: none;">
|
||
<h3>Analysis Results</h3>
|
||
<div id="analysisContent" style="white-space: pre-wrap; background: var(--bg); padding: 20px; border-radius: var(--radius); border: 1px solid var(--border); margin-top: 10px;"></div>
|
||
</div>
|
||
|
||
<div id="analysisExport" style="display: none; margin-top: 20px;">
|
||
<h3>Export Results</h3>
|
||
<div class="actions">
|
||
<button class="btn" onclick="exportResults('pdf')">📄 Export PDF</button>
|
||
<button class="btn" onclick="exportResults('text')">📝 Export Text</button>
|
||
<button class="btn" onclick="exportResults('email')">📧 Email Results</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chat Interface -->
|
||
<div id="chatInterface" style="margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border); display: none;">
|
||
<h3>💬 Ask AI About This Survey</h3>
|
||
<div id="chatMessages" style="background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 15px; margin-bottom: 15px; max-height: 300px; overflow-y: auto;">
|
||
<div style="color: var(--text-2); text-align: center; padding: 20px;">Click "Generate Recommendations" first to enable chat</div>
|
||
</div>
|
||
<div style="display: flex; gap: 10px;">
|
||
<input type="text" id="chatInput" placeholder="Ask a question about the network infrastructure..." style="flex: 1;">
|
||
<button class="btn btn-primary" onclick="sendChatMessage()">Send</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// State
|
||
let surveys = [];
|
||
let currentSurvey = null;
|
||
let currentSurveyId = null;
|
||
let surveyResponses = {};
|
||
let selectedPhotos = [];
|
||
let availableModels = [];
|
||
let defaultModel = 'llava:latest';
|
||
|
||
// Sample data
|
||
const SAMPLE_DATA = {
|
||
'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.'
|
||
};
|
||
|
||
// Initialize
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadSurveys();
|
||
loadModels();
|
||
loadSurveyTemplate();
|
||
});
|
||
|
||
// Tab switching
|
||
function switchTab(tabName) {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||
|
||
document.querySelectorAll('.tab').forEach(btn => {
|
||
if (btn.getAttribute('onclick')?.includes(`'${tabName}'`)) {
|
||
btn.classList.add('active');
|
||
}
|
||
});
|
||
|
||
const contentEl = document.getElementById(tabName + 'Tab');
|
||
if (contentEl) {
|
||
contentEl.classList.add('active');
|
||
}
|
||
}
|
||
|
||
// Load survey template and render questions
|
||
async function loadSurveyTemplate() {
|
||
try {
|
||
const response = await fetch('/api/surveys/template');
|
||
const data = await response.json();
|
||
if (data.template && data.template.questions) {
|
||
renderQuestions(data.template.questions);
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to load survey template:', e);
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
// Load sample data
|
||
function loadSampleData() {
|
||
if (!confirm('This will fill the survey with sample/test data. Any existing entries will be replaced. Continue?')) {
|
||
return;
|
||
}
|
||
|
||
// Fill 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';
|
||
|
||
// Fill questions
|
||
Object.keys(SAMPLE_DATA).forEach(qId => {
|
||
const field = document.getElementById('q_' + qId);
|
||
if (field) {
|
||
const value = SAMPLE_DATA[qId];
|
||
if (Array.isArray(value)) {
|
||
// Handle checkboxes
|
||
const checkboxes = document.querySelectorAll(`input[type="checkbox"]`);
|
||
checkboxes.forEach(cb => {
|
||
const onchange = cb.getAttribute('onchange') || '';
|
||
if (onchange.includes(`'${qId}'`) && value.includes(cb.value)) {
|
||
cb.checked = true;
|
||
saveMultiResponse(qId, cb.value, true);
|
||
}
|
||
});
|
||
} else {
|
||
field.value = value;
|
||
saveResponse(qId, value);
|
||
}
|
||
}
|
||
});
|
||
|
||
alert('Sample data loaded! Review and modify as needed, then click Submit Survey.');
|
||
}
|
||
|
||
// Photo handling
|
||
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({
|
||
file: file,
|
||
dataUrl: e.target.result
|
||
});
|
||
renderPhotoPreviews();
|
||
};
|
||
reader.readAsDataURL(file);
|
||
});
|
||
}
|
||
|
||
function renderPhotoPreviews() {
|
||
const grid = document.getElementById('photoPreviewGrid');
|
||
grid.innerHTML = selectedPhotos.map((photo, i) => `
|
||
<div class="photo-preview">
|
||
<img src="${photo.dataUrl}" alt="Photo ${i + 1}">
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// Submit survey
|
||
async function submitSurvey() {
|
||
const name = document.getElementById('surveyName').value;
|
||
const clientName = document.getElementById('clientName').value;
|
||
const siteName = document.getElementById('siteName').value;
|
||
const description = document.getElementById('surveyDescription').value;
|
||
const submitterName = document.getElementById('submitterName').value;
|
||
|
||
if (!name || !clientName || !siteName) {
|
||
alert('Please fill in all required fields (Name, Client, Site)');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Create survey
|
||
const createResponse = await fetch('/api/surveys', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
name,
|
||
client_name: clientName,
|
||
site_name: siteName,
|
||
description,
|
||
responses: surveyResponses
|
||
})
|
||
});
|
||
|
||
const createData = await createResponse.json();
|
||
const surveyId = createData.survey.id;
|
||
|
||
// Upload photos if any
|
||
if (selectedPhotos.length > 0) {
|
||
for (const photo of selectedPhotos) {
|
||
const formData = new FormData();
|
||
formData.append('photo', photo.file);
|
||
await fetch(`/api/surveys/${surveyId}/photos`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
}
|
||
}
|
||
|
||
// Clear form
|
||
document.getElementById('surveyName').value = '';
|
||
document.getElementById('clientName').value = '';
|
||
document.getElementById('siteName').value = '';
|
||
document.getElementById('surveyDescription').value = '';
|
||
document.getElementById('submitterName').value = '';
|
||
surveyResponses = {};
|
||
selectedPhotos = [];
|
||
renderPhotoPreviews();
|
||
|
||
// Reset questions
|
||
document.querySelectorAll('[id^="q_"]').forEach(field => {
|
||
field.value = '';
|
||
if (field.type === 'checkbox') {
|
||
field.checked = false;
|
||
}
|
||
});
|
||
|
||
alert('Survey submitted successfully!');
|
||
switchTab('surveys');
|
||
loadSurveys();
|
||
|
||
} catch (e) {
|
||
console.error('Error submitting survey:', e);
|
||
alert('Failed to submit survey: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// Load surveys list
|
||
async function loadSurveys() {
|
||
try {
|
||
const response = await fetch('/api/surveys');
|
||
const data = await response.json();
|
||
surveys = data.surveys || [];
|
||
renderSurveyList();
|
||
} catch (e) {
|
||
console.error('Failed to load surveys:', e);
|
||
document.getElementById('surveyList').innerHTML = '<div class="alert error">Failed to load surveys</div>';
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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>
|
||
</div>
|
||
`;
|
||
|
||
html += surveys.map(s => `
|
||
<div class="survey-item" style="position: relative;">
|
||
<div onclick="viewSurvey('${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_count || 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;
|
||
}
|
||
|
||
// View survey (load for analysis)
|
||
async function viewSurvey(surveyId) {
|
||
try {
|
||
const response = await fetch(`/api/surveys/${surveyId}`);
|
||
const data = await response.json();
|
||
currentSurvey = data.survey;
|
||
currentSurveyId = surveyId;
|
||
|
||
// Show analyze tab
|
||
document.getElementById('analyzeTab').style.display = 'inline-block';
|
||
switchTab('analyze');
|
||
|
||
} catch (e) {
|
||
console.error('Error loading survey:', e);
|
||
alert('Failed to load survey');
|
||
}
|
||
}
|
||
|
||
// Delete survey
|
||
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) {
|
||
loadSurveys();
|
||
if (currentSurveyId === surveyId) {
|
||
currentSurveyId = null;
|
||
currentSurvey = null;
|
||
document.getElementById('analyzeTab').style.display = 'none';
|
||
}
|
||
} else {
|
||
alert(data.error || 'Failed to delete survey');
|
||
}
|
||
} catch (e) {
|
||
console.error('Error deleting survey:', e);
|
||
alert('Failed to delete survey');
|
||
}
|
||
}
|
||
|
||
// Clear all surveys
|
||
async function clearAllSurveys() {
|
||
try {
|
||
const response = await fetch('/api/surveys', {
|
||
method: 'DELETE'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
loadSurveys();
|
||
currentSurveyId = null;
|
||
currentSurvey = null;
|
||
document.getElementById('analyzeTab').style.display = 'none';
|
||
} else {
|
||
alert(data.error || 'Failed to clear surveys');
|
||
}
|
||
} catch (e) {
|
||
console.error('Error clearing surveys:', e);
|
||
alert('Failed to clear surveys');
|
||
}
|
||
}
|
||
|
||
function confirmClearAll() {
|
||
if (confirm(`Are you sure you want to delete ALL ${surveys.length} surveys?\n\nThis action cannot be undone.`)) {
|
||
clearAllSurveys();
|
||
}
|
||
}
|
||
|
||
// AI Analysis
|
||
async function runAIAnalysis() {
|
||
if (!currentSurveyId) {
|
||
alert('Please select a survey first');
|
||
return;
|
||
}
|
||
|
||
const modelSelect = document.getElementById('analyzeModelSelect');
|
||
const selectedModel = modelSelect.value || defaultModel;
|
||
|
||
document.getElementById('analyzeSpinner').style.display = 'inline-block';
|
||
|
||
try {
|
||
const response = await fetch(`/api/surveys/${currentSurveyId}/analyze`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({model: selectedModel})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.analysis) {
|
||
document.getElementById('analysisResult').style.display = 'block';
|
||
document.getElementById('analysisContent').textContent = data.analysis;
|
||
document.getElementById('analysisExport').style.display = 'block';
|
||
} else {
|
||
alert(data.error || 'Analysis failed');
|
||
}
|
||
|
||
} catch (e) {
|
||
console.error('Analysis error:', e);
|
||
alert('Failed to run analysis');
|
||
} finally {
|
||
document.getElementById('analyzeSpinner').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Export results
|
||
async function exportResults(format) {
|
||
if (!currentSurveyId) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/surveys/${currentSurveyId}/export`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({format})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (format === 'text' && data.text) {
|
||
// Show text in modal
|
||
alert('Text export:\n\n' + data.text.substring(0, 500) + '...');
|
||
} else if (format === 'email') {
|
||
// Open email client
|
||
const subject = encodeURIComponent(`IT Site Survey: ${currentSurvey.name}`);
|
||
const body = encodeURIComponent(data.text || 'Survey results attached');
|
||
window.location.href = `mailto:?subject=${subject}&body=${body}`;
|
||
} else {
|
||
alert('Export functionality coming soon!');
|
||
}
|
||
|
||
} catch (e) {
|
||
console.error('Export error:', e);
|
||
alert('Export failed');
|
||
}
|
||
}
|
||
|
||
// Chat functionality
|
||
let chatHistory = [];
|
||
|
||
async function sendChatMessage() {
|
||
const input = document.getElementById('chatInput');
|
||
const messagesDiv = document.getElementById('chatMessages');
|
||
const message = input.value.trim();
|
||
|
||
if (!message) return;
|
||
if (!currentSurvey) {
|
||
alert('Please select a survey first');
|
||
return;
|
||
}
|
||
|
||
// Add user message
|
||
chatHistory.push({role: 'user', content: message});
|
||
appendChatMessage('You', message, 'user');
|
||
input.value = '';
|
||
|
||
try {
|
||
const modelSelect = document.getElementById('analyzeModelSelect');
|
||
const selectedModel = modelSelect.value || defaultModel;
|
||
|
||
const response = await fetch(`/api/surveys/${currentSurveyId}/chat`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
message: message,
|
||
history: chatHistory,
|
||
model: selectedModel
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.response) {
|
||
chatHistory.push({role: 'assistant', content: data.response});
|
||
appendChatMessage('AI', data.response, 'assistant');
|
||
} else {
|
||
appendChatMessage('Error', data.error || 'Failed to get response', 'error');
|
||
}
|
||
|
||
} catch (e) {
|
||
console.error('Chat error:', e);
|
||
appendChatMessage('Error', 'Failed to send message', 'error');
|
||
}
|
||
}
|
||
|
||
function appendChatMessage(sender, text, type) {
|
||
const messagesDiv = document.getElementById('chatMessages');
|
||
const messageDiv = document.createElement('div');
|
||
messageDiv.style.cssText = `
|
||
margin-bottom: 10px;
|
||
padding: 10px;
|
||
border-radius: var(--radius);
|
||
background: ${type === 'user' ? 'rgba(88, 166, 255, 0.1)' : type === 'error' ? 'rgba(248, 81, 73, 0.1)' : 'var(--bg-3)'};
|
||
border-left: 3px solid ${type === 'user' ? 'var(--accent)' : type === 'error' ? 'var(--danger)' : 'var(--success)'};
|
||
`;
|
||
messageDiv.innerHTML = `
|
||
<div style="font-size: 0.8rem; color: var(--text-2); margin-bottom: 5px;">${sender}</div>
|
||
<div style="white-space: pre-wrap;">${text}</div>
|
||
`;
|
||
messagesDiv.appendChild(messageDiv);
|
||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||
}
|
||
|
||
// Improved AI Analysis with streaming
|
||
async function runAIAnalysis() {
|
||
if (!currentSurveyId) {
|
||
alert('Please select a survey first');
|
||
return;
|
||
}
|
||
|
||
const modelSelect = document.getElementById('analyzeModelSelect');
|
||
const selectedModel = modelSelect.value || defaultModel;
|
||
|
||
document.getElementById('analyzeSpinner').style.display = 'inline-block';
|
||
document.getElementById('analysisResult').style.display = 'block';
|
||
document.getElementById('analysisContent').textContent = 'Generating analysis...';
|
||
|
||
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();
|
||
let analysisText = '';
|
||
|
||
while (true) {
|
||
const {done, value} = await reader.read();
|
||
if (done) break;
|
||
|
||
const chunk = decoder.decode(value);
|
||
const lines = chunk.split('\n');
|
||
|
||
for (const line of lines) {
|
||
if (line.trim()) {
|
||
try {
|
||
const data = JSON.parse(line);
|
||
if (data.response) {
|
||
analysisText += data.response;
|
||
document.getElementById('analysisContent').textContent = analysisText;
|
||
}
|
||
if (data.done) {
|
||
document.getElementById('analysisExport').style.display = 'block';
|
||
document.getElementById('chatInterface').style.display = 'block';
|
||
// Clear chat history for new analysis
|
||
chatHistory = [];
|
||
document.getElementById('chatMessages').innerHTML = '';
|
||
appendChatMessage('AI', 'Analysis complete! You can now ask questions about the recommendations.', 'assistant');
|
||
}
|
||
} catch (e) {
|
||
// Ignore parse errors for incomplete chunks
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
} catch (e) {
|
||
console.error('Analysis error:', e);
|
||
document.getElementById('analysisContent').textContent = 'Error: ' + e.message;
|
||
} finally {
|
||
document.getElementById('analyzeSpinner').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Load models
|
||
async function loadModels() {
|
||
try {
|
||
const resp = await fetch('/api/models');
|
||
const data = await resp.json();
|
||
availableModels = data.models || [];
|
||
defaultModel = data.default || 'llava:latest';
|
||
|
||
const analyzeSelect = document.getElementById('analyzeModelSelect');
|
||
const createSelect = document.getElementById('createModelSelect');
|
||
|
||
const options = availableModels.map(m =>
|
||
`<option value="${m}" ${m === defaultModel ? 'selected' : ''}>${m}</option>`
|
||
).join('');
|
||
|
||
analyzeSelect.innerHTML = options;
|
||
createSelect.innerHTML = options;
|
||
|
||
} catch (e) {
|
||
console.error('Failed to load models:', e);
|
||
}
|
||
}
|
||
|
||
// Handle Enter key in chat
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const chatInput = document.getElementById('chatInput');
|
||
if (chatInput) {
|
||
chatInput.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
sendChatMessage();
|
||
}
|
||
});
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|