"use client"; import { useEffect, useRef, useState, type Dispatch, type SetStateAction, } from "react"; import type { ChatMessage, ConnectionStatus, WorkPackage, } from "@/lib/work-package-types"; import type { LlmConfig } from "@/lib/llm-config"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { ScrollArea } from "@/components/ui/scroll-area"; import { applyComposerSuggestion, getComposerSuggestions, type ComposerSuggestion, } from "@/lib/composer-autocomplete"; import { AlertCircle, CheckCircle2, LoaderCircle, Send, Settings2, Sparkles, X, } from "lucide-react"; function statusTone(status: ConnectionStatus) { if (status === "connected") return "text-emerald-700"; if (status === "checking") return "text-amber-700"; if (status === "error") return "text-rose-700"; return "text-muted-foreground"; } function StatusIcon(props: { status: ConnectionStatus }) { const { status } = props; if (status === "connected") return ; if (status === "checking") { return ; } if (status === "error") return ; return ; } export function ChatPanel(props: { messages: ChatMessage[]; draft: string; setDraft: (v: string) => void; busy: boolean; productTitle: string; currentActivity: string; workPackages: WorkPackage[]; selectedWorkPackageId?: string; onSelectWorkPackage: (id: string) => void; llmConfig: LlmConfig; setLlmConfig: Dispatch>; isMockMode: boolean; connectionStatus: ConnectionStatus; connectionMessage: string; canTestConnection: boolean; onTestConnection: () => void; onSend: (text: string) => void | Promise; initialSettingsOpen?: boolean; }) { const { messages, draft, setDraft, busy, productTitle, currentActivity, workPackages, selectedWorkPackageId, onSelectWorkPackage, llmConfig, setLlmConfig, isMockMode, connectionStatus, connectionMessage, canTestConnection, onTestConnection, onSend, initialSettingsOpen, } = props; const bottomRef = useRef(null); const [settingsOpen, setSettingsOpen] = useState(initialSettingsOpen ?? false); const [activeSuggestionIndex, setActiveSuggestionIndex] = useState(0); const showSettings = settingsOpen; const selectedWorkPackage = workPackages.find( (workPackage) => workPackage.id === selectedWorkPackageId, ); const suggestions = getComposerSuggestions(draft, workPackages); useEffect(() => { bottomRef.current?.scrollIntoView({ block: "end" }); }, [messages.length]); function applySuggestion(suggestion: ComposerSuggestion) { setDraft( applyComposerSuggestion({ draft, suggestion, selectedWorkPackage, }), ); setActiveSuggestionIndex(0); if (suggestion.kind === "package") { onSelectWorkPackage(suggestion.id); } } return ( {productTitle} Use @SRS ask,{" "} @Design FMEA execute,{" "} /plan, or paste a product idea to auto-run the whole board. {busy ? currentActivity : "Ready. You can describe a product or target a package directly."} Tip: type @ for packages or{" "} / for ask, plan, change, and execute shortcuts. {messages.map((message) => ( {message.role === "user" ? "You" : productTitle} {message.content} ))} {suggestions.length ? ( {suggestions.map((suggestion, index) => { const active = index === activeSuggestionIndex; return ( applySuggestion(suggestion)} > {suggestion.kind === "package" ? `@${suggestion.shortName}` : suggestion.label} {suggestion.kind === "package" ? suggestion.title : suggestion.description} {suggestion.kind === "package" ? "Package" : "Action"} ); })} ) : null} {showSettings ? ( Model Settings Stored locally in this browser. Live mode turns on after the connection test succeeds. setSettingsOpen(false)} > {connectionMessage} API Key setLlmConfig((prev) => ({ ...prev, apiKey: e.target.value, })) } /> Base URL setLlmConfig((prev) => ({ ...prev, baseUrl: e.target.value, })) } /> Model setLlmConfig((prev) => ({ ...prev, model: e.target.value, })) } /> {connectionStatus === "checking" ? "Testing connection..." : "Test connection"} ) : null} { event.preventDefault(); onSend(draft); }} > { setDraft(event.target.value); setActiveSuggestionIndex(0); }} placeholder="Type a product idea, @Package..., or /ask /plan /change /execute ..." className="min-h-[148px] resize-none rounded-[24px] bg-background/96 px-4 py-3.5 text-sm leading-6 shadow-sm ring-1 ring-black/5" onKeyDown={(event) => { if (suggestions.length && event.key === "ArrowDown") { event.preventDefault(); setActiveSuggestionIndex((current) => Math.min(current + 1, suggestions.length - 1), ); return; } if (suggestions.length && event.key === "ArrowUp") { event.preventDefault(); setActiveSuggestionIndex((current) => Math.max(current - 1, 0)); return; } if (suggestions.length && event.key === "Escape") { event.preventDefault(); setDraft(draft.replace(/(?:^|\s)([@/][A-Za-z\s-]*)$/, "")); return; } if ( suggestions.length && event.key === "Enter" && !event.metaKey && !event.ctrlKey ) { event.preventDefault(); applySuggestion(suggestions[activeSuggestionIndex] ?? suggestions[0]); return; } if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) { event.preventDefault(); onSend(draft); } }} /> Press Ctrl+ Enter to send. Use{" "} @ or{" "} / for suggestions. setSettingsOpen((value) => !value)} > {isMockMode ? "Model Settings" : "Live Settings"} ); }