"use client"; import { useState } from "react"; import { toast } from "sonner"; import { Mic, Send, Sparkles, Square } from "lucide-react"; import { cn } from "@/lib/utils"; import { colorModeFromTheme } from "@/components/theme/use-dark-theme"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { MarkdownPreview } from "@/components/markdown/markdown-widgets"; import { useBoard } from "@/components/board/board-context"; import { useSpeechRecognition } from "@/hooks/use-speech-recognition"; import { converseAboutGroup, type GroupAiMessage } from "@/lib/actions/group-ai"; import type { GroupDTO } from "@/types/board"; export function GroupAiDialog({ group, open, onOpenChange, }: { group: GroupDTO; open: boolean; onOpenChange: (open: boolean) => void; }) { const { saveNote } = useBoard(); const colorMode = colorModeFromTheme(); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [sending, setSending] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); // Set once the model finalizes -- shown as a review step the user has // to explicitly accept before it touches the group's real notes. const [pendingNotes, setPendingNotes] = useState(null); const { supported: micSupported, listening, start, stop } = useSpeechRecognition({ onFinalResult: (transcript) => setInput((prev) => (prev ? `${prev} ${transcript}` : transcript)), onError: setError, }); // Starts fresh every time it's opened -- a half-finished conversation // from last time would be confusing to resume out of context. function handleOpenChange(next: boolean) { if (!next) { stop(); setMessages([]); setInput(""); setError(null); setPendingNotes(null); } onOpenChange(next); } async function callAi(nextMessages: GroupAiMessage[], forceFinalize: boolean) { setError(null); setSending(true); const result = await converseAboutGroup(group.id, group.title, nextMessages, forceFinalize); setSending(false); if ("error" in result) { setError(result.error); return; } if (result.action === "ask") { setMessages([...nextMessages, { role: "assistant", content: result.question }]); } else { setMessages(nextMessages); setPendingNotes(result.notes); } } async function handleSend() { const text = input.trim(); if (!text || sending) return; if (listening) stop(); const next: GroupAiMessage[] = [...messages, { role: "user", content: text }]; setMessages(next); setInput(""); await callAi(next, false); } async function handleGenerateNow() { if (sending || messages.length === 0) return; if (listening) stop(); await callAi(messages, true); } async function handleAccept() { if (!pendingNotes) return; setSaving(true); const ok = await saveNote(group.id, group.categoryId, pendingNotes); setSaving(false); if (ok) { toast.success("Notes updated."); handleOpenChange(false); } } function handleDiscard() { setPendingNotes(null); } const hasExistingNotes = group.noteContent.trim().length > 0; return ( Ask AI — {group.title} {pendingNotes ? ( <>
{hasExistingNotes && (

This will replace the group's existing notes.

)}
) : ( <>
{messages.length === 0 ? (

Tell me about this group — what it's for, or what should be tracked here.

) : ( messages.map((message, index) => (
{message.content}
)) )} {sending && (
Thinking…
)} {error &&

{error}

}