Files
openclaw-workspace-2026/Projects/site-survey-ai/index.html
T
JC Beasley cb5e26b561 Update memory service with working NocoDB configuration
- Set correct table ID: mx149yctebfwvys (ai_data_Memory)
- Updated type column with SingleSelect options
- Added all required columns for corrections, preferences, episodes, decisions, validation
- Fixed column options for type, severity, status fields
2026-07-04 17:02:40 -07:00

520 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IT Site Survey AI | Network Infrastructure Planning</title>
<style>
:root {
--bg: #0d1117;
--bg-2: #161b22;
--bg-3: #21262d;
--border: #30363d;
--text: #c9d1d9;
--text-2: #8b949e;
--accent: #58a6ff;
--accent-2: #1f6feb;
--success: #3fb950;
--warning: #d29922;
--danger: #f85149;
--radius: 8px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
line-height: 1.5;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px 0;
border-bottom: 1px solid var(--border);
}
.logo {
font-size: 3rem;
margin-bottom: 10px;
}
h1 {
font-size: 1.5rem;
margin-bottom: 5px;
}
.subtitle {
color: var(--text-2);
font-size: 1rem;
}
.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-weight: 500;
}
.form-group input, .form-group select, .form-group textarea {
width: 100%;
padding: 12px;
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-family: inherit;
}
.form-group textarea {
min-height: 100px;
resize: vertical;
}
.form-group select {
cursor: pointer;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-item input {
width: auto;
}
.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;
}
.btn {
background: var(--accent);
color: white;
border: none;
border-radius: var(--radius);
padding: 12px 24px;
font-family: inherit;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
}
.btn:hover {
background: var(--accent-2);
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(88,166,255,0.3);
}
.btn:disabled {
background: var(--bg-3) !important;
color: var(--text-2) !important;
cursor: not-allowed;
transform: none !important;
}
.btn-success {
background: var(--success);
}
.btn-success:hover {
background: #2ea043;
box-shadow: 0 4px 15px rgba(63,185,80,0.3);
}
.btn-secondary {
background: var(--bg-3);
color: var(--text);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: var(--bg-3);
}
.actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.alert {
padding: 15px;
border-radius: var(--radius);
margin-bottom: 20px;
}
.alert.info {
background: rgba(88,166,255,0.1);
border: 1px solid var(--accent);
color: var(--accent);
}
.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);
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.hidden {
display: none;
}
.photo-preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
margin-bottom: 15px;
}
.photo-preview {
position: relative;
border-radius: var(--radius);
overflow: hidden;
height: 120px;
}
.photo-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.photo-preview .remove-photo {
position: absolute;
top: 5px;
right: 5px;
background: rgba(0,0,0,0.7);
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
cursor: pointer;
font-size: 12px;
}
.photo-upload-area {
border: 2px dashed var(--border);
border-radius: var(--radius);
padding: 30px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s;
}
.photo-upload-area:hover {
border-color: var(--accent);
}
.photo-upload-area input {
display: none;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">🏢</div>
<h1>IT Site Survey AI</h1>
<div class="subtitle">Network Infrastructure Planning & Recommendations</div>
<div style="margin-top: 15px;">
<a href="/dashboard.html" class="btn btn-secondary">📊 Dashboard</a>
</div>
</header>
<div class="panel">
<h2>📋 Network Site Survey</h2>
<p style="color: var(--text-2); margin-bottom: 20px;">Complete this concise survey to generate AI-powered network infrastructure recommendations.</p>
<div id="surveyForm">
<div id="surveyQuestions">
<div class="alert info">Loading survey questions...</div>
</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 (Optional)</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 id="errorMessage" class="alert error hidden"></div>
<div id="successMessage" class="alert success hidden">
Survey submitted successfully! You can now view AI recommendations.
</div>
<div class="actions">
<button class="btn btn-success" onclick="submitSurvey()" id="submitBtn">
<span id="submitSpinner" class="spinner hidden"></span>
Submit Survey
</button>
</div>
</div>
</div>
</div>
<script>
let surveyTemplate = null;
let surveyResponses = {};
let selectedPhotos = [];
// Initialize
document.addEventListener("DOMContentLoaded", function() {
loadTemplate();
});
async function loadTemplate() {
try {
const response = await fetch("/api/surveys/template");
const data = await response.json();
surveyTemplate = data.template;
renderQuestions(surveyTemplate.questions);
} catch (e) {
console.error("Failed to load survey template:", e);
document.getElementById("surveyQuestions").innerHTML = "<div class=\"alert error\">Failed to load survey questions. Please refresh the page.</div>";
}
}
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>";
if (q.options) {
q.options.forEach(opt => {
inputHtml += "<option value=\"" + opt + "\">" + opt + "</option>";
});
}
inputHtml += "</select>";
break;
case "multiselect":
inputHtml = "<div class=\"checkbox-group\">";
if (q.options) {
q.options.forEach(opt => {
inputHtml += "<label class=\"checkbox-item\">" +
"<input type=\"checkbox\" value=\"" + opt + "\" onchange=\"saveMultiResponse( + q.id + , + opt + , this.checked)\">" +
opt +
"</label>";
});
}
inputHtml += "</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, optionValue, isChecked) {
if (!surveyResponses[questionId]) {
surveyResponses[questionId] = [];
}
if (isChecked) {
surveyResponses[questionId].push(optionValue);
} else {
surveyResponses[questionId] = surveyResponses[questionId].filter(v => v !== optionValue);
}
}
function handlePhotoSelect(files) {
for (const file of files) {
if (file.type.startsWith("image/")) {
const reader = new FileReader();
reader.onload = (e) => {
selectedPhotos.push({
file: file,
preview: e.target.result
});
renderPhotoPreviews();
};
reader.readAsDataURL(file);
}
}
}
function removePhoto(index) {
selectedPhotos.splice(index, 1);
renderPhotoPreviews();
}
function renderPhotoPreviews() {
const container = document.getElementById("photoPreviewGrid");
container.innerHTML = selectedPhotos.map((photo, index) => {
return "<div class=\"photo-preview\">" +
"<img src=\"" + photo.preview + "\" alt=\"Preview\">" +
"<button class=\"remove-photo\" onclick=\"removePhoto(" + index + ")\">×</button>" +
"</div>";
}).join("");
}
function showError(message) {
const errorEl = document.getElementById("errorMessage");
errorEl.textContent = message;
errorEl.classList.remove("hidden");
// Hide success message if shown
document.getElementById("successMessage").classList.add("hidden");
}
function hideError() {
document.getElementById("errorMessage").classList.add("hidden");
}
function showSuccess() {
document.getElementById("successMessage").classList.remove("hidden");
// Hide error message if shown
document.getElementById("errorMessage").classList.add("hidden");
}
async function submitSurvey() {
const btn = document.getElementById("submitBtn");
const spinner = document.getElementById("submitSpinner");
const submitterName = document.getElementById("submitterName").value.trim();
// Validate input
if (!submitterName) {
showError("Please enter your name");
return;
}
// Disable button and show spinner
btn.disabled = true;
spinner.classList.remove("hidden");
hideError();
try {
// Create survey first with default values
const createResponse = await fetch("/api/surveys", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "Network Site Survey",
client_name: "Client",
site_name: "Site",
description: "One-page survey submission"
})
});
if (!createResponse.ok) {
const errorText = await createResponse.text();
throw new Error("Failed to create survey: " + createResponse.status + " - " + errorText);
}
const createData = await createResponse.json();
const surveyId = createData.survey.id;
// Submit responses with submitter name
const response = await fetch("/api/surveys/" + surveyId + "/responses", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
submitter_name: submitterName,
responses: surveyResponses
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error("Failed to submit responses: " + response.status + " - " + errorText);
}
// 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
});
}
}
console.log("Survey submitted successfully!");
showSuccess();
btn.textContent = "Submitted!";
} catch (error) {
console.error("Error submitting survey:", error);
showError("Error submitting survey: " + error.message);
btn.disabled = false;
spinner.classList.add("hidden");
btn.textContent = "Submit Survey";
}
}
</script>
</body>
</html>