"use client" import { useEffect, useMemo, useState } from "react" import { ArrowDown, ArrowUp, Copy, Download, RotateCcw, Search, Send, X } from "lucide-react" import fieldLibraryJson from "@/data/survey/eval-schema-fields.json" import { Navigation } from "@/components/navigation" import { PageHeader } from "@/components/page-header" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { SURVEY_CONFIG, SURVEY_SOURCE_LABELS, SURVEY_TOOL_URL, type StakeholderTag, } from "@/lib/survey-content" import { cn } from "@/lib/utils" interface SurveyField { id: string source: string section: string field: string schemaPath: string fullPath: string type: string description: string required: string } interface SurveyState { participantName: string organization: string roleTitle: string stakeholderTag: StakeholderTag stakeholderGroupingNotes: string answers: Record rankedFieldIds: string[] fieldRankingNotes: string missingFieldNotes: string finalNotes: string } const FIELD_LIBRARY = fieldLibraryJson as SurveyField[] const STORAGE_KEY = "eval-cards-survey-v2" const SOURCE_FILTERS = [ { id: "all", label: "All fields" }, { id: "autobenchmarkcard", label: SURVEY_SOURCE_LABELS.autobenchmarkcard }, { id: "eee_eval", label: SURVEY_SOURCE_LABELS.eee_eval }, { id: "eee_instance_level_eval", label: SURVEY_SOURCE_LABELS.eee_instance_level_eval }, ] as const function getTodayString() { return new Date().toISOString().slice(0, 10) } function createInitialState(): SurveyState { return { participantName: "", organization: "", roleTitle: "", stakeholderTag: "researcher", stakeholderGroupingNotes: "", answers: {}, rankedFieldIds: [...SURVEY_CONFIG.defaultFieldIds], fieldRankingNotes: "", missingFieldNotes: "", finalNotes: "", } } function mergeSurveyState(value: Partial | undefined): SurveyState { const fallback = createInitialState() const validFieldIds = new Set(FIELD_LIBRARY.map((field) => field.id)) const validStakeholderTags = new Set(SURVEY_CONFIG.stakeholderTags.map((tag) => tag.id)) return { ...fallback, ...value, stakeholderTag: value?.stakeholderTag && validStakeholderTags.has(value.stakeholderTag) ? value.stakeholderTag : fallback.stakeholderTag, answers: { ...fallback.answers, ...(value?.answers ?? {}), }, rankedFieldIds: Array.isArray(value?.rankedFieldIds) ? value.rankedFieldIds.filter((fieldId) => validFieldIds.has(fieldId)) : fallback.rankedFieldIds, } } function buildSummaryText(state: SurveyState, fieldMap: Map) { const stakeholderLabel = SURVEY_CONFIG.stakeholderTags.find((tag) => tag.id === state.stakeholderTag)?.label ?? state.stakeholderTag const lines: string[] = [ "Eval Cards Survey", `Stakeholder tag: ${stakeholderLabel}`, `Date: ${getTodayString()}`, `Participant: ${state.participantName || "Anonymous"}`, `Organization: ${state.organization || "Not provided"}`, `Role: ${state.roleTitle || "Not provided"}`, ] if (state.stakeholderGroupingNotes.trim()) { lines.push(`Grouping notes: ${state.stakeholderGroupingNotes.trim()}`) } lines.push("", "Ranked schema fields:") if (state.rankedFieldIds.length === 0) { lines.push("[No ranked fields yet]") } else { state.rankedFieldIds.forEach((fieldId, index) => { const field = fieldMap.get(fieldId) if (!field) return lines.push( `${index + 1}. ${field.fullPath} (${field.type}; required: ${field.required || "unknown"})` ) lines.push(` ${field.description}`) }) } if (state.fieldRankingNotes.trim()) { lines.push("", "Why these fields matter:", state.fieldRankingNotes.trim()) } if (state.missingFieldNotes.trim()) { lines.push("", "Fields missing from the schema:", state.missingFieldNotes.trim()) } SURVEY_CONFIG.sections.forEach((section) => { lines.push("", section.title) section.questions.forEach((question) => { lines.push(`- ${question.prompt}`) lines.push(` ${state.answers[question.id]?.trim() || "[No response]"}`) }) }) if (state.finalNotes.trim()) { lines.push("", "Additional notes:", state.finalNotes.trim()) } return lines.join("\n") } function slugifySegment(value: string) { return value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") } export default function SurveyPage() { const [surveyState, setSurveyState] = useState(createInitialState) const [fieldQuery, setFieldQuery] = useState("") const [sourceFilter, setSourceFilter] = useState<(typeof SOURCE_FILTERS)[number]["id"]>("all") const [copyState, setCopyState] = useState<"idle" | "copied" | "error">("idle") const [submitState, setSubmitState] = useState<"idle" | "submitting" | "submitted" | "error">("idle") useEffect(() => { const storedValue = window.localStorage.getItem(STORAGE_KEY) if (!storedValue) return try { const parsed = JSON.parse(storedValue) as Partial setSurveyState(mergeSurveyState(parsed)) } catch (error) { console.error("Failed to restore survey state", error) } }, []) useEffect(() => { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(surveyState)) }, [surveyState]) useEffect(() => { if (copyState === "idle") return const timeout = window.setTimeout(() => setCopyState("idle"), 2000) return () => window.clearTimeout(timeout) }, [copyState]) const fieldMap = useMemo( () => new Map(FIELD_LIBRARY.map((field) => [field.id, field])), [] ) const selectedFieldSet = useMemo( () => new Set(surveyState.rankedFieldIds), [surveyState.rankedFieldIds] ) const suggestedFields = useMemo( () => SURVEY_CONFIG.defaultFieldIds .map((fieldId) => fieldMap.get(fieldId)) .filter((field): field is SurveyField => Boolean(field)), [fieldMap] ) const filteredAvailableFields = useMemo(() => { const query = fieldQuery.trim().toLowerCase() return FIELD_LIBRARY.filter((field) => { if (selectedFieldSet.has(field.id)) { return false } if (sourceFilter !== "all" && field.source !== sourceFilter) { return false } if (!query) { return true } return [ field.fullPath, field.schemaPath, field.description, field.type, field.required, SURVEY_SOURCE_LABELS[field.source] ?? field.source, ].some((value) => value.toLowerCase().includes(query)) }).slice(0, 18) }, [fieldQuery, selectedFieldSet, sourceFilter]) const summaryText = useMemo( () => buildSummaryText(surveyState, fieldMap), [surveyState, fieldMap] ) const setAnswer = (questionId: string, value: string) => { setSurveyState((current) => ({ ...current, answers: { ...current.answers, [questionId]: value, }, })) } const addRankedField = (fieldId: string) => { setSurveyState((current) => { if (current.rankedFieldIds.includes(fieldId)) { return current } return { ...current, rankedFieldIds: [...current.rankedFieldIds, fieldId], } }) } const removeRankedField = (fieldId: string) => { setSurveyState((current) => ({ ...current, rankedFieldIds: current.rankedFieldIds.filter((id) => id !== fieldId), })) } const moveRankedField = (fieldId: string, direction: -1 | 1) => { setSurveyState((current) => { const index = current.rankedFieldIds.indexOf(fieldId) const nextIndex = index + direction if (index === -1 || nextIndex < 0 || nextIndex >= current.rankedFieldIds.length) { return current } const reordered = [...current.rankedFieldIds] const [field] = reordered.splice(index, 1) reordered.splice(nextIndex, 0, field) return { ...current, rankedFieldIds: reordered, } }) } const resetSurvey = () => { setSurveyState(createInitialState()) setFieldQuery("") setCopyState("idle") } const copySummary = async () => { try { await navigator.clipboard.writeText(summaryText) setCopyState("copied") } catch (error) { console.error("Failed to copy survey summary", error) setCopyState("error") } } const downloadSummary = () => { const participantSlug = slugifySegment(surveyState.participantName) || "participant" const dateSlug = getTodayString() const filename = `eval-cards-survey-${participantSlug}-${dateSlug}.md` const blob = new Blob([summaryText], { type: "text/markdown;charset=utf-8" }) const url = window.URL.createObjectURL(blob) const link = document.createElement("a") link.href = url link.download = filename document.body.appendChild(link) link.click() link.remove() window.URL.revokeObjectURL(url) } const submitSurvey = async () => { setSubmitState("submitting") try { const res = await fetch("/api/survey-submit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...surveyState, submittedAt: new Date().toISOString(), date: getTodayString(), summaryText, }), }) if (!res.ok) { const data = await res.json().catch(() => ({})) console.error("Survey submission failed:", data) setSubmitState("error") alert("Survey submission failed. Please try again.") return } setSubmitState("submitted") alert("Thank you! Your survey response has been submitted successfully.") } catch (err) { console.error("Survey submission error:", err) setSubmitState("error") alert("Survey submission failed due to a network error. Please try again.") } } return (
tag.id === surveyState.stakeholderTag)?.label ?? surveyState.stakeholderTag, }, ]} />
Before You Start

{SURVEY_CONFIG.audienceSummary}

{SURVEY_CONFIG.goalsSummary}

Use the stakeholder tag only as a grouping aid for later analysis. The useful part is your concrete feedback, not the label.

What We Want From You

{SURVEY_CONFIG.usabilityPrompt}

Please explore{" "} the current prototype {" "} and then write your notes below, one section at a time.

Public survey Group later

{SURVEY_CONFIG.title}

Fill this in like an interview worksheet: answer the core questions, rank the fields that matter, and leave direct notes on what should change.

About You

All fields are optional. Responses are saved anonymously if left blank.

Grouping Tag

Tag for later analysis

Pick the closest fit for grouping purposes.

Optional segmentation layer
{SURVEY_CONFIG.stakeholderTags.map((tag) => ( ))}