| "use client"; |
|
|
| import Link from "next/link"; |
| import { Suspense, useEffect, useState } from "react"; |
| import { useSearchParams } from "next/navigation"; |
|
|
| type SeverityInfo = { |
| label: string; |
| color: string; |
| description: string; |
| }; |
|
|
| type ResultData = { |
| scores: number[]; |
| total_score: number; |
| severity: SeverityInfo; |
| filler_count: number; |
| negative_words: string[]; |
| positive_words: string[]; |
| item_names: string[]; |
| answers: string[]; |
| questions: string[]; |
| itemShortNames: string[]; |
| date: string; |
| fallback?: boolean; |
| }; |
|
|
| const SEVERITY_STYLES: Record<string, { bg: string; text: string; border: string }> = { |
| Minimal: { bg: "bg-green-50", text: "text-green-700", border: "border-green-200" }, |
| Mild: { bg: "bg-amber-50", text: "text-amber-700", border: "border-amber-200" }, |
| Moderate: { bg: "bg-orange-50", text: "text-orange-700", border: "border-orange-200" }, |
| "Moderately Severe": { bg: "bg-red-50", text: "text-red-600", border: "border-red-200" }, |
| Severe: { bg: "bg-red-100", text: "text-red-800", border: "border-red-300" }, |
| }; |
|
|
| const BAR_COLORS = [ |
| "bg-primary", |
| "bg-primary-container", |
| "bg-[#F57C00]", |
| "bg-error", |
| ]; |
|
|
| function getBarColor(score: number): string { |
| const idx = Math.min(Math.round(score), 3); |
| return BAR_COLORS[idx]; |
| } |
|
|
| const SHORT_NAMES = [ |
| "Loss of Interest", |
| "Feeling Depressed", |
| "Sleep Issues", |
| "Fatigue", |
| "Appetite Changes", |
| "Self-Criticism", |
| "Concentration", |
| "Psychomotor", |
| ]; |
|
|
| function ResultContent() { |
| const searchParams = useSearchParams(); |
| const [result, setResult] = useState<ResultData | null>(null); |
|
|
| useEffect(() => { |
| const historyId = searchParams.get("id"); |
|
|
| if (historyId !== null) { |
| |
| const historyRaw = localStorage.getItem("mindcheck_history"); |
| if (historyRaw) { |
| try { |
| const history = JSON.parse(historyRaw) as ResultData[]; |
| const idx = parseInt(historyId, 10); |
| if (history[idx]) setResult(history[idx]); |
| } catch { } |
| } |
| } else { |
| |
| const raw = localStorage.getItem("mindcheck_latest_result"); |
| if (raw) { |
| try { |
| setResult(JSON.parse(raw)); |
| } catch { } |
| } |
| } |
| }, [searchParams]); |
|
|
| const handleSave = () => { |
| if (!result) return; |
| const historyRaw = localStorage.getItem("mindcheck_history"); |
| const history: ResultData[] = historyRaw ? JSON.parse(historyRaw) : []; |
| history.unshift(result); |
| localStorage.setItem("mindcheck_history", JSON.stringify(history)); |
| alert("Results saved to history!"); |
| }; |
|
|
| if (!result) { |
| return ( |
| <> |
| <header className="bg-surface border-b border-outline-variant w-full top-0 sticky z-50"> |
| <div className="flex justify-between items-center h-16 px-gutter max-w-container-max mx-auto"> |
| <Link href="/" className="text-lg md:text-hero-lg md:font-hero-lg font-bold text-primary">MindCheck</Link> |
| <div className="font-section-heading text-section-heading hidden md:block">Screening Results</div> |
| <Link href="/chat" className="text-sm px-3 py-2 md:px-[20px] md:py-[12px] bg-transparent border border-primary text-primary rounded hover:bg-surface-container transition-colors"> |
| Start Screening |
| </Link> |
| </div> |
| </header> |
| <main className="flex-grow flex flex-col items-center justify-center px-gutter py-section-padding max-w-container-max mx-auto w-full"> |
| <span className="material-symbols-outlined text-outline-variant text-[64px] mb-4" data-icon="assignment_late">assignment_late</span> |
| <h1 className="font-section-heading text-section-heading text-on-surface mb-2">No Results Available</h1> |
| <p className="text-on-surface-variant mb-6">Complete a screening first to see your results.</p> |
| <Link href="/chat" className="bg-primary-container text-on-primary px-5 py-3 rounded hover:opacity-90 transition-opacity"> |
| Start Screening |
| </Link> |
| </main> |
| </> |
| ); |
| } |
|
|
| const sevStyle = SEVERITY_STYLES[result.severity.label] || SEVERITY_STYLES.Moderate; |
| const maxScore = 24; |
|
|
| return ( |
| <> |
| {/* TopNavBar */} |
| <header className="bg-surface border-b border-outline-variant w-full top-0 sticky z-50"> |
| <div className="flex justify-between items-center h-16 px-gutter max-w-container-max mx-auto"> |
| <Link href="/" className="text-lg md:text-hero-lg md:font-hero-lg font-bold text-primary">MindCheck</Link> |
| <div className="font-section-heading text-section-heading hidden md:block">Screening Results</div> |
| <Link href="/chat" className="text-sm px-3 py-2 md:px-[20px] md:py-[12px] bg-transparent border border-primary text-primary rounded hover:bg-surface-container transition-colors"> |
| Restart Screening |
| </Link> |
| </div> |
| </header> |
| |
| {/* Main Content */} |
| <main className="flex-grow w-full max-w-container-max mx-auto px-gutter py-section-padding"> |
| <div className="grid grid-cols-1 md:grid-cols-12 gap-gutter"> |
| {/* Left Column: Core Results */} |
| <div className="md:col-span-7 flex flex-col gap-stack-lg"> |
| {/* Score Section */} |
| <section className="flex flex-col gap-stack-md" style={{ animation: "fadeSlideIn 0.5s ease-out" }}> |
| <h1 className="font-section-heading text-section-heading text-on-surface">Your Result</h1> |
| <div className="flex items-center gap-4 mt-2"> |
| <div className="flex items-baseline"> |
| <span className="text-[40px] md:text-[56px] font-bold text-primary leading-none tracking-tight"> |
| {Math.round(result.total_score)} |
| </span> |
| <span className="text-2xl text-on-surface-variant ml-1 font-medium">/{maxScore}</span> |
| </div> |
| <div className={`px-4 py-1.5 rounded-[20px] font-caption-sm text-caption-sm border-[0.5px] ${sevStyle.bg} ${sevStyle.text} ${sevStyle.border}`}> |
| {result.severity.label} |
| </div> |
| </div> |
| <p className="text-on-surface-variant max-w-md">{result.severity.description}</p> |
| {result.fallback && ( |
| <div className="flex items-center gap-2 text-[#F57C00] bg-orange-50 border border-orange-200 rounded p-3 mt-2"> |
| <span className="material-symbols-outlined text-[18px]" data-icon="warning">warning</span> |
| <span className="font-caption-sm text-caption-sm">Scored using keyword matching (model server unavailable)</span> |
| </div> |
| )} |
| <p className="text-on-surface-variant text-xs opacity-60"> |
| Screened on {new Date(result.date).toLocaleDateString("en-US", { day: "numeric", month: "long", year: "numeric", hour: "2-digit", minute: "2-digit" })} |
| </p> |
| </section> |
| |
| {/* Bar Chart Section */} |
| <section |
| className="bg-surface-lowest border-[0.5px] border-outline-variant rounded-lg p-4 md:p-section-padding bg-white" |
| style={{ animation: "fadeSlideIn 0.6s ease-out 0.1s both" }} |
| > |
| <h2 className="font-body-md text-body-md font-semibold mb-6">Score per question</h2> |
| <div className="flex flex-col gap-4"> |
| {result.scores.map((score, i) => { |
| const name = result.itemShortNames?.[i] || SHORT_NAMES[i] || `Q${i + 1}`; |
| const displayScore = score + 1; |
| const pct = Math.min((displayScore / 4) * 100, 100); |
| return ( |
| <div key={i} className="flex items-center gap-4 group"> |
| <div className="w-[100px] md:w-[140px] truncate text-on-surface-variant text-sm" title={name}> |
| {name} |
| </div> |
| <div className="flex-grow h-2.5 bg-surface-container rounded-full overflow-hidden flex"> |
| <div |
| className={`h-full rounded-full transition-all duration-1000 ease-out ${getBarColor(score)}`} |
| style={{ width: `${pct}%`, transitionDelay: `${i * 80}ms` }} |
| /> |
| </div> |
| <div className={`w-6 text-right font-medium ${displayScore === 1 ? "text-outline" : ""}`}> |
| {displayScore.toFixed(1)} |
| </div> |
| </div> |
| ); |
| })} |
| </div> |
| </section> |
| |
| {/* Disclaimer */} |
| <div className="bg-surface-container-low border-[0.5px] border-outline-variant rounded-lg p-4 flex gap-4 items-start"> |
| <span className="material-symbols-outlined text-primary mt-0.5" data-icon="info">info</span> |
| <p className="font-caption-sm text-caption-sm text-on-surface-variant"> |
| MindCheck is not a replacement for professional medical diagnosis. Please contact mental health professionals if you are in an emergency. |
| </p> |
| </div> |
| </div> |
| |
| {/* Right Column: Metrics & Actions */} |
| <div className="md:col-span-5 flex flex-col gap-stack-lg mt-8 md:mt-0"> |
| {/* 2x2 Metric Grid */} |
| <div |
| className="grid grid-cols-2 gap-4" |
| style={{ animation: "fadeSlideIn 0.5s ease-out 0.2s both" }} |
| > |
| <div className="bg-white border-[0.5px] border-outline-variant rounded-lg p-4 flex flex-col gap-2 hover:shadow-sm transition-shadow"> |
| <span className="font-caption-sm text-caption-sm text-on-surface-variant">Total Score</span> |
| <span className="font-section-heading text-section-heading text-on-surface">{Math.round(result.total_score)}</span> |
| </div> |
| <div className="bg-white border-[0.5px] border-outline-variant rounded-lg p-4 flex flex-col gap-2 hover:shadow-sm transition-shadow"> |
| <span className="font-caption-sm text-caption-sm text-on-surface-variant">Category</span> |
| <span className={`font-section-heading text-section-heading ${sevStyle.text}`}>{result.severity.label}</span> |
| </div> |
| <div className="bg-white border-[0.5px] border-outline-variant rounded-lg p-4 flex flex-col gap-2 hover:shadow-sm transition-shadow"> |
| <span className="font-caption-sm text-caption-sm text-on-surface-variant">Negative Words</span> |
| <span className="font-section-heading text-section-heading text-on-surface">{result.negative_words.length}</span> |
| {result.negative_words.length > 0 && ( |
| <div className="flex flex-wrap gap-1 mt-1"> |
| {result.negative_words.slice(0, 5).map((w) => ( |
| <span key={w} className="text-[10px] px-1.5 py-0.5 bg-red-50 text-red-600 rounded border border-red-100">{w}</span> |
| ))} |
| {result.negative_words.length > 5 && ( |
| <span className="text-[10px] px-1.5 py-0.5 text-on-surface-variant">+{result.negative_words.length - 5} more</span> |
| )} |
| </div> |
| )} |
| </div> |
| <div className="bg-white border-[0.5px] border-outline-variant rounded-lg p-4 flex flex-col gap-2 hover:shadow-sm transition-shadow"> |
| <span className="font-caption-sm text-caption-sm text-on-surface-variant">Filler Words</span> |
| <span className="font-section-heading text-section-heading text-on-surface">{result.filler_count}</span> |
| </div> |
| </div> |
| |
| {/* Recommendation Card */} |
| <div |
| className="bg-white border-[0.5px] border-outline-variant border-l-4 border-l-primary rounded-lg p-4 md:p-section-padding flex flex-col gap-stack-sm" |
| style={{ animation: "fadeSlideIn 0.5s ease-out 0.3s both" }} |
| > |
| <h3 className="font-body-md text-body-md font-semibold">Recommended Next Steps</h3> |
| <p className="text-on-surface-variant font-caption-sm text-caption-sm"> |
| {result.total_score <= 4 |
| ? "Your results suggest minimal symptoms. Continue monitoring your well-being and maintain healthy habits." |
| : result.total_score <= 9 |
| ? "Your results suggest mild symptoms. Consider speaking with a trusted friend or family member, and monitor your progress." |
| : result.total_score <= 14 |
| ? "Based on these results, we recommend scheduling a consultation with a mental health professional for further evaluation." |
| : "Based on these results, we strongly recommend seeking professional help immediately. Please contact a psychologist, psychiatrist, or your nearest mental health service."} |
| </p> |
| </div> |
| |
| {/* Action Buttons */} |
| <div |
| className="flex flex-col sm:flex-row gap-4 mt-auto" |
| style={{ animation: "fadeSlideIn 0.5s ease-out 0.4s both" }} |
| > |
| <button |
| onClick={handleSave} |
| className="flex-1 px-[20px] py-[12px] bg-transparent border border-primary text-primary rounded hover:bg-surface-container transition-all duration-200 text-center justify-center flex items-center gap-2 active:scale-95" |
| > |
| <span className="material-symbols-outlined text-[20px]" data-icon="download">download</span> |
| Save Results |
| </button> |
| <Link href="/history" className="flex-1 px-[20px] py-[12px] bg-primary text-on-primary rounded hover:opacity-90 transition-all duration-200 text-center justify-center flex items-center active:scale-95"> |
| View History |
| </Link> |
| </div> |
| </div> |
| </div> |
| </main> |
| |
| <style jsx global>{` |
| @keyframes fadeSlideIn { |
| from { |
| opacity: 0; |
| transform: translateY(16px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| `}</style> |
| </> |
| ); |
| } |
|
|
| export default function ResultPage() { |
| return ( |
| <Suspense fallback={ |
| <div className="flex-grow flex items-center justify-center"> |
| <div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" /> |
| </div> |
| }> |
| <ResultContent /> |
| </Suspense> |
| ); |
| } |
|
|