From bee8c80ea40dca728517b2b68967a07975a3c3b5 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 5 Mar 2026 01:56:15 -0500 Subject: [PATCH] feat: add invite token handling to survey page Co-Authored-By: Claude Opus 4.6 --- frontend/src/pages/SurveyPage.tsx | 634 ++++++++++++++++++++++++++++++ 1 file changed, 634 insertions(+) create mode 100644 frontend/src/pages/SurveyPage.tsx diff --git a/frontend/src/pages/SurveyPage.tsx b/frontend/src/pages/SurveyPage.tsx new file mode 100644 index 00000000..8ea3bea1 --- /dev/null +++ b/frontend/src/pages/SurveyPage.tsx @@ -0,0 +1,634 @@ +import { useState, useCallback, useRef, useEffect } from 'react' +import { useSearchParams } from 'react-router-dom' +import { BrandLogo } from '@/components/common/BrandLogo' + +// ── Survey Data Types ── + +type QuestionType = 'mc' | 'mc-multi' | 'range' | 'text' | 'rank' + +interface SurveyQuestion { + id: string + type: QuestionType + num: string + text: string + hint?: string + options?: string[] + items?: string[] + min?: number + max?: number + step?: number + suffix?: string + low_label?: string + high_label?: string +} + +interface SurveySlideData { + id: string + questions: SurveyQuestion[] + scenario?: { + title: string + symptom: string + details: string + } +} + +// ── Survey Questions (from reference HTML) ── + +const SLIDES: SurveySlideData[] = [ + { + 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?', + 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' }, + ] + }, + { + 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?', + 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?', + options: ["After 2–3 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"] }, + ] + }, + { + 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?' }, + ] + }, + { + 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?', 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.' }, + ] + }, + { + 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)', + 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" + ]}, + ] + }, + { + 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" + ]}, + ] + }, + { + id: 'flowpilot', + questions: [ + { id: 'detail_level', type: 'mc', num: '15', text: 'When an AI suggests a diagnostic step, how specific should it be?', + 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?', + 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" + ]}, + ] + }, +] + +const TOTAL_QUESTIONS = SLIDES.reduce((n, s) => n + s.questions.length, 0) + +// ── Component ── + +export default function SurveyPage() { + const [currentSlide, setCurrentSlide] = useState(0) + const [answers, setAnswers] = useState>(() => { + // Initialize rank answers with default order + const init: Record = {} + SLIDES.forEach(s => s.questions.forEach(q => { + if (q.type === 'rank' && q.items) init[q.id] = [...q.items] + })) + return init + }) + const [isSubmitting, setIsSubmitting] = useState(false) + const [isComplete, setIsComplete] = useState(false) + const [submitError, setSubmitError] = useState('') + + const [searchParams] = useSearchParams() + const token = searchParams.get('t') + const [inviteName, setInviteName] = useState(null) + const [alreadyCompleted, setAlreadyCompleted] = useState(false) + const [tokenLoading, setTokenLoading] = useState(!!token) + + useEffect(() => { + if (!token) return + const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000' + fetch(`${apiUrl}/api/v1/survey/invite/${token}`) + .then(r => r.ok ? r.json() : null) + .then(data => { + if (data?.status === 'completed') setAlreadyCompleted(true) + if (data?.name) setInviteName(data.name) + }) + .catch(() => {}) + .finally(() => setTokenLoading(false)) + }, [token]) + + const setAnswer = useCallback((qid: string, value: string | string[]) => { + setAnswers(prev => ({ ...prev, [qid]: value })) + }, []) + + const answeredCount = SLIDES.reduce((count, slide) => { + return count + slide.questions.filter(q => { + const a = answers[q.id] + return a !== undefined && a !== '' && (!Array.isArray(a) || a.length > 0) + }).length + }, 0) + const progressPct = Math.round((answeredCount / TOTAL_QUESTIONS) * 100) + + const goSlide = (idx: number) => { + setCurrentSlide(idx) + window.scrollTo({ top: 0, behavior: 'smooth' }) + } + + const handleSubmit = async () => { + setIsSubmitting(true) + setSubmitError('') + try { + const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000' + const res = await fetch(`${apiUrl}/api/v1/survey/submit`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ respondent_name: inviteName || undefined, responses: answers, token: token || undefined }), + }) + if (!res.ok) { + if (res.status === 409) { + setAlreadyCompleted(true) + return + } + const errData = await res.json().catch(() => null) + throw new Error(errData?.detail || `Submission failed (${res.status})`) + } + setIsComplete(true) + } catch (err) { + setSubmitError(err instanceof Error ? err.message : 'Submission failed. Try copying your responses instead.') + } finally { + setIsSubmitting(false) + } + } + + const 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 + } + + const copyAll = async () => { + try { + await navigator.clipboard.writeText(buildOutput()) + alert('Copied! Paste into an email and send.') + } catch { + alert('Copy failed — try selecting the text manually.') + } + } + + if (tokenLoading) { + return ( +
+
Loading...
+
+ ) + } + + if (alreadyCompleted) { + return ( +
+