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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user