1596 lines
63 KiB
HTML
1596 lines
63 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 | 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>
|
||
|
||
<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>
|
||
<button class="btn" onclick="loadSampleData()" style="background: var(--success); color: #fff; margin-bottom: 15px;">📋 Load Sample Data</button>
|
||
<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">×</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})">×</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...');
|
||
|
||
// 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>
|