chore: archive old plan docs + add survey foundation files

Move completed plan docs to docs/plans/archive/. Add survey migration 046
and reference HTML/plan files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-05 02:03:38 -05:00
parent da3788afbc
commit 932927b9df
52 changed files with 1250 additions and 0 deletions

682
docs/plans/survey.html Normal file
View File

@@ -0,0 +1,682 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FlowPilot Survey — ResolutionFlow</title>
<meta name="description" content="Help shape FlowPilot, an AI assistant built for MSP engineers. 5-minute survey.">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--gradient-start: #818cf8;
--gradient-mid: #9389fa;
--gradient-end: #a78bfa;
--bg-deep: #050507;
--bg-dark: #09090b;
--bg-card: #111114;
--bg-input: #0c0c0f;
--border-subtle: #1e1e24;
--border-hover: #2a2a32;
--border-focus: #818cf8;
--text-primary: #f4f4f5;
--text-secondary: #a1a1aa;
--text-muted: #52525b;
--text-hint: #3f3f46;
--accent: #818cf8;
--accent-dim: rgba(129, 140, 248, 0.1);
--accent-glow: rgba(129, 140, 248, 0.06);
--success: #34d399;
--success-dim: rgba(52, 211, 153, 0.1);
}
body {
font-family: 'Inter', -apple-system, sans-serif;
background: var(--bg-deep);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
/* ── Ambient ── */
.ambient { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.orb { position: absolute; border-radius: 50%; filter: blur(140px); opacity: 0.05; }
.orb-1 { width: 600px; height: 600px; background: var(--gradient-start); top: -200px; left: -100px; animation: d1 25s ease-in-out infinite; }
.orb-2 { width: 500px; height: 500px; background: var(--gradient-end); bottom: -150px; right: -100px; animation: d2 30s ease-in-out infinite; }
@keyframes d1 { 0%,100% { transform: translate(0,0); } 50% { transform: translate(40px,50px); } }
@keyframes d2 { 0%,100% { transform: translate(0,0); } 50% { transform: translate(-30px,-40px); } }
.grid-pattern {
position: fixed; inset: 0; z-index: 0; pointer-events: none;
background-image: linear-gradient(rgba(129,140,248,0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(129,140,248,0.02) 1px, transparent 1px);
background-size: 60px 60px;
mask-image: radial-gradient(ellipse 70% 50% at 50% 30%, black, transparent);
}
/* ── Layout ── */
.container { position: relative; z-index: 1; max-width: 680px; margin: 0 auto; padding: 0 20px 100px; }
/* ── Top Bar ── */
.topbar {
position: sticky; top: 0; z-index: 100;
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
background: rgba(5,5,7,0.85); border-bottom: 1px solid var(--border-subtle);
padding: 14px 20px; margin: 0 -20px;
display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.topbar-brand { display: flex; align-items: center; gap: 10px; font-family: 'Plus Jakarta Sans', sans-serif; font-weight: 700; font-size: 14px; color: var(--text-secondary); white-space: nowrap; text-decoration: none; }
.topbar-logo { width: 28px; height: 28px; background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); border-radius: 7px; display: flex; align-items: center; justify-content: center; font-size: 14px; color: white; font-weight: 800; flex-shrink: 0; }
.topbar-brand .flow { background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.progress-wrap { flex: 1; max-width: 240px; display: flex; align-items: center; gap: 10px; }
.progress-bar { flex: 1; height: 3px; background: var(--border-subtle); border-radius: 2px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, var(--gradient-start), var(--gradient-end)); border-radius: 2px; transition: width 0.5s ease; width: 0%; }
.progress-text { font-size: 11px; color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* ── Hero ── */
.hero { text-align: center; padding: 72px 0 40px; }
.hero-badge { display: inline-flex; align-items: center; gap: 6px; padding: 5px 14px; border-radius: 100px; background: var(--accent-dim); border: 1px solid rgba(129,140,248,0.15); font-size: 11px; font-weight: 600; color: var(--accent); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 20px; }
.hero h1 { font-family: 'Plus Jakarta Sans', sans-serif; font-size: clamp(26px, 5vw, 36px); font-weight: 800; line-height: 1.2; margin-bottom: 12px; }
.hero h1 span { background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.hero p { font-size: 15px; color: var(--text-secondary); max-width: 500px; margin: 0 auto; line-height: 1.65; }
.hero-stats { display: flex; justify-content: center; gap: 28px; margin-top: 20px; font-size: 12px; color: var(--text-muted); }
.hero-stats span { display: flex; align-items: center; gap: 5px; }
.hero-stats svg { width: 13px; height: 13px; stroke: var(--accent); fill: none; stroke-width: 2; }
/* ── Steps indicator ── */
.steps-bar { display: flex; gap: 4px; margin-bottom: 36px; }
.step-dot { flex: 1; height: 3px; border-radius: 2px; background: var(--border-subtle); transition: background 0.3s; }
.step-dot.done { background: var(--success); }
.step-dot.active { background: linear-gradient(90deg, var(--gradient-start), var(--gradient-end)); }
/* ── Form Slides ── */
.slide { display: none; animation: fadeIn 0.35s ease; }
.slide.active { display: block; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
/* ── Question Card ── */
.q-card {
background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: 14px;
padding: 28px; margin-bottom: 16px; transition: border-color 0.2s;
}
.q-card:focus-within { border-color: rgba(129,140,248,0.25); }
.q-num { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--accent); margin-bottom: 6px; font-weight: 500; }
.q-text { font-family: 'Plus Jakarta Sans', sans-serif; font-size: 15px; font-weight: 600; color: var(--text-primary); line-height: 1.5; margin-bottom: 4px; }
.q-hint { font-size: 12px; color: var(--text-muted); margin-bottom: 16px; line-height: 1.5; }
/* ── MC Options ── */
.mc-options { display: flex; flex-direction: column; gap: 8px; }
.mc-opt {
display: flex; align-items: center; gap: 12px;
padding: 12px 16px; border-radius: 9px;
border: 1px solid var(--border-subtle); background: var(--bg-input);
cursor: pointer; transition: all 0.15s; font-size: 14px; color: var(--text-secondary);
user-select: none;
}
.mc-opt:hover { border-color: var(--border-hover); color: var(--text-primary); }
.mc-opt.selected { border-color: var(--accent); background: var(--accent-dim); color: var(--text-primary); }
.mc-radio { width: 18px; height: 18px; border-radius: 50%; border: 2px solid var(--border-subtle); flex-shrink: 0; transition: all 0.15s; display: flex; align-items: center; justify-content: center; }
.mc-opt.selected .mc-radio { border-color: var(--accent); }
.mc-opt.selected .mc-radio::after { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
.mc-check { width: 18px; height: 18px; border-radius: 5px; border: 2px solid var(--border-subtle); flex-shrink: 0; transition: all 0.15s; display: flex; align-items: center; justify-content: center; font-size: 11px; color: transparent; }
.mc-opt.selected .mc-check { border-color: var(--accent); background: var(--accent); color: white; }
/* ── Range Slider ── */
.range-wrap { padding: 8px 0; }
.range-labels { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-muted); margin-bottom: 10px; }
.range-val { text-align: center; font-family: 'JetBrains Mono', monospace; font-size: 24px; font-weight: 600; color: var(--accent); margin-bottom: 12px; }
input[type="range"] {
-webkit-appearance: none; width: 100%; height: 4px; border-radius: 2px;
background: var(--border-subtle); outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; width: 20px; height: 20px; border-radius: 50%;
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
cursor: pointer; border: 3px solid var(--bg-card); box-shadow: 0 0 0 1px rgba(129,140,248,0.3);
}
input[type="range"]::-moz-range-thumb {
width: 20px; height: 20px; border-radius: 50%;
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
cursor: pointer; border: 3px solid var(--bg-card);
}
/* ── Textarea ── */
textarea {
width: 100%; min-height: 100px; background: var(--bg-input); border: 1px solid var(--border-subtle);
border-radius: 9px; padding: 14px 16px; color: var(--text-primary); font-family: 'Inter', sans-serif;
font-size: 14px; line-height: 1.6; resize: vertical; transition: border-color 0.2s, box-shadow 0.2s;
}
textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
textarea::placeholder { color: var(--text-hint); }
/* ── Drag Ranking ── */
.rank-list { display: flex; flex-direction: column; gap: 6px; }
.rank-item {
display: flex; align-items: center; gap: 12px;
padding: 11px 16px; border-radius: 9px;
border: 1px solid var(--border-subtle); background: var(--bg-input);
cursor: grab; transition: all 0.15s; font-size: 14px; color: var(--text-secondary);
user-select: none;
}
.rank-item:active { cursor: grabbing; }
.rank-item.dragging { opacity: 0.5; border-color: var(--accent); }
.rank-item.drag-over { border-color: var(--accent); background: var(--accent-dim); }
.rank-num { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--accent); font-weight: 600; width: 20px; text-align: center; flex-shrink: 0; }
.rank-grip { color: var(--text-hint); flex-shrink: 0; display: flex; align-items: center; }
.rank-label { flex: 1; }
/* ── Scenario Box ── */
.scenario-box {
background: linear-gradient(135deg, rgba(129,140,248,0.06), rgba(167,139,250,0.03));
border: 1px solid rgba(129,140,248,0.12); border-radius: 10px;
padding: 16px 20px; margin-bottom: 16px; font-size: 13px;
}
.scenario-box .sc-label { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--accent); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 8px; font-weight: 600; }
.scenario-box .sc-row { display: flex; gap: 8px; margin-bottom: 4px; }
.scenario-box .sc-key { color: var(--text-muted); font-weight: 500; white-space: nowrap; }
.scenario-box .sc-val { color: var(--text-secondary); }
/* ── Buttons ── */
.nav-btns { display: flex; justify-content: space-between; margin-top: 28px; gap: 12px; }
.btn {
font-family: 'Inter', sans-serif; font-size: 14px; font-weight: 600; padding: 12px 24px;
border-radius: 9px; border: none; cursor: pointer; transition: all 0.2s;
display: inline-flex; align-items: center; gap: 8px;
}
.btn-ghost { background: transparent; border: 1px solid var(--border-subtle); color: var(--text-secondary); }
.btn-ghost:hover { border-color: var(--accent); color: var(--text-primary); }
.btn-accent { background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); color: white; }
.btn-accent:hover { opacity: 0.9; transform: translateY(-1px); }
.btn-accent:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
.btn-success { background: var(--success-dim); border: 1px solid rgba(52,211,153,0.3); color: var(--success); }
.btn-success:hover { background: rgba(52,211,153,0.15); }
/* ── Completion ── */
.complete { display: none; text-align: center; padding: 60px 0; animation: fadeIn 0.4s ease; }
.complete.active { display: block; }
.complete-icon { width: 64px; height: 64px; margin: 0 auto 20px; background: var(--success-dim); border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.complete-icon svg { width: 28px; height: 28px; stroke: var(--success); fill: none; stroke-width: 2.5; }
.complete h2 { font-family: 'Plus Jakarta Sans', sans-serif; font-size: 24px; font-weight: 700; margin-bottom: 10px; }
.complete p { color: var(--text-secondary); font-size: 14px; max-width: 440px; margin: 0 auto 28px; line-height: 1.65; }
.complete-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
/* ── Toast ── */
.toast { position: fixed; bottom: 20px; right: 20px; z-index: 200; padding: 10px 18px; border-radius: 8px; font-size: 13px; font-weight: 500; transform: translateY(80px); opacity: 0; transition: all 0.3s ease; background: var(--success-dim); border: 1px solid rgba(52,211,153,0.3); color: var(--success); }
.toast.show { transform: translateY(0); opacity: 1; }
@media (max-width: 640px) {
.container { padding: 0 16px 80px; }
.topbar { padding: 12px 16px; margin: 0 -16px; }
.hero { padding: 56px 0 28px; }
.hero-stats { flex-wrap: wrap; gap: 12px; }
.q-card { padding: 20px; }
}
</style>
</head>
<body>
<div class="ambient"><div class="orb orb-1"></div><div class="orb orb-2"></div></div>
<div class="grid-pattern"></div>
<div class="container">
<!-- Top Bar -->
<div class="topbar">
<a class="topbar-brand" href="https://resolutionflow.com" target="_blank">
<div class="topbar-logo">R</div>
Resolution<span class="flow">Flow</span>
</a>
<div class="progress-wrap">
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
<span class="progress-text" id="progressText">0 of 16</span>
</div>
</div>
<!-- Hero -->
<div class="hero" id="heroSection">
<div class="hero-badge">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
FlowPilot Research
</div>
<h1>Help Build an AI That<br>Thinks Like <span>You</span></h1>
<p>We're building an AI assistant for MSP engineers. Your expertise shapes how it thinks. Takes about 5 minutes.</p>
<div class="hero-stats">
<span><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>~5 minutes</span>
<span><svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>Confidential</span>
<span><svg viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>16 questions</span>
</div>
</div>
<!-- Steps -->
<div class="steps-bar" id="stepsBar"></div>
<!-- Slides -->
<div id="slidesWrap"></div>
<!-- Completion -->
<div class="complete" id="completeScreen">
<div class="complete-icon"><svg viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg></div>
<h2>Done — Thank You!</h2>
<p>Your answers will directly shape how FlowPilot troubleshoots. Please send your responses using one of the options below.</p>
<div class="complete-actions">
<button class="btn btn-accent" onclick="copyAll()">Copy Responses to Clipboard</button>
<button class="btn btn-success" onclick="downloadText()">Download as Text</button>
</div>
<p style="margin-top: 24px; font-size: 12px; color: var(--text-muted);">Paste into an email to <strong style="color: var(--text-secondary);"><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="a1ccc8c2c9c0c4cde1d3c4d2cecdd4d5c8cecfc7cdced68fc2cecc">[email&#160;protected]</a></strong> or hand off however works best.</p>
</div>
</div>
<div class="toast" id="toast"></div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script>
// ══════════════════════════════════════════
// ██ SURVEY DATA — 16 questions
// ══════════════════════════════════════════
const SLIDES = [
// ── Slide 0: Pre-Troubleshooting & Preferences ──
{
id: "prework",
questions: [
{ id: "prereqs", type: "mc-multi", num: "1", text: "Before you start troubleshooting, what info do you need? (Select all that apply)", hint: "What do you gather or verify before you even start diagnosing?",
options: ["Who's affected (one user, group, everyone)", "What changed recently (patches, config, new software)", "Is there an existing/related ticket", "Client environment details (domain, VPN, on-prem vs. cloud)", "How long the issue has been happening", "User's exact error message or screenshot", "Whether the user has already tried anything", "RMM/monitoring status for the device or service"] },
{ id: "verify_fix", type: "mc", num: "2", text: "After you apply a fix, how do you verify it actually worked?", hint: null,
options: ["Have the user confirm it's working", "Test it myself from their machine or account", "Run the same diagnostic command that showed the failure", "Check monitoring/logs to confirm the service is healthy", "All of the above in sequence", "Fix it and move on — if they don't call back, it worked"] },
{ id: "steps_at_a_time", type: "range", num: "3", text: "When following a troubleshooting guide, how many steps do you prefer to see at once?", hint: "1 = show me one step at a time, 10 = give me the full picture upfront.",
min: 1, max: 10, step: 1, suffix: " steps", low_label: "One at a time", high_label: "Show me everything" },
]
},
// ── Slide 1: Troubleshooting Philosophy ──
{
id: "philosophy",
questions: [
{ id: "first_step", type: "mc", num: "4", text: "A vague ticket comes in: \"Internet is down.\" What's your FIRST move?", hint: "Before any technical check — what's the first mental step?",
options: ["Check if it's one user or many (scope it)", "Look at monitoring / RMM dashboard", "Check for recent changes or maintenance", "Ask the user clarifying questions", "Ping / basic connectivity test", "Check if other tickets are related"] },
{ id: "junior_mistake", type: "mc", num: "5", text: "What's the most common mistake you see junior engineers make?", hint: null,
options: ["Jumping to a fix before understanding the problem", "Not checking scope (one user vs. many)", "Ignoring recent changes as a cause", "Googling the error instead of reading it", "Restarting things without checking logs first", "Not documenting what they tried"] },
{ id: "pivot", type: "mc", num: "6", text: "How do you decide when to stop pursuing one theory and pivot?", hint: null,
options: ["After 23 checks that don't support the theory", "Time-based — if I've spent 15+ min with no progress", "When I find evidence that contradicts the theory", "Gut feeling / experience", "When I run out of ideas on that path"] },
]
},
// ── Slide 2: Scenario ──
{
id: "scenario",
scenario: {
title: "Incoming Ticket",
symptom: "Multiple users can't access \\\\fileserver\\shared",
details: "Started ~9 AM. Some users still can. No known recent changes. Server appears online."
},
questions: [
{ id: "scenario_approach", type: "text", num: "7",
text: "Walk through your first 3 diagnostic steps for this ticket.",
hint: "Include specific commands/tools, what you expect to see, and what a bad result tells you." },
{ id: "scenario_deeper", type: "text", num: "8",
text: "Server pings fine, you can RDP in. What do you check next on the server itself?",
hint: "Services, shares, permissions, event logs — what's your sequence?" },
]
},
// ── Slide 3: Commands & Tools ──
{
id: "commands",
questions: [
{ id: "doc_pct", type: "range", num: "9",
text: "Be honest: what percentage of your troubleshooting steps do you actually document in the ticket?",
hint: null, min: 0, max: 100, step: 10, suffix: "%", low_label: "Almost none", high_label: "Everything" },
{ id: "go_to_commands", type: "text", num: "10",
text: "What are your top 3 go-to PowerShell commands or one-liners? Include exact syntax.",
hint: "The ones you type from muscle memory — we're building FlowPilot's command library from real usage." },
{ id: "secret_weapon", type: "text", num: "11",
text: "Name a command, tool, or technique that junior engineers don't know about but saves you significant time.",
hint: "The secret weapon stuff. This is gold." },
]
},
// ── Slide 4: Tribal Knowledge ──
{
id: "tribal",
questions: [
{ id: "gotcha", type: "text", num: "12",
text: "Describe an issue where the obvious diagnosis was WRONG. What did everyone assume, and what was it actually?",
hint: "These 'gotcha' patterns help FlowPilot warn engineers before they go down the wrong path." },
{ id: "hard_rules", type: "mc-multi", num: "13",
text: "Which of these \"rules\" do you follow? (Select all that apply)",
hint: null,
options: [
"Always check recent changes before deep-diving",
"Screenshot/document current state before making changes",
"Never restart a service without checking logs first",
"Verify the user's report before troubleshooting",
"Check if it's a known issue / existing ticket first",
"Test the fix, don't assume it worked",
"Always have a rollback plan"
]},
]
},
// ── Slide 5: Domain Ranking ──
{
id: "ranking",
questions: [
{ id: "domain_rank", type: "rank", num: "14",
text: "Drag to rank: which technical domains should FlowPilot handle first?",
hint: "Most important at the top.",
items: [
"Windows Server / Active Directory",
"Microsoft 365 / Exchange",
"Networking (DNS, DHCP, VPN, Firewall)",
"Security / Compliance",
"Virtualization (Hyper-V, VMware)",
"Backup & Disaster Recovery",
"Cloud (Azure / AWS)",
"Endpoint Management"
]},
]
},
// ── Slide 6: FlowPilot Feedback ──
{
id: "flowpilot",
questions: [
{ id: "detail_level", type: "mc", num: "15",
text: "When an AI suggests a diagnostic step, how specific should it be?",
hint: null,
options: [
"High-level only (\"Check DNS resolution\")",
"Moderate (\"Run nslookup against the domain controller\")",
"Exact syntax (\"nslookup hostname dc01.domain.local — verify IP matches expected\")",
"Exact syntax + explanation of WHY and what to look for"
]},
{ id: "ai_personality", type: "mc", num: "16",
text: "What would make an AI troubleshooting assistant feel like a useful colleague instead of a chatbot?",
hint: null,
options: [
"Suggests things I haven't thought of yet",
"Knows the right diagnostic order (not just alphabetical)",
"Challenges my assumptions constructively",
"Includes real commands with exact syntax",
"Explains WHY we check things in a certain order",
"Doesn't waste my time with obvious stuff"
]},
]
},
];
// ══════════════════════════════════════════
// ██ STATE
// ══════════════════════════════════════════
let currentSlide = 0;
const answers = {};
const totalQuestions = SLIDES.reduce((n, s) => n + s.questions.length, 0);
// ══════════════════════════════════════════
// ██ RENDER
// ══════════════════════════════════════════
function init() {
// Steps bar
const bar = document.getElementById("stepsBar");
bar.innerHTML = SLIDES.map((_, i) => `<div class="step-dot" id="step-${i}"></div>`).join("");
// Slides
const wrap = document.getElementById("slidesWrap");
wrap.innerHTML = SLIDES.map((slide, si) => {
let html = `<div class="slide" id="slide-${si}">`;
// Scenario box
if (slide.scenario) {
html += `<div class="scenario-box">
<div class="sc-label">${slide.scenario.title}</div>
<div class="sc-row"><span class="sc-key">Symptom:</span><span class="sc-val">${slide.scenario.symptom}</span></div>
<div class="sc-row"><span class="sc-key">Details:</span><span class="sc-val">${slide.scenario.details}</span></div>
</div>`;
}
// Questions
slide.questions.forEach(q => {
html += `<div class="q-card" id="card-${q.id}">`;
html += `<div class="q-num">Q${q.num}</div>`;
html += `<div class="q-text">${q.text}</div>`;
if (q.hint) html += `<div class="q-hint">${q.hint}</div>`;
if (q.type === "mc") {
html += `<div class="mc-options" data-qid="${q.id}" data-type="single">`;
q.options.forEach((opt, oi) => {
html += `<div class="mc-opt" data-val="${opt}" onclick="selectMC(this, '${q.id}', 'single')"><div class="mc-radio"></div><span>${opt}</span></div>`;
});
html += `</div>`;
}
else if (q.type === "mc-multi") {
html += `<div class="mc-options" data-qid="${q.id}" data-type="multi">`;
q.options.forEach((opt, oi) => {
html += `<div class="mc-opt" data-val="${opt}" onclick="selectMC(this, '${q.id}', 'multi')"><div class="mc-check"></div><span>${opt}</span></div>`;
});
html += `</div>`;
}
else if (q.type === "range") {
html += `<div class="range-wrap">
<div class="range-val" id="rv-${q.id}">${q.min}${q.suffix || ''}</div>
<input type="range" min="${q.min}" max="${q.max}" step="${q.step}" value="${q.min}" oninput="onRange('${q.id}', this.value, '${q.suffix || ''}')">
<div class="range-labels"><span>${q.low_label}</span><span>${q.high_label}</span></div>
</div>`;
}
else if (q.type === "text") {
html += `<textarea id="ta-${q.id}" placeholder="Type your answer here..." oninput="onText('${q.id}', this.value)"></textarea>`;
}
else if (q.type === "rank") {
html += `<div class="rank-list" id="rank-${q.id}">`;
q.items.forEach((item, ii) => {
html += `<div class="rank-item" draggable="true" data-idx="${ii}" data-qid="${q.id}">
<div class="rank-grip"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/></svg></div>
<div class="rank-num">${ii + 1}</div>
<div class="rank-label">${item}</div>
</div>`;
});
html += `</div>`;
}
html += `</div>`; // close q-card
});
// Nav
html += `<div class="nav-btns">`;
if (si > 0) html += `<button class="btn btn-ghost" onclick="goSlide(${si - 1})"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg> Back</button>`;
else html += `<div></div>`;
if (si < SLIDES.length - 1) html += `<button class="btn btn-accent" onclick="goSlide(${si + 1})">Next <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg></button>`;
else html += `<button class="btn btn-accent" onclick="finish()">Submit <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6L9 17l-5-5"/></svg></button>`;
html += `</div>`;
html += `</div>`; // close slide
return html;
}).join("");
goSlide(0);
initDragDrop();
}
function goSlide(idx) {
document.querySelectorAll(".slide").forEach(s => s.classList.remove("active"));
document.getElementById(`slide-${idx}`).classList.add("active");
document.getElementById("completeScreen").classList.remove("active");
currentSlide = idx;
// Steps
SLIDES.forEach((_, i) => {
const dot = document.getElementById(`step-${i}`);
dot.className = "step-dot" + (i < idx ? " done" : "") + (i === idx ? " active" : "");
});
// Hero shrinks after first advance
document.getElementById("heroSection").style.display = idx === 0 ? "" : "none";
window.scrollTo({ top: 0, behavior: "smooth" });
updateProgress();
}
//
// INTERACTIONS
//
function selectMC(el, qid, mode) {
if (mode === "single") {
el.parentElement.querySelectorAll(".mc-opt").forEach(o => o.classList.remove("selected"));
el.classList.add("selected");
answers[qid] = el.dataset.val;
} else {
el.classList.toggle("selected");
const selected = [...el.parentElement.querySelectorAll(".mc-opt.selected")].map(o => o.dataset.val);
answers[qid] = selected;
}
updateProgress();
}
function onRange(qid, val, suffix) {
document.getElementById(`rv-${qid}`).textContent = val + suffix;
answers[qid] = val + suffix;
updateProgress();
}
function onText(qid, val) {
answers[qid] = val.trim();
updateProgress();
}
// Drag and drop ranking
function initDragDrop() {
document.querySelectorAll(".rank-list").forEach(list => {
let dragItem = null;
list.querySelectorAll(".rank-item").forEach(item => {
item.addEventListener("dragstart", e => {
dragItem = item;
item.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
});
item.addEventListener("dragend", () => {
item.classList.remove("dragging");
list.querySelectorAll(".rank-item").forEach(i => i.classList.remove("drag-over"));
// Update numbers and store answer
const qid = item.dataset.qid;
const items = [...list.querySelectorAll(".rank-item")];
items.forEach((it, idx) => it.querySelector(".rank-num").textContent = idx + 1);
answers[qid] = items.map(it => it.querySelector(".rank-label").textContent);
updateProgress();
});
item.addEventListener("dragover", e => {
e.preventDefault();
if (item !== dragItem) item.classList.add("drag-over");
});
item.addEventListener("dragleave", () => item.classList.remove("drag-over"));
item.addEventListener("drop", e => {
e.preventDefault();
item.classList.remove("drag-over");
if (item !== dragItem && dragItem) {
const allItems = [...list.querySelectorAll(".rank-item")];
const fromIdx = allItems.indexOf(dragItem);
const toIdx = allItems.indexOf(item);
if (fromIdx < toIdx) item.after(dragItem);
else item.before(dragItem);
}
});
});
// Touch drag support
let touchItem = null;
let touchClone = null;
let touchStartY = 0;
list.addEventListener("touchstart", e => {
const item = e.target.closest(".rank-item");
if (!item) return;
touchItem = item;
touchStartY = e.touches[0].clientY;
item.classList.add("dragging");
}, { passive: true });
list.addEventListener("touchmove", e => {
if (!touchItem) return;
e.preventDefault();
const touch = e.touches[0];
const items = [...list.querySelectorAll(".rank-item")];
items.forEach(i => i.classList.remove("drag-over"));
const target = document.elementFromPoint(touch.clientX, touch.clientY)?.closest(".rank-item");
if (target && target !== touchItem) target.classList.add("drag-over");
}, { passive: false });
list.addEventListener("touchend", e => {
if (!touchItem) return;
const touch = e.changedTouches[0];
const target = document.elementFromPoint(touch.clientX, touch.clientY)?.closest(".rank-item");
if (target && target !== touchItem) {
const allItems = [...list.querySelectorAll(".rank-item")];
const fromIdx = allItems.indexOf(touchItem);
const toIdx = allItems.indexOf(target);
if (fromIdx < toIdx) target.after(touchItem);
else target.before(touchItem);
}
touchItem.classList.remove("dragging");
list.querySelectorAll(".rank-item").forEach(i => i.classList.remove("drag-over"));
const qid = touchItem.dataset.qid;
const items = [...list.querySelectorAll(".rank-item")];
items.forEach((it, idx) => it.querySelector(".rank-num").textContent = idx + 1);
answers[qid] = items.map(it => it.querySelector(".rank-label").textContent);
touchItem = null;
updateProgress();
});
});
// Set initial rank answers
document.querySelectorAll(".rank-list").forEach(list => {
const items = [...list.querySelectorAll(".rank-item")];
if (items.length > 0) {
const qid = items[0].dataset.qid;
answers[qid] = items.map(it => it.querySelector(".rank-label").textContent);
}
});
}
// ══════════════════════════════════════════
// ██ PROGRESS
// ══════════════════════════════════════════
function updateProgress() {
let answered = 0;
SLIDES.forEach(s => s.questions.forEach(q => {
const a = answers[q.id];
if (a !== undefined && a !== "" && (!Array.isArray(a) || a.length > 0)) answered++;
}));
// Rank always counts as answered since it has a default order
const pct = Math.round((answered / totalQuestions) * 100);
document.getElementById("progressFill").style.width = pct + "%";
document.getElementById("progressText").textContent = `${answered} of ${totalQuestions}`;
}
// ══════════════════════════════════════════
// ██ FINISH & EXPORT
// ══════════════════════════════════════════
function finish() {
document.querySelectorAll(".slide").forEach(s => s.classList.remove("active"));
document.getElementById("heroSection").style.display = "none";
document.getElementById("stepsBar").style.display = "none";
document.getElementById("completeScreen").classList.add("active");
window.scrollTo({ top: 0, behavior: "smooth" });
}
function buildOutput() {
let out = "FLOWPILOT SURVEY RESPONSE\n========================\n\n";
SLIDES.forEach(slide => {
slide.questions.forEach(q => {
out += `Q${q.num}. ${q.text}\n`;
const a = answers[q.id];
if (Array.isArray(a)) out += a.map((v, i) => ` ${i + 1}. ${v}`).join("\n") + "\n";
else if (a) out += ` ${a}\n`;
else out += ` (no answer)\n`;
out += "\n";
});
});
return out;
}
function copyAll() {
navigator.clipboard.writeText(buildOutput()).then(() => toast("Copied! Paste into an email and send.")).catch(() => toast("Copy failed — try download instead."));
}
function downloadText() {
const blob = new Blob([buildOutput()], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = "flowpilot-survey-response.txt";
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(url);
t