Files

1069 lines
42 KiB
HTML
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.
<!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>