COURTRIX / src /components /chat /case-chat-panel.tsx
Ali-Developments's picture
Upload 125 files
ad79323 verified
Raw
History Blame Contribute Delete
6.57 kB
"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>
);
}