"use client"; /* * Style-agent chat panel — the new "שיחה" tab on /training. * * Layout: two columns. * - Sidebar: list of conversations + "+ שיחה חדשה" button * - Main: thread of messages + composer with SSE streaming * * Each message is persisted to the legal-ai DB; the LLM call goes * out via FastAPI → host's legal-chat-service → claude CLI. There * is no API cost — the claude CLI uses Daphna's claude.ai * subscription via the host's auth. * * Health gate: if /api/training/chat/health reports the host service * is unreachable, the composer is replaced by a setup notice telling * the chair to start the pm2 service. */ import { useEffect, useRef, useState } from "react"; import { Send, Plus, Trash2, Loader2, MessageSquare, Sparkles, AlertTriangle, } from "lucide-react"; import { toast } from "sonner"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { chatKeys, useChatConversation, useChatConversations, useChatHealth, useCorpus, useCreateChat, useDeleteChat, type ChatMessage, } from "@/lib/api/training"; import { useQueryClient } from "@tanstack/react-query"; export function ChatPanel() { const [activeId, setActiveId] = useState(null); const health = useChatHealth(); return (
{health.data && !health.data.reachable && ( )} {activeId ? ( ) : (

בחר שיחה קיימת או פתח חדשה כדי להתחיל לדבר עם סוכן הסגנון.

הסוכן רץ על claude CLI מקומי דרך legal-chat-service. אין עלות API.

)}
); } // ── Sidebar: list + new ──────────────────────────────────────────── function ConversationsSidebar({ activeId, onSelect, }: { activeId: string | null; onSelect: (id: string | null) => void; }) { const { data: convs, isPending } = useChatConversations(); const { data: corpus } = useCorpus(); const create = useCreateChat(); const del = useDeleteChat(); const [creating, setCreating] = useState(false); const [newTitle, setNewTitle] = useState(""); const [newCorpusId, setNewCorpusId] = useState("__none__"); const onCreate = async () => { try { const conv = await create.mutateAsync({ title: newTitle.trim() || "שיחה חדשה", style_corpus_id: newCorpusId === "__none__" ? null : newCorpusId, }); onSelect(conv.id); setCreating(false); setNewTitle(""); setNewCorpusId("__none__"); } catch (e) { toast.error(e instanceof Error ? e.message : "כשל ביצירת שיחה"); } }; const onDelete = async (id: string) => { if (!window.confirm("למחוק את השיחה? פעולה זו לא ניתנת לביטול.")) return; try { await del.mutateAsync(id); if (activeId === id) onSelect(null); toast.success("השיחה נמחקה"); } catch (e) { toast.error(e instanceof Error ? e.message : "כשל במחיקה"); } }; return ( {!creating ? ( ) : (