@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,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProvenanceSectionIcon — Stub for the provenance icon in accordion section headers.
|
|
3
|
+
*
|
|
4
|
+
* Consumer apps that support provenance tracking should override this component
|
|
5
|
+
* by importing and registering their own implementation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
interface ProvenanceSectionIconProps {
|
|
11
|
+
sectionKey: string;
|
|
12
|
+
config: unknown;
|
|
13
|
+
children_payloads?: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Default no-op stub — renders nothing. Consumer apps override for provenance UI. */
|
|
17
|
+
export function ProvenanceSectionIcon(_props: ProvenanceSectionIconProps) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AccordionRenderer — Generic collapsible sections renderer.
|
|
3
|
+
*
|
|
4
|
+
* Reads section definitions from renderer_config.sections, renders each
|
|
5
|
+
* as a collapsible panel. Supports prose mode via useProseExtraction.
|
|
6
|
+
*
|
|
7
|
+
* All sections dispatch through a resilient fallback chain:
|
|
8
|
+
* 1. Check section_renderers[key] for a configured sub-renderer
|
|
9
|
+
* 2. Pre-render compatibility check: skip if data type mismatches renderer
|
|
10
|
+
* 3. SubRendererFallback wrapper: catch null output via useLayoutEffect
|
|
11
|
+
* 4. Auto-detect sub-renderer from data shape
|
|
12
|
+
* 5. GenericSectionRenderer as final fallback (handles any data)
|
|
13
|
+
*
|
|
14
|
+
* renderer_config keys:
|
|
15
|
+
* sections: Array<{key, title}> — sections to render
|
|
16
|
+
* expand_first: boolean — auto-expand first section
|
|
17
|
+
* prose_endpoint: string — endpoint key for prose extraction
|
|
18
|
+
* section_renderers: Record<string, {renderer_type, config?, sub_renderers?}>
|
|
19
|
+
*
|
|
20
|
+
* Per-section polish keys (threaded via config._*):
|
|
21
|
+
* _onPolishSection: (sectionKey, feedback) => void
|
|
22
|
+
* _onResetSection: (sectionKey) => void
|
|
23
|
+
* _sectionPolishState: Record<string, 'idle'|'polishing'|'polished'|'error'>
|
|
24
|
+
* _section_overrides: Record<string, {style_overrides, renderer_config_patch?}>
|
|
25
|
+
* _section_descriptions: Record<string, string> — section subtitle text from polish
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
29
|
+
import { RendererProps } from '../types';
|
|
30
|
+
import { useProseExtraction } from '../hooks/useProseExtraction';
|
|
31
|
+
import { resolveSubRenderer, autoDetectSubRenderer } from '../sub-renderers/SubRenderers';
|
|
32
|
+
import { isRendererCompatible, SubRendererFallback, GenericSectionRenderer } from '../dispatch/SubRendererDispatch';
|
|
33
|
+
import { ProvenanceSectionIcon } from '../provenance/ProvenanceSectionIcon';
|
|
34
|
+
import { StyleOverrides } from '../types/styles';
|
|
35
|
+
import { useDesignTokens } from '../tokens/DesignTokenContext';
|
|
36
|
+
// CSS: import '@caii/analysis-renderers/styles';
|
|
37
|
+
|
|
38
|
+
interface SectionDef {
|
|
39
|
+
key: string;
|
|
40
|
+
title: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type SectionPolishState = 'idle' | 'polishing' | 'polished' | 'error';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract a short preview string from various data shapes.
|
|
47
|
+
* Used to show a hint of section content when collapsed.
|
|
48
|
+
*/
|
|
49
|
+
function extractPreviewText(data: unknown, maxLen = 80): string {
|
|
50
|
+
if (typeof data === 'string') {
|
|
51
|
+
const clean = data.replace(/\n/g, ' ').trim();
|
|
52
|
+
return clean.length > maxLen ? clean.slice(0, maxLen) + '\u2026' : clean;
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
55
|
+
const first = data[0];
|
|
56
|
+
if (typeof first === 'string') {
|
|
57
|
+
const clean = first.replace(/\n/g, ' ').trim();
|
|
58
|
+
return clean.length > maxLen ? clean.slice(0, maxLen) + '\u2026' : clean;
|
|
59
|
+
}
|
|
60
|
+
if (typeof first === 'object' && first !== null) {
|
|
61
|
+
const obj = first as Record<string, unknown>;
|
|
62
|
+
for (const key of ['name', 'title', 'term', 'summary', 'description', 'commitment']) {
|
|
63
|
+
if (typeof obj[key] === 'string') {
|
|
64
|
+
const val = (obj[key] as string).replace(/\n/g, ' ').trim();
|
|
65
|
+
return val.length > maxLen ? val.slice(0, maxLen) + '\u2026' : val;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
|
|
71
|
+
const obj = data as Record<string, unknown>;
|
|
72
|
+
for (const key of ['summary', 'description', 'overview', 'analysis', 'assessment']) {
|
|
73
|
+
if (typeof obj[key] === 'string') {
|
|
74
|
+
const val = (obj[key] as string).replace(/\n/g, ' ').trim();
|
|
75
|
+
return val.length > maxLen ? val.slice(0, maxLen) + '\u2026' : val;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function AccordionRenderer({ data, config }: RendererProps) {
|
|
83
|
+
const { getSemanticColor } = useDesignTokens();
|
|
84
|
+
const sections = (config.sections as SectionDef[]) || [];
|
|
85
|
+
const expandFirst = config.expand_first as boolean | undefined;
|
|
86
|
+
const proseEndpoint = (config.prose_endpoint as string) || 'conditions';
|
|
87
|
+
const styleOverrides = config._style_overrides as StyleOverrides | undefined;
|
|
88
|
+
|
|
89
|
+
// Section descriptions from polish (threaded by Phase 1 or available directly)
|
|
90
|
+
const sectionDescriptions = config._section_descriptions as Record<string, string> | undefined;
|
|
91
|
+
|
|
92
|
+
// Per-section polish controls (threaded from GenealogyPage)
|
|
93
|
+
const onPolishSection = config._onPolishSection as
|
|
94
|
+
| ((sectionKey: string, feedback: string) => void)
|
|
95
|
+
| undefined;
|
|
96
|
+
const onResetSection = config._onResetSection as
|
|
97
|
+
| ((sectionKey: string) => void)
|
|
98
|
+
| undefined;
|
|
99
|
+
const sectionPolishState = config._sectionPolishState as
|
|
100
|
+
| Record<string, SectionPolishState>
|
|
101
|
+
| undefined;
|
|
102
|
+
const sectionOverrides = config._section_overrides as
|
|
103
|
+
| Record<string, { style_overrides: StyleOverrides; renderer_config_patch?: Record<string, unknown> }>
|
|
104
|
+
| undefined;
|
|
105
|
+
// Capture mode support (threaded from CaptureContext → V2TabContent)
|
|
106
|
+
const captureMode = config._captureMode as boolean | undefined;
|
|
107
|
+
const onCapture = config._onCapture as
|
|
108
|
+
| ((sel: Record<string, unknown>) => void)
|
|
109
|
+
| undefined;
|
|
110
|
+
const captureJobId = config._captureJobId as string | undefined;
|
|
111
|
+
const captureViewKey = config._captureViewKey as string | undefined;
|
|
112
|
+
const captureSourceType = config._captureSourceType as string | undefined;
|
|
113
|
+
const captureEntityId = config._captureEntityId as string | undefined;
|
|
114
|
+
const captureStatusMap = config._captureStatusMap as Record<string, Array<{
|
|
115
|
+
destination: string | null;
|
|
116
|
+
research_status: string | null;
|
|
117
|
+
has_answer: boolean;
|
|
118
|
+
}>> | undefined;
|
|
119
|
+
|
|
120
|
+
const provenanceEnabled = config._provenanceEnabled as boolean | undefined;
|
|
121
|
+
const provenanceChildren = config._provenanceChildren as
|
|
122
|
+
| Array<{ view_key: string; view_name: string; engine_key: string | null; renderer_type: string; [key: string]: unknown }>
|
|
123
|
+
| undefined;
|
|
124
|
+
|
|
125
|
+
// Prose extraction
|
|
126
|
+
const { data: extractedData, loading, error, isProseMode } = useProseExtraction<unknown>(
|
|
127
|
+
data as unknown,
|
|
128
|
+
config._jobId as string | undefined,
|
|
129
|
+
proseEndpoint,
|
|
130
|
+
{ apiPathPrefix: config._apiPathPrefix as string | undefined }
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const workingData = (isProseMode ? extractedData : data) as Record<string, unknown> | null;
|
|
134
|
+
|
|
135
|
+
// Track which sections are expanded
|
|
136
|
+
const [expandedSections, setExpandedSections] = useState<Set<string>>(() => {
|
|
137
|
+
if (expandFirst && sections.length > 0) {
|
|
138
|
+
return new Set([sections[0].key]);
|
|
139
|
+
}
|
|
140
|
+
return new Set<string>();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Track ever-expanded for animation (keep content in DOM after first expand)
|
|
144
|
+
const [everExpanded, setEverExpanded] = useState<Set<string>>(() => {
|
|
145
|
+
if (expandFirst && sections.length > 0) {
|
|
146
|
+
return new Set([sections[0].key]);
|
|
147
|
+
}
|
|
148
|
+
return new Set<string>();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Track which sections have the feedback row open
|
|
152
|
+
const [feedbackOpen, setFeedbackOpen] = useState<Set<string>>(new Set());
|
|
153
|
+
// Track feedback text per section
|
|
154
|
+
const [feedbackText, setFeedbackText] = useState<Record<string, string>>({});
|
|
155
|
+
|
|
156
|
+
// ── Deep-link support: auto-expand, scroll, highlight target section ──
|
|
157
|
+
const deepLinkSection = config._deepLinkSection as string | null | undefined;
|
|
158
|
+
const onDeepLinkConsumed = config._onDeepLinkConsumed as (() => void) | undefined;
|
|
159
|
+
const deepLinkProcessedRef = useRef(false);
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (!deepLinkSection || deepLinkProcessedRef.current) return;
|
|
163
|
+
const targetSection = sections.find(s => s.key === deepLinkSection);
|
|
164
|
+
if (!targetSection) {
|
|
165
|
+
console.warn(`[DeepLink] section_key "${deepLinkSection}" not found in accordion sections`);
|
|
166
|
+
onDeepLinkConsumed?.();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Expand the target section
|
|
170
|
+
setExpandedSections(prev => { const next = new Set(prev); next.add(deepLinkSection); return next; });
|
|
171
|
+
setEverExpanded(prev => { const next = new Set(prev); next.add(deepLinkSection); return next; });
|
|
172
|
+
deepLinkProcessedRef.current = true;
|
|
173
|
+
|
|
174
|
+
// Wait for DOM to render the expanded section, then scroll + highlight
|
|
175
|
+
requestAnimationFrame(() => {
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
const el = document.getElementById(`section-${deepLinkSection}`);
|
|
178
|
+
if (el) {
|
|
179
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
180
|
+
el.classList.add('gen-accordion-section--highlighted');
|
|
181
|
+
setTimeout(() => el.classList.remove('gen-accordion-section--highlighted'), 2000);
|
|
182
|
+
}
|
|
183
|
+
onDeepLinkConsumed?.();
|
|
184
|
+
}, 150);
|
|
185
|
+
});
|
|
186
|
+
}, [deepLinkSection, sections, onDeepLinkConsumed]);
|
|
187
|
+
|
|
188
|
+
const toggleSection = (key: string) => {
|
|
189
|
+
setExpandedSections(prev => {
|
|
190
|
+
const next = new Set(prev);
|
|
191
|
+
if (next.has(key)) {
|
|
192
|
+
next.delete(key);
|
|
193
|
+
} else {
|
|
194
|
+
next.add(key);
|
|
195
|
+
}
|
|
196
|
+
return next;
|
|
197
|
+
});
|
|
198
|
+
// Track ever-expanded so content stays in DOM for animation
|
|
199
|
+
setEverExpanded(prev => {
|
|
200
|
+
if (prev.has(key)) return prev;
|
|
201
|
+
const next = new Set(prev);
|
|
202
|
+
next.add(key);
|
|
203
|
+
return next;
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const toggleFeedback = (key: string) => {
|
|
208
|
+
setFeedbackOpen(prev => {
|
|
209
|
+
const next = new Set(prev);
|
|
210
|
+
if (next.has(key)) {
|
|
211
|
+
next.delete(key);
|
|
212
|
+
} else {
|
|
213
|
+
next.add(key);
|
|
214
|
+
}
|
|
215
|
+
return next;
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const handlePolishClick = (sectionKey: string) => {
|
|
220
|
+
if (onPolishSection) {
|
|
221
|
+
onPolishSection(sectionKey, feedbackText[sectionKey] || '');
|
|
222
|
+
setFeedbackOpen(prev => {
|
|
223
|
+
const next = new Set(prev);
|
|
224
|
+
next.delete(sectionKey);
|
|
225
|
+
return next;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (loading) {
|
|
231
|
+
return (
|
|
232
|
+
<div className="gen-conditions-tab">
|
|
233
|
+
<div className="gen-extracting-notice">
|
|
234
|
+
<div className="gen-extracting-spinner" />
|
|
235
|
+
<p>Preparing structured view from analytical prose...</p>
|
|
236
|
+
<p className="gen-extracting-detail">
|
|
237
|
+
Extracting structured data for display.
|
|
238
|
+
</p>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (error) {
|
|
245
|
+
return (
|
|
246
|
+
<div className="gen-conditions-tab">
|
|
247
|
+
<div className="gen-extraction-error">
|
|
248
|
+
<p>Could not extract structured data: {error}</p>
|
|
249
|
+
<p className="gen-extraction-fallback">
|
|
250
|
+
Try refreshing the page or running the analysis again.
|
|
251
|
+
</p>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!workingData) {
|
|
258
|
+
return <p className="gen-empty">No data available yet.</p>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check if synthetic_judgment is already included as an accordion section
|
|
262
|
+
const hasSyntheticSection = sections.some(s => s.key === 'synthetic_judgment');
|
|
263
|
+
// Check if counterfactual_analysis is handled (either as 'counterfactual_analysis' or legacy 'counterfactuals')
|
|
264
|
+
const hasCounterfactualSection = sections.some(s => s.key === 'counterfactual_analysis' || s.key === 'counterfactuals');
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div className="gen-conditions-tab">
|
|
268
|
+
{isProseMode && (
|
|
269
|
+
<div className="gen-prose-mode-badge">
|
|
270
|
+
Schema-on-read: extracted from analytical prose
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
|
|
274
|
+
{sections.map(section => {
|
|
275
|
+
// Resolve section data — with backward-compat fallback for renamed keys
|
|
276
|
+
let sectionData = workingData[section.key];
|
|
277
|
+
if (!sectionData && section.key === 'counterfactuals') {
|
|
278
|
+
sectionData = workingData.counterfactual_analysis;
|
|
279
|
+
}
|
|
280
|
+
if (!sectionData) return null;
|
|
281
|
+
|
|
282
|
+
const isExpanded = expandedSections.has(section.key);
|
|
283
|
+
const hasEverExpanded = everExpanded.has(section.key);
|
|
284
|
+
const polishState = sectionPolishState?.[section.key] || 'idle';
|
|
285
|
+
const hasOverride = !!sectionOverrides?.[section.key];
|
|
286
|
+
const isFeedbackOpen = feedbackOpen.has(section.key);
|
|
287
|
+
|
|
288
|
+
// Per-section style overrides take precedence over view-level
|
|
289
|
+
const effectiveSO = sectionOverrides?.[section.key]?.style_overrides || styleOverrides;
|
|
290
|
+
|
|
291
|
+
// Section description from polish or config
|
|
292
|
+
const description = sectionDescriptions?.[section.key];
|
|
293
|
+
|
|
294
|
+
// Preview text for collapsed state
|
|
295
|
+
const previewText = !isExpanded ? extractPreviewText(sectionData) : '';
|
|
296
|
+
|
|
297
|
+
// Accent color for border and badge
|
|
298
|
+
const accentColor = effectiveSO?.accent_color;
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<div
|
|
302
|
+
key={section.key}
|
|
303
|
+
id={`section-${section.key}`}
|
|
304
|
+
className={`gen-accordion-section ${isExpanded ? 'gen-accordion-section--expanded' : ''}`}
|
|
305
|
+
>
|
|
306
|
+
{/* Section Header */}
|
|
307
|
+
<div
|
|
308
|
+
className="gen-accordion-header"
|
|
309
|
+
onClick={() => toggleSection(section.key)}
|
|
310
|
+
style={{
|
|
311
|
+
...(effectiveSO?.section_header || {}),
|
|
312
|
+
borderLeftColor: accentColor || undefined,
|
|
313
|
+
}}
|
|
314
|
+
>
|
|
315
|
+
<div className="gen-accordion-header-row">
|
|
316
|
+
<span className={`gen-accordion-chevron ${isExpanded ? 'gen-accordion-chevron--open' : ''}`}>
|
|
317
|
+
▸
|
|
318
|
+
</span>
|
|
319
|
+
|
|
320
|
+
<span
|
|
321
|
+
className="gen-accordion-title"
|
|
322
|
+
style={effectiveSO?.section_title || undefined}
|
|
323
|
+
>
|
|
324
|
+
{section.title}
|
|
325
|
+
</span>
|
|
326
|
+
|
|
327
|
+
{/* Item count badge for arrays */}
|
|
328
|
+
{Array.isArray(sectionData) && (
|
|
329
|
+
<span
|
|
330
|
+
className="gen-accordion-count"
|
|
331
|
+
style={accentColor ? { backgroundColor: accentColor, color: '#fff' } : undefined}
|
|
332
|
+
>
|
|
333
|
+
{sectionData.length}
|
|
334
|
+
</span>
|
|
335
|
+
)}
|
|
336
|
+
|
|
337
|
+
{/* Provenance section icon */}
|
|
338
|
+
{provenanceEnabled && (
|
|
339
|
+
<ProvenanceSectionIcon
|
|
340
|
+
sectionKey={section.key}
|
|
341
|
+
config={(config.section_renderers as Record<string, unknown> | undefined)?.[section.key] as any}
|
|
342
|
+
children_payloads={provenanceChildren}
|
|
343
|
+
/>
|
|
344
|
+
)}
|
|
345
|
+
|
|
346
|
+
{/* Capture status dots — always visible */}
|
|
347
|
+
{(() => {
|
|
348
|
+
const statusKey = `${captureViewKey || ''}::${section.key}`;
|
|
349
|
+
const statuses = captureStatusMap?.[statusKey];
|
|
350
|
+
if (!statuses?.length) return null;
|
|
351
|
+
const hasArsenal = statuses.some(s => s.destination === 'arsenal');
|
|
352
|
+
const hasResearchAnswered = statuses.some(s => s.destination === 'research_todo' && s.has_answer);
|
|
353
|
+
const hasResearchPending = statuses.some(s => s.destination === 'research_todo' && !s.has_answer);
|
|
354
|
+
return (
|
|
355
|
+
<span className="capture-status-dots" onClick={e => e.stopPropagation()}>
|
|
356
|
+
{hasArsenal && <span className="capture-status-dot capture-status-dot--arsenal" title="Sent to Arsenal" />}
|
|
357
|
+
{hasResearchAnswered && <span className="capture-status-dot capture-status-dot--answered" title="Research answered" />}
|
|
358
|
+
{hasResearchPending && <span className="capture-status-dot capture-status-dot--research" title="Research question pending" />}
|
|
359
|
+
</span>
|
|
360
|
+
);
|
|
361
|
+
})()}
|
|
362
|
+
|
|
363
|
+
{/* Capture button — shown only in capture mode */}
|
|
364
|
+
{captureMode && onCapture && (
|
|
365
|
+
<button
|
|
366
|
+
className="section-capture-btn"
|
|
367
|
+
title="Capture this section"
|
|
368
|
+
onClick={e => {
|
|
369
|
+
e.stopPropagation();
|
|
370
|
+
onCapture({
|
|
371
|
+
source_view_key: captureViewKey || '',
|
|
372
|
+
source_section_key: section.key,
|
|
373
|
+
source_renderer_type: 'accordion',
|
|
374
|
+
content_type: 'section',
|
|
375
|
+
selected_text: previewText || extractPreviewText(sectionData) || section.title || section.key,
|
|
376
|
+
structured_data: sectionData,
|
|
377
|
+
context_title: `${captureViewKey || 'Analysis'} > ${section.title || section.key}`,
|
|
378
|
+
source_type: (captureSourceType || 'analysis') as string,
|
|
379
|
+
entity_id: captureEntityId || captureJobId || '',
|
|
380
|
+
depth_level: 'L1_section',
|
|
381
|
+
});
|
|
382
|
+
}}
|
|
383
|
+
style={{
|
|
384
|
+
marginLeft: onPolishSection ? '0' : 'auto',
|
|
385
|
+
background: 'none',
|
|
386
|
+
border: '1px solid #475569',
|
|
387
|
+
borderRadius: '4px',
|
|
388
|
+
color: '#94a3b8',
|
|
389
|
+
cursor: 'pointer',
|
|
390
|
+
padding: '2px 6px',
|
|
391
|
+
fontSize: '0.75rem',
|
|
392
|
+
lineHeight: 1,
|
|
393
|
+
}}
|
|
394
|
+
>
|
|
395
|
+
📌
|
|
396
|
+
</button>
|
|
397
|
+
)}
|
|
398
|
+
|
|
399
|
+
{/* Per-section polish controls — right side of header */}
|
|
400
|
+
{onPolishSection && (
|
|
401
|
+
<span
|
|
402
|
+
className="section-polish-controls"
|
|
403
|
+
onClick={e => e.stopPropagation()}
|
|
404
|
+
style={{
|
|
405
|
+
marginLeft: 'auto',
|
|
406
|
+
display: 'inline-flex',
|
|
407
|
+
alignItems: 'center',
|
|
408
|
+
gap: '0.375rem',
|
|
409
|
+
}}
|
|
410
|
+
>
|
|
411
|
+
{/* Polishing spinner */}
|
|
412
|
+
{polishState === 'polishing' && (
|
|
413
|
+
<span className="section-polish-spinner" title="Polishing..." />
|
|
414
|
+
)}
|
|
415
|
+
|
|
416
|
+
{/* Polished checkmark */}
|
|
417
|
+
{hasOverride && polishState !== 'polishing' && (
|
|
418
|
+
<span
|
|
419
|
+
style={{ color: getSemanticColor('severity', 'low')?.text || '#16a34a', fontSize: '0.85rem', cursor: 'default' }}
|
|
420
|
+
title="Section polished"
|
|
421
|
+
>
|
|
422
|
+
✓
|
|
423
|
+
</span>
|
|
424
|
+
)}
|
|
425
|
+
|
|
426
|
+
{/* Reset link */}
|
|
427
|
+
{hasOverride && onResetSection && (
|
|
428
|
+
<button
|
|
429
|
+
className="section-polish-btn section-polish-reset"
|
|
430
|
+
onClick={() => onResetSection(section.key)}
|
|
431
|
+
title="Reset section polish"
|
|
432
|
+
>
|
|
433
|
+
Reset
|
|
434
|
+
</button>
|
|
435
|
+
)}
|
|
436
|
+
|
|
437
|
+
{/* Pencil button to open feedback row */}
|
|
438
|
+
{!hasOverride && polishState !== 'polishing' && (
|
|
439
|
+
<button
|
|
440
|
+
className="section-polish-btn section-polish-pencil"
|
|
441
|
+
onClick={() => toggleFeedback(section.key)}
|
|
442
|
+
title="Polish this section"
|
|
443
|
+
>
|
|
444
|
+
✎
|
|
445
|
+
</button>
|
|
446
|
+
)}
|
|
447
|
+
|
|
448
|
+
{/* Error indicator */}
|
|
449
|
+
{polishState === 'error' && (
|
|
450
|
+
<span style={{ color: getSemanticColor('severity', 'high')?.text || '#dc2626', fontSize: '0.72rem' }}>failed</span>
|
|
451
|
+
)}
|
|
452
|
+
</span>
|
|
453
|
+
)}
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
{/* Section description (from polish or config) */}
|
|
457
|
+
{description && (
|
|
458
|
+
<div
|
|
459
|
+
className="gen-accordion-description"
|
|
460
|
+
style={effectiveSO?.section_description || undefined}
|
|
461
|
+
>
|
|
462
|
+
{description}
|
|
463
|
+
</div>
|
|
464
|
+
)}
|
|
465
|
+
|
|
466
|
+
{/* Preview text when collapsed */}
|
|
467
|
+
{!isExpanded && previewText && (
|
|
468
|
+
<div className="gen-accordion-preview">
|
|
469
|
+
{previewText}
|
|
470
|
+
</div>
|
|
471
|
+
)}
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
{/* Feedback row — collapsible input below header */}
|
|
475
|
+
{isFeedbackOpen && !hasOverride && polishState !== 'polishing' && (
|
|
476
|
+
<div className="section-polish-feedback-row">
|
|
477
|
+
<input
|
|
478
|
+
type="text"
|
|
479
|
+
className="section-polish-feedback-input"
|
|
480
|
+
placeholder="Optional: describe what to improve..."
|
|
481
|
+
value={feedbackText[section.key] || ''}
|
|
482
|
+
onChange={e =>
|
|
483
|
+
setFeedbackText(prev => ({ ...prev, [section.key]: e.target.value }))
|
|
484
|
+
}
|
|
485
|
+
onKeyDown={e => {
|
|
486
|
+
if (e.key === 'Enter') handlePolishClick(section.key);
|
|
487
|
+
}}
|
|
488
|
+
/>
|
|
489
|
+
<button
|
|
490
|
+
className="section-polish-btn section-polish-go"
|
|
491
|
+
onClick={() => handlePolishClick(section.key)}
|
|
492
|
+
>
|
|
493
|
+
Polish
|
|
494
|
+
</button>
|
|
495
|
+
<button
|
|
496
|
+
className="section-polish-btn section-polish-cancel"
|
|
497
|
+
onClick={() => toggleFeedback(section.key)}
|
|
498
|
+
>
|
|
499
|
+
×
|
|
500
|
+
</button>
|
|
501
|
+
</div>
|
|
502
|
+
)}
|
|
503
|
+
|
|
504
|
+
{/* Collapsible content with smooth animation */}
|
|
505
|
+
<div className={`gen-section-collapse ${isExpanded ? 'gen-section-expanded' : ''}`}>
|
|
506
|
+
<div className="gen-section-collapse-inner">
|
|
507
|
+
{hasEverExpanded && (
|
|
508
|
+
<div className="gen-section-content" style={effectiveSO?.section_content || undefined}>
|
|
509
|
+
{(() => {
|
|
510
|
+
// Sub-renderer dispatch with resilient fallback chain:
|
|
511
|
+
// 1. Try configured renderer (if compatible with data)
|
|
512
|
+
// 2. If incompatible or unresolved, try nested_sections
|
|
513
|
+
// 3. Auto-detect renderer based on data shape
|
|
514
|
+
// 4. GenericSectionRenderer as final fallback
|
|
515
|
+
|
|
516
|
+
// Forward capture config so sub-renderers (CardGrid, Card, etc.) show capture buttons
|
|
517
|
+
const captureForward = {
|
|
518
|
+
_captureMode: captureMode,
|
|
519
|
+
_onCapture: onCapture,
|
|
520
|
+
_captureJobId: captureJobId,
|
|
521
|
+
_captureViewKey: captureViewKey,
|
|
522
|
+
_parentSectionKey: section.key,
|
|
523
|
+
_parentSectionTitle: section.title,
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const sectionHints = config.section_renderers as Record<string, { renderer_type: string; config?: Record<string, unknown>; sub_renderers?: Record<string, { renderer_type: string; config?: Record<string, unknown> }> }> | undefined;
|
|
527
|
+
const hint = sectionHints?.[section.key];
|
|
528
|
+
if (hint) {
|
|
529
|
+
const SectionRenderer = resolveSubRenderer(hint.renderer_type);
|
|
530
|
+
const subConfig = { ...(hint.config || {}), _style_overrides: effectiveSO, ...captureForward };
|
|
531
|
+
|
|
532
|
+
if (SectionRenderer) {
|
|
533
|
+
// Pre-render compatibility check: skip renderer if
|
|
534
|
+
// data type doesn't match (e.g. chip_grid given a string)
|
|
535
|
+
if (!isRendererCompatible(hint.renderer_type, sectionData, hint.config)) {
|
|
536
|
+
console.warn(
|
|
537
|
+
`[AccordionRenderer] Configured '${hint.renderer_type}' incompatible with ${Array.isArray(sectionData) ? 'array' : typeof sectionData} data for section '${section.key}' — falling through to auto-detection`
|
|
538
|
+
);
|
|
539
|
+
} else {
|
|
540
|
+
// Wrap in SubRendererFallback for defense-in-depth:
|
|
541
|
+
// catches cases where data type matches but content
|
|
542
|
+
// still causes null (e.g. empty array, wrong item shape)
|
|
543
|
+
return (
|
|
544
|
+
<SubRendererFallback
|
|
545
|
+
Renderer={SectionRenderer}
|
|
546
|
+
data={sectionData}
|
|
547
|
+
config={subConfig}
|
|
548
|
+
sectionKey={section.key}
|
|
549
|
+
/>
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// nested_sections: pass sub_renderers to generic
|
|
555
|
+
if (hint.sub_renderers) {
|
|
556
|
+
return <GenericSectionRenderer data={sectionData} subRenderers={hint.sub_renderers} />;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Auto-detect the best sub-renderer based on data shape
|
|
561
|
+
const autoRenderer = autoDetectSubRenderer(sectionData);
|
|
562
|
+
if (autoRenderer) {
|
|
563
|
+
const AutoComp = resolveSubRenderer(autoRenderer);
|
|
564
|
+
if (AutoComp) {
|
|
565
|
+
return <AutoComp data={sectionData} config={{ _style_overrides: effectiveSO, ...captureForward }} />;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Final fallback: generic renderer handles any data shape
|
|
570
|
+
return <GenericSectionRenderer data={sectionData} />;
|
|
571
|
+
})()}
|
|
572
|
+
</div>
|
|
573
|
+
)}
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
);
|
|
578
|
+
})}
|
|
579
|
+
|
|
580
|
+
{/* Synthetic judgment — always visible if present AND not already an accordion section */}
|
|
581
|
+
{!hasSyntheticSection && workingData.synthetic_judgment ? (
|
|
582
|
+
<div className="gen-synthetic-judgment">
|
|
583
|
+
<h3>Synthetic Judgment</h3>
|
|
584
|
+
<div className="gen-judgment-text">
|
|
585
|
+
{String(workingData.synthetic_judgment).split('\n').map((p, i) => (
|
|
586
|
+
<p key={i}>{p}</p>
|
|
587
|
+
))}
|
|
588
|
+
</div>
|
|
589
|
+
</div>
|
|
590
|
+
) : null}
|
|
591
|
+
|
|
592
|
+
{/* Counterfactual analysis — standalone if not already in sections config */}
|
|
593
|
+
{!hasCounterfactualSection && workingData.counterfactual_analysis ? (
|
|
594
|
+
<div className="gen-counterfactual">
|
|
595
|
+
<h3>Counterfactual Analysis</h3>
|
|
596
|
+
<p className="gen-section-desc">
|
|
597
|
+
What the argument would look like without the author's prior work
|
|
598
|
+
</p>
|
|
599
|
+
<div className="gen-counterfactual-text">
|
|
600
|
+
{String(workingData.counterfactual_analysis).split('\n').map((p, i) => (
|
|
601
|
+
<p key={i}>{p}</p>
|
|
602
|
+
))}
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
) : null}
|
|
606
|
+
</div>
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|