New "KI" tab on case detail page with three sub-panels: - KI-Strategie: one-click strategic analysis with next steps, risks, timeline - KI-Entwurf: document drafting with template selection, language, instructions - Aehnliche Faelle: UPC similar case search with relevance scores Components: CaseStrategy, DocumentDrafter, SimilarCaseFinder Types: StrategyRecommendation, DocumentDraft, SimilarCase, etc.
184 lines
6.4 KiB
TypeScript
184 lines
6.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import { api } from "@/lib/api";
|
|
import type { SimilarCasesResponse } from "@/lib/types";
|
|
import {
|
|
Loader2,
|
|
Search,
|
|
ExternalLink,
|
|
AlertTriangle,
|
|
Scale,
|
|
RefreshCw,
|
|
} from "lucide-react";
|
|
|
|
interface SimilarCaseFinderProps {
|
|
caseId: string;
|
|
}
|
|
|
|
const inputClass =
|
|
"w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm text-neutral-900 outline-none transition-colors focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400";
|
|
|
|
function RelevanceBadge({ score }: { score: number }) {
|
|
const pct = Math.round(score * 100);
|
|
let color = "bg-neutral-100 text-neutral-600";
|
|
if (pct >= 80) color = "bg-emerald-50 text-emerald-700";
|
|
else if (pct >= 60) color = "bg-blue-50 text-blue-700";
|
|
else if (pct >= 40) color = "bg-amber-50 text-amber-700";
|
|
return (
|
|
<span
|
|
className={`inline-block shrink-0 rounded-full px-2 py-0.5 text-xs font-medium ${color}`}
|
|
>
|
|
{pct}%
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export function SimilarCaseFinder({ caseId }: SimilarCaseFinderProps) {
|
|
const [description, setDescription] = useState("");
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: (req: { case_id: string; description: string }) =>
|
|
api.post<SimilarCasesResponse>("/ai/similar-cases", req),
|
|
});
|
|
|
|
function handleSearch(e?: React.FormEvent) {
|
|
e?.preventDefault();
|
|
mutation.mutate({ case_id: caseId, description });
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<form onSubmit={handleSearch} className="space-y-3">
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-neutral-500">
|
|
Zusaetzliche Beschreibung (optional)
|
|
</label>
|
|
<textarea
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="z.B. 'SEP-Lizenzierung im Mobilfunkbereich, FRAND-Verteidigung...'"
|
|
rows={2}
|
|
className={inputClass}
|
|
disabled={mutation.isPending}
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={mutation.isPending}
|
|
className="inline-flex items-center gap-2 rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-neutral-800 disabled:opacity-50"
|
|
>
|
|
{mutation.isPending ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
Suche laeuft...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Search className="h-4 w-4" />
|
|
Aehnliche Faelle suchen
|
|
</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
|
|
{mutation.isError && (
|
|
<div className="flex flex-col items-center gap-3 py-6 text-center">
|
|
<div className="rounded-xl bg-red-50 p-3">
|
|
<AlertTriangle className="h-6 w-6 text-red-500" />
|
|
</div>
|
|
<p className="text-sm text-neutral-900">Suche fehlgeschlagen</p>
|
|
<p className="text-xs text-neutral-500">
|
|
Die youpc.org-Datenbank ist moeglicherweise nicht verfuegbar.
|
|
</p>
|
|
<button
|
|
onClick={() => handleSearch()}
|
|
className="inline-flex items-center gap-1 text-sm text-neutral-500 transition-colors hover:text-neutral-700"
|
|
>
|
|
<RefreshCw className="h-3.5 w-3.5" />
|
|
Erneut versuchen
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{mutation.data && (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-xs text-neutral-500">
|
|
{mutation.data.count} aehnliche{" "}
|
|
{mutation.data.count === 1 ? "Fall" : "Faelle"} gefunden
|
|
</p>
|
|
<button
|
|
onClick={() => handleSearch()}
|
|
disabled={mutation.isPending}
|
|
className="inline-flex items-center gap-1 rounded-md border border-neutral-200 px-2.5 py-1.5 text-xs font-medium text-neutral-600 transition-colors hover:bg-neutral-50"
|
|
>
|
|
<RefreshCw className="h-3.5 w-3.5" />
|
|
Aktualisieren
|
|
</button>
|
|
</div>
|
|
|
|
{mutation.data.cases?.length === 0 && (
|
|
<div className="flex flex-col items-center gap-2 py-6 text-center">
|
|
<Scale className="h-6 w-6 text-neutral-300" />
|
|
<p className="text-sm text-neutral-500">
|
|
Keine aehnlichen UPC-Faelle gefunden.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{mutation.data.cases?.map((c, i) => (
|
|
<div
|
|
key={i}
|
|
className="rounded-md border border-neutral-200 bg-white px-4 py-3"
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<RelevanceBadge score={c.relevance} />
|
|
<span className="text-xs font-medium text-neutral-400">
|
|
{c.case_number}
|
|
</span>
|
|
{c.url && (
|
|
<a
|
|
href={c.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-neutral-400 transition-colors hover:text-neutral-600"
|
|
>
|
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
</a>
|
|
)}
|
|
</div>
|
|
<p className="mt-1 text-sm font-medium text-neutral-900">
|
|
{c.title}
|
|
</p>
|
|
<div className="mt-1 flex flex-wrap gap-x-3 text-xs text-neutral-400">
|
|
{c.court && <span>{c.court}</span>}
|
|
{c.date && <span>{c.date}</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="mt-2 text-sm text-neutral-600">{c.explanation}</p>
|
|
|
|
{c.key_holdings && (
|
|
<div className="mt-2 rounded border border-neutral-100 bg-neutral-50 px-3 py-2">
|
|
<p className="text-xs font-medium text-neutral-500">
|
|
Relevante Entscheidungsgruende
|
|
</p>
|
|
<p className="mt-0.5 text-xs text-neutral-600">
|
|
{c.key_holdings}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|