Files
KanzlAI-mGMT/frontend/src/components/ai/SimilarCaseFinder.tsx
m fdb4ac55a1 feat: frontend AI tab — KI-Strategie, KI-Entwurf, Aehnliche Faelle
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.
2026-03-30 11:26:01 +02:00

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>
);
}