Spaces:
Runtime error
Runtime error
| "use client"; | |
| import { useState, useTransition } from "react"; | |
| import { MessageMultiple01Icon, QuestionIcon } from "@hugeicons/core-free-icons"; | |
| import { Button } from "@/components/ui/button"; | |
| import { | |
| Card, | |
| CardContent, | |
| CardDescription, | |
| CardHeader, | |
| CardTitle, | |
| } from "@/components/ui/card"; | |
| import { Textarea } from "@/components/ui/textarea"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { formatDateTime } from "@/lib/format"; | |
| import { Icon } from "@/components/shared/icon"; | |
| type ChatSource = { | |
| file_id?: string; | |
| file_name?: string; | |
| page_number?: number | null; | |
| snippet?: string; | |
| }; | |
| type ChatTurn = { | |
| id: string; | |
| question: string; | |
| answer: string; | |
| sources?: unknown; | |
| createdAt: Date | string; | |
| }; | |
| type CaseChatPanelProps = { | |
| customerId: string; | |
| caseId: string; | |
| initialTurns: ChatTurn[]; | |
| }; | |
| export function CaseChatPanel({ | |
| customerId, | |
| caseId, | |
| initialTurns, | |
| }: CaseChatPanelProps) { | |
| const [question, setQuestion] = useState(""); | |
| const [turns, setTurns] = useState(initialTurns); | |
| const [error, setError] = useState<string | null>(null); | |
| const [isPending, startTransition] = useTransition(); | |
| function handleSubmit(event: React.FormEvent<HTMLFormElement>) { | |
| event.preventDefault(); | |
| setError(null); | |
| if (!question.trim()) { | |
| setError("اكتب سؤالك الأول."); | |
| return; | |
| } | |
| startTransition(async () => { | |
| const response = await fetch( | |
| `/api/customers/${customerId}/cases/${caseId}/chat`, | |
| { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| }, | |
| body: JSON.stringify({ | |
| question, | |
| }), | |
| }, | |
| ); | |
| const payload = (await response.json().catch(() => null)) as | |
| | { message?: string; turn?: ChatTurn } | |
| | null; | |
| if (!response.ok || !payload?.turn) { | |
| setError(payload?.message || "تعذر الحصول على إجابة دلوقتي."); | |
| return; | |
| } | |
| setTurns((current) => [...current, payload.turn as ChatTurn]); | |
| setQuestion(""); | |
| }); | |
| } | |
| return ( | |
| <div className="grid gap-5 xl:grid-cols-[1fr_340px]"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>محادثة القضية</CardTitle> | |
| <CardDescription> | |
| اسأل عن ملفات القضية PDF، وسيتم الرد اعتمادًا على المحتوى المفهرس فقط. | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <form className="space-y-3" onSubmit={handleSubmit}> | |
| <Textarea | |
| placeholder="مثال: ما أهم النقاط المذكورة في آخر مذكرة؟" | |
| value={question} | |
| onChange={(event) => setQuestion(event.target.value)} | |
| /> | |
| {error ? ( | |
| <p className="rounded-2xl bg-[color:rgba(185,28,28,0.08)] px-4 py-3 text-sm font-medium text-[var(--destructive)]"> | |
| {error} | |
| </p> | |
| ) : null} | |
| <div className="flex justify-start"> | |
| <Button disabled={isPending} type="submit"> | |
| <span>{isPending ? "جارٍ التفكير..." : "اسأل"}</span> | |
| <Icon icon={QuestionIcon} size={18} /> | |
| </Button> | |
| </div> | |
| </form> | |
| <div className="space-y-4"> | |
| {turns.length ? ( | |
| turns.map((turn) => ( | |
| <TurnCard key={turn.id} turn={turn} /> | |
| )) | |
| ) : ( | |
| <div className="rounded-[28px] border border-dashed border-[var(--border)] bg-[var(--paper)] px-6 py-12 text-center text-sm leading-7 text-[var(--foreground-muted)]"> | |
| لسه مفيش أسئلة في القضية دي. ابدأ أول سؤال بعد ما تتأكد إن ملف | |
| الـ PDF اتحضر للمحادثة. | |
| </div> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card className="h-fit"> | |
| <CardHeader> | |
| <div className="mb-2 flex h-11 w-11 items-center justify-center rounded-2xl bg-[var(--accent-soft)] text-[var(--accent)]"> | |
| <Icon icon={MessageMultiple01Icon} size={20} /> | |
| </div> | |
| <CardTitle>طريقة الاستخدام</CardTitle> | |
| <CardDescription> | |
| الردود هنا مبنية على ملفات القضية الجاهزة في خدمة الـ RAG فقط. | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-3 text-right text-sm leading-7 text-[var(--foreground-muted)]"> | |
| <p>ارفع ملفات PDF للقضية وسيتم تجهيزها تلقائيًا للفهم والبحث.</p> | |
| <p>اسأل بصياغة واضحة ومباشرة لتحصل على إجابة أفضل.</p> | |
| <p>لو المعلومة مش موجودة في الملفات، الرد لازم يوضح ده بدل التخمين.</p> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| ); | |
| } | |
| function TurnCard({ turn }: { turn: ChatTurn }) { | |
| const sources = Array.isArray(turn.sources) ? (turn.sources as ChatSource[]) : []; | |
| return ( | |
| <div className="space-y-3 rounded-[28px] border border-[var(--border)] bg-white/85 p-4"> | |
| <div className="rounded-[24px] bg-[var(--paper)] p-4 text-right"> | |
| <p className="text-sm text-[var(--foreground-soft)]">سؤالك</p> | |
| <p className="mt-2 font-semibold text-[var(--foreground)]"> | |
| {turn.question} | |
| </p> | |
| </div> | |
| <div className="rounded-[24px] bg-[var(--accent-soft)]/60 p-4 text-right"> | |
| <p className="text-sm text-[var(--foreground-soft)]">الرد</p> | |
| <p className="mt-2 whitespace-pre-wrap leading-8 text-[var(--foreground)]"> | |
| {turn.answer} | |
| </p> | |
| </div> | |
| {sources.length ? ( | |
| <div className="space-y-2 text-right"> | |
| <p className="text-sm font-semibold text-[var(--foreground)]"> | |
| المراجع | |
| </p> | |
| <div className="flex flex-wrap gap-2"> | |
| {sources.map((source, index) => ( | |
| <Badge key={`${turn.id}-${index}`} variant="outline"> | |
| {source.file_name || "ملف"}{" "} | |
| {source.page_number ? `- ص ${source.page_number}` : ""} | |
| </Badge> | |
| ))} | |
| </div> | |
| </div> | |
| ) : null} | |
| <p className="text-right text-xs text-[var(--foreground-soft)]"> | |
| {formatDateTime(turn.createdAt)} | |
| </p> | |
| </div> | |
| ); | |
| } | |