@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,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CardRenderer — Expandable detail cards with subsections.
|
|
3
|
+
*
|
|
4
|
+
* Unlike CardGridRenderer (many small cards in a grid), this renders a smaller
|
|
5
|
+
* number of detailed cards with rich subsection content. Each card can be
|
|
6
|
+
* expanded/collapsed.
|
|
7
|
+
*
|
|
8
|
+
* Supports the same sub-renderer dispatch chain as AccordionRenderer:
|
|
9
|
+
* 1. Check section_renderers[key] for a configured sub-renderer
|
|
10
|
+
* 2. Pre-render compatibility check: skip if data type mismatches renderer
|
|
11
|
+
* 3. SubRendererFallback wrapper: catch null output via useLayoutEffect
|
|
12
|
+
* 4. nested_sections → GenericSectionRenderer with sub_renderers
|
|
13
|
+
* 5. Auto-detect sub-renderer from data shape
|
|
14
|
+
* 6. GenericSectionRenderer as final fallback (handles any data)
|
|
15
|
+
*
|
|
16
|
+
* renderer_config keys:
|
|
17
|
+
* card_title_field: string — field for card header (default: "title")
|
|
18
|
+
* subsections: (string | {key, title})[] — named sections within each card
|
|
19
|
+
* section_renderers: Record<string, {renderer_type, config?, sub_renderers?}>
|
|
20
|
+
* show_relationship_badge: boolean — show relationship_type badge
|
|
21
|
+
* expandable: boolean — collapse/expand on click (default: true)
|
|
22
|
+
* items_path: string — dotted path to extract items
|
|
23
|
+
* prose_endpoint: string — for useProseExtraction
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import React, { useState, useMemo } from 'react';
|
|
27
|
+
import { RendererProps } from '../types';
|
|
28
|
+
import { useProseExtraction } from '../hooks/useProseExtraction';
|
|
29
|
+
import { resolveSubRenderer, autoDetectSubRenderer } from '../sub-renderers/SubRenderers';
|
|
30
|
+
import { isRendererCompatible, SubRendererFallback, GenericSectionRenderer } from '../dispatch/SubRendererDispatch';
|
|
31
|
+
// CSS: import '@caii/analysis-renderers/styles'; // shared CSS classes (gen-subsection-heading, gen-keyword-tag, etc.)
|
|
32
|
+
|
|
33
|
+
interface SubsectionDef {
|
|
34
|
+
key: string;
|
|
35
|
+
title: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeToArray(data: unknown): Array<Record<string, unknown>> {
|
|
39
|
+
if (Array.isArray(data)) return data;
|
|
40
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
41
|
+
return Object.entries(data as Record<string, unknown>).map(([key, value]) => {
|
|
42
|
+
if (value && typeof value === 'object') {
|
|
43
|
+
return { _itemKey: key, ...(value as Record<string, unknown>) };
|
|
44
|
+
}
|
|
45
|
+
return { _itemKey: key, value };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getPath(obj: unknown, path: string): unknown {
|
|
52
|
+
if (!path) return obj;
|
|
53
|
+
const parts = path.split('.');
|
|
54
|
+
let current: unknown = obj;
|
|
55
|
+
for (const part of parts) {
|
|
56
|
+
if (current == null || typeof current !== 'object') return undefined;
|
|
57
|
+
current = (current as Record<string, unknown>)[part];
|
|
58
|
+
}
|
|
59
|
+
return current;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Render inline markdown: **bold**, *italic*, `code`.
|
|
64
|
+
* Returns React nodes for use inside JSX.
|
|
65
|
+
*/
|
|
66
|
+
function renderInlineMarkdown(text: string): React.ReactNode {
|
|
67
|
+
// Split on bold (**...**), italic (*...*), and code (`...`) patterns
|
|
68
|
+
const parts: React.ReactNode[] = [];
|
|
69
|
+
let remaining = text;
|
|
70
|
+
let key = 0;
|
|
71
|
+
|
|
72
|
+
while (remaining.length > 0) {
|
|
73
|
+
// Find the earliest inline pattern
|
|
74
|
+
const boldMatch = remaining.match(/\*\*(.+?)\*\*/);
|
|
75
|
+
const italicMatch = remaining.match(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/);
|
|
76
|
+
const codeMatch = remaining.match(/`([^`]+)`/);
|
|
77
|
+
|
|
78
|
+
// Find which match comes first
|
|
79
|
+
const matches = [
|
|
80
|
+
boldMatch ? { type: 'bold', match: boldMatch, index: boldMatch.index! } : null,
|
|
81
|
+
italicMatch ? { type: 'italic', match: italicMatch, index: italicMatch.index! } : null,
|
|
82
|
+
codeMatch ? { type: 'code', match: codeMatch, index: codeMatch.index! } : null,
|
|
83
|
+
].filter(Boolean).sort((a, b) => a!.index - b!.index);
|
|
84
|
+
|
|
85
|
+
if (matches.length === 0) {
|
|
86
|
+
parts.push(remaining);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const first = matches[0]!;
|
|
91
|
+
// Push text before the match
|
|
92
|
+
if (first.index > 0) {
|
|
93
|
+
parts.push(remaining.substring(0, first.index));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (first.type === 'bold') {
|
|
97
|
+
parts.push(
|
|
98
|
+
<strong key={key++} style={{ fontWeight: 700, color: 'var(--dt-text-default)' }}>
|
|
99
|
+
{first.match[1]}
|
|
100
|
+
</strong>
|
|
101
|
+
);
|
|
102
|
+
} else if (first.type === 'italic') {
|
|
103
|
+
parts.push(
|
|
104
|
+
<em key={key++} style={{ fontStyle: 'italic' }}>
|
|
105
|
+
{first.match[1]}
|
|
106
|
+
</em>
|
|
107
|
+
);
|
|
108
|
+
} else if (first.type === 'code') {
|
|
109
|
+
parts.push(
|
|
110
|
+
<code key={key++} style={{
|
|
111
|
+
fontFamily: 'var(--font-mono, monospace)',
|
|
112
|
+
fontSize: '0.9em',
|
|
113
|
+
padding: '1px 4px',
|
|
114
|
+
borderRadius: '3px',
|
|
115
|
+
background: 'var(--dt-surface-alt, rgba(0,0,0,0.05))',
|
|
116
|
+
}}>
|
|
117
|
+
{first.match[1]}
|
|
118
|
+
</code>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
remaining = remaining.substring(first.index + first.match[0].length);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return parts.length === 1 && typeof parts[0] === 'string' ? parts[0] : <>{parts}</>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveTitle(item: Record<string, unknown>, titleField: string): string {
|
|
129
|
+
if (item[titleField]) return String(item[titleField]);
|
|
130
|
+
if (item._display_title) return String(item._display_title);
|
|
131
|
+
const pwi = item.prior_work_info as Record<string, unknown> | undefined;
|
|
132
|
+
if (pwi?.title) return String(pwi.title);
|
|
133
|
+
return String(item._itemKey || item.name || item.title || 'Untitled');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function CardRenderer({ data, config }: RendererProps) {
|
|
137
|
+
const cardTitleField = (config.card_title_field as string) || 'title';
|
|
138
|
+
const rawSubsections = config.subsections as (string | SubsectionDef)[] | undefined;
|
|
139
|
+
const showRelBadge = (config.show_relationship_badge as boolean) ?? false;
|
|
140
|
+
const expandable = (config.expandable as boolean) ?? true;
|
|
141
|
+
const itemsPath = config.items_path as string | undefined;
|
|
142
|
+
const proseEndpoint = config.prose_endpoint as string | undefined;
|
|
143
|
+
const sectionRenderers = config.section_renderers as Record<string, {
|
|
144
|
+
renderer_type: string;
|
|
145
|
+
config?: Record<string, unknown>;
|
|
146
|
+
sub_renderers?: Record<string, { renderer_type: string; config?: Record<string, unknown> }>;
|
|
147
|
+
}> | undefined;
|
|
148
|
+
|
|
149
|
+
// Capture mode
|
|
150
|
+
const captureMode = config._captureMode as boolean | undefined;
|
|
151
|
+
const onCapture = config._onCapture as
|
|
152
|
+
| ((sel: Record<string, unknown>) => void)
|
|
153
|
+
| undefined;
|
|
154
|
+
const captureJobId = config._captureJobId as string | undefined;
|
|
155
|
+
const captureViewKey = config._captureViewKey as string | undefined;
|
|
156
|
+
const captureSourceType = config._captureSourceType as string | undefined;
|
|
157
|
+
const captureEntityId = config._captureEntityId as string | undefined;
|
|
158
|
+
|
|
159
|
+
const subsections: SubsectionDef[] = useMemo(() => {
|
|
160
|
+
if (!rawSubsections) return [];
|
|
161
|
+
return rawSubsections.map(s =>
|
|
162
|
+
typeof s === 'string' ? { key: s, title: s.replace(/_/g, ' ') } : s
|
|
163
|
+
);
|
|
164
|
+
}, [rawSubsections]);
|
|
165
|
+
|
|
166
|
+
const { data: extractedData, loading, error, isProseMode } = useProseExtraction<unknown>(
|
|
167
|
+
data as unknown,
|
|
168
|
+
config._jobId as string | undefined,
|
|
169
|
+
proseEndpoint || 'data',
|
|
170
|
+
{ apiPathPrefix: config._apiPathPrefix as string | undefined }
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const workingData = isProseMode ? extractedData : data;
|
|
174
|
+
|
|
175
|
+
const items = useMemo(() => {
|
|
176
|
+
if (!workingData) return [];
|
|
177
|
+
const extracted = itemsPath ? getPath(workingData, itemsPath) : workingData;
|
|
178
|
+
return normalizeToArray(extracted);
|
|
179
|
+
}, [workingData, itemsPath]);
|
|
180
|
+
|
|
181
|
+
const [expandedCards, setExpandedCards] = useState<Set<number>>(new Set());
|
|
182
|
+
|
|
183
|
+
const toggleCard = (idx: number) => {
|
|
184
|
+
setExpandedCards(prev => {
|
|
185
|
+
const next = new Set(prev);
|
|
186
|
+
if (next.has(idx)) {
|
|
187
|
+
next.delete(idx);
|
|
188
|
+
} else {
|
|
189
|
+
next.add(idx);
|
|
190
|
+
}
|
|
191
|
+
return next;
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if (loading) {
|
|
196
|
+
return (
|
|
197
|
+
<div style={{ padding: '2rem', textAlign: 'center' as const }}>
|
|
198
|
+
<div className="gen-extracting-spinner" />
|
|
199
|
+
<p>Preparing card data...</p>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (error) {
|
|
205
|
+
return (
|
|
206
|
+
<div className="gen-extraction-error" style={{ padding: '1rem' }}>
|
|
207
|
+
<p>Could not load card data: {error}</p>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!workingData || items.length === 0) {
|
|
213
|
+
return <p className="gen-empty">No items to display.</p>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className="ar-card-renderer">
|
|
218
|
+
{isProseMode ? (
|
|
219
|
+
<div className="gen-prose-badge">
|
|
220
|
+
<span className="gen-prose-indicator">Extracted from analytical prose</span>
|
|
221
|
+
</div>
|
|
222
|
+
) : null}
|
|
223
|
+
|
|
224
|
+
<div style={{ display: 'flex', flexDirection: 'column' as const, gap: '10px' }}>
|
|
225
|
+
{items.map((item, idx) => {
|
|
226
|
+
const isExpanded = !expandable || expandedCards.has(idx);
|
|
227
|
+
const title = resolveTitle(item, cardTitleField);
|
|
228
|
+
const relType = (item.prior_work_info as Record<string, unknown> | undefined)?.relationship_type
|
|
229
|
+
|| item.relationship_type
|
|
230
|
+
|| (item.meta as Record<string, unknown> | undefined)?.relationship_type;
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div
|
|
234
|
+
key={String(item._itemKey || idx)}
|
|
235
|
+
style={{
|
|
236
|
+
background: 'var(--color-surface-elev, #f5f3f0)',
|
|
237
|
+
border: `1px solid ${isExpanded ? 'var(--dt-page-accent-border, rgba(181, 52, 58, 0.3))' : 'var(--color-border, #e2e5e9)'}`,
|
|
238
|
+
borderRadius: '8px',
|
|
239
|
+
overflow: 'hidden',
|
|
240
|
+
transition: 'border-color 0.2s',
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
{/* Card header */}
|
|
244
|
+
<div
|
|
245
|
+
onClick={expandable ? () => toggleCard(idx) : undefined}
|
|
246
|
+
style={{
|
|
247
|
+
padding: 'var(--space-sm, 0.75rem) var(--space-md, 1rem)',
|
|
248
|
+
cursor: expandable ? 'pointer' : 'default',
|
|
249
|
+
display: 'flex',
|
|
250
|
+
alignItems: 'center',
|
|
251
|
+
gap: 'var(--space-sm, 0.5rem)',
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
{expandable ? (
|
|
255
|
+
<span style={{ color: 'var(--dt-text-faint)', fontSize: '0.625rem', flexShrink: 0, transition: 'transform 150ms ease' }}>
|
|
256
|
+
{isExpanded ? '\u25BC' : '\u25B6'}
|
|
257
|
+
</span>
|
|
258
|
+
) : null}
|
|
259
|
+
<strong style={{
|
|
260
|
+
flex: 1,
|
|
261
|
+
fontSize: 'var(--type-body, 0.9375rem)',
|
|
262
|
+
fontFamily: "'Source Serif 4', 'Source Serif Pro', Georgia, serif",
|
|
263
|
+
fontWeight: 'var(--weight-semibold, 600)' as unknown as number,
|
|
264
|
+
color: 'var(--dt-text-default)',
|
|
265
|
+
lineHeight: 'var(--leading-snug, 1.35)',
|
|
266
|
+
}}>
|
|
267
|
+
{title}
|
|
268
|
+
</strong>
|
|
269
|
+
{showRelBadge && relType ? (
|
|
270
|
+
<span className="gen-keyword-tag">
|
|
271
|
+
{String(relType).replace(/_/g, ' ')}
|
|
272
|
+
</span>
|
|
273
|
+
) : null}
|
|
274
|
+
{captureMode && onCapture && (
|
|
275
|
+
<button
|
|
276
|
+
title="Capture this card"
|
|
277
|
+
onClick={e => {
|
|
278
|
+
e.stopPropagation();
|
|
279
|
+
const parentSectionKey = config._parentSectionKey as string | undefined;
|
|
280
|
+
const parentSectionTitle = config._parentSectionTitle as string | undefined;
|
|
281
|
+
onCapture({
|
|
282
|
+
source_view_key: captureViewKey || '',
|
|
283
|
+
source_item_index: idx,
|
|
284
|
+
source_renderer_type: 'card',
|
|
285
|
+
content_type: 'card',
|
|
286
|
+
selected_text: typeof item === 'object'
|
|
287
|
+
? (item.summary || item.analysis || JSON.stringify(item)).toString().slice(0, 500)
|
|
288
|
+
: String(item).slice(0, 500),
|
|
289
|
+
structured_data: item,
|
|
290
|
+
context_title: parentSectionKey
|
|
291
|
+
? `${captureViewKey || 'Analysis'} > ${parentSectionTitle || ''} > ${title}`
|
|
292
|
+
: `${captureViewKey || 'Analysis'} > ${title}`,
|
|
293
|
+
source_type: (captureSourceType || 'analysis') as string,
|
|
294
|
+
entity_id: captureEntityId || captureJobId || '',
|
|
295
|
+
depth_level: parentSectionKey ? 'L2_element' : 'L1_section',
|
|
296
|
+
parent_context: parentSectionKey ? {
|
|
297
|
+
section_key: parentSectionKey,
|
|
298
|
+
section_title: parentSectionTitle || '',
|
|
299
|
+
} : undefined,
|
|
300
|
+
});
|
|
301
|
+
}}
|
|
302
|
+
style={{
|
|
303
|
+
background: 'none',
|
|
304
|
+
border: '1px solid var(--color-border, #ccc)',
|
|
305
|
+
borderRadius: '4px',
|
|
306
|
+
color: 'var(--dt-text-faint)',
|
|
307
|
+
cursor: 'pointer',
|
|
308
|
+
padding: '2px 6px',
|
|
309
|
+
fontSize: '0.7rem',
|
|
310
|
+
lineHeight: 1,
|
|
311
|
+
flexShrink: 0,
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
📌
|
|
315
|
+
</button>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
{/* Card body with subsections */}
|
|
320
|
+
{isExpanded ? (
|
|
321
|
+
<div style={{ padding: '0 16px 16px', borderTop: '1px solid var(--color-border, #e2e5e9)' }}>
|
|
322
|
+
{(() => {
|
|
323
|
+
// Check if item is prose-only (from per_item views without structured data).
|
|
324
|
+
// When _prose_output or _raw_prose exists, always prefer prose rendering
|
|
325
|
+
// over subsection rendering — subsection keys won't exist in prose items.
|
|
326
|
+
const hasProse = Boolean(item._raw_prose || item._prose_output);
|
|
327
|
+
const isProseOnlyItem = hasProse && item._output_mode === 'prose';
|
|
328
|
+
|
|
329
|
+
// Even if subsections are configured, check whether ANY actually exist in the item.
|
|
330
|
+
// If none match, fall through to prose/fallback rendering.
|
|
331
|
+
const matchingSubsections = subsections.filter(sub => item[sub.key] != null);
|
|
332
|
+
const useSubsections = !isProseOnlyItem && matchingSubsections.length > 0;
|
|
333
|
+
|
|
334
|
+
return useSubsections ? (
|
|
335
|
+
matchingSubsections.map(sub => {
|
|
336
|
+
const sectionData = item[sub.key];
|
|
337
|
+
return (
|
|
338
|
+
<div key={sub.key} style={{ marginTop: '12px' }}>
|
|
339
|
+
<h5 className="gen-subsection-heading">
|
|
340
|
+
{sub.title}
|
|
341
|
+
</h5>
|
|
342
|
+
{/* Sub-renderer dispatch chain (same as AccordionRenderer):
|
|
343
|
+
1. Configured renderer → compatibility check → SubRendererFallback
|
|
344
|
+
2. nested_sections → GenericSectionRenderer with sub_renderers
|
|
345
|
+
3. Auto-detect from data shape
|
|
346
|
+
4. GenericSectionRenderer as final fallback */}
|
|
347
|
+
{(() => {
|
|
348
|
+
const hint = sectionRenderers?.[sub.key];
|
|
349
|
+
if (hint) {
|
|
350
|
+
const SectionRenderer = resolveSubRenderer(hint.renderer_type);
|
|
351
|
+
const subConfig = { ...(hint.config || {}) };
|
|
352
|
+
|
|
353
|
+
if (SectionRenderer) {
|
|
354
|
+
if (!isRendererCompatible(hint.renderer_type, sectionData, hint.config)) {
|
|
355
|
+
console.warn(
|
|
356
|
+
`[CardRenderer] Configured '${hint.renderer_type}' incompatible with ${Array.isArray(sectionData) ? 'array' : typeof sectionData} data for section '${sub.key}' — falling through to auto-detection`
|
|
357
|
+
);
|
|
358
|
+
} else {
|
|
359
|
+
return (
|
|
360
|
+
<SubRendererFallback
|
|
361
|
+
Renderer={SectionRenderer}
|
|
362
|
+
data={sectionData}
|
|
363
|
+
config={subConfig}
|
|
364
|
+
sectionKey={sub.key}
|
|
365
|
+
/>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// nested_sections: pass sub_renderers to GenericSectionRenderer
|
|
371
|
+
if (hint.sub_renderers) {
|
|
372
|
+
return <GenericSectionRenderer data={sectionData} subRenderers={hint.sub_renderers} />;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Auto-detect the best sub-renderer from data shape
|
|
377
|
+
const autoRenderer = autoDetectSubRenderer(sectionData);
|
|
378
|
+
if (autoRenderer) {
|
|
379
|
+
const AutoComp = resolveSubRenderer(autoRenderer);
|
|
380
|
+
if (AutoComp) {
|
|
381
|
+
return <AutoComp data={sectionData} config={{}} />;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Final fallback: GenericSectionRenderer handles any data shape
|
|
386
|
+
return <GenericSectionRenderer data={sectionData} />;
|
|
387
|
+
})()}
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
})
|
|
391
|
+
) : (
|
|
392
|
+
/* No subsections: render prose content or all non-meta fields */
|
|
393
|
+
<div style={{ marginTop: '12px' }}>
|
|
394
|
+
{/* Render _raw_prose or _prose_output if present (per-item prose cards) */}
|
|
395
|
+
{(item._raw_prose || item._prose_output) ? (
|
|
396
|
+
<div style={{
|
|
397
|
+
fontSize: '13px',
|
|
398
|
+
color: 'var(--dt-text-muted)',
|
|
399
|
+
lineHeight: '1.6',
|
|
400
|
+
}}>
|
|
401
|
+
{String(item._raw_prose || item._prose_output).split('\n').map((line, i) => {
|
|
402
|
+
const trimmed = line.trim();
|
|
403
|
+
if (!trimmed) return <br key={i} />;
|
|
404
|
+
// Order matters: check longer prefixes first
|
|
405
|
+
if (trimmed.startsWith('#### ')) {
|
|
406
|
+
return (
|
|
407
|
+
<h6 key={i} style={{
|
|
408
|
+
fontSize: '0.75rem',
|
|
409
|
+
fontWeight: 600,
|
|
410
|
+
color: 'var(--dt-text-muted)',
|
|
411
|
+
margin: '12px 0 4px 0',
|
|
412
|
+
letterSpacing: '0.02em',
|
|
413
|
+
}}>
|
|
414
|
+
{renderInlineMarkdown(trimmed.replace(/^####\s*/, ''))}
|
|
415
|
+
</h6>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (trimmed.startsWith('### ')) {
|
|
419
|
+
return (
|
|
420
|
+
<h5 key={i} style={{
|
|
421
|
+
fontSize: '0.8125rem',
|
|
422
|
+
fontWeight: 600,
|
|
423
|
+
color: 'var(--dt-text-default)',
|
|
424
|
+
margin: '14px 0 6px 0',
|
|
425
|
+
borderBottom: '1px solid var(--dt-border-light, #eef0f2)',
|
|
426
|
+
paddingBottom: '4px',
|
|
427
|
+
}}>
|
|
428
|
+
{renderInlineMarkdown(trimmed.replace(/^###\s*/, ''))}
|
|
429
|
+
</h5>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (trimmed.startsWith('## ')) {
|
|
433
|
+
const heading = trimmed.replace(/^##\s*/, '');
|
|
434
|
+
if (/^\[[\w_]+\]$/.test(heading)) {
|
|
435
|
+
return (
|
|
436
|
+
<div key={i} className="gen-inline-label" style={{
|
|
437
|
+
margin: '16px 0 4px 0',
|
|
438
|
+
}}>
|
|
439
|
+
{heading.replace(/^\[|\]$/g, '').replace(/_/g, ' ')}
|
|
440
|
+
</div>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
return (
|
|
444
|
+
<h4 key={i} style={{
|
|
445
|
+
fontSize: '0.875rem',
|
|
446
|
+
fontWeight: 600,
|
|
447
|
+
color: 'var(--dt-text-default)',
|
|
448
|
+
margin: '16px 0 6px 0',
|
|
449
|
+
borderBottom: '1px solid var(--dt-border-light, #eef0f2)',
|
|
450
|
+
paddingBottom: '4px',
|
|
451
|
+
}}>
|
|
452
|
+
{renderInlineMarkdown(heading)}
|
|
453
|
+
</h4>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
if (trimmed.startsWith('# ')) {
|
|
457
|
+
return (
|
|
458
|
+
<h3 key={i} style={{
|
|
459
|
+
fontSize: '0.9375rem',
|
|
460
|
+
fontWeight: 600,
|
|
461
|
+
fontFamily: "'Source Serif 4', 'Source Serif Pro', Georgia, serif",
|
|
462
|
+
color: 'var(--dt-text-default)',
|
|
463
|
+
margin: '20px 0 8px 0',
|
|
464
|
+
}}>
|
|
465
|
+
{renderInlineMarkdown(trimmed.replace(/^#\s*/, ''))}
|
|
466
|
+
</h3>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
if (trimmed.startsWith('---')) {
|
|
470
|
+
return <hr key={i} style={{ border: 'none', borderTop: '1px solid var(--color-border, #e2e5e9)', margin: '12px 0' }} />;
|
|
471
|
+
}
|
|
472
|
+
if (/^\[[\w_]+\]$/.test(trimmed)) {
|
|
473
|
+
return (
|
|
474
|
+
<div key={i} className="gen-inline-label" style={{
|
|
475
|
+
margin: '8px 0 4px 0',
|
|
476
|
+
}}>
|
|
477
|
+
{trimmed.replace(/^\[|\]$/g, '').replace(/_/g, ' ')}
|
|
478
|
+
</div>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
return (
|
|
482
|
+
<p key={i} style={{ margin: '0 0 6px 0' }}>
|
|
483
|
+
{renderInlineMarkdown(trimmed)}
|
|
484
|
+
</p>
|
|
485
|
+
);
|
|
486
|
+
})}
|
|
487
|
+
</div>
|
|
488
|
+
) : (
|
|
489
|
+
/* Fallback: render all non-meta fields via GenericSectionRenderer */
|
|
490
|
+
Object.entries(item).map(([key, val]) => {
|
|
491
|
+
if (key === cardTitleField || key.startsWith('_') || key === 'prior_work_info' || val == null) return null;
|
|
492
|
+
return (
|
|
493
|
+
<div key={key} style={{ marginBottom: '10px' }}>
|
|
494
|
+
<h5 className="gen-subsection-heading">
|
|
495
|
+
{key.replace(/_/g, ' ')}
|
|
496
|
+
</h5>
|
|
497
|
+
<GenericSectionRenderer data={val} />
|
|
498
|
+
</div>
|
|
499
|
+
);
|
|
500
|
+
})
|
|
501
|
+
)}
|
|
502
|
+
</div>
|
|
503
|
+
);
|
|
504
|
+
})()}
|
|
505
|
+
</div>
|
|
506
|
+
) : null}
|
|
507
|
+
</div>
|
|
508
|
+
);
|
|
509
|
+
})}
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<div style={{ marginTop: 'var(--space-sm, 0.5rem)', fontSize: 'var(--type-label, 0.6875rem)', color: 'var(--dt-text-faint)' }}>
|
|
513
|
+
{items.length} item{items.length !== 1 ? 's' : ''}
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
516
|
+
);
|
|
517
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProseRenderer — Generic renderer for narrative/prose content.
|
|
3
|
+
*
|
|
4
|
+
* Renders text data as formatted prose with section anchors.
|
|
5
|
+
* Used as the default fallback for unknown renderer_types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { RendererProps } from '../types';
|
|
10
|
+
|
|
11
|
+
export function ProseRenderer({ data, config }: RendererProps) {
|
|
12
|
+
if (!data) {
|
|
13
|
+
return <p className="gen-empty">No data available for this view.</p>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// If data is a string, render directly
|
|
17
|
+
if (typeof data === 'string') {
|
|
18
|
+
return (
|
|
19
|
+
<div className="gen-prose-renderer">
|
|
20
|
+
<div
|
|
21
|
+
className="gen-prose-content"
|
|
22
|
+
dangerouslySetInnerHTML={{ __html: formatProse(data) }}
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// If data is an object with text fields, render them
|
|
29
|
+
if (typeof data === 'object') {
|
|
30
|
+
const obj = data as Record<string, unknown>;
|
|
31
|
+
|
|
32
|
+
// Handle _prose_output marker (prose-mode workflow output)
|
|
33
|
+
if (typeof obj._prose_output === 'string') {
|
|
34
|
+
return (
|
|
35
|
+
<div className="gen-prose-renderer">
|
|
36
|
+
<div
|
|
37
|
+
className="gen-prose-content"
|
|
38
|
+
dangerouslySetInnerHTML={{ __html: formatProse(obj._prose_output) }}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Try known prose field names
|
|
45
|
+
const proseFields = ['text', 'content', 'prose', 'narrative', 'summary',
|
|
46
|
+
'executive_summary', 'genealogical_portrait', 'description'];
|
|
47
|
+
|
|
48
|
+
for (const field of proseFields) {
|
|
49
|
+
if (typeof obj[field] === 'string') {
|
|
50
|
+
return (
|
|
51
|
+
<div className="gen-prose-renderer">
|
|
52
|
+
<div
|
|
53
|
+
className="gen-prose-content"
|
|
54
|
+
dangerouslySetInnerHTML={{ __html: formatProse(obj[field] as string) }}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Fallback: show JSON
|
|
62
|
+
return (
|
|
63
|
+
<div className="gen-prose-renderer">
|
|
64
|
+
<pre className="gen-raw-json">
|
|
65
|
+
{JSON.stringify(data, null, 2)}
|
|
66
|
+
</pre>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return <p className="gen-empty">Unsupported data format.</p>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function formatProse(text: string): string {
|
|
75
|
+
// Convert markdown-like headers and paragraphs to HTML
|
|
76
|
+
return text
|
|
77
|
+
.replace(/^### (.+)$/gm, '<h4>$1</h4>')
|
|
78
|
+
.replace(/^## (.+)$/gm, '<h3>$1</h3>')
|
|
79
|
+
.replace(/^# (.+)$/gm, '<h2>$1</h2>')
|
|
80
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
81
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
82
|
+
.replace(/\n\n/g, '</p><p>')
|
|
83
|
+
.replace(/^/, '<p>')
|
|
84
|
+
.replace(/$/, '</p>');
|
|
85
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RawJsonRenderer — Debug renderer showing raw JSON data.
|
|
3
|
+
*
|
|
4
|
+
* Used for on_demand debug views. Collapsible with syntax highlighting.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState } from 'react';
|
|
8
|
+
import { RendererProps } from '../types';
|
|
9
|
+
|
|
10
|
+
export function RawJsonRenderer({ data, config }: RendererProps) {
|
|
11
|
+
const [collapsed, setCollapsed] = useState(true);
|
|
12
|
+
|
|
13
|
+
if (!data) {
|
|
14
|
+
return <p className="gen-empty">No raw data available.</p>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const jsonString = JSON.stringify(data, null, 2);
|
|
18
|
+
const lineCount = jsonString.split('\n').length;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="gen-raw-json-renderer">
|
|
22
|
+
<div className="gen-raw-header">
|
|
23
|
+
<button
|
|
24
|
+
className="gen-raw-toggle"
|
|
25
|
+
onClick={() => setCollapsed(!collapsed)}
|
|
26
|
+
>
|
|
27
|
+
{collapsed ? '\u25B6' : '\u25BC'} Raw JSON ({lineCount} lines)
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
{!collapsed && (
|
|
31
|
+
<pre className="gen-raw-json">
|
|
32
|
+
{jsonString}
|
|
33
|
+
</pre>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|