feat(advisor): v3 Clarity Gate — Case model + clarify/answer contract, [n] citations

Builds the FE against the SDK<->FE Clarity-Gate contract (board 2026-07-01 /
advisor-clarity-gate-contract). The advisor is now a CASE, not a chat:
- Request {question, context?}; response {mode: clarify|answer, clarity, general_answer,
  answer, evidence, citations, visual_evidence, footnotes}.
- clarify mode: short L1 general answer (marked "allgemeine Definition, ohne Rechtsquelle")
  + domain context chips; picking a chip re-runs the case scoped (-> answer).
- answer mode: markdown answer with clickable [n] citation markers coupled to evidence
  cards (highlight + scroll), evidence grouped by document family, visual_evidence
  (visual_type), footnotes, honest summary counts (no trust score).
- FE never parses the answer for structure — only the deliberate [n] markers, mapped via
  citations[]. New: contract.ts, useAdvisorCase, useCitationHighlight, ClarifyView,
  EvidenceUnitCard, VisualEvidencePane, CaseView. Removed the v2 stream/chat components.

NOT deployed: FE shape-switch (JSON modes) must deploy TOGETHER with the SDK endpoint
delivering the contract (board deploy-coupling). Proxy/route.ts unchanged (SDK-owned).
tsc clean, 16 vitest (incl. clarify+answer fixtures), check-loc 0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-07-01 11:31:28 +02:00
parent 591cae5ebc
commit f9b7ba2424
20 changed files with 671 additions and 453 deletions
@@ -0,0 +1,70 @@
'use client'
import { ExternalLink, Image as ImageIcon } from 'lucide-react'
import type { VisualEvidence } from '@/lib/sdk/advisor/contract'
import { PaneHeader } from './PaneHeader'
function VisualCard({ v }: { v: VisualEvidence }) {
const canOpen = !!v.image_ref && /^https?:\/\//i.test(v.image_ref)
return (
<div className="rounded-lg border border-gray-200 bg-white p-2.5">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="text-xs font-semibold text-gray-900">{v.caption || v.visual_type}</div>
<div className="mt-0.5 flex flex-wrap items-center gap-1 text-[11px] text-gray-500">
<span className="rounded bg-gray-100 px-1 text-[10px] uppercase tracking-wide text-gray-500">
{v.visual_type}
</span>
<span>Quelle: {v.document}</span>
</div>
</div>
{canOpen && (
<a
href={v.image_ref}
target="_blank"
rel="noopener noreferrer"
className="flex flex-shrink-0 items-center gap-0.5 rounded px-1.5 py-0.5 text-[11px] font-medium text-indigo-600 hover:bg-indigo-50"
>
<ExternalLink className="h-3 w-3" />
Original anzeigen
</a>
)}
</div>
{canOpen ? (
<a href={v.image_ref} target="_blank" rel="noopener noreferrer" className="mt-1.5 block">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={v.image_ref}
alt={v.caption || v.visual_type}
loading="lazy"
className="max-h-44 w-full rounded border border-gray-100 object-contain"
/>
</a>
) : (
<div className="mt-1.5 flex items-center justify-center rounded border border-dashed border-gray-200 bg-gray-50 px-3 py-5 text-[11px] text-gray-400">
Original-Darstellung folgt
</div>
)}
{v.vision_summary && <p className="mt-1.5 text-[11px] italic text-gray-500">{v.vision_summary}</p>}
</div>
)
}
/** Visual evidence (C8) — diagrams/figures, rendered only when present. */
export function VisualEvidencePane({ items }: { items: VisualEvidence[] }) {
if (items.length === 0) return null
return (
<section>
<PaneHeader
icon={<ImageIcon className="h-3.5 w-3.5 text-gray-500" />}
title="Diagramme & Abbildungen"
count={items.length}
/>
<div className="space-y-1.5">
{items.map((v) => (
<VisualCard key={v.visual_id} v={v} />
))}
</div>
</section>
)
}