import { useState, useCallback, useRef, useEffect } from 'react' import { useSearchParams, useNavigate } from 'react-router-dom' import { BrandLogo } from '@/components/common/BrandLogo' import { PageMeta } from '@/components/common/PageMeta' // ── 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: 'prioritization', type: 'rank', num: '14', text: 'Rank these factors by how much they influence your diagnostic order.', hint: 'What drives which theory you investigate first? Drag to reorder — most influential at the top.', items: [ "Likelihood of being the root cause", "How fast I can test or rule it out", "Blast radius — impact if it's the problem", "How recently something changed", "Tool / access availability right now", "Past experience with similar symptoms" ]}, ] }, { 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 [emailInput, setEmailInput] = useState('') const [emailSending, setEmailSending] = useState(false) const [emailSent, setEmailSent] = useState(false) const [emailError, setEmailError] = useState('') const [responseId, setResponseId] = useState(null) const navigate = useNavigate() 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 topRef = useRef(null) const goSlide = (idx: number) => { setCurrentSlide(idx) } // Scroll to top whenever the active slide changes — mobile browsers // (especially iOS Safari) ignore window.scrollTo, so we use // scrollIntoView on a ref at the top of the page as primary method const isFirstRender = useRef(true) useEffect(() => { if (isFirstRender.current) { isFirstRender.current = false return } if (topRef.current) { topRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }) } else { window.scrollTo({ top: 0, behavior: 'smooth' }) } }, [currentSlide]) 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) window.scrollTo({ top: 0, behavior: 'smooth' }) return } const errData = await res.json().catch(() => null) throw new Error(errData?.detail || `Submission failed (${res.status})`) } const data = await res.json() setResponseId(data.id) setIsComplete(true) window.scrollTo({ top: 0, behavior: 'smooth' }) } 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 (