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.
This commit is contained in:
51
frontend/src/app/(app)/cases/[id]/ki/page.tsx
Normal file
51
frontend/src/app/(app)/cases/[id]/ki/page.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { Brain, FileText, Search } from "lucide-react";
|
||||||
|
import { CaseStrategy } from "@/components/ai/CaseStrategy";
|
||||||
|
import { DocumentDrafter } from "@/components/ai/DocumentDrafter";
|
||||||
|
import { SimilarCaseFinder } from "@/components/ai/SimilarCaseFinder";
|
||||||
|
|
||||||
|
type AITab = "strategy" | "draft" | "similar";
|
||||||
|
|
||||||
|
const TABS: { id: AITab; label: string; icon: typeof Brain }[] = [
|
||||||
|
{ id: "strategy", label: "KI-Strategie", icon: Brain },
|
||||||
|
{ id: "draft", label: "KI-Entwurf", icon: FileText },
|
||||||
|
{ id: "similar", label: "Aehnliche Faelle", icon: Search },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function CaseAIPage() {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const [activeTab, setActiveTab] = useState<AITab>("strategy");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Sub-tabs */}
|
||||||
|
<div className="mb-6 flex gap-1 rounded-lg border border-neutral-200 bg-neutral-50 p-1">
|
||||||
|
{TABS.map((tab) => {
|
||||||
|
const isActive = activeTab === tab.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`inline-flex flex-1 items-center justify-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-colors ${
|
||||||
|
isActive
|
||||||
|
? "bg-white text-neutral-900 shadow-sm"
|
||||||
|
: "text-neutral-500 hover:text-neutral-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<tab.icon className="h-4 w-4" />
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
{activeTab === "strategy" && <CaseStrategy caseId={id} />}
|
||||||
|
{activeTab === "draft" && <DocumentDrafter caseId={id} />}
|
||||||
|
{activeTab === "similar" && <SimilarCaseFinder caseId={id} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
StickyNote,
|
StickyNote,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
ScrollText,
|
ScrollText,
|
||||||
|
Brain,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { de } from "date-fns/locale";
|
import { de } from "date-fns/locale";
|
||||||
@@ -48,6 +49,7 @@ const TABS = [
|
|||||||
{ segment: "mitarbeiter", label: "Mitarbeiter", icon: UserCheck },
|
{ segment: "mitarbeiter", label: "Mitarbeiter", icon: UserCheck },
|
||||||
{ segment: "notizen", label: "Notizen", icon: StickyNote },
|
{ segment: "notizen", label: "Notizen", icon: StickyNote },
|
||||||
{ segment: "protokoll", label: "Protokoll", icon: ScrollText },
|
{ segment: "protokoll", label: "Protokoll", icon: ScrollText },
|
||||||
|
{ segment: "ki", label: "KI", icon: Brain },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const TAB_LABELS: Record<string, string> = {
|
const TAB_LABELS: Record<string, string> = {
|
||||||
@@ -58,6 +60,7 @@ const TAB_LABELS: Record<string, string> = {
|
|||||||
mitarbeiter: "Mitarbeiter",
|
mitarbeiter: "Mitarbeiter",
|
||||||
notizen: "Notizen",
|
notizen: "Notizen",
|
||||||
protokoll: "Protokoll",
|
protokoll: "Protokoll",
|
||||||
|
ki: "KI",
|
||||||
};
|
};
|
||||||
|
|
||||||
function CaseDetailSkeleton() {
|
function CaseDetailSkeleton() {
|
||||||
|
|||||||
226
frontend/src/components/ai/CaseStrategy.tsx
Normal file
226
frontend/src/components/ai/CaseStrategy.tsx
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { api } from "@/lib/api";
|
||||||
|
import type { StrategyRecommendation } from "@/lib/types";
|
||||||
|
import {
|
||||||
|
Loader2,
|
||||||
|
Brain,
|
||||||
|
AlertTriangle,
|
||||||
|
ArrowRight,
|
||||||
|
Shield,
|
||||||
|
Calendar,
|
||||||
|
RefreshCw,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface CaseStrategyProps {
|
||||||
|
caseId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRIORITY_STYLES = {
|
||||||
|
high: "bg-red-50 text-red-700 border-red-200",
|
||||||
|
medium: "bg-amber-50 text-amber-700 border-amber-200",
|
||||||
|
low: "bg-emerald-50 text-emerald-700 border-emerald-200",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const IMPORTANCE_STYLES = {
|
||||||
|
critical: "border-l-red-500",
|
||||||
|
important: "border-l-amber-500",
|
||||||
|
routine: "border-l-neutral-300",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function CaseStrategy({ caseId }: CaseStrategyProps) {
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: () =>
|
||||||
|
api.post<StrategyRecommendation>("/ai/case-strategy", {
|
||||||
|
case_id: caseId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mutation.data && !mutation.isPending && !mutation.isError) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-3 py-8 text-center">
|
||||||
|
<div className="rounded-xl bg-neutral-100 p-3">
|
||||||
|
<Brain className="h-6 w-6 text-neutral-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-neutral-900">
|
||||||
|
KI-Strategieanalyse
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-neutral-500">
|
||||||
|
Claude analysiert die Akte und gibt strategische Empfehlungen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => mutation.mutate()}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Brain className="h-4 w-4" />
|
||||||
|
Strategie analysieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mutation.isPending) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-3 py-12 text-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-neutral-400" />
|
||||||
|
<p className="text-sm text-neutral-500">
|
||||||
|
Claude analysiert die Akte...
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-neutral-400">
|
||||||
|
Dies kann bis zu 30 Sekunden dauern.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mutation.isError) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-3 py-8 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">Analyse fehlgeschlagen</p>
|
||||||
|
<button
|
||||||
|
onClick={() => mutation.mutate()}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = mutation.data!;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-semibold text-neutral-900">
|
||||||
|
KI-Strategieanalyse
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => mutation.mutate()}
|
||||||
|
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>
|
||||||
|
|
||||||
|
{/* Summary */}
|
||||||
|
<div className="rounded-md border border-blue-100 bg-blue-50 px-4 py-3 text-sm text-blue-800">
|
||||||
|
{data.summary}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Next Steps */}
|
||||||
|
{data.next_steps?.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-neutral-500">
|
||||||
|
<ArrowRight className="h-3.5 w-3.5" />
|
||||||
|
Naechste Schritte
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{data.next_steps.map((step, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="rounded-md border border-neutral-200 bg-white px-4 py-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span
|
||||||
|
className={`mt-0.5 inline-block shrink-0 rounded-full border px-2 py-0.5 text-xs font-medium ${PRIORITY_STYLES[step.priority]}`}
|
||||||
|
>
|
||||||
|
{step.priority === "high"
|
||||||
|
? "Hoch"
|
||||||
|
: step.priority === "medium"
|
||||||
|
? "Mittel"
|
||||||
|
: "Niedrig"}
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-sm font-medium text-neutral-900">
|
||||||
|
{step.action}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-neutral-500">
|
||||||
|
{step.reasoning}
|
||||||
|
</p>
|
||||||
|
{step.deadline && (
|
||||||
|
<p className="mt-1 text-xs text-neutral-400">
|
||||||
|
Frist: {step.deadline}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Risk Assessment */}
|
||||||
|
{data.risk_assessment?.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-neutral-500">
|
||||||
|
<Shield className="h-3.5 w-3.5" />
|
||||||
|
Risikobewertung
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{data.risk_assessment.map((risk, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="rounded-md border border-neutral-200 bg-white px-4 py-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span
|
||||||
|
className={`mt-0.5 inline-block shrink-0 rounded-full border px-2 py-0.5 text-xs font-medium ${PRIORITY_STYLES[risk.level]}`}
|
||||||
|
>
|
||||||
|
{risk.level === "high"
|
||||||
|
? "Hoch"
|
||||||
|
: risk.level === "medium"
|
||||||
|
? "Mittel"
|
||||||
|
: "Niedrig"}
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-sm font-medium text-neutral-900">
|
||||||
|
{risk.risk}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-neutral-500">
|
||||||
|
Massnahme: {risk.mitigation}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Timeline */}
|
||||||
|
{data.timeline?.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-neutral-500">
|
||||||
|
<Calendar className="h-3.5 w-3.5" />
|
||||||
|
Zeitplan
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{data.timeline.map((item, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`border-l-2 py-2 pl-4 ${IMPORTANCE_STYLES[item.importance]}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="shrink-0 text-xs font-medium text-neutral-400">
|
||||||
|
{item.date}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-neutral-900">{item.event}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
198
frontend/src/components/ai/DocumentDrafter.tsx
Normal file
198
frontend/src/components/ai/DocumentDrafter.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { api } from "@/lib/api";
|
||||||
|
import type { DocumentDraft, DraftDocumentRequest } from "@/lib/types";
|
||||||
|
import { FileText, Loader2, Copy, Check, Download } from "lucide-react";
|
||||||
|
|
||||||
|
const TEMPLATES = {
|
||||||
|
klageschrift: "Klageschrift",
|
||||||
|
klageerwiderung: "Klageerwiderung",
|
||||||
|
abmahnung: "Abmahnung",
|
||||||
|
schriftsatz: "Schriftsatz",
|
||||||
|
berufung: "Berufungsschrift",
|
||||||
|
antrag: "Antrag",
|
||||||
|
stellungnahme: "Stellungnahme",
|
||||||
|
gutachten: "Gutachten",
|
||||||
|
vertrag: "Vertrag",
|
||||||
|
vollmacht: "Vollmacht",
|
||||||
|
upc_claim: "UPC Statement of Claim",
|
||||||
|
upc_defence: "UPC Statement of Defence",
|
||||||
|
upc_counterclaim: "UPC Counterclaim for Revocation",
|
||||||
|
upc_injunction: "UPC Provisional Measures",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const LANGUAGES = [
|
||||||
|
{ value: "de", label: "Deutsch" },
|
||||||
|
{ value: "en", label: "English" },
|
||||||
|
{ value: "fr", label: "Francais" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
interface DocumentDrafterProps {
|
||||||
|
caseId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocumentDrafter({ caseId }: DocumentDrafterProps) {
|
||||||
|
const [templateType, setTemplateType] = useState("");
|
||||||
|
const [instructions, setInstructions] = useState("");
|
||||||
|
const [language, setLanguage] = useState("de");
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (req: DraftDocumentRequest) =>
|
||||||
|
api.post<DocumentDraft>("/ai/draft-document", req),
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!templateType) return;
|
||||||
|
mutation.mutate({
|
||||||
|
case_id: caseId,
|
||||||
|
template_type: templateType,
|
||||||
|
instructions,
|
||||||
|
language,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCopy() {
|
||||||
|
if (mutation.data?.content) {
|
||||||
|
navigator.clipboard.writeText(mutation.data.content);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDownload() {
|
||||||
|
if (!mutation.data?.content) return;
|
||||||
|
const blob = new Blob([mutation.data.content], { type: "text/plain;charset=utf-8" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${templateType}_entwurf.txt`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-neutral-500">
|
||||||
|
Dokumenttyp
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={templateType}
|
||||||
|
onChange={(e) => setTemplateType(e.target.value)}
|
||||||
|
className={inputClass}
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
>
|
||||||
|
<option value="">Dokumenttyp waehlen...</option>
|
||||||
|
{Object.entries(TEMPLATES).map(([key, label]) => (
|
||||||
|
<option key={key} value={key}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-neutral-500">
|
||||||
|
Sprache
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={language}
|
||||||
|
onChange={(e) => setLanguage(e.target.value)}
|
||||||
|
className={inputClass}
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
>
|
||||||
|
{LANGUAGES.map((lang) => (
|
||||||
|
<option key={lang.value} value={lang.value}>
|
||||||
|
{lang.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-neutral-500">
|
||||||
|
Anweisungen (optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={instructions}
|
||||||
|
onChange={(e) => setInstructions(e.target.value)}
|
||||||
|
placeholder="z.B. 'Fokus auf Patentanspruch 1, besonders die technischen Merkmale...'"
|
||||||
|
rows={3}
|
||||||
|
className={inputClass}
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!templateType || 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" />
|
||||||
|
Dokument wird erstellt...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
KI-Entwurf erstellen
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{mutation.isError && (
|
||||||
|
<div className="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||||
|
Fehler beim Erstellen des Entwurfs. Bitte versuchen Sie es erneut.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mutation.data && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="text-sm font-medium text-neutral-900">
|
||||||
|
{mutation.data.title}
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<Check className="h-3.5 w-3.5 text-emerald-500" />
|
||||||
|
Kopiert
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
Kopieren
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Download className="h-3.5 w-3.5" />
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre className="max-h-[600px] overflow-auto whitespace-pre-wrap rounded-md border border-neutral-200 bg-neutral-50 p-4 text-sm text-neutral-800">
|
||||||
|
{mutation.data.content}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
183
frontend/src/components/ai/SimilarCaseFinder.tsx
Normal file
183
frontend/src/components/ai/SimilarCaseFinder.tsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -329,3 +329,81 @@ export interface ExtractionResponse {
|
|||||||
deadlines: ExtractedDeadline[];
|
deadlines: ExtractedDeadline[];
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AI Document Drafting
|
||||||
|
|
||||||
|
export interface DocumentDraft {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DraftDocumentRequest {
|
||||||
|
case_id: string;
|
||||||
|
template_type: string;
|
||||||
|
instructions: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TEMPLATE_TYPES: Record<string, string> = {
|
||||||
|
klageschrift: "Klageschrift",
|
||||||
|
klageerwiderung: "Klageerwiderung",
|
||||||
|
abmahnung: "Abmahnung",
|
||||||
|
schriftsatz: "Schriftsatz",
|
||||||
|
berufung: "Berufungsschrift",
|
||||||
|
antrag: "Antrag",
|
||||||
|
stellungnahme: "Stellungnahme",
|
||||||
|
gutachten: "Gutachten",
|
||||||
|
vertrag: "Vertrag",
|
||||||
|
vollmacht: "Vollmacht",
|
||||||
|
upc_claim: "UPC Statement of Claim",
|
||||||
|
upc_defence: "UPC Statement of Defence",
|
||||||
|
upc_counterclaim: "UPC Counterclaim for Revocation",
|
||||||
|
upc_injunction: "UPC Provisional Measures",
|
||||||
|
};
|
||||||
|
|
||||||
|
// AI Case Strategy
|
||||||
|
|
||||||
|
export interface StrategyStep {
|
||||||
|
priority: "high" | "medium" | "low";
|
||||||
|
action: string;
|
||||||
|
reasoning: string;
|
||||||
|
deadline?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RiskItem {
|
||||||
|
level: "high" | "medium" | "low";
|
||||||
|
risk: string;
|
||||||
|
mitigation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimelineItem {
|
||||||
|
date: string;
|
||||||
|
event: string;
|
||||||
|
importance: "critical" | "important" | "routine";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StrategyRecommendation {
|
||||||
|
summary: string;
|
||||||
|
next_steps: StrategyStep[];
|
||||||
|
risk_assessment: RiskItem[];
|
||||||
|
timeline: TimelineItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI Similar Case Finder
|
||||||
|
|
||||||
|
export interface SimilarCase {
|
||||||
|
case_number: string;
|
||||||
|
title: string;
|
||||||
|
court: string;
|
||||||
|
date: string;
|
||||||
|
relevance: number;
|
||||||
|
explanation: string;
|
||||||
|
key_holdings: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SimilarCasesResponse {
|
||||||
|
cases: SimilarCase[];
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user