@the-syllabus/analysis-renderers 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cells/RelationshipCardCell.d.ts +10 -0
- package/dist/cells/RelationshipCardCell.d.ts.map +1 -0
- package/dist/cells/RelationshipCardCell.js +91 -0
- package/dist/cells/RelationshipCardCell.js.map +1 -0
- package/dist/cells/TacticCardCell.d.ts +12 -0
- package/dist/cells/TacticCardCell.d.ts.map +1 -0
- package/dist/cells/TacticCardCell.js +77 -0
- package/dist/cells/TacticCardCell.js.map +1 -0
- package/dist/cells/TemplateCardCell.d.ts +29 -0
- package/dist/cells/TemplateCardCell.d.ts.map +1 -0
- package/dist/cells/TemplateCardCell.js +202 -0
- package/dist/cells/TemplateCardCell.js.map +1 -0
- package/dist/cells/index.d.ts +15 -0
- package/dist/cells/index.d.ts.map +1 -0
- package/dist/cells/index.js +85 -0
- package/dist/cells/index.js.map +1 -0
- package/dist/components/ConditionCards.d.ts +18 -0
- package/dist/components/ConditionCards.d.ts.map +1 -0
- package/dist/components/ConditionCards.js +28 -0
- package/dist/components/ConditionCards.js.map +1 -0
- package/dist/components/EvidenceTrail.d.ts +54 -0
- package/dist/components/EvidenceTrail.d.ts.map +1 -0
- package/dist/components/EvidenceTrail.js +98 -0
- package/dist/components/EvidenceTrail.js.map +1 -0
- package/dist/dispatch/SubRendererDispatch.d.ts +39 -0
- package/dist/dispatch/SubRendererDispatch.d.ts.map +1 -0
- package/dist/dispatch/SubRendererDispatch.js +153 -0
- package/dist/dispatch/SubRendererDispatch.js.map +1 -0
- package/dist/hooks/useProseExtraction.d.ts +38 -0
- package/dist/hooks/useProseExtraction.d.ts.map +1 -0
- package/dist/hooks/useProseExtraction.js +93 -0
- package/dist/hooks/useProseExtraction.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/provenance/ProvenanceSectionIcon.d.ts +15 -0
- package/dist/provenance/ProvenanceSectionIcon.d.ts.map +1 -0
- package/dist/provenance/ProvenanceSectionIcon.js +11 -0
- package/dist/provenance/ProvenanceSectionIcon.js.map +1 -0
- package/dist/renderers/AccordionRenderer.d.ts +29 -0
- package/dist/renderers/AccordionRenderer.d.ts.map +1 -0
- package/dist/renderers/AccordionRenderer.js +315 -0
- package/dist/renderers/AccordionRenderer.js.map +1 -0
- package/dist/renderers/CardGridRenderer.d.ts +24 -0
- package/dist/renderers/CardGridRenderer.d.ts.map +1 -0
- package/dist/renderers/CardGridRenderer.js +321 -0
- package/dist/renderers/CardGridRenderer.js.map +1 -0
- package/dist/renderers/CardRenderer.d.ts +27 -0
- package/dist/renderers/CardRenderer.d.ts.map +1 -0
- package/dist/renderers/CardRenderer.js +337 -0
- package/dist/renderers/CardRenderer.js.map +1 -0
- package/dist/renderers/IdeaEvolutionRenderer.d.ts +16 -0
- package/dist/renderers/IdeaEvolutionRenderer.d.ts.map +1 -0
- package/dist/renderers/IdeaEvolutionRenderer.js +187 -0
- package/dist/renderers/IdeaEvolutionRenderer.js.map +1 -0
- package/dist/renderers/ProseRenderer.d.ts +10 -0
- package/dist/renderers/ProseRenderer.d.ts.map +1 -0
- package/dist/renderers/ProseRenderer.js +42 -0
- package/dist/renderers/ProseRenderer.js.map +1 -0
- package/dist/renderers/RawJsonRenderer.d.ts +8 -0
- package/dist/renderers/RawJsonRenderer.d.ts.map +1 -0
- package/dist/renderers/RawJsonRenderer.js +17 -0
- package/dist/renderers/RawJsonRenderer.js.map +1 -0
- package/dist/renderers/StatSummaryRenderer.d.ts +12 -0
- package/dist/renderers/StatSummaryRenderer.d.ts.map +1 -0
- package/dist/renderers/StatSummaryRenderer.js +93 -0
- package/dist/renderers/StatSummaryRenderer.js.map +1 -0
- package/dist/renderers/SynthesisRenderer.d.ts +15 -0
- package/dist/renderers/SynthesisRenderer.d.ts.map +1 -0
- package/dist/renderers/SynthesisRenderer.js +60 -0
- package/dist/renderers/SynthesisRenderer.js.map +1 -0
- package/dist/renderers/TableRenderer.d.ts +19 -0
- package/dist/renderers/TableRenderer.d.ts.map +1 -0
- package/dist/renderers/TableRenderer.js +273 -0
- package/dist/renderers/TableRenderer.js.map +1 -0
- package/dist/styles/accordion.css +376 -0
- package/dist/styles/index.css +5 -0
- package/dist/styles/renderers.css +1049 -0
- package/dist/sub-renderers/SubRenderers.d.ts +73 -0
- package/dist/sub-renderers/SubRenderers.d.ts.map +1 -0
- package/dist/sub-renderers/SubRenderers.js +2462 -0
- package/dist/sub-renderers/SubRenderers.js.map +1 -0
- package/dist/tokens/DesignTokenContext.d.ts +40 -0
- package/dist/tokens/DesignTokenContext.d.ts.map +1 -0
- package/dist/tokens/DesignTokenContext.js +408 -0
- package/dist/tokens/DesignTokenContext.js.map +1 -0
- package/dist/types/designTokens.d.ts +220 -0
- package/dist/types/designTokens.d.ts.map +1 -0
- package/dist/types/designTokens.js +8 -0
- package/dist/types/designTokens.js.map +1 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/styles.d.ts +38 -0
- package/dist/types/styles.d.ts.map +1 -0
- package/dist/types/styles.js +14 -0
- package/dist/types/styles.js.map +1 -0
- package/dist/utils/tokenFlattener.d.ts +14 -0
- package/dist/utils/tokenFlattener.d.ts.map +1 -0
- package/dist/utils/tokenFlattener.js +56 -0
- package/dist/utils/tokenFlattener.js.map +1 -0
- package/package.json +31 -0
- package/src/cells/TemplateCardCell.tsx +439 -0
- package/src/cells/index.ts +98 -0
- package/src/components/ConditionCards.tsx +109 -0
- package/src/components/EvidenceTrail.tsx +203 -0
- package/src/dispatch/SubRendererDispatch.tsx +282 -0
- package/src/hooks/useProseExtraction.ts +125 -0
- package/src/index.ts +82 -0
- package/src/provenance/ProvenanceSectionIcon.tsx +19 -0
- package/src/renderers/AccordionRenderer.tsx +609 -0
- package/src/renderers/CardGridRenderer.tsx +608 -0
- package/src/renderers/CardRenderer.tsx +517 -0
- package/src/renderers/ProseRenderer.tsx +85 -0
- package/src/renderers/RawJsonRenderer.tsx +37 -0
- package/src/renderers/StatSummaryRenderer.tsx +182 -0
- package/src/renderers/TableRenderer.tsx +470 -0
- package/src/styles/accordion.css +376 -0
- package/src/styles/index.css +5 -0
- package/src/styles/renderers.css +1049 -0
- package/src/sub-renderers/SubRenderers.tsx +3487 -0
- package/src/tokens/DesignTokenContext.tsx +502 -0
- package/src/types/designTokens.ts +236 -0
- package/src/types/index.ts +53 -0
- package/src/types/styles.ts +44 -0
- package/src/utils/tokenFlattener.ts +64 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EvidenceTrail — Reusable vertical chain of evidence steps.
|
|
3
|
+
*
|
|
4
|
+
* Renders a narrative progression with dot markers, gradient connectors,
|
|
5
|
+
* and quoted content. Each step can contain multiple items (with title,
|
|
6
|
+
* quote, citation) or a single text block.
|
|
7
|
+
*
|
|
8
|
+
* Two usage modes:
|
|
9
|
+
* 1. Direct — import EvidenceTrail and pass pre-built steps array
|
|
10
|
+
* 2. Sub-renderer — use EvidenceTrailSubRenderer with config-driven
|
|
11
|
+
* field mapping (registered in SubRenderers.tsx)
|
|
12
|
+
*
|
|
13
|
+
* CSS classes used (from renderers.css):
|
|
14
|
+
* .gen-evidence-trail, .gen-trail-chain, .gen-trail-step,
|
|
15
|
+
* .gen-trail-marker, .gen-trail-dot, .gen-trail-label,
|
|
16
|
+
* .gen-trail-content, .gen-trail-connector, .gen-trail-quote,
|
|
17
|
+
* .gen-quote-mark, .gen-trail-cite, .ar-card-assessment
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import React from 'react';
|
|
21
|
+
|
|
22
|
+
// ── Public types ─────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export interface EvidenceTrailItem {
|
|
25
|
+
title?: string;
|
|
26
|
+
quote?: string;
|
|
27
|
+
cite?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EvidenceTrailStep {
|
|
31
|
+
label: string;
|
|
32
|
+
variant: 'prior' | 'current' | 'assessment';
|
|
33
|
+
items?: EvidenceTrailItem[];
|
|
34
|
+
text?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface EvidenceTrailProps {
|
|
38
|
+
steps: EvidenceTrailStep[];
|
|
39
|
+
accentColor?: string;
|
|
40
|
+
borderColor?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Core component ───────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export function EvidenceTrail({ steps, accentColor, borderColor }: EvidenceTrailProps) {
|
|
46
|
+
if (steps.length === 0) return null;
|
|
47
|
+
|
|
48
|
+
const accent = accentColor || 'var(--dt-text-muted)';
|
|
49
|
+
const border = borderColor || 'var(--dt-border-light)';
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="gen-trail-chain">
|
|
53
|
+
{steps.map((step, idx) => {
|
|
54
|
+
const prevStep = idx > 0 ? steps[idx - 1] : null;
|
|
55
|
+
const showConnector = Boolean(prevStep);
|
|
56
|
+
const isLast = step.variant === 'assessment';
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<React.Fragment key={idx}>
|
|
60
|
+
{/* Connector between steps */}
|
|
61
|
+
{showConnector && (
|
|
62
|
+
<div
|
|
63
|
+
className={`gen-trail-connector${isLast ? ' gen-trail-connector--final' : ''}`}
|
|
64
|
+
style={{
|
|
65
|
+
background: isLast
|
|
66
|
+
? border
|
|
67
|
+
: `linear-gradient(to bottom, ${border}, ${accent})`,
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
{/* Step */}
|
|
73
|
+
<div className={`gen-trail-step gen-trail-step--${step.variant}`}>
|
|
74
|
+
<div className="gen-trail-marker">
|
|
75
|
+
<span
|
|
76
|
+
className={`gen-trail-dot gen-trail-dot--${step.variant}`}
|
|
77
|
+
style={step.variant === 'current' ? { background: accent } : undefined}
|
|
78
|
+
/>
|
|
79
|
+
<span className="gen-trail-label">{step.label}</span>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="gen-trail-content">
|
|
82
|
+
{/* Multi-item steps (prior work, current evidence) */}
|
|
83
|
+
{step.items && step.items.map((item, i) => {
|
|
84
|
+
if (step.variant === 'prior') {
|
|
85
|
+
return (
|
|
86
|
+
<div key={i} className="gen-trail-ref">
|
|
87
|
+
{item.title && <span className="gen-ref-title">{item.title}</span>}
|
|
88
|
+
{item.quote && (
|
|
89
|
+
<blockquote className="gen-trail-quote">
|
|
90
|
+
<span className="gen-quote-mark">“</span>
|
|
91
|
+
{item.quote}
|
|
92
|
+
<span className="gen-quote-mark">”</span>
|
|
93
|
+
</blockquote>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
// current and other variants with items
|
|
99
|
+
return (
|
|
100
|
+
<blockquote
|
|
101
|
+
key={i}
|
|
102
|
+
className="gen-trail-quote gen-trail-quote--current"
|
|
103
|
+
style={{ borderLeftColor: border }}
|
|
104
|
+
>
|
|
105
|
+
<span className="gen-quote-mark">“</span>
|
|
106
|
+
{item.quote || item.title || ''}
|
|
107
|
+
<span className="gen-quote-mark">”</span>
|
|
108
|
+
{item.cite && <cite className="gen-trail-cite">{item.cite}</cite>}
|
|
109
|
+
</blockquote>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
|
|
113
|
+
{/* Single-text step (assessment) */}
|
|
114
|
+
{step.text && (
|
|
115
|
+
<p className="ar-card-assessment">{step.text}</p>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</React.Fragment>
|
|
120
|
+
);
|
|
121
|
+
})}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Sub-renderer wrapper (config-driven) ─────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Config shape for sub-renderer usage:
|
|
130
|
+
* steps: Array<{
|
|
131
|
+
* label: string; — display label ("Prior work")
|
|
132
|
+
* field: string; — data field to read from
|
|
133
|
+
* variant: string; — "prior" | "current" | "assessment"
|
|
134
|
+
* item_title_field?: string; — field within each item for title
|
|
135
|
+
* item_quote_field?: string; — field within each item for quote
|
|
136
|
+
* item_cite_field?: string; — field within each item for citation
|
|
137
|
+
* is_text?: boolean; — treat field as single text (not array)
|
|
138
|
+
* }>
|
|
139
|
+
* accent_color?: string;
|
|
140
|
+
* border_color?: string;
|
|
141
|
+
*/
|
|
142
|
+
export function EvidenceTrailSubRenderer({
|
|
143
|
+
data,
|
|
144
|
+
config,
|
|
145
|
+
}: {
|
|
146
|
+
data: unknown;
|
|
147
|
+
config: Record<string, unknown>;
|
|
148
|
+
}) {
|
|
149
|
+
const obj = (data && typeof data === 'object' && !Array.isArray(data))
|
|
150
|
+
? data as Record<string, unknown>
|
|
151
|
+
: {};
|
|
152
|
+
|
|
153
|
+
const stepConfigs = config.steps as Array<{
|
|
154
|
+
label: string;
|
|
155
|
+
field: string;
|
|
156
|
+
variant: string;
|
|
157
|
+
item_title_field?: string;
|
|
158
|
+
item_quote_field?: string;
|
|
159
|
+
item_cite_field?: string;
|
|
160
|
+
is_text?: boolean;
|
|
161
|
+
}> | undefined;
|
|
162
|
+
|
|
163
|
+
if (!stepConfigs || !Array.isArray(stepConfigs)) return null;
|
|
164
|
+
|
|
165
|
+
const steps: EvidenceTrailStep[] = [];
|
|
166
|
+
|
|
167
|
+
for (const sc of stepConfigs) {
|
|
168
|
+
const raw = obj[sc.field];
|
|
169
|
+
if (raw === undefined || raw === null || raw === '') continue;
|
|
170
|
+
|
|
171
|
+
const variant = (sc.variant || 'prior') as EvidenceTrailStep['variant'];
|
|
172
|
+
|
|
173
|
+
if (sc.is_text || typeof raw === 'string') {
|
|
174
|
+
steps.push({ label: sc.label, variant, text: String(raw) });
|
|
175
|
+
} else if (Array.isArray(raw)) {
|
|
176
|
+
const items: EvidenceTrailItem[] = raw.map((entry: unknown) => {
|
|
177
|
+
if (typeof entry === 'string') return { quote: entry };
|
|
178
|
+
if (typeof entry === 'object' && entry !== null) {
|
|
179
|
+
const e = entry as Record<string, unknown>;
|
|
180
|
+
return {
|
|
181
|
+
title: sc.item_title_field ? String(e[sc.item_title_field] || '') : undefined,
|
|
182
|
+
quote: sc.item_quote_field ? String(e[sc.item_quote_field] || '') : undefined,
|
|
183
|
+
cite: sc.item_cite_field ? String(e[sc.item_cite_field] || '') : undefined,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return { quote: String(entry) };
|
|
187
|
+
});
|
|
188
|
+
if (items.length > 0) {
|
|
189
|
+
steps.push({ label: sc.label, variant, items });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (steps.length === 0) return null;
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<EvidenceTrail
|
|
198
|
+
steps={steps}
|
|
199
|
+
accentColor={config.accent_color as string | undefined}
|
|
200
|
+
borderColor={config.border_color as string | undefined}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubRendererDispatch — Shared dispatch utilities for sub-renderer resolution.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from AccordionRenderer to be shared by both AccordionRenderer
|
|
5
|
+
* and CardRenderer. Provides:
|
|
6
|
+
* - Pre-render compatibility checking (data type vs renderer expectations)
|
|
7
|
+
* - Defense-in-depth fallback wrapper (catches empty output at layout time)
|
|
8
|
+
* - Generic recursive renderer for arbitrary data shapes
|
|
9
|
+
* - Enum color resolution via design tokens
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React, { useState, useLayoutEffect, useRef } from 'react';
|
|
13
|
+
import { resolveSubRenderer, autoDetectSubRenderer } from '../sub-renderers/SubRenderers';
|
|
14
|
+
import type { SubRendererProps } from '../types';
|
|
15
|
+
import { useDesignTokens } from '../tokens/DesignTokenContext';
|
|
16
|
+
import type { SemanticTriple } from '../types/designTokens';
|
|
17
|
+
|
|
18
|
+
// ── Pre-render compatibility check ──────────────────────
|
|
19
|
+
// Sub-renderers silently return null when data doesn't match their
|
|
20
|
+
// expectations (e.g. chip_grid given a string). This check prevents
|
|
21
|
+
// blank sections by falling through to auto-detection on mismatch.
|
|
22
|
+
|
|
23
|
+
export const REQUIRES_ARRAY = new Set([
|
|
24
|
+
'chip_grid', 'mini_card_list', 'timeline_strip',
|
|
25
|
+
'comparison_panel', 'definition_list',
|
|
26
|
+
'intensity_matrix', 'move_repertoire', 'grouped_card_list', 'rich_description_list',
|
|
27
|
+
]);
|
|
28
|
+
export const REQUIRES_OBJECT = new Set(['stat_row', 'phase_timeline', 'distribution_summary']);
|
|
29
|
+
|
|
30
|
+
export function isRendererCompatible(
|
|
31
|
+
rendererType: string,
|
|
32
|
+
data: unknown,
|
|
33
|
+
rendererConfig?: Record<string, unknown>,
|
|
34
|
+
): boolean {
|
|
35
|
+
if (REQUIRES_ARRAY.has(rendererType) && !Array.isArray(data)) return false;
|
|
36
|
+
if (REQUIRES_OBJECT.has(rendererType) && (typeof data !== 'object' || Array.isArray(data) || data === null)) return false;
|
|
37
|
+
// evidence_trail requires config.steps array — without it, always returns null
|
|
38
|
+
if (rendererType === 'evidence_trail' && (!rendererConfig?.steps || !Array.isArray(rendererConfig.steps))) return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Defense-in-depth fallback wrapper ────────────────────
|
|
43
|
+
// Even after the pre-render compatibility check, a sub-renderer might
|
|
44
|
+
// return null for reasons we can't predict (e.g. data is an array but
|
|
45
|
+
// items are wrong shape). This wrapper detects empty output via
|
|
46
|
+
// useLayoutEffect (before browser paint) and swaps in auto-detection.
|
|
47
|
+
|
|
48
|
+
export function SubRendererFallback({ Renderer, data, config, sectionKey }: {
|
|
49
|
+
Renderer: React.FC<SubRendererProps>;
|
|
50
|
+
data: unknown;
|
|
51
|
+
config: Record<string, unknown>;
|
|
52
|
+
sectionKey: string;
|
|
53
|
+
}) {
|
|
54
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
55
|
+
const [fallback, setFallback] = useState(false);
|
|
56
|
+
|
|
57
|
+
useLayoutEffect(() => {
|
|
58
|
+
if (ref.current && ref.current.innerHTML.trim() === '') {
|
|
59
|
+
console.warn(
|
|
60
|
+
`[SubRendererDispatch] Renderer produced empty output for section '${sectionKey}' — falling back to auto-detection`
|
|
61
|
+
);
|
|
62
|
+
setFallback(true);
|
|
63
|
+
}
|
|
64
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
65
|
+
|
|
66
|
+
if (fallback) {
|
|
67
|
+
const autoType = autoDetectSubRenderer(data);
|
|
68
|
+
const AutoComp = autoType ? resolveSubRenderer(autoType) : null;
|
|
69
|
+
if (AutoComp) {
|
|
70
|
+
return <AutoComp data={data} config={config} />;
|
|
71
|
+
}
|
|
72
|
+
return <GenericSectionRenderer data={data} />;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return <div ref={ref}><Renderer data={data} config={config} /></div>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Enum color resolution ───────────────────────────────
|
|
79
|
+
// Resolve enum-like string values to semantic token colors.
|
|
80
|
+
// Tries each semantic scale in order; returns first match or null.
|
|
81
|
+
const SEMANTIC_SCALES = ['severity', 'visibility', 'change', 'modality'];
|
|
82
|
+
|
|
83
|
+
export function resolveEnumColor(
|
|
84
|
+
getSemanticColor: (scale: string, level: string) => SemanticTriple | null,
|
|
85
|
+
value: string,
|
|
86
|
+
): { bg: string; text: string } | null {
|
|
87
|
+
for (const scale of SEMANTIC_SCALES) {
|
|
88
|
+
const result = getSemanticColor(scale, value);
|
|
89
|
+
if (result) return result;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Generic Section Renderer ────────────────────────────
|
|
95
|
+
// Recursively renders arbitrary structured data shapes:
|
|
96
|
+
// - string → paragraphs (with enum badge detection)
|
|
97
|
+
// - string[] → chip grid
|
|
98
|
+
// - object[] → mini-cards with key-value rendering
|
|
99
|
+
// - object → recursive render with indentation (with sub-renderer dispatch)
|
|
100
|
+
// - primitive → inline display
|
|
101
|
+
|
|
102
|
+
export function GenericSectionRenderer({ data, depth = 0, subRenderers }: {
|
|
103
|
+
data: unknown;
|
|
104
|
+
depth?: number;
|
|
105
|
+
subRenderers?: Record<string, { renderer_type: string; config?: Record<string, unknown> }>;
|
|
106
|
+
}) {
|
|
107
|
+
const { getSemanticColor } = useDesignTokens();
|
|
108
|
+
|
|
109
|
+
if (data === null || data === undefined) return null;
|
|
110
|
+
|
|
111
|
+
// String → paragraphs
|
|
112
|
+
if (typeof data === 'string') {
|
|
113
|
+
// Check if it looks like an enum value via semantic token lookup
|
|
114
|
+
if (data.length < 30) {
|
|
115
|
+
const enumStyle = resolveEnumColor(getSemanticColor, data);
|
|
116
|
+
if (enumStyle) {
|
|
117
|
+
return (
|
|
118
|
+
<span className="gen-enum-badge" style={{ backgroundColor: enumStyle.bg, color: enumStyle.text }}>
|
|
119
|
+
{data.replace(/_/g, ' ')}
|
|
120
|
+
</span>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return (
|
|
125
|
+
<div className="gen-field-text" style={{ marginBottom: 'var(--space-xs, 0.25rem)' }}>
|
|
126
|
+
{data.split('\n').map((p, i) => (
|
|
127
|
+
<p key={i}>{p}</p>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Number/boolean → inline
|
|
134
|
+
if (typeof data === 'number') {
|
|
135
|
+
return <span className="gen-number-value">{String(data)}</span>;
|
|
136
|
+
}
|
|
137
|
+
if (typeof data === 'boolean') {
|
|
138
|
+
const boolColor = getSemanticColor('severity', data ? 'low' : 'high');
|
|
139
|
+
return (
|
|
140
|
+
<span className="gen-enum-badge" style={{
|
|
141
|
+
backgroundColor: boolColor?.bg || 'rgba(34, 197, 94, 0.12)',
|
|
142
|
+
color: boolColor?.text || '#16a34a',
|
|
143
|
+
}}>
|
|
144
|
+
{String(data)}
|
|
145
|
+
</span>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Array of strings → chip grid
|
|
150
|
+
if (Array.isArray(data) && data.length > 0 && data.every(d => typeof d === 'string')) {
|
|
151
|
+
return (
|
|
152
|
+
<div className="gen-chip-grid">
|
|
153
|
+
{data.map((item, i) => (
|
|
154
|
+
<span key={i} className="gen-chip-inline">
|
|
155
|
+
{item}
|
|
156
|
+
</span>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Array of objects → mini-cards
|
|
163
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
164
|
+
return (
|
|
165
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-sm, 0.5rem)', margin: 'var(--space-xs, 0.25rem) 0' }}>
|
|
166
|
+
{data.map((item, i) => (
|
|
167
|
+
<GenericMiniCard key={i} data={item} depth={depth} />
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Empty array
|
|
174
|
+
if (Array.isArray(data) && data.length === 0) {
|
|
175
|
+
return <p className="gen-empty-list">None</p>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Object → key-value pairs (with optional sub-renderer dispatch)
|
|
179
|
+
if (typeof data === 'object') {
|
|
180
|
+
const obj = data as Record<string, unknown>;
|
|
181
|
+
const entries = Object.entries(obj).filter(([, v]) => v !== null && v !== undefined && v !== '');
|
|
182
|
+
if (entries.length === 0) return null;
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div className={depth > 0 ? 'gen-nested-content' : undefined}>
|
|
186
|
+
{entries.map(([key, value]) => {
|
|
187
|
+
// Check if a sub-renderer is configured for this key
|
|
188
|
+
const subHint = subRenderers?.[key];
|
|
189
|
+
if (subHint) {
|
|
190
|
+
const SubComp = resolveSubRenderer(subHint.renderer_type);
|
|
191
|
+
if (SubComp) {
|
|
192
|
+
return (
|
|
193
|
+
<div key={key} className="gen-field-row">
|
|
194
|
+
<div className="gen-field-label">
|
|
195
|
+
{key.replace(/_/g, ' ')}:
|
|
196
|
+
</div>
|
|
197
|
+
<SubComp data={value} config={subHint.config || {}} />
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div key={key} className="gen-field-row">
|
|
205
|
+
<span className="gen-field-label">
|
|
206
|
+
{key.replace(/_/g, ' ')}:
|
|
207
|
+
</span>
|
|
208
|
+
{typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ? (
|
|
209
|
+
<span className="gen-field-value-inline">
|
|
210
|
+
<GenericSectionRenderer data={value} depth={depth + 1} />
|
|
211
|
+
</span>
|
|
212
|
+
) : (
|
|
213
|
+
<div className="gen-field-value-block">
|
|
214
|
+
<GenericSectionRenderer data={value} depth={depth + 1} />
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
})}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Fallback
|
|
225
|
+
return <span style={{ fontSize: 'var(--type-body, 0.9375rem)' }}>{String(data)}</span>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function GenericMiniCard({ data, depth }: { data: unknown; depth: number }) {
|
|
229
|
+
const { getSemanticColor } = useDesignTokens();
|
|
230
|
+
|
|
231
|
+
if (typeof data !== 'object' || data === null) {
|
|
232
|
+
return <GenericSectionRenderer data={data} depth={depth + 1} />;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const obj = data as Record<string, unknown>;
|
|
236
|
+
const entries = Object.entries(obj).filter(([, v]) => v !== null && v !== undefined && v !== '');
|
|
237
|
+
if (entries.length === 0) return null;
|
|
238
|
+
|
|
239
|
+
// Heuristic: find a "name" or "title" field for the card header
|
|
240
|
+
const nameKey = entries.find(([k]) => ['name', 'term', 'title', 'commitment', 'cluster_name', 'channel', 'evidence_type'].includes(k));
|
|
241
|
+
const typeKey = entries.find(([k]) => ['type', 'centrality', 'drift_type', 'explicitness'].includes(k));
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<div className="gen-mini-card">
|
|
245
|
+
{/* Header: name + type badge */}
|
|
246
|
+
{nameKey && (
|
|
247
|
+
<div className="gen-mini-card-header">
|
|
248
|
+
<span className="gen-mini-card-name">
|
|
249
|
+
{String(nameKey[1])}
|
|
250
|
+
</span>
|
|
251
|
+
{typeKey && (
|
|
252
|
+
<GenericSectionRenderer data={typeKey[1]} depth={depth + 1} />
|
|
253
|
+
)}
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
|
|
257
|
+
{/* Remaining fields */}
|
|
258
|
+
{entries
|
|
259
|
+
.filter(([k]) => k !== nameKey?.[0] && k !== typeKey?.[0])
|
|
260
|
+
.map(([key, value]) => (
|
|
261
|
+
<div key={key} className="gen-mini-card-field">
|
|
262
|
+
<span className="gen-mini-card-label">
|
|
263
|
+
{key.replace(/_/g, ' ')}:
|
|
264
|
+
</span>
|
|
265
|
+
{typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ? (
|
|
266
|
+
<span className="gen-mini-card-value" style={{ marginLeft: 'var(--space-xs, 0.25rem)' }}>
|
|
267
|
+
{typeof value === 'string' && value.length < 30 && resolveEnumColor(getSemanticColor, value) ? (
|
|
268
|
+
<GenericSectionRenderer data={value} depth={depth + 1} />
|
|
269
|
+
) : (
|
|
270
|
+
String(value)
|
|
271
|
+
)}
|
|
272
|
+
</span>
|
|
273
|
+
) : (
|
|
274
|
+
<div style={{ marginTop: 'var(--space-2xs, 0.125rem)' }}>
|
|
275
|
+
<GenericSectionRenderer data={value} depth={depth + 1} />
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useProseExtraction — Detects prose-mode output and extracts structured data.
|
|
3
|
+
*
|
|
4
|
+
* Several workflow passes can produce output in "prose mode" (rich analytical
|
|
5
|
+
* narrative) instead of structured JSON. When this happens the data contains
|
|
6
|
+
* a `_prose_output` marker. This hook:
|
|
7
|
+
*
|
|
8
|
+
* 1. Detects the marker
|
|
9
|
+
* 2. Calls the presentation extraction endpoint to get structured data
|
|
10
|
+
* 3. Manages loading / error / extracted state
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const { data, loading, error, isProseMode } = useProseExtraction<Pass5Result>(
|
|
14
|
+
* result.pass5_tactics,
|
|
15
|
+
* result._job_id,
|
|
16
|
+
* 'tactics'
|
|
17
|
+
* );
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useState, useEffect } from 'react';
|
|
21
|
+
|
|
22
|
+
// Consumer apps set this via env var. Supports CRA and Next.js conventions.
|
|
23
|
+
const API_BASE =
|
|
24
|
+
(typeof process !== 'undefined' && process.env?.REACT_APP_API_URL) ||
|
|
25
|
+
(typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_API_URL) ||
|
|
26
|
+
'http://localhost:5555/api';
|
|
27
|
+
|
|
28
|
+
interface ProseMarker {
|
|
29
|
+
_prose_output: string;
|
|
30
|
+
_output_mode: 'prose';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ProseExtractionResult<T> {
|
|
34
|
+
/** The structured data — either raw (if not prose) or extracted */
|
|
35
|
+
data: T | null;
|
|
36
|
+
/** Whether extraction is in progress */
|
|
37
|
+
loading: boolean;
|
|
38
|
+
/** Extraction error message, if any */
|
|
39
|
+
error: string | null;
|
|
40
|
+
/** Whether the source data was prose mode */
|
|
41
|
+
isProseMode: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isProseMarker(value: unknown): value is ProseMarker {
|
|
45
|
+
return (
|
|
46
|
+
value != null &&
|
|
47
|
+
typeof value === 'object' &&
|
|
48
|
+
'_prose_output' in (value as Record<string, unknown>)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useProseExtraction<T>(
|
|
53
|
+
rawData: T | ProseMarker | undefined,
|
|
54
|
+
jobId: string | undefined,
|
|
55
|
+
presentEndpoint: string, // e.g. 'tactics', 'conditions', 'synthesis', 'functional'
|
|
56
|
+
options?: { apiPathPrefix?: string }
|
|
57
|
+
): ProseExtractionResult<T> {
|
|
58
|
+
const [extracted, setExtracted] = useState<T | null>(null);
|
|
59
|
+
const [loading, setLoading] = useState(false);
|
|
60
|
+
const [error, setError] = useState<string | null>(null);
|
|
61
|
+
|
|
62
|
+
const proseMode = isProseMarker(rawData);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
// Not prose mode — nothing to extract
|
|
66
|
+
if (!proseMode || !jobId) return;
|
|
67
|
+
// Already extracted
|
|
68
|
+
if (extracted) return;
|
|
69
|
+
|
|
70
|
+
let cancelled = false;
|
|
71
|
+
|
|
72
|
+
const fetchPresentation = async () => {
|
|
73
|
+
setLoading(true);
|
|
74
|
+
setError(null);
|
|
75
|
+
try {
|
|
76
|
+
const prefix = options?.apiPathPrefix || 'analysis/default';
|
|
77
|
+
const response = await fetch(
|
|
78
|
+
`${API_BASE}/${prefix}/${jobId}/present/${presentEndpoint}`,
|
|
79
|
+
{ method: 'POST' }
|
|
80
|
+
);
|
|
81
|
+
if (cancelled) return;
|
|
82
|
+
|
|
83
|
+
if (response.ok) {
|
|
84
|
+
const data = await response.json();
|
|
85
|
+
setExtracted(data.data as T);
|
|
86
|
+
} else {
|
|
87
|
+
const errData = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
|
88
|
+
setError(errData.detail || `Extraction failed (${response.status})`);
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
if (!cancelled) {
|
|
92
|
+
setError(`Network error: ${e}`);
|
|
93
|
+
}
|
|
94
|
+
} finally {
|
|
95
|
+
if (!cancelled) {
|
|
96
|
+
setLoading(false);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
fetchPresentation();
|
|
102
|
+
|
|
103
|
+
return () => {
|
|
104
|
+
cancelled = true;
|
|
105
|
+
};
|
|
106
|
+
}, [proseMode, jobId, presentEndpoint, extracted]);
|
|
107
|
+
|
|
108
|
+
// Not prose mode — return raw data directly
|
|
109
|
+
if (!proseMode) {
|
|
110
|
+
return {
|
|
111
|
+
data: (rawData as T) ?? null,
|
|
112
|
+
loading: false,
|
|
113
|
+
error: null,
|
|
114
|
+
isProseMode: false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Prose mode — return extracted data (or loading/error state)
|
|
119
|
+
return {
|
|
120
|
+
data: extracted,
|
|
121
|
+
loading,
|
|
122
|
+
error,
|
|
123
|
+
isProseMode: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @the-syllabus/analysis-renderers — Shared renderer components for analyzer-v2 view definitions.
|
|
3
|
+
*
|
|
4
|
+
* This package provides the complete rendering pipeline:
|
|
5
|
+
* - Container renderers (accordion, card_grid, prose, table, etc.)
|
|
6
|
+
* - Sub-renderers (chip_grid, mini_card_list, distribution_summary, etc.)
|
|
7
|
+
* - Cell renderers (template_card, default auto-classify, etc.)
|
|
8
|
+
* - Design token system (DesignTokenProvider + useDesignTokens hook)
|
|
9
|
+
* - Renderer registry (view_key → component resolution)
|
|
10
|
+
*
|
|
11
|
+
* CSS: import '@the-syllabus/analysis-renderers/styles' for all renderer styles.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ── Types ────────────────────────────────────────────────
|
|
15
|
+
export type {
|
|
16
|
+
RendererProps,
|
|
17
|
+
RendererComponent,
|
|
18
|
+
CellRendererProps,
|
|
19
|
+
CellRendererComponent,
|
|
20
|
+
SubRendererProps,
|
|
21
|
+
SubRendererComponent,
|
|
22
|
+
StyleOverrides,
|
|
23
|
+
DesignTokenSet,
|
|
24
|
+
SemanticTriple,
|
|
25
|
+
CategoricalItem,
|
|
26
|
+
PrimitiveTokens,
|
|
27
|
+
SurfaceTokens,
|
|
28
|
+
ScaleTokens,
|
|
29
|
+
SemanticTokens,
|
|
30
|
+
CategoricalTokens,
|
|
31
|
+
ComponentTokens,
|
|
32
|
+
} from './types';
|
|
33
|
+
export { getSO } from './types';
|
|
34
|
+
|
|
35
|
+
// ── Design Tokens ────────────────────────────────────────
|
|
36
|
+
export {
|
|
37
|
+
DesignTokenProvider,
|
|
38
|
+
useDesignTokens,
|
|
39
|
+
FALLBACK_TOKENS,
|
|
40
|
+
} from './tokens/DesignTokenContext';
|
|
41
|
+
|
|
42
|
+
// ── Utilities ────────────────────────────────────────────
|
|
43
|
+
export { flattenTokens } from './utils/tokenFlattener';
|
|
44
|
+
|
|
45
|
+
// ── Container Renderers ──────────────────────────────────
|
|
46
|
+
export { AccordionRenderer } from './renderers/AccordionRenderer';
|
|
47
|
+
export { CardGridRenderer } from './renderers/CardGridRenderer';
|
|
48
|
+
export { CardRenderer } from './renderers/CardRenderer';
|
|
49
|
+
export { ProseRenderer, formatProse } from './renderers/ProseRenderer';
|
|
50
|
+
export { TableRenderer } from './renderers/TableRenderer';
|
|
51
|
+
export { StatSummaryRenderer } from './renderers/StatSummaryRenderer';
|
|
52
|
+
export { RawJsonRenderer } from './renderers/RawJsonRenderer';
|
|
53
|
+
|
|
54
|
+
// ── Sub-Renderers ────────────────────────────────────────
|
|
55
|
+
export {
|
|
56
|
+
resolveSubRenderer,
|
|
57
|
+
autoDetectSubRenderer,
|
|
58
|
+
DistributionSummary,
|
|
59
|
+
} from './sub-renderers/SubRenderers';
|
|
60
|
+
|
|
61
|
+
// ── Sub-Renderer Dispatch ────────────────────────────────
|
|
62
|
+
export {
|
|
63
|
+
isRendererCompatible,
|
|
64
|
+
SubRendererFallback,
|
|
65
|
+
GenericSectionRenderer,
|
|
66
|
+
GenericMiniCard,
|
|
67
|
+
resolveEnumColor,
|
|
68
|
+
REQUIRES_ARRAY,
|
|
69
|
+
REQUIRES_OBJECT,
|
|
70
|
+
} from './dispatch/SubRendererDispatch';
|
|
71
|
+
|
|
72
|
+
// ── Cell Renderers ───────────────────────────────────────
|
|
73
|
+
export { cellRenderers, DefaultCardCell } from './cells';
|
|
74
|
+
export { TemplateCardCell } from './cells/TemplateCardCell';
|
|
75
|
+
|
|
76
|
+
// ── Shared Components ────────────────────────────────────
|
|
77
|
+
export { EvidenceTrail, EvidenceTrailSubRenderer } from './components/EvidenceTrail';
|
|
78
|
+
export type { EvidenceTrailStep, EvidenceTrailItem } from './components/EvidenceTrail';
|
|
79
|
+
export { EnableConditionsSubRenderer, ConstrainConditionsSubRenderer } from './components/ConditionCards';
|
|
80
|
+
|
|
81
|
+
// ── Hooks ────────────────────────────────────────────────
|
|
82
|
+
export { useProseExtraction } from './hooks/useProseExtraction';
|