Files
breakpilot-compliance/admin-compliance/components/sdk/advisor/CaseView.tsx
T
Benjamin Admin f9b7ba2424 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>
2026-07-01 11:31:28 +02:00

76 lines
2.7 KiB
TypeScript

'use client'
import type { AdvisorResponse } from '@/lib/sdk/advisor/contract'
import type { AdvisorCase } from './useAdvisorCase'
import { ClarifyView } from './ClarifyView'
import { EvidenceSummary } from './EvidenceSummary'
import { EvidencePane } from './EvidencePane'
import { VisualEvidencePane } from './VisualEvidencePane'
import { FootnotesPane } from './FootnotesPane'
import { Markdown } from './Markdown'
import { useCitationHighlight } from './useCitationHighlight'
export function LoadingDots() {
return (
<div className="flex space-x-1 px-1 py-2" aria-label="Antwort wird erstellt">
<span className="h-2 w-2 animate-bounce rounded-full bg-gray-400" />
<span className="h-2 w-2 animate-bounce rounded-full bg-gray-400" style={{ animationDelay: '0.1s' }} />
<span className="h-2 w-2 animate-bounce rounded-full bg-gray-400" style={{ animationDelay: '0.2s' }} />
</div>
)
}
export function ErrorBox({ msg }: { msg?: string }) {
return (
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
{msg || 'Verbindung fehlgeschlagen'}
</div>
)
}
/** Answer mode body (stacked): summary + answer (with [n] coupling) + evidence/visual/footnotes. */
export function AnswerBody({ response }: { response: AdvisorResponse }) {
const { highlightedId, cite } = useCitationHighlight(response.citations)
return (
<div className="space-y-3">
<EvidenceSummary response={response} />
<div className="rounded-lg border border-gray-200 bg-white px-3 py-2">
<Markdown content={response.answer || ''} citations={cite} />
</div>
<EvidencePane evidence={response.evidence} highlightedId={highlightedId} />
<VisualEvidencePane items={response.visual_evidence} />
<FootnotesPane footnotes={response.footnotes} />
</div>
)
}
/** One case rendered stacked (narrow mode). Clarify -> L1 + chips; answer -> full evidence body. */
export function CaseView({
c,
onSelectContext,
busy,
showQuestion,
}: {
c: AdvisorCase
onSelectContext: (ctx: string) => void
busy: boolean
showQuestion?: boolean
}) {
const r = c.response
return (
<div className="space-y-2 border-b border-gray-100 pb-4 last:border-0">
{showQuestion && (
<div className="text-xs text-gray-500">
<span className="font-medium text-gray-400">Frage:</span> {c.question}
</div>
)}
{c.status === 'loading' && <LoadingDots />}
{c.status === 'error' && <ErrorBox msg={c.error} />}
{r && r.mode === 'clarify' && (
<ClarifyView response={r} onSelectContext={onSelectContext} busy={busy} />
)}
{r && r.mode === 'answer' && <AnswerBody response={r} />}
</div>
)
}