"use client"; import Link from "next/link"; import { useState, useRef, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; /* ── 8 PHQ-8 Interview Questions (DAIC-WOZ style) ───────────────────────── */ const QUESTIONS = [ "Let's start by talking about things you usually enjoy. Lately, have you felt like you've lost interest or pleasure in doing them? Tell me a bit about how you've been feeling about your hobbies or daily activities.", "I'd like to hear about your mood recently. Over the past couple of weeks, have you been feeling down, depressed, or perhaps a bit hopeless? Could you share what's been on your mind?", "Let's talk about your sleep. How have you been sleeping lately? Tell me if you've had any trouble falling asleep, staying asleep, or maybe even sleeping too much.", "How has your energy level been? Do you find yourself feeling tired or exhausted often? Tell me about a typical day and how you feel physically.", "What about your eating habits? Have you noticed any changes, like a poor appetite or perhaps overeating? Tell me how your relationship with food has been lately.", "How have you been feeling about yourself? Have there been moments where you felt bad, or like you let yourself or your family down? Feel free to share your thoughts openly.", "Can you tell me about your focus and concentration? For example, when you read or watch TV, do you find your mind wandering? Tell me how it's been for you.", "Have you or others noticed any changes in how you move or speak? Like moving much slower than usual, or maybe the opposite—feeling restless and unable to sit still? Tell me about it.", ]; const QUICK_OPTIONS = [ "Not at all", "Several days", "More than half the days", "Nearly every day", ]; const ITEM_SHORT_NAMES = [ "Loss of Interest", "Feeling Depressed", "Sleep Issues", "Fatigue", "Appetite Changes", "Self-Criticism", "Concentration", "Psychomotor", ]; type Message = { role: "bot" | "user"; text: string }; export default function ChatScreening() { const router = useRouter(); const [messages, setMessages] = useState([ { role: "bot", text: "Hello! I'm going to ask you 8 short questions based on the PHQ-8 screening. Please answer honestly — all data stays anonymous and is not stored on any server.\n\nLet's begin." }, { role: "bot", text: QUESTIONS[0] }, ]); const [currentStep, setCurrentStep] = useState(0); const [answers, setAnswers] = useState([]); const [inputValue, setInputValue] = useState(""); const [isTyping, setIsTyping] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const chatEndRef = useRef(null); const inputRef = useRef(null); /* auto-scroll on new messages */ useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, isTyping]); /* focus input */ useEffect(() => { if (!isSubmitting) inputRef.current?.focus(); }, [currentStep, isSubmitting]); /* ── Submit all answers to backend ──────────────────────────────────── */ const submitToBackend = useCallback( async (allAnswers: string[]) => { setIsSubmitting(true); setIsTyping(true); setMessages((prev) => [ ...prev, { role: "bot", text: "Thank you for completing all 8 questions! I'm now analyzing your responses..." }, ]); try { const res = await fetch("/api/predict", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ answers: allAnswers }), }); if (!res.ok) { console.warn(`Backend returned ${res.status}, falling back to local scoring`); throw "Fallback"; } const data = await res.json(); /* Store result in localStorage */ const result = { ...data, answers: allAnswers, questions: QUESTIONS, itemShortNames: ITEM_SHORT_NAMES, date: new Date().toISOString(), }; localStorage.setItem("mindcheck_latest_result", JSON.stringify(result)); setIsTyping(false); setMessages((prev) => [ ...prev, { role: "bot", text: "Analysis complete! Redirecting you to your results..." }, ]); setTimeout(() => router.push("/result"), 1200); } catch (err) { console.warn("Prediction error:", err); setIsTyping(false); /* ── Fallback: keyword-based scoring ────────────────── */ const fallbackScores: number[] = allAnswers.map((ans) => { const a = ans.toLowerCase().trim(); if (/\b(not at all|never|no|none|tidak|ngga|ga|tidak pernah)\b/.test(a)) return 0; if (/\b(several days|sometimes|a little|occasionally|kadang|jarang)\b/.test(a)) return 1; if (/\b(more than half|often|frequently|most days|sering|lumayan sering)\b/.test(a)) return 2; if (/\b(nearly every day|always|every day|constantly|everyday|selalu|tiap hari|hampir tiap hari)\b/.test(a)) return 3; /* Sentiment-based fallback */ const negWords = ["hopeless","worthless","empty","tired","sad","lonely","fail","useless","burden","numb","anxious","hate","depressed","miserable","terrible","awful","struggling","suffering","exhausted","overwhelmed","stressed","worried", "sedih", "capek", "lelah", "hampa", "kesepian", "gagal", "beban", "benci", "stres", "bingung", "hancur", "buruk"]; const words = a.split(/\s+/); const negCount = words.filter((w) => negWords.includes(w)).length; if (negCount >= 3) return 3; if (negCount >= 2) return 2; if (negCount >= 1) return 1; return 1; // default middle score for ambiguous text }); const totalScore = fallbackScores.reduce((s, v) => s + v, 0); let severity; if (totalScore <= 4) severity = { label: "Minimal", color: "green", description: "Minimal depression — no treatment typically needed." }; else if (totalScore <= 9) severity = { label: "Mild", color: "amber", description: "Mild depression — watchful waiting recommended." }; else if (totalScore <= 14) severity = { label: "Moderate", color: "orange", description: "Moderate depression — consultation recommended." }; else if (totalScore <= 19) severity = { label: "Moderately Severe", color: "red-orange", description: "Moderately severe depression — active treatment recommended." }; else severity = { label: "Severe", color: "red", description: "Severe depression — immediate referral recommended." }; const combined = allAnswers.join(" ").toLowerCase(); const negWordsList = ["hopeless","worthless","empty","tired","sad","lonely","fail","useless","burden","numb","anxious","hate","depressed","miserable","terrible","awful","struggling","suffering","exhausted","overwhelmed","stressed","worried", "sedih", "capek", "lelah", "hampa", "kesepian", "gagal", "beban", "benci", "stres", "bingung", "hancur", "buruk"]; const foundNeg = negWordsList.filter((w) => combined.includes(w)); const fillerPattern = /\b(um|uh|uhm|hmm|hm|like|you know)\b/gi; const fillerCount = (combined.match(fillerPattern) || []).length; const result = { scores: fallbackScores, total_score: totalScore, severity, filler_count: fillerCount, negative_words: foundNeg, positive_words: [], item_names: ITEM_SHORT_NAMES.map((n) => n.replace(/ /g, "")), answers: allAnswers, questions: QUESTIONS, itemShortNames: ITEM_SHORT_NAMES, date: new Date().toISOString(), fallback: true, }; localStorage.setItem("mindcheck_latest_result", JSON.stringify(result)); setMessages((prev) => [ ...prev, { role: "bot", text: "Analysis complete (using local scoring as the model server is unavailable). Redirecting to your results..." }, ]); setTimeout(() => router.push("/result"), 1200); } }, [router], ); /* ── Handle user answer ─────────────────────────────────────────────── */ const handleAnswer = useCallback( (text: string) => { if (isSubmitting || !text.trim()) return; const newAnswers = [...answers, text.trim()]; setAnswers(newAnswers); setMessages((prev) => [...prev, { role: "user", text: text.trim() }]); setInputValue(""); const nextStep = currentStep + 1; if (nextStep >= QUESTIONS.length) { /* All questions answered → submit */ setCurrentStep(nextStep); submitToBackend(newAnswers); } else { /* Show typing indicator, then next question */ setIsTyping(true); setCurrentStep(nextStep); setTimeout(() => { setIsTyping(false); setMessages((prev) => [ ...prev, { role: "bot", text: QUESTIONS[nextStep] }, ]); }, 800); } }, [answers, currentStep, isSubmitting, submitToBackend], ); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleAnswer(inputValue); } }; const progress = Math.min(((currentStep) / QUESTIONS.length) * 100, 100); const isDone = currentStep >= QUESTIONS.length; return ( <> {/* TopAppBar */}
psychology MindCheck
{isDone ? "Complete" : `Question ${currentStep + 1} of ${QUESTIONS.length}`}
Exit
{/* LEFT SIDEBAR */} {/* RIGHT MAIN */}
{/* Progress Bar */}
{/* Chat Area */}
{messages.map((msg, i) => (
{msg.role === "bot" && (
psychology
)}

{msg.text}

))} {/* Typing Indicator */} {isTyping && (
psychology
)}
{/* Input Area Fixed Bottom */} {!isDone && (
{/* Text Input */}
setInputValue(e.target.value)} onKeyDown={handleKeyDown} disabled={isTyping || isSubmitting} />
)}
{/* Inline animation keyframes */} ); }