49171e841f
Rebuilds the Compliance Advisor floating widget from a plain chat into an Evidence
Workspace: pinned last question, markdown-rendered answer (clean prose), and separate
panes for Sources (hierarchical Knowledge Units), Figures (C8, conditional) and
Footnotes (C-FN), plus a stats bar (Quellen/Regelwerke/Diagramme/Fußnoten). Scrollable
turn history; stays a floating icon on every SDK page.
Architecture (user direction): the frontend renders ONLY structured evidence and NEVER
parses the answer text. The proxy now returns a JSON AdvisorEvidenceMeta line followed
by the streamed markdown answer; advisor-rag exposes structured results; an adapter maps
RAG/compiler output to the frontend envelope. Figures/footnotes wire in once the
RAG-ingestion contract lands (requested on the board) — figures pane is conditional.
- lib/sdk/advisor/{evidence,evidence-adapter}.ts (+ adapter test, 7 cases)
- components/sdk/advisor/* panes + in-house safe Markdown (no new dep, no dangerouslySetInnerHTML) + test
- useAdvisorStream (meta-line parse + streamed answer) + useAdvisorEmail (escaped)
- proxy: evidence-meta-v1 envelope + clean-prose prompt (no inline citations)
- tsc clean, 11 vitest pass, check-loc 0. ESLint not installed in this node_modules -> CI lints on push.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
104 lines
3.7 KiB
TypeScript
104 lines
3.7 KiB
TypeScript
// Structured evidence contract for the Compliance Advisor "Evidence Workspace".
|
|
//
|
|
// HARD RULE (architecture): the frontend renders ONLY these structured fields and
|
|
// NEVER parses the answer text. All structure (sources, figures, footnotes) is owned
|
|
// by the SDK/compiler (C-stages) and surfaced as data. The proxy is the adapter that
|
|
// fills this envelope from RAG/compiler output. See memory: advisor-evidence-workspace-no-parse.
|
|
|
|
/** A regulation / document reference (CRA, EDPB WP248, MaschinenVO, ...). */
|
|
export interface RegulationRef {
|
|
code: string // canonical id, e.g. "cra", "edpb_wp248", "maschinenvo"
|
|
name?: string // full name
|
|
short?: string // short label shown in the card header
|
|
}
|
|
|
|
/** Openable targets for an evidence item — present only when the SDK can resolve them. */
|
|
export interface OpenTargets {
|
|
originalUrl?: string // original text / source_url
|
|
chunkId?: string // retrieved chunk
|
|
footnoteId?: string // C-FN
|
|
figureId?: string // C8
|
|
}
|
|
|
|
/**
|
|
* A retrieved source as a hierarchical Knowledge Unit, mirroring the compiler:
|
|
* Regelwerk -> Section (C1/C2) -> Paragraph -> Footnote (C-FN).
|
|
* Rendered as a card, not a text-list line. E.g. "EDPB WP248 / Kapitel III.B / Fußnote 17".
|
|
*/
|
|
export interface KnowledgeUnit {
|
|
id: string
|
|
regulation: RegulationRef
|
|
section?: string // "Annex I" / "Kapitel III.B" / "Anhang III"
|
|
subsection?: string // "Abschnitt 2.3"
|
|
paragraph?: string // Absatz / paragraph
|
|
footnoteRef?: string // "Fußnote 17" when this unit IS a footnote-backed source
|
|
label?: string // pre-formatted citation fallback, e.g. "BDSG § 38 Abs. 1"
|
|
score?: number // retrieval score (optional)
|
|
snippet?: string // short passage preview (optional) — lets the user peek the cited text
|
|
open?: OpenTargets
|
|
}
|
|
|
|
/** A figure (C8) as a Knowledge Unit — never a bare image. Only present when figures exist. */
|
|
export interface FigureUnit {
|
|
id: string // figure_id
|
|
label: string // "Abbildung 3"
|
|
caption?: string // "PDCA-Zyklus"
|
|
topic?: string
|
|
source: RegulationRef // "EDPB ..."
|
|
section?: string
|
|
visionSummary?: string // vision/LLM description of the figure
|
|
imageUrl?: string // Playwright PNG; undefined until the RAG-ingestion contract delivers it
|
|
}
|
|
|
|
/** A footnote (C-FN) as a first-class evidence item. */
|
|
export interface FootnoteUnit {
|
|
id: string
|
|
ref: string // "Fußnote 17"
|
|
source: RegulationRef
|
|
section?: string
|
|
text?: string
|
|
}
|
|
|
|
/** Counts for the stats bar above the answer ("Diese Antwort basiert auf N Quellen"). */
|
|
export interface AdvisorStats {
|
|
sources: number
|
|
regulations: number // distinct Regelwerke
|
|
figures: number
|
|
footnotes: number
|
|
}
|
|
|
|
/**
|
|
* Meta sent by the proxy FIRST (one JSON line), then the answer streams as tokens.
|
|
* RAG runs before the LLM, so all evidence is known up front and the panes render
|
|
* immediately while the answer streams in.
|
|
*/
|
|
export interface AdvisorEvidenceMeta {
|
|
stats: AdvisorStats
|
|
sources: KnowledgeUnit[]
|
|
figures: FigureUnit[]
|
|
footnotes: FootnoteUnit[]
|
|
relatedDocs?: KnowledgeUnit[]
|
|
}
|
|
|
|
/** The full evidence a single answer turn holds (meta + the streamed answer markdown). */
|
|
export interface AdvisorEvidence extends AdvisorEvidenceMeta {
|
|
answer: string // markdown prose, NO inline citations (sources live in the pane)
|
|
}
|
|
|
|
export function emptyStats(): AdvisorStats {
|
|
return { sources: 0, regulations: 0, figures: 0, footnotes: 0 }
|
|
}
|
|
|
|
/** Pure derivation of the stats bar from the evidence items (no parsing of answer text). */
|
|
export function deriveStats(
|
|
e: Pick<AdvisorEvidenceMeta, 'sources' | 'figures' | 'footnotes'>,
|
|
): AdvisorStats {
|
|
const regulations = new Set(e.sources.map((s) => s.regulation.code))
|
|
return {
|
|
sources: e.sources.length,
|
|
regulations: regulations.size,
|
|
figures: e.figures.length,
|
|
footnotes: e.footnotes.length,
|
|
}
|
|
}
|