@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.
Files changed (128) hide show
  1. package/dist/cells/RelationshipCardCell.d.ts +10 -0
  2. package/dist/cells/RelationshipCardCell.d.ts.map +1 -0
  3. package/dist/cells/RelationshipCardCell.js +91 -0
  4. package/dist/cells/RelationshipCardCell.js.map +1 -0
  5. package/dist/cells/TacticCardCell.d.ts +12 -0
  6. package/dist/cells/TacticCardCell.d.ts.map +1 -0
  7. package/dist/cells/TacticCardCell.js +77 -0
  8. package/dist/cells/TacticCardCell.js.map +1 -0
  9. package/dist/cells/TemplateCardCell.d.ts +29 -0
  10. package/dist/cells/TemplateCardCell.d.ts.map +1 -0
  11. package/dist/cells/TemplateCardCell.js +202 -0
  12. package/dist/cells/TemplateCardCell.js.map +1 -0
  13. package/dist/cells/index.d.ts +15 -0
  14. package/dist/cells/index.d.ts.map +1 -0
  15. package/dist/cells/index.js +85 -0
  16. package/dist/cells/index.js.map +1 -0
  17. package/dist/components/ConditionCards.d.ts +18 -0
  18. package/dist/components/ConditionCards.d.ts.map +1 -0
  19. package/dist/components/ConditionCards.js +28 -0
  20. package/dist/components/ConditionCards.js.map +1 -0
  21. package/dist/components/EvidenceTrail.d.ts +54 -0
  22. package/dist/components/EvidenceTrail.d.ts.map +1 -0
  23. package/dist/components/EvidenceTrail.js +98 -0
  24. package/dist/components/EvidenceTrail.js.map +1 -0
  25. package/dist/dispatch/SubRendererDispatch.d.ts +39 -0
  26. package/dist/dispatch/SubRendererDispatch.d.ts.map +1 -0
  27. package/dist/dispatch/SubRendererDispatch.js +153 -0
  28. package/dist/dispatch/SubRendererDispatch.js.map +1 -0
  29. package/dist/hooks/useProseExtraction.d.ts +38 -0
  30. package/dist/hooks/useProseExtraction.d.ts.map +1 -0
  31. package/dist/hooks/useProseExtraction.js +93 -0
  32. package/dist/hooks/useProseExtraction.js.map +1 -0
  33. package/dist/index.d.ts +32 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +38 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/provenance/ProvenanceSectionIcon.d.ts +15 -0
  38. package/dist/provenance/ProvenanceSectionIcon.d.ts.map +1 -0
  39. package/dist/provenance/ProvenanceSectionIcon.js +11 -0
  40. package/dist/provenance/ProvenanceSectionIcon.js.map +1 -0
  41. package/dist/renderers/AccordionRenderer.d.ts +29 -0
  42. package/dist/renderers/AccordionRenderer.d.ts.map +1 -0
  43. package/dist/renderers/AccordionRenderer.js +315 -0
  44. package/dist/renderers/AccordionRenderer.js.map +1 -0
  45. package/dist/renderers/CardGridRenderer.d.ts +24 -0
  46. package/dist/renderers/CardGridRenderer.d.ts.map +1 -0
  47. package/dist/renderers/CardGridRenderer.js +321 -0
  48. package/dist/renderers/CardGridRenderer.js.map +1 -0
  49. package/dist/renderers/CardRenderer.d.ts +27 -0
  50. package/dist/renderers/CardRenderer.d.ts.map +1 -0
  51. package/dist/renderers/CardRenderer.js +337 -0
  52. package/dist/renderers/CardRenderer.js.map +1 -0
  53. package/dist/renderers/IdeaEvolutionRenderer.d.ts +16 -0
  54. package/dist/renderers/IdeaEvolutionRenderer.d.ts.map +1 -0
  55. package/dist/renderers/IdeaEvolutionRenderer.js +187 -0
  56. package/dist/renderers/IdeaEvolutionRenderer.js.map +1 -0
  57. package/dist/renderers/ProseRenderer.d.ts +10 -0
  58. package/dist/renderers/ProseRenderer.d.ts.map +1 -0
  59. package/dist/renderers/ProseRenderer.js +42 -0
  60. package/dist/renderers/ProseRenderer.js.map +1 -0
  61. package/dist/renderers/RawJsonRenderer.d.ts +8 -0
  62. package/dist/renderers/RawJsonRenderer.d.ts.map +1 -0
  63. package/dist/renderers/RawJsonRenderer.js +17 -0
  64. package/dist/renderers/RawJsonRenderer.js.map +1 -0
  65. package/dist/renderers/StatSummaryRenderer.d.ts +12 -0
  66. package/dist/renderers/StatSummaryRenderer.d.ts.map +1 -0
  67. package/dist/renderers/StatSummaryRenderer.js +93 -0
  68. package/dist/renderers/StatSummaryRenderer.js.map +1 -0
  69. package/dist/renderers/SynthesisRenderer.d.ts +15 -0
  70. package/dist/renderers/SynthesisRenderer.d.ts.map +1 -0
  71. package/dist/renderers/SynthesisRenderer.js +60 -0
  72. package/dist/renderers/SynthesisRenderer.js.map +1 -0
  73. package/dist/renderers/TableRenderer.d.ts +19 -0
  74. package/dist/renderers/TableRenderer.d.ts.map +1 -0
  75. package/dist/renderers/TableRenderer.js +273 -0
  76. package/dist/renderers/TableRenderer.js.map +1 -0
  77. package/dist/styles/accordion.css +376 -0
  78. package/dist/styles/index.css +5 -0
  79. package/dist/styles/renderers.css +1049 -0
  80. package/dist/sub-renderers/SubRenderers.d.ts +73 -0
  81. package/dist/sub-renderers/SubRenderers.d.ts.map +1 -0
  82. package/dist/sub-renderers/SubRenderers.js +2462 -0
  83. package/dist/sub-renderers/SubRenderers.js.map +1 -0
  84. package/dist/tokens/DesignTokenContext.d.ts +40 -0
  85. package/dist/tokens/DesignTokenContext.d.ts.map +1 -0
  86. package/dist/tokens/DesignTokenContext.js +408 -0
  87. package/dist/tokens/DesignTokenContext.js.map +1 -0
  88. package/dist/types/designTokens.d.ts +220 -0
  89. package/dist/types/designTokens.d.ts.map +1 -0
  90. package/dist/types/designTokens.js +8 -0
  91. package/dist/types/designTokens.js.map +1 -0
  92. package/dist/types/index.d.ts +32 -0
  93. package/dist/types/index.d.ts.map +1 -0
  94. package/dist/types/index.js +5 -0
  95. package/dist/types/index.js.map +1 -0
  96. package/dist/types/styles.d.ts +38 -0
  97. package/dist/types/styles.d.ts.map +1 -0
  98. package/dist/types/styles.js +14 -0
  99. package/dist/types/styles.js.map +1 -0
  100. package/dist/utils/tokenFlattener.d.ts +14 -0
  101. package/dist/utils/tokenFlattener.d.ts.map +1 -0
  102. package/dist/utils/tokenFlattener.js +56 -0
  103. package/dist/utils/tokenFlattener.js.map +1 -0
  104. package/package.json +31 -0
  105. package/src/cells/TemplateCardCell.tsx +439 -0
  106. package/src/cells/index.ts +98 -0
  107. package/src/components/ConditionCards.tsx +109 -0
  108. package/src/components/EvidenceTrail.tsx +203 -0
  109. package/src/dispatch/SubRendererDispatch.tsx +282 -0
  110. package/src/hooks/useProseExtraction.ts +125 -0
  111. package/src/index.ts +82 -0
  112. package/src/provenance/ProvenanceSectionIcon.tsx +19 -0
  113. package/src/renderers/AccordionRenderer.tsx +609 -0
  114. package/src/renderers/CardGridRenderer.tsx +608 -0
  115. package/src/renderers/CardRenderer.tsx +517 -0
  116. package/src/renderers/ProseRenderer.tsx +85 -0
  117. package/src/renderers/RawJsonRenderer.tsx +37 -0
  118. package/src/renderers/StatSummaryRenderer.tsx +182 -0
  119. package/src/renderers/TableRenderer.tsx +470 -0
  120. package/src/styles/accordion.css +376 -0
  121. package/src/styles/index.css +5 -0
  122. package/src/styles/renderers.css +1049 -0
  123. package/src/sub-renderers/SubRenderers.tsx +3487 -0
  124. package/src/tokens/DesignTokenContext.tsx +502 -0
  125. package/src/types/designTokens.ts +236 -0
  126. package/src/types/index.ts +53 -0
  127. package/src/types/styles.ts +44 -0
  128. 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">&ldquo;</span>
91
+ {item.quote}
92
+ <span className="gen-quote-mark">&rdquo;</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">&ldquo;</span>
106
+ {item.quote || item.title || ''}
107
+ <span className="gen-quote-mark">&rdquo;</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';