"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 ( ); })}
) : null} {showSettings ? (
Model Settings
Stored locally in this browser. Live mode turns on after the connection test succeeds.
{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, })) } />
) : null}
{ event.preventDefault(); onSend(draft); }} >