@the-syllabus/analysis-renderers 0.2.0 → 0.4.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/dispatch/SubRendererDispatch.js +1 -1
- package/dist/dispatch/SubRendererDispatch.js.map +1 -1
- package/dist/sub-renderers/SubRenderers.d.ts +2 -28
- package/dist/sub-renderers/SubRenderers.d.ts.map +1 -1
- package/dist/sub-renderers/SubRenderers.js +281 -0
- package/dist/sub-renderers/SubRenderers.js.map +1 -1
- package/package.json +1 -1
- package/src/dispatch/SubRendererDispatch.tsx +1 -1
- package/src/sub-renderers/SubRenderers.tsx +359 -0
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
* dialectical_pair — Two-panel tension visualization for thesis/antithesis contrasts
|
|
22
22
|
* rich_description_list — Stacked items with colored borders for paragraph-length descriptions
|
|
23
23
|
* phase_timeline — Connected timeline with prominent phase nodes for temporal data
|
|
24
|
+
* annotated_prose — Enhanced prose with paragraph markers, pull-quotes, and rhetorical highlighting
|
|
25
|
+
* dependency_matrix — Adjacency matrix / heatmap for directed chapter dependencies
|
|
24
26
|
* distribution_summary — Visual bar chart with dominant highlight, counts, and optional narrative
|
|
25
27
|
*/
|
|
26
28
|
|
|
@@ -57,6 +59,8 @@ const SUB_RENDERER_MAP: Record<string, React.FC<SubRendererProps>> = {
|
|
|
57
59
|
dialectical_pair: DialecticalPair,
|
|
58
60
|
rich_description_list: RichDescriptionList,
|
|
59
61
|
phase_timeline: PhaseTimeline,
|
|
62
|
+
dependency_matrix: DependencyMatrix,
|
|
63
|
+
annotated_prose: AnnotatedProse,
|
|
60
64
|
distribution_summary: DistributionSummary,
|
|
61
65
|
};
|
|
62
66
|
|
|
@@ -3333,6 +3337,361 @@ function PhaseTimeline({ data, config }: SubRendererProps) {
|
|
|
3333
3337
|
* _activeFilter?: string | null -- highlights active bar
|
|
3334
3338
|
* _groups?: Group[] -- live groups (overrides distribution field)
|
|
3335
3339
|
*/
|
|
3340
|
+
|
|
3341
|
+
// ── annotated_prose ───────────────────────────────────────────
|
|
3342
|
+
// Enhanced prose rendering with paragraph detection, pull-quote extraction,
|
|
3343
|
+
// and subtle numbered markers. Upgrades prose_block for longer analytical text.
|
|
3344
|
+
//
|
|
3345
|
+
// Config:
|
|
3346
|
+
// pull_quote_min_length — minimum sentence length to be considered a pull-quote (default 80)
|
|
3347
|
+
// show_paragraph_numbers — show subtle paragraph numbers (default true)
|
|
3348
|
+
// highlight_markers — rhetorical markers to highlight (default: common analytical terms)
|
|
3349
|
+
|
|
3350
|
+
const DEFAULT_MARKERS = [
|
|
3351
|
+
'crucial', 'fundamentally', 'the key claim', 'central to', 'decisive',
|
|
3352
|
+
'turning point', 'paradox', 'contradiction', 'however', 'nevertheless',
|
|
3353
|
+
'most importantly', 'significantly', 'critically', 'the core',
|
|
3354
|
+
];
|
|
3355
|
+
|
|
3356
|
+
function AnnotatedProse({ data, config }: SubRendererProps) {
|
|
3357
|
+
const { tokens } = useDesignTokens();
|
|
3358
|
+
|
|
3359
|
+
const text = typeof data === 'string'
|
|
3360
|
+
? data
|
|
3361
|
+
: typeof data === 'object' && data !== null
|
|
3362
|
+
? (data as Record<string, unknown>)._prose_output as string
|
|
3363
|
+
|| (data as Record<string, unknown>).text as string
|
|
3364
|
+
|| (data as Record<string, unknown>).content as string
|
|
3365
|
+
|| (data as Record<string, unknown>).prose as string
|
|
3366
|
+
|| ''
|
|
3367
|
+
: '';
|
|
3368
|
+
|
|
3369
|
+
if (!text) return null;
|
|
3370
|
+
|
|
3371
|
+
const pullQuoteMinLen = (config.pull_quote_min_length as number) || 80;
|
|
3372
|
+
const showParaNums = config.show_paragraph_numbers !== false;
|
|
3373
|
+
const markers = (config.highlight_markers as string[]) || DEFAULT_MARKERS;
|
|
3374
|
+
|
|
3375
|
+
// Split into paragraphs
|
|
3376
|
+
const paragraphs = text.split(/\n\n+/).map(p => p.trim()).filter(Boolean);
|
|
3377
|
+
|
|
3378
|
+
// Find the best pull-quote: longest sentence across all paragraphs that contains a marker
|
|
3379
|
+
let bestQuote = '';
|
|
3380
|
+
let bestQuoteParaIdx = -1;
|
|
3381
|
+
for (let pi = 0; pi < paragraphs.length; pi++) {
|
|
3382
|
+
const sentences = paragraphs[pi].split(/(?<=[.!?])\s+/);
|
|
3383
|
+
for (const sent of sentences) {
|
|
3384
|
+
if (sent.length >= pullQuoteMinLen && sent.length > bestQuote.length) {
|
|
3385
|
+
const lower = sent.toLowerCase();
|
|
3386
|
+
if (markers.some(m => lower.includes(m))) {
|
|
3387
|
+
bestQuote = sent;
|
|
3388
|
+
bestQuoteParaIdx = pi;
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
// Fallback: just pick longest sentence if no marker match
|
|
3394
|
+
if (!bestQuote) {
|
|
3395
|
+
for (let pi = 0; pi < paragraphs.length; pi++) {
|
|
3396
|
+
const sentences = paragraphs[pi].split(/(?<=[.!?])\s+/);
|
|
3397
|
+
for (const sent of sentences) {
|
|
3398
|
+
if (sent.length >= pullQuoteMinLen && sent.length > bestQuote.length) {
|
|
3399
|
+
bestQuote = sent;
|
|
3400
|
+
bestQuoteParaIdx = pi;
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
const accentColor = tokens.primitives.series_palette[0] || '#6366f1';
|
|
3407
|
+
|
|
3408
|
+
// Highlight markers in text
|
|
3409
|
+
const highlightText = (text: string): React.ReactNode[] => {
|
|
3410
|
+
if (markers.length === 0) return [text];
|
|
3411
|
+
const pattern = new RegExp(`(${markers.map(m => m.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})`, 'gi');
|
|
3412
|
+
const parts = text.split(pattern);
|
|
3413
|
+
return parts.map((part, i) => {
|
|
3414
|
+
if (pattern.test(part)) {
|
|
3415
|
+
return React.createElement('span', {
|
|
3416
|
+
key: i,
|
|
3417
|
+
style: {
|
|
3418
|
+
fontWeight: 600,
|
|
3419
|
+
color: accentColor,
|
|
3420
|
+
},
|
|
3421
|
+
}, part);
|
|
3422
|
+
}
|
|
3423
|
+
return part;
|
|
3424
|
+
});
|
|
3425
|
+
};
|
|
3426
|
+
|
|
3427
|
+
return (
|
|
3428
|
+
<div style={{
|
|
3429
|
+
fontFamily: 'var(--font-serif, Georgia, serif)',
|
|
3430
|
+
lineHeight: 1.75,
|
|
3431
|
+
color: 'var(--dt-ink-primary, #1a1a2e)',
|
|
3432
|
+
}}>
|
|
3433
|
+
{paragraphs.map((para, idx) => {
|
|
3434
|
+
const isQuotePara = idx === bestQuoteParaIdx;
|
|
3435
|
+
|
|
3436
|
+
return React.createElement(React.Fragment, { key: idx },
|
|
3437
|
+
// Pull-quote callout (inserted after the paragraph it was extracted from)
|
|
3438
|
+
showParaNums ? React.createElement('div', {
|
|
3439
|
+
style: {
|
|
3440
|
+
display: 'flex',
|
|
3441
|
+
gap: '12px',
|
|
3442
|
+
marginBottom: idx === paragraphs.length - 1 ? 0 : '1.1em',
|
|
3443
|
+
},
|
|
3444
|
+
},
|
|
3445
|
+
// Paragraph number
|
|
3446
|
+
React.createElement('span', {
|
|
3447
|
+
style: {
|
|
3448
|
+
fontSize: '0.7rem',
|
|
3449
|
+
fontFamily: 'var(--font-mono, monospace)',
|
|
3450
|
+
color: 'var(--dt-ink-tertiary, #9ca3af)',
|
|
3451
|
+
minWidth: '18px',
|
|
3452
|
+
textAlign: 'right',
|
|
3453
|
+
paddingTop: '3px',
|
|
3454
|
+
userSelect: 'none',
|
|
3455
|
+
},
|
|
3456
|
+
}, `${idx + 1}`),
|
|
3457
|
+
// Paragraph text
|
|
3458
|
+
React.createElement('p', {
|
|
3459
|
+
style: {
|
|
3460
|
+
margin: 0,
|
|
3461
|
+
fontSize: '0.92rem',
|
|
3462
|
+
},
|
|
3463
|
+
}, ...highlightText(para)),
|
|
3464
|
+
) : React.createElement('p', {
|
|
3465
|
+
style: {
|
|
3466
|
+
margin: 0,
|
|
3467
|
+
marginBottom: idx === paragraphs.length - 1 ? 0 : '1.1em',
|
|
3468
|
+
fontSize: '0.92rem',
|
|
3469
|
+
},
|
|
3470
|
+
}, ...highlightText(para)),
|
|
3471
|
+
|
|
3472
|
+
// Pull-quote callout after the source paragraph
|
|
3473
|
+
isQuotePara && bestQuote ? React.createElement('blockquote', {
|
|
3474
|
+
key: `quote-${idx}`,
|
|
3475
|
+
style: {
|
|
3476
|
+
margin: '1em 0 1.2em 0',
|
|
3477
|
+
padding: '12px 16px',
|
|
3478
|
+
borderLeft: `3px solid ${accentColor}`,
|
|
3479
|
+
background: 'var(--dt-surface-card, #f8f6f3)',
|
|
3480
|
+
borderRadius: '0 var(--radius-sm, 4px) var(--radius-sm, 4px) 0',
|
|
3481
|
+
fontStyle: 'italic',
|
|
3482
|
+
fontSize: '0.88rem',
|
|
3483
|
+
lineHeight: 1.65,
|
|
3484
|
+
color: 'var(--dt-ink-secondary, #374151)',
|
|
3485
|
+
},
|
|
3486
|
+
}, `"${bestQuote}"`) : null,
|
|
3487
|
+
);
|
|
3488
|
+
})}
|
|
3489
|
+
</div>
|
|
3490
|
+
);
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
// ── dependency_matrix ─────────────────────────────────────────
|
|
3494
|
+
// Adjacency matrix / heatmap for directed relationships.
|
|
3495
|
+
// Data: array of {source, target, type} objects.
|
|
3496
|
+
// Config:
|
|
3497
|
+
// source_field — field name for row source (default "chapter")
|
|
3498
|
+
// target_field — field name for column target (default "depends_on")
|
|
3499
|
+
// type_field — field name for relationship type (default "dependency_type")
|
|
3500
|
+
// abbreviate_labels — shorten labels (default true)
|
|
3501
|
+
|
|
3502
|
+
function DependencyMatrix({ data, config }: SubRendererProps) {
|
|
3503
|
+
const [hoveredCell, setHoveredCell] = React.useState<{ row: number; col: number } | null>(null);
|
|
3504
|
+
const { tokens } = useDesignTokens();
|
|
3505
|
+
|
|
3506
|
+
if (!data || !Array.isArray(data) || data.length === 0) return null;
|
|
3507
|
+
|
|
3508
|
+
const sourceField = (config.source_field as string) || 'chapter';
|
|
3509
|
+
const targetField = (config.target_field as string) || 'depends_on';
|
|
3510
|
+
const typeField = (config.type_field as string) || 'dependency_type';
|
|
3511
|
+
const abbreviate = config.abbreviate_labels !== false;
|
|
3512
|
+
|
|
3513
|
+
const palette = tokens.primitives.series_palette;
|
|
3514
|
+
|
|
3515
|
+
// Extract unique labels (preserving order of first appearance)
|
|
3516
|
+
const labelSet = new Set<string>();
|
|
3517
|
+
for (const item of data) {
|
|
3518
|
+
const obj = item as Record<string, unknown>;
|
|
3519
|
+
const src = String(obj[sourceField] || '');
|
|
3520
|
+
const tgt = String(obj[targetField] || '');
|
|
3521
|
+
if (src) labelSet.add(src);
|
|
3522
|
+
if (tgt) labelSet.add(tgt);
|
|
3523
|
+
}
|
|
3524
|
+
const labels = Array.from(labelSet);
|
|
3525
|
+
|
|
3526
|
+
// Extract unique types for legend
|
|
3527
|
+
const typeSet = new Set<string>();
|
|
3528
|
+
for (const item of data) {
|
|
3529
|
+
const obj = item as Record<string, unknown>;
|
|
3530
|
+
const t = String(obj[typeField] || '');
|
|
3531
|
+
if (t) typeSet.add(t);
|
|
3532
|
+
}
|
|
3533
|
+
const types = Array.from(typeSet);
|
|
3534
|
+
const typeColorMap: Record<string, string> = {};
|
|
3535
|
+
types.forEach((t, i) => { typeColorMap[t] = palette[i % palette.length]; });
|
|
3536
|
+
|
|
3537
|
+
// Build adjacency map: [rowIdx][colIdx] = type
|
|
3538
|
+
const adjacency: Record<string, Record<string, string>> = {};
|
|
3539
|
+
for (const item of data) {
|
|
3540
|
+
const obj = item as Record<string, unknown>;
|
|
3541
|
+
const src = String(obj[sourceField] || '');
|
|
3542
|
+
const tgt = String(obj[targetField] || '');
|
|
3543
|
+
const typ = String(obj[typeField] || '');
|
|
3544
|
+
if (src && tgt) {
|
|
3545
|
+
if (!adjacency[src]) adjacency[src] = {};
|
|
3546
|
+
adjacency[src][tgt] = typ;
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
// Abbreviation helper
|
|
3551
|
+
const abbrev = (label: string): string => {
|
|
3552
|
+
if (!abbreviate) return label;
|
|
3553
|
+
// "Chapter 1" → "Ch1", "Appendix 1" → "App1", or first 4 chars
|
|
3554
|
+
return label
|
|
3555
|
+
.replace(/^Chapter\s*/i, 'Ch')
|
|
3556
|
+
.replace(/^Appendix\s*/i, 'App')
|
|
3557
|
+
.replace(/^Part\s*/i, 'P')
|
|
3558
|
+
.slice(0, 6);
|
|
3559
|
+
};
|
|
3560
|
+
|
|
3561
|
+
const cellSize = 32;
|
|
3562
|
+
const labelWidth = 80;
|
|
3563
|
+
const hovered = hoveredCell ? {
|
|
3564
|
+
src: labels[hoveredCell.row],
|
|
3565
|
+
tgt: labels[hoveredCell.col],
|
|
3566
|
+
type: adjacency[labels[hoveredCell.row]]?.[labels[hoveredCell.col]],
|
|
3567
|
+
} : null;
|
|
3568
|
+
|
|
3569
|
+
return (
|
|
3570
|
+
<div style={{ overflowX: 'auto' }}>
|
|
3571
|
+
{/* Tooltip */}
|
|
3572
|
+
{hovered?.type && (
|
|
3573
|
+
<div style={{
|
|
3574
|
+
padding: '6px 10px',
|
|
3575
|
+
marginBottom: '8px',
|
|
3576
|
+
fontSize: '0.82rem',
|
|
3577
|
+
background: 'var(--dt-surface-card, #f8f6f3)',
|
|
3578
|
+
border: '1px solid var(--color-border, #e2e5e9)',
|
|
3579
|
+
borderRadius: 'var(--radius-sm, 4px)',
|
|
3580
|
+
color: 'var(--dt-ink-primary, #1a1a2e)',
|
|
3581
|
+
}}>
|
|
3582
|
+
<strong>{hovered.src}</strong> → <strong>{hovered.tgt}</strong>: {hovered.type.replace(/_/g, ' ')}
|
|
3583
|
+
</div>
|
|
3584
|
+
)}
|
|
3585
|
+
|
|
3586
|
+
<table style={{
|
|
3587
|
+
borderCollapse: 'collapse',
|
|
3588
|
+
fontSize: '0.75rem',
|
|
3589
|
+
fontFamily: 'var(--font-mono, monospace)',
|
|
3590
|
+
}}>
|
|
3591
|
+
{/* Column headers */}
|
|
3592
|
+
<thead>
|
|
3593
|
+
<tr>
|
|
3594
|
+
<th style={{ width: labelWidth, minWidth: labelWidth }} />
|
|
3595
|
+
{labels.map((label, ci) => (
|
|
3596
|
+
<th key={ci} style={{
|
|
3597
|
+
width: cellSize,
|
|
3598
|
+
minWidth: cellSize,
|
|
3599
|
+
textAlign: 'center',
|
|
3600
|
+
padding: '2px',
|
|
3601
|
+
fontWeight: 500,
|
|
3602
|
+
color: 'var(--dt-ink-secondary, #6b7280)',
|
|
3603
|
+
writingMode: labels.length > 6 ? 'vertical-rl' : undefined,
|
|
3604
|
+
transform: labels.length > 6 ? 'rotate(180deg)' : undefined,
|
|
3605
|
+
height: labels.length > 6 ? 60 : undefined,
|
|
3606
|
+
}}>
|
|
3607
|
+
{abbrev(label)}
|
|
3608
|
+
</th>
|
|
3609
|
+
))}
|
|
3610
|
+
</tr>
|
|
3611
|
+
</thead>
|
|
3612
|
+
<tbody>
|
|
3613
|
+
{labels.map((rowLabel, ri) => (
|
|
3614
|
+
<tr key={ri}>
|
|
3615
|
+
<td style={{
|
|
3616
|
+
padding: '2px 6px',
|
|
3617
|
+
textAlign: 'right',
|
|
3618
|
+
fontWeight: 500,
|
|
3619
|
+
color: 'var(--dt-ink-secondary, #6b7280)',
|
|
3620
|
+
whiteSpace: 'nowrap',
|
|
3621
|
+
}}>
|
|
3622
|
+
{abbrev(rowLabel)}
|
|
3623
|
+
</td>
|
|
3624
|
+
{labels.map((colLabel, ci) => {
|
|
3625
|
+
const cellType = adjacency[rowLabel]?.[colLabel];
|
|
3626
|
+
const isDiagonal = ri === ci;
|
|
3627
|
+
const isHovered = hoveredCell?.row === ri && hoveredCell?.col === ci;
|
|
3628
|
+
|
|
3629
|
+
return (
|
|
3630
|
+
<td
|
|
3631
|
+
key={ci}
|
|
3632
|
+
onMouseEnter={() => setHoveredCell({ row: ri, col: ci })}
|
|
3633
|
+
onMouseLeave={() => setHoveredCell(null)}
|
|
3634
|
+
style={{
|
|
3635
|
+
width: cellSize,
|
|
3636
|
+
height: cellSize,
|
|
3637
|
+
textAlign: 'center',
|
|
3638
|
+
border: '1px solid var(--color-border-light, #eef0f2)',
|
|
3639
|
+
background: isDiagonal
|
|
3640
|
+
? 'var(--dt-surface-bg, #f0ede6)'
|
|
3641
|
+
: cellType
|
|
3642
|
+
? typeColorMap[cellType]
|
|
3643
|
+
: 'transparent',
|
|
3644
|
+
opacity: cellType ? (isHovered ? 1 : 0.75) : 1,
|
|
3645
|
+
cursor: cellType ? 'pointer' : 'default',
|
|
3646
|
+
transition: 'opacity 0.15s',
|
|
3647
|
+
borderRadius: isHovered ? '2px' : undefined,
|
|
3648
|
+
boxShadow: isHovered && cellType ? '0 0 0 2px var(--dt-ink-primary, #1a1a2e)' : undefined,
|
|
3649
|
+
}}
|
|
3650
|
+
>
|
|
3651
|
+
{cellType && (
|
|
3652
|
+
<span style={{
|
|
3653
|
+
display: 'inline-block',
|
|
3654
|
+
width: 10,
|
|
3655
|
+
height: 10,
|
|
3656
|
+
borderRadius: 2,
|
|
3657
|
+
background: '#fff',
|
|
3658
|
+
opacity: 0.6,
|
|
3659
|
+
}} />
|
|
3660
|
+
)}
|
|
3661
|
+
</td>
|
|
3662
|
+
);
|
|
3663
|
+
})}
|
|
3664
|
+
</tr>
|
|
3665
|
+
))}
|
|
3666
|
+
</tbody>
|
|
3667
|
+
</table>
|
|
3668
|
+
|
|
3669
|
+
{/* Legend */}
|
|
3670
|
+
<div style={{
|
|
3671
|
+
display: 'flex',
|
|
3672
|
+
gap: '12px',
|
|
3673
|
+
marginTop: '10px',
|
|
3674
|
+
flexWrap: 'wrap',
|
|
3675
|
+
fontSize: '0.78rem',
|
|
3676
|
+
color: 'var(--dt-ink-secondary, #6b7280)',
|
|
3677
|
+
}}>
|
|
3678
|
+
{types.map((type, i) => (
|
|
3679
|
+
<span key={type} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
3680
|
+
<span style={{
|
|
3681
|
+
display: 'inline-block',
|
|
3682
|
+
width: 12,
|
|
3683
|
+
height: 12,
|
|
3684
|
+
borderRadius: 2,
|
|
3685
|
+
background: typeColorMap[type],
|
|
3686
|
+
}} />
|
|
3687
|
+
{type.replace(/_/g, ' ')}
|
|
3688
|
+
</span>
|
|
3689
|
+
))}
|
|
3690
|
+
</div>
|
|
3691
|
+
</div>
|
|
3692
|
+
);
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3336
3695
|
export function DistributionSummary({ data, config }: SubRendererProps) {
|
|
3337
3696
|
const { getCategoryColor, getLabel } = useDesignTokens();
|
|
3338
3697
|
const [narrativeExpanded, setNarrativeExpanded] = useState(false);
|