"use client"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { Check, Copy, Sparkles } from "lucide-react"; import { colorModeFromTheme } from "@/components/theme/use-dark-theme"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { MarkdownPreview } from "@/components/markdown/markdown-widgets"; import { ChatMessageList, ChatComposer, type ChatBubbleMessage } from "@/components/ai/chat-thread"; import { useSpeechRecognition } from "@/hooks/use-speech-recognition"; import { converseAboutGroupStatus, type StatusChatMessage, } from "@/lib/actions/group-status-ai"; import type { GroupDTO } from "@/types/board"; export function StatusUpdateDialog({ group, open, onOpenChange, }: { group: GroupDTO; open: boolean; onOpenChange: (open: boolean) => void; }) { const colorMode = colorModeFromTheme(); // The generated summary, shown permanently once produced -- there's no // accept/discard step, since nothing here is ever saved back to the // group's notes (export-only, by design). const [summary, setSummary] = useState(null); const [generating, setGenerating] = useState(false); const [copied, setCopied] = useState(false); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [sending, setSending] = useState(false); const [error, setError] = useState(null); const { supported: micSupported, listening, start, stop } = useSpeechRecognition({ onFinalResult: (transcript) => setInput((prev) => (prev ? `${prev} ${transcript}` : transcript)), onError: setError, }); // Fires the initial summary the moment the dialog opens -- no user // action needed, per spec. Guarded by summary === null so it doesn't // re-fire on every re-render while open, and skipped once a summary (or // an error in its place) already exists for this open session. useEffect(() => { if (!open || summary !== null || generating) return; let cancelled = false; setGenerating(true); setError(null); converseAboutGroupStatus(group.id, []).then((result) => { if (cancelled) return; setGenerating(false); if ("error" in result) { setError(result.error); return; } setSummary(result.content); }); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); // Starts fresh every time it's reopened -- matches GroupAiDialog/ // TodoAiDialog's existing behavior. function handleOpenChange(next: boolean) { if (!next) { stop(); setSummary(null); setGenerating(false); setCopied(false); setMessages([]); setInput(""); setError(null); } onOpenChange(next); } async function handleCopy() { if (!summary) return; try { await navigator.clipboard.writeText(summary); setCopied(true); toast.success("Copied to clipboard."); setTimeout(() => setCopied(false), 2000); } catch { toast.error("Couldn't copy to clipboard."); } } async function handleSend() { const text = input.trim(); if (!text || sending) return; if (listening) stop(); const next: StatusChatMessage[] = [...messages, { role: "user", content: text }]; setMessages(next); setInput(""); setError(null); setSending(true); const result = await converseAboutGroupStatus(group.id, next); setSending(false); if ("error" in result) { setError(result.error); return; } setMessages([...next, { role: "assistant", content: result.content }]); } return ( Status Update — {group.title} {generating && !summary && (

Generating status update…

)} {error && !summary && (

{error}

)} {summary && (
)}
); }