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>
52 lines
1.5 KiB
TypeScript
52 lines
1.5 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef } from 'react'
|
|
import type { AdvisorTurn } from './useAdvisorStream'
|
|
import { StickyQuestion } from './StickyQuestion'
|
|
import { TurnView } from './TurnView'
|
|
import { AdvisorEmptyState } from './EmptyState'
|
|
|
|
/**
|
|
* The Evidence Workspace body: a pinned "last question" + a scrollable history of turns, each
|
|
* showing the answer alongside its sources / figures / footnotes. Scroll up to revisit a past
|
|
* answer with its full evidence.
|
|
*/
|
|
export function EvidenceWorkspace({
|
|
turns,
|
|
exampleQuestions,
|
|
onExample,
|
|
}: {
|
|
turns: AdvisorTurn[]
|
|
exampleQuestions: string[]
|
|
onExample: (q: string) => void
|
|
}) {
|
|
const endRef = useRef<HTMLDivElement>(null)
|
|
const latest = turns[turns.length - 1]
|
|
|
|
// Scroll to the newest turn when a question is added (not on every streamed token,
|
|
// so the user can scroll up to review history while the answer streams).
|
|
useEffect(() => {
|
|
endRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
}, [turns.length])
|
|
|
|
if (turns.length === 0) {
|
|
return (
|
|
<div className="flex-1 overflow-y-auto bg-gray-50">
|
|
<AdvisorEmptyState exampleQuestions={exampleQuestions} onExampleClick={onExample} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="flex-1 overflow-y-auto bg-gray-50">
|
|
{latest && <StickyQuestion question={latest.question} />}
|
|
<div className="space-y-4 p-4">
|
|
{turns.map((t, i) => (
|
|
<TurnView key={t.id} turn={t} showQuestion={i !== turns.length - 1} />
|
|
))}
|
|
<div ref={endRef} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|