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:
@@ -2,11 +2,17 @@
|
||||
|
||||
// Minimal, SAFE markdown -> React renderer. No dangerouslySetInnerHTML, no dependency.
|
||||
// Covers the subset LLMs emit: headings, bold, italic, inline code, fenced code, ul/ol, links.
|
||||
// (The Evidence Workspace renders citations in a separate pane, so links are rarely needed.)
|
||||
// Plus deliberate [n] citation markers (mapped via `citations`, NOT parsed for structure).
|
||||
|
||||
const INLINE_RE = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*\s][^*]*\*|_[^_]+_|\[[^\]]+\]\([^)]+\))/g
|
||||
export interface CiteHandler {
|
||||
count: number
|
||||
onSelect: (n: number) => void
|
||||
}
|
||||
|
||||
function renderInline(text: string, kp: string): React.ReactNode[] {
|
||||
const INLINE_RE =
|
||||
/(`[^`]+`|\*\*[^*]+\*\*|\*[^*\s][^*]*\*|_[^_]+_|\[[^\]]+\]\([^)]+\)|\[\d+\])/g
|
||||
|
||||
function renderInline(text: string, kp: string, cite?: CiteHandler): React.ReactNode[] {
|
||||
const nodes: React.ReactNode[] = []
|
||||
let last = 0
|
||||
let idx = 0
|
||||
@@ -30,6 +36,23 @@ function renderInline(text: string, kp: string): React.ReactNode[] {
|
||||
)
|
||||
} else if (tok.startsWith('*') || tok.startsWith('_')) {
|
||||
nodes.push(<em key={key}>{tok.slice(1, -1)}</em>)
|
||||
} else if (/^\[\d+\]$/.test(tok)) {
|
||||
const n = parseInt(tok.slice(1, -1), 10)
|
||||
if (cite && n >= 1 && n <= cite.count) {
|
||||
nodes.push(
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => cite.onSelect(n)}
|
||||
className="mx-0.5 align-super text-[10px] font-semibold text-indigo-600 hover:underline"
|
||||
title={`Beleg ${n} anzeigen`}
|
||||
>
|
||||
[{n}]
|
||||
</button>,
|
||||
)
|
||||
} else {
|
||||
nodes.push(tok)
|
||||
}
|
||||
} else {
|
||||
const mm = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(tok)
|
||||
if (mm && /^https?:\/\//i.test(mm[2])) {
|
||||
@@ -54,8 +77,8 @@ function renderInline(text: string, kp: string): React.ReactNode[] {
|
||||
return nodes
|
||||
}
|
||||
|
||||
function Heading({ level, kp, text }: { level: number; kp: string; text: string }) {
|
||||
const children = renderInline(text, kp)
|
||||
function Heading({ level, kp, text, cite }: { level: number; kp: string; text: string; cite?: CiteHandler }) {
|
||||
const children = renderInline(text, kp, cite)
|
||||
if (level <= 1) return <h3 className="mb-1 mt-3 text-base font-bold text-gray-900">{children}</h3>
|
||||
if (level === 2) return <h4 className="mb-1 mt-3 text-sm font-bold text-gray-900">{children}</h4>
|
||||
return <h5 className="mb-1 mt-2 text-sm font-semibold text-gray-800">{children}</h5>
|
||||
@@ -65,13 +88,13 @@ const UL_RE = /^\s*[-*]\s+/
|
||||
const OL_RE = /^\s*\d+\.\s+/
|
||||
const H_RE = /^(#{1,6})\s+(.*)$/
|
||||
|
||||
export function Markdown({ content }: { content: string }) {
|
||||
export function Markdown({ content, citations }: { content: string; citations?: CiteHandler }) {
|
||||
const lines = (content || '').replace(/\r\n/g, '\n').split('\n')
|
||||
const blocks: React.ReactNode[] = []
|
||||
let i = 0
|
||||
while (i < lines.length) {
|
||||
const line = lines[i]
|
||||
const key = `b${blocks.length}` // unique per pushed block (blocks.length is the next index)
|
||||
const key = `b${blocks.length}`
|
||||
|
||||
if (line.trim().startsWith('```')) {
|
||||
const buf: string[] = []
|
||||
@@ -97,7 +120,7 @@ export function Markdown({ content }: { content: string }) {
|
||||
}
|
||||
const h = H_RE.exec(line)
|
||||
if (h) {
|
||||
blocks.push(<Heading key={key} kp={key} level={h[1].length} text={h[2]} />)
|
||||
blocks.push(<Heading key={key} kp={key} level={h[1].length} text={h[2]} cite={citations} />)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
@@ -110,7 +133,7 @@ export function Markdown({ content }: { content: string }) {
|
||||
blocks.push(
|
||||
<ul key={key} className="my-1.5 ml-4 list-disc space-y-1 text-gray-700">
|
||||
{items.map((it, k) => (
|
||||
<li key={k}>{renderInline(it, `${key}-${k}`)}</li>
|
||||
<li key={k}>{renderInline(it, `${key}-${k}`, citations)}</li>
|
||||
))}
|
||||
</ul>,
|
||||
)
|
||||
@@ -125,7 +148,7 @@ export function Markdown({ content }: { content: string }) {
|
||||
blocks.push(
|
||||
<ol key={key} className="my-1.5 ml-5 list-decimal space-y-1 text-gray-700">
|
||||
{items.map((it, k) => (
|
||||
<li key={k}>{renderInline(it, `${key}-${k}`)}</li>
|
||||
<li key={k}>{renderInline(it, `${key}-${k}`, citations)}</li>
|
||||
))}
|
||||
</ol>,
|
||||
)
|
||||
@@ -145,7 +168,7 @@ export function Markdown({ content }: { content: string }) {
|
||||
}
|
||||
blocks.push(
|
||||
<p key={key} className="my-1.5 leading-relaxed text-gray-700">
|
||||
{renderInline(para.join(' '), key)}
|
||||
{renderInline(para.join(' '), key, citations)}
|
||||
</p>,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user