@puredesktop/puredesktop-ui-bridge 2.1.6 → 2.1.8
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/package.json +55 -1
- package/src/bridge/agentTypes.ts +21 -1
- package/src/bridge/collectionImagePaste.ts +19 -1
- package/src/bridge/context.mjs +32 -0
- package/src/bridge/documents.d.mts +76 -0
- package/src/bridge/documents.mjs +40 -0
- package/src/bridge/methods.d.mts +23 -0
- package/src/bridge/methods.mjs +23 -0
- package/src/bridge/pureRender/base.css +44 -0
- package/src/bridge/pureRender/document.ts +29 -2
- package/src/bridge/pureRender/index.ts +3 -0
- package/src/bridge/pureRender/normalize.ts +167 -0
- package/src/bridge/pureRender/sanitizeExportBody.test.ts +51 -0
- package/src/bridge/pureRender/sanitizeExportBody.ts +61 -0
- package/src/bridge/pureRender/styles.ts +125 -5
- package/src/bridge/pureRender/theme.test.ts +259 -0
- package/src/bridge/pureRender/theme.ts +19 -1
- package/src/bridge/pureRender/types.ts +33 -0
- package/src/bridge/pureRender/visualLayoutInspection.test.ts +26 -0
- package/src/bridge/pureRender/visualLayoutInspection.ts +75 -0
- package/src/bridge/react/useDocumentHotkeys.ts +41 -0
- package/src/bridge/react/useDocumentLifecycle.tsx +359 -0
- package/src/bridge/react/usePlatformAppSettings.test.tsx +105 -0
- package/src/bridge/react/usePlatformAppSettings.tsx +104 -0
- package/src/bridge/react/usePlatformJsonStore.test.tsx +152 -0
- package/src/bridge/react/usePlatformJsonStore.tsx +149 -0
- package/src/bridge/storage.d.mts +3 -1
- package/src/bridge/storage.test.ts +2 -2
- package/src/bridge/types.ts +7 -0
- package/src/components/agents/AgentDrawerPanel.tsx +2 -0
- package/src/components/agents/AgentMessageList.tsx +6 -1
- package/src/components/agents/AgentToolCallCard.tsx +20 -20
- package/src/components/agents/ContextPicker.tsx +239 -0
- package/src/components/agents/agentPanelStyles.ts +3 -4
- package/src/components/agents/agentTypes.ts +2 -0
- package/src/components/chrome/WorkspaceTabStrip.tsx +18 -1
- package/src/components/chrome/workspaceTabTypes.ts +2 -0
- package/src/components/common/containers/AppFrame.test.tsx +4 -4
- package/src/components/common/containers/AppFrame.tsx +10 -1
- package/src/components/common/containers/AppHeader.tsx +22 -0
- package/src/components/common/documents/DocumentHeaderActions.tsx +206 -0
- package/src/components/common/documents/DocumentSwitcher.tsx +1176 -0
- package/src/components/common/documents/index.ts +8 -0
- package/src/components/common/research/EvidenceDossier.tsx +1232 -150
- package/src/components/common/research/index.ts +2 -0
- package/src/components/print-design/PrintDesignPanel.test.tsx +430 -40
- package/src/components/print-design/PrintDesignPanel.tsx +1770 -207
- package/src/components/print-design/index.ts +2 -0
- package/src/components/print-design/previewContent.test.ts +32 -0
- package/src/components/print-design/previewContent.ts +31 -0
- package/src/context/buildContextDocument.test.ts +240 -0
- package/src/context/buildContextDocument.ts +309 -0
- package/src/context/contextAccess.ts +66 -0
- package/src/context/contextConfig.ts +72 -0
- package/src/context/contextDocument.test.ts +155 -0
- package/src/context/contextDocument.ts +300 -0
- package/src/context/contextSections.test.ts +88 -0
- package/src/context/contextSections.ts +84 -0
- package/src/context/htmlToContextMarkdown.test.ts +134 -0
- package/src/context/htmlToContextMarkdown.ts +369 -0
- package/src/theme/appAccents.test.ts +36 -35
- package/src/theme/appAccents.ts +87 -104
- package/src/theme/appIdentityCss.mjs +169 -231
- package/src/theme/appIdentityCss.ts +213 -233
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, type ReactNode } from 'react'
|
|
1
|
+
import { Fragment, useState, type ReactNode } from 'react'
|
|
2
2
|
import { styled } from 'styled-components'
|
|
3
3
|
|
|
4
4
|
export type EvidenceStrength = 'strong' | 'working' | 'weak'
|
|
@@ -10,16 +10,32 @@ export interface EvidenceSource {
|
|
|
10
10
|
url?: string
|
|
11
11
|
note: string
|
|
12
12
|
quote?: string
|
|
13
|
+
sourceDate?: string
|
|
14
|
+
freshness?: 'fresh' | 'stale' | 'undated'
|
|
15
|
+
freshnessMode?: 'recent' | 'open'
|
|
13
16
|
strength?: EvidenceStrength
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
/** A jargon term found in a finding's claim with a plain-language note. */
|
|
20
|
+
export interface EvidenceTermNote {
|
|
21
|
+
term: string
|
|
22
|
+
explanation: string
|
|
23
|
+
/** Spelled-out form when the term is or contains an acronym. */
|
|
24
|
+
expansion?: string
|
|
25
|
+
/** Authoritative explainer link, included only when clearly canonical. */
|
|
26
|
+
url?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
export interface EvidenceFinding {
|
|
17
30
|
id: string
|
|
18
31
|
title: string
|
|
19
32
|
claim: string
|
|
20
33
|
confidence: EvidenceStrength
|
|
21
34
|
annotation?: string
|
|
35
|
+
freshnessMode?: 'recent' | 'open'
|
|
22
36
|
evidence: EvidenceSource[]
|
|
37
|
+
/** Plain-language notes for specialist terms, rendered as inline annotations. */
|
|
38
|
+
glossary?: EvidenceTermNote[]
|
|
23
39
|
}
|
|
24
40
|
|
|
25
41
|
export interface EvidenceDossierProps {
|
|
@@ -33,6 +49,7 @@ export interface EvidenceDossierProps {
|
|
|
33
49
|
nextSteps: string[]
|
|
34
50
|
onTitleChange?: (title: string) => void
|
|
35
51
|
onDeleteFinding?: (findingId: string) => void
|
|
52
|
+
onUpdate?: () => void
|
|
36
53
|
footer?: ReactNode
|
|
37
54
|
className?: string
|
|
38
55
|
}
|
|
@@ -43,26 +60,28 @@ const strengthLabels: Record<EvidenceStrength, string> = {
|
|
|
43
60
|
weak: 'needs support',
|
|
44
61
|
}
|
|
45
62
|
|
|
46
|
-
type DossierRenderMode = 'read' | 'review' | 'audit'
|
|
63
|
+
type DossierRenderMode = 'read' | 'review' | 'audit' | 'markdown'
|
|
47
64
|
|
|
48
65
|
const modeLabels: Record<DossierRenderMode, string> = {
|
|
49
66
|
read: 'Read',
|
|
50
67
|
review: 'Claims',
|
|
51
68
|
audit: 'Sources',
|
|
69
|
+
markdown: 'MD',
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
const modeDescriptions: Record<DossierRenderMode, string> = {
|
|
55
73
|
read: 'Polished dossier with quiet evidence links.',
|
|
56
74
|
review: 'Every claim opened with its supporting evidence.',
|
|
57
75
|
audit: 'Source-first trace for verification.',
|
|
76
|
+
markdown: 'Markdown handoff for agents, slides, and reports.',
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
function cleanDisplayText(value: unknown): string {
|
|
61
80
|
return String(value ?? '')
|
|
62
|
-
.replace(/\*\*([
|
|
63
|
-
.replace(/\*([
|
|
64
|
-
.replace(/__([^_]+)__/g, '$1')
|
|
65
|
-
.replace(/_([^_]+)_/g, '$1')
|
|
81
|
+
.replace(/\*\*([^*\n]+)\*\*/g, '$1')
|
|
82
|
+
.replace(/\*([^*\n]+)\*/g, '$1')
|
|
83
|
+
.replace(/__([^_\n]+)__/g, '$1')
|
|
84
|
+
.replace(/_([^_\n]+)_/g, '$1')
|
|
66
85
|
.replace(/\s*\[(?:\d+(?:,\s*)?)+\]/g, '')
|
|
67
86
|
.replace(/\s*\[\d+\](?=\[\d+\])/g, '')
|
|
68
87
|
.replace(/\[(\d+)\]/g, '')
|
|
@@ -71,13 +90,241 @@ function cleanDisplayText(value: unknown): string {
|
|
|
71
90
|
.trim()
|
|
72
91
|
}
|
|
73
92
|
|
|
93
|
+
function displayPrompt(value: string): string {
|
|
94
|
+
return cleanDisplayText(value)
|
|
95
|
+
.replace(/\bcodign\b/gi, 'coding')
|
|
96
|
+
.replace(/\bmodles\b/gi, 'models')
|
|
97
|
+
.replace(/\bmdoels\b/gi, 'models')
|
|
98
|
+
.replace(/\bresearhc\b/gi, 'research')
|
|
99
|
+
.replace(/\bquesiton\b/gi, 'question')
|
|
100
|
+
.replace(/\bquesitons\b/gi, 'questions')
|
|
101
|
+
.replace(/\bsoruce\b/gi, 'source')
|
|
102
|
+
.replace(/\bsoruces\b/gi, 'sources')
|
|
103
|
+
.replace(/\s+/g, ' ')
|
|
104
|
+
.trim()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function displayTitle(value: string, fallback = 'Research finding'): string {
|
|
108
|
+
const cleaned = cleanDisplayText(value) || fallback
|
|
109
|
+
return cleaned.replace(/[A-Za-z]/, letter => letter.toUpperCase())
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function cleanRichText(value: unknown): string {
|
|
113
|
+
return String(value ?? '')
|
|
114
|
+
.replace(/\*\*([^*\n]+)\*\*/g, '$1')
|
|
115
|
+
.replace(/\*([^*\n]+)\*/g, '$1')
|
|
116
|
+
.replace(/__([^_\n]+)__/g, '$1')
|
|
117
|
+
.replace(/_([^_\n]+)_/g, '$1')
|
|
118
|
+
.replace(/\s*\[(?:\d+(?:,\s*)?)+\]/g, '')
|
|
119
|
+
.replace(/\s*\[\d+\](?=\[\d+\])/g, '')
|
|
120
|
+
.replace(/\[(\d+)\]/g, '')
|
|
121
|
+
.replace(/[^\S\r\n]+([,.;:])/g, '$1')
|
|
122
|
+
.replace(/[^\S\r\n]{2,}/g, ' ')
|
|
123
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
124
|
+
.trim()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function structuredFindingFields(value: string): {
|
|
128
|
+
title?: string
|
|
129
|
+
claim?: string
|
|
130
|
+
annotation?: string
|
|
131
|
+
} | null {
|
|
132
|
+
const decodeJsonString = (raw: string): string => {
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(`"${raw}"`) as string
|
|
135
|
+
} catch {
|
|
136
|
+
return raw.replace(/\\"/g, '"').replace(/\\n/g, '\n')
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const readField = (source: string, key: string): string => {
|
|
140
|
+
const match = source.match(
|
|
141
|
+
new RegExp(`"${key}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)`, 'is'),
|
|
142
|
+
)
|
|
143
|
+
return match?.[1] ? decodeJsonString(match[1]) : ''
|
|
144
|
+
}
|
|
145
|
+
const trimmed = value
|
|
146
|
+
.trim()
|
|
147
|
+
.replace(/^```(?:json)?\s*/i, '')
|
|
148
|
+
.replace(/\s*```$/i, '')
|
|
149
|
+
.trim()
|
|
150
|
+
const objectStart = trimmed.indexOf('{')
|
|
151
|
+
if (objectStart < 0) return null
|
|
152
|
+
const jsonish = trimmed.slice(objectStart)
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(jsonish) as Record<string, unknown>
|
|
155
|
+
return {
|
|
156
|
+
title:
|
|
157
|
+
typeof parsed.title === 'string'
|
|
158
|
+
? cleanDisplayText(parsed.title)
|
|
159
|
+
: undefined,
|
|
160
|
+
claim:
|
|
161
|
+
typeof parsed.claim === 'string'
|
|
162
|
+
? cleanRichText(parsed.claim)
|
|
163
|
+
: typeof parsed.summary === 'string'
|
|
164
|
+
? cleanRichText(parsed.summary)
|
|
165
|
+
: undefined,
|
|
166
|
+
annotation:
|
|
167
|
+
typeof parsed.annotation === 'string'
|
|
168
|
+
? cleanDisplayText(parsed.annotation)
|
|
169
|
+
: typeof parsed.caveat === 'string'
|
|
170
|
+
? cleanDisplayText(parsed.caveat)
|
|
171
|
+
: undefined,
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
const title = cleanDisplayText(readField(jsonish, 'title'))
|
|
175
|
+
const claim =
|
|
176
|
+
cleanRichText(readField(jsonish, 'claim')) ||
|
|
177
|
+
cleanRichText(readField(jsonish, 'summary')) ||
|
|
178
|
+
cleanRichText(readField(jsonish, 'finding'))
|
|
179
|
+
const annotation =
|
|
180
|
+
cleanDisplayText(readField(jsonish, 'annotation')) ||
|
|
181
|
+
cleanDisplayText(readField(jsonish, 'caveat'))
|
|
182
|
+
return title || claim || annotation ? { title, claim, annotation } : null
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function tableCells(line: string): string[] {
|
|
187
|
+
const trimmed = line.trim()
|
|
188
|
+
if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) return []
|
|
189
|
+
return trimmed
|
|
190
|
+
.slice(1, -1)
|
|
191
|
+
.split('|')
|
|
192
|
+
.map(cell => cleanDisplayText(cell))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isTableSeparator(line: string): boolean {
|
|
196
|
+
const cells = tableCells(line)
|
|
197
|
+
return Boolean(cells.length) && cells.every(cell => /^:?-{3,}:?$/.test(cell))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function readableParagraphs(text: string): string[] {
|
|
201
|
+
const cleaned = cleanDisplayText(text)
|
|
202
|
+
if (!cleaned) return []
|
|
203
|
+
const numberedSections = cleaned
|
|
204
|
+
.replace(/\s*;\s*(?=\(\d+\)\s)/g, '\n')
|
|
205
|
+
.split(/\n+/)
|
|
206
|
+
.map(section => section.trim())
|
|
207
|
+
.filter(Boolean)
|
|
208
|
+
if (
|
|
209
|
+
numberedSections.length >= 3 &&
|
|
210
|
+
numberedSections
|
|
211
|
+
.slice(1)
|
|
212
|
+
.every(section => /^\(\d+\)\s/.test(section))
|
|
213
|
+
) {
|
|
214
|
+
return numberedSections
|
|
215
|
+
}
|
|
216
|
+
if (cleaned.length < 420) return [cleaned]
|
|
217
|
+
// Split only at whitespace AFTER sentence punctuation — lossless, and never
|
|
218
|
+
// breaks inside tokens like "2.5", "AGENTS.md", or "e.g." (no space there).
|
|
219
|
+
const sentences = cleaned.split(/(?<=[.!?])\s+/)
|
|
220
|
+
if (sentences.length < 4) return [cleaned]
|
|
221
|
+
const paragraphs: string[] = []
|
|
222
|
+
let current = ''
|
|
223
|
+
for (const sentence of sentences.map(item => item.trim()).filter(Boolean)) {
|
|
224
|
+
const next = current ? `${current} ${sentence}` : sentence
|
|
225
|
+
if (current && next.length > 260) {
|
|
226
|
+
paragraphs.push(current)
|
|
227
|
+
current = sentence
|
|
228
|
+
} else {
|
|
229
|
+
current = next
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (current) paragraphs.push(current)
|
|
233
|
+
return paragraphs
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function summarySentence(value: string, fallback: string): string {
|
|
237
|
+
const cleaned = cleanDisplayText(value || fallback)
|
|
238
|
+
if (!cleaned) return fallback
|
|
239
|
+
const firstSentence = cleaned.split(/(?<=[.!?])\s+/)[0]?.trim()
|
|
240
|
+
const summary = firstSentence && firstSentence.length >= 70 ? firstSentence : cleaned
|
|
241
|
+
return summary.length > 280 ? `${summary.slice(0, 277).trim()}...` : summary
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function sourceNoteSummary(value: string): string {
|
|
245
|
+
const cleaned = cleanDisplayText(value)
|
|
246
|
+
.replace(/^Source date:\s*[^.]+\.\s*/i, '')
|
|
247
|
+
.replace(/^https?:\/\/\S+\s*/i, '')
|
|
248
|
+
.trim()
|
|
249
|
+
if (!cleaned) return 'Linked source.'
|
|
250
|
+
const tableStart = cleaned.indexOf('|')
|
|
251
|
+
if (tableStart > 0 && tableStart < 180) {
|
|
252
|
+
return cleaned.slice(0, tableStart).trim()
|
|
253
|
+
}
|
|
254
|
+
const firstSentence = cleaned.split(/(?<=[.!?])\s+/)[0]?.trim() || cleaned
|
|
255
|
+
return firstSentence.length > 150
|
|
256
|
+
? `${firstSentence.slice(0, 147).trim()}...`
|
|
257
|
+
: firstSentence
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function pipeSummaryRows(value: string): Array<[string, string]> {
|
|
261
|
+
const cleaned = cleanDisplayText(value)
|
|
262
|
+
const pipeIndex = cleaned.indexOf('|')
|
|
263
|
+
if (pipeIndex < 0) return []
|
|
264
|
+
const pipeText = cleaned.slice(pipeIndex)
|
|
265
|
+
const cells = pipeText
|
|
266
|
+
.split('|')
|
|
267
|
+
.map(cell => cleanDisplayText(cell))
|
|
268
|
+
.filter(Boolean)
|
|
269
|
+
.filter(cell => !/^-+$/.test(cell))
|
|
270
|
+
if (cells.length < 4) return []
|
|
271
|
+
const rows: Array<[string, string]> = []
|
|
272
|
+
for (let index = 0; index < Math.min(cells.length - 1, 10); index += 2) {
|
|
273
|
+
rows.push([cells[index], cells[index + 1]])
|
|
274
|
+
}
|
|
275
|
+
return rows.slice(0, 4)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function sourceUrlDisplay(url: string): string {
|
|
279
|
+
try {
|
|
280
|
+
const parsed = new URL(url)
|
|
281
|
+
return `${parsed.hostname.replace(/^www\./, '')}${parsed.pathname}`
|
|
282
|
+
} catch {
|
|
283
|
+
return url
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function LinkifiedText({ text }: { text: string }): ReactNode {
|
|
288
|
+
const parts = text.split(/(https?:\/\/[^\s]+)/g)
|
|
289
|
+
return (
|
|
290
|
+
<>
|
|
291
|
+
{parts.map((part, index) => {
|
|
292
|
+
if (!/^https?:\/\//i.test(part)) return part
|
|
293
|
+
const trailing = part.match(/[),.;:]+$/)?.[0] ?? ''
|
|
294
|
+
const href = trailing ? part.slice(0, -trailing.length) : part
|
|
295
|
+
return (
|
|
296
|
+
<span key={`${href}-${index}`}>
|
|
297
|
+
<a href={href} target="_blank" rel="noreferrer">
|
|
298
|
+
{sourceUrlDisplay(href)}
|
|
299
|
+
</a>
|
|
300
|
+
{trailing}
|
|
301
|
+
</span>
|
|
302
|
+
)
|
|
303
|
+
})}
|
|
304
|
+
</>
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Untrusted model/citation URLs: only http(s) may become a clickable link
|
|
309
|
+
* (a `javascript:`/`data:` href would execute script on click). */
|
|
310
|
+
function safeHttpUrl(url: string | undefined): string | undefined {
|
|
311
|
+
if (!url) return undefined
|
|
312
|
+
try {
|
|
313
|
+
const protocol = new URL(url).protocol
|
|
314
|
+
return protocol === 'http:' || protocol === 'https:' ? url : undefined
|
|
315
|
+
} catch {
|
|
316
|
+
return undefined
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
74
320
|
function sourceUrlWithTextFragment(source: EvidenceSource): string | undefined {
|
|
75
|
-
|
|
321
|
+
const safe = safeHttpUrl(source.url)
|
|
322
|
+
if (!safe) return undefined
|
|
76
323
|
const quote = cleanDisplayText(source.quote ?? '')
|
|
77
|
-
if (!quote) return
|
|
324
|
+
if (!quote) return safe
|
|
78
325
|
const fragment = quote.slice(0, 140).trim()
|
|
79
|
-
if (!fragment) return
|
|
80
|
-
const baseUrl =
|
|
326
|
+
if (!fragment) return safe
|
|
327
|
+
const baseUrl = safe.split('#')[0]
|
|
81
328
|
return `${baseUrl}#:~:text=${encodeURIComponent(fragment)}`
|
|
82
329
|
}
|
|
83
330
|
|
|
@@ -112,6 +359,17 @@ function normalizeEvidenceSource(
|
|
|
112
359
|
url: cleanDisplayText(value.url) || undefined,
|
|
113
360
|
note: cleanDisplayText(value.note),
|
|
114
361
|
quote: cleanDisplayText(value.quote) || undefined,
|
|
362
|
+
sourceDate: cleanDisplayText(value.sourceDate) || undefined,
|
|
363
|
+
freshness:
|
|
364
|
+
value.freshness === 'fresh' ||
|
|
365
|
+
value.freshness === 'stale' ||
|
|
366
|
+
value.freshness === 'undated'
|
|
367
|
+
? value.freshness
|
|
368
|
+
: undefined,
|
|
369
|
+
freshnessMode:
|
|
370
|
+
value.freshnessMode === 'recent' || value.freshnessMode === 'open'
|
|
371
|
+
? value.freshnessMode
|
|
372
|
+
: undefined,
|
|
115
373
|
strength:
|
|
116
374
|
value.strength === 'strong' ||
|
|
117
375
|
value.strength === 'working' ||
|
|
@@ -128,15 +386,20 @@ function normalizeEvidenceFinding(
|
|
|
128
386
|
if (!finding || typeof finding !== 'object') return null
|
|
129
387
|
const value = finding as Partial<EvidenceFinding>
|
|
130
388
|
const title = cleanDisplayText(value.title)
|
|
131
|
-
const claim =
|
|
389
|
+
const claim = cleanRichText(value.claim)
|
|
132
390
|
if (!title && !claim) return null
|
|
391
|
+
const structured =
|
|
392
|
+
structuredFindingFields(claim) || structuredFindingFields(title)
|
|
133
393
|
const evidence = Array.isArray(value.evidence)
|
|
134
394
|
? value.evidence.map(normalizeEvidenceSource)
|
|
135
395
|
: []
|
|
136
396
|
return {
|
|
137
397
|
id: cleanDisplayText(value.id) || `finding-${index}`,
|
|
138
|
-
title:
|
|
139
|
-
|
|
398
|
+
title:
|
|
399
|
+
(!title || /^research finding$/i.test(title)) && structured?.title
|
|
400
|
+
? displayTitle(structured.title)
|
|
401
|
+
: displayTitle(title || structured?.title || `Finding ${index + 1}`),
|
|
402
|
+
claim: structured?.claim || claim,
|
|
140
403
|
confidence:
|
|
141
404
|
value.confidence === 'strong' ||
|
|
142
405
|
value.confidence === 'working' ||
|
|
@@ -145,8 +408,23 @@ function normalizeEvidenceFinding(
|
|
|
145
408
|
: evidence.length
|
|
146
409
|
? 'working'
|
|
147
410
|
: 'weak',
|
|
148
|
-
annotation:
|
|
411
|
+
annotation:
|
|
412
|
+
structured?.annotation || cleanDisplayText(value.annotation) || undefined,
|
|
413
|
+
freshnessMode:
|
|
414
|
+
value.freshnessMode === 'recent' || value.freshnessMode === 'open'
|
|
415
|
+
? value.freshnessMode
|
|
416
|
+
: undefined,
|
|
149
417
|
evidence,
|
|
418
|
+
glossary: Array.isArray(value.glossary)
|
|
419
|
+
? value.glossary.filter(
|
|
420
|
+
(note): note is EvidenceTermNote =>
|
|
421
|
+
Boolean(note) &&
|
|
422
|
+
typeof note.term === 'string' &&
|
|
423
|
+
typeof note.explanation === 'string' &&
|
|
424
|
+
Boolean(note.term.trim()) &&
|
|
425
|
+
Boolean(note.explanation.trim()),
|
|
426
|
+
)
|
|
427
|
+
: undefined,
|
|
150
428
|
}
|
|
151
429
|
}
|
|
152
430
|
|
|
@@ -159,11 +437,32 @@ function isEvidenceFinding(
|
|
|
159
437
|
//#region styled-components
|
|
160
438
|
|
|
161
439
|
const Dossier = styled.article`
|
|
440
|
+
--dossier-serif: var(
|
|
441
|
+
--platform-typography-font-family-content,
|
|
442
|
+
'Source Serif 4',
|
|
443
|
+
Georgia,
|
|
444
|
+
serif
|
|
445
|
+
);
|
|
446
|
+
--dossier-sans: var(
|
|
447
|
+
--platform-typography-font-family,
|
|
448
|
+
'Archivo',
|
|
449
|
+
system-ui,
|
|
450
|
+
sans-serif
|
|
451
|
+
);
|
|
452
|
+
--dossier-mono: var(
|
|
453
|
+
--platform-typography-font-family-mono,
|
|
454
|
+
'JetBrains Mono',
|
|
455
|
+
ui-monospace,
|
|
456
|
+
monospace
|
|
457
|
+
);
|
|
458
|
+
|
|
162
459
|
display: grid;
|
|
163
460
|
gap: 24px;
|
|
164
461
|
min-width: 0;
|
|
165
462
|
position: relative;
|
|
166
463
|
color: var(--platform-colors-text, #1c1a17);
|
|
464
|
+
font-family: var(--dossier-serif);
|
|
465
|
+
font-optical-sizing: auto;
|
|
167
466
|
`
|
|
168
467
|
|
|
169
468
|
const Header = styled.header`
|
|
@@ -180,6 +479,7 @@ const ModeDock = styled.div`
|
|
|
180
479
|
z-index: 3;
|
|
181
480
|
justify-self: end;
|
|
182
481
|
gap: 5px;
|
|
482
|
+
font-family: var(--dossier-sans);
|
|
183
483
|
|
|
184
484
|
@media (max-width: 760px) {
|
|
185
485
|
justify-self: start;
|
|
@@ -216,11 +516,11 @@ const ModeButton = styled.button<{ $active: boolean }>`
|
|
|
216
516
|
border: 0;
|
|
217
517
|
border-radius: var(--platform-radius-sm);
|
|
218
518
|
background: ${({ $active }) =>
|
|
219
|
-
$active ? 'var(--platform-colors-surface-hover)' : 'transparent'};
|
|
519
|
+
$active ? 'var(--app-bg, var(--platform-colors-surface-hover))' : 'transparent'};
|
|
220
520
|
padding: 0 10px;
|
|
221
521
|
color: ${({ $active }) =>
|
|
222
522
|
$active
|
|
223
|
-
? 'var(--platform-colors-text, #1c1a17)'
|
|
523
|
+
? 'var(--app-text, var(--platform-colors-text, #1c1a17))'
|
|
224
524
|
: 'var(--platform-colors-text-secondary, #8f98a4)'};
|
|
225
525
|
font: inherit;
|
|
226
526
|
font-size: 12px;
|
|
@@ -230,6 +530,7 @@ const ModeButton = styled.button<{ $active: boolean }>`
|
|
|
230
530
|
|
|
231
531
|
const Kicker = styled.div`
|
|
232
532
|
color: var(--platform-colors-text-secondary, #8f897f);
|
|
533
|
+
font-family: var(--dossier-mono);
|
|
233
534
|
font-size: 11px;
|
|
234
535
|
font-weight: 800;
|
|
235
536
|
letter-spacing: 0.08em;
|
|
@@ -238,11 +539,12 @@ const Kicker = styled.div`
|
|
|
238
539
|
`
|
|
239
540
|
|
|
240
541
|
const Title = styled.h1`
|
|
241
|
-
max-width:
|
|
542
|
+
max-width: 28ch;
|
|
242
543
|
margin: 0;
|
|
243
544
|
color: var(--platform-colors-text, #20242a);
|
|
545
|
+
font-family: var(--dossier-serif);
|
|
244
546
|
font-size: clamp(34px, 4.2vw, 54px);
|
|
245
|
-
font-weight:
|
|
547
|
+
font-weight: 600;
|
|
246
548
|
letter-spacing: 0;
|
|
247
549
|
line-height: 1.05;
|
|
248
550
|
overflow-wrap: anywhere;
|
|
@@ -251,15 +553,15 @@ const Title = styled.h1`
|
|
|
251
553
|
const TitleInput = styled.textarea`
|
|
252
554
|
display: block;
|
|
253
555
|
width: 100%;
|
|
254
|
-
max-width:
|
|
556
|
+
max-width: 28ch;
|
|
255
557
|
min-height: 1.1em;
|
|
256
558
|
margin: 0;
|
|
257
559
|
border: 0;
|
|
258
560
|
background: transparent;
|
|
259
561
|
color: var(--platform-colors-text, #20242a);
|
|
260
|
-
font:
|
|
562
|
+
font-family: var(--dossier-serif);
|
|
261
563
|
font-size: clamp(34px, 4.2vw, 54px);
|
|
262
|
-
font-weight:
|
|
564
|
+
font-weight: 600;
|
|
263
565
|
letter-spacing: 0;
|
|
264
566
|
line-height: 1.05;
|
|
265
567
|
outline: none;
|
|
@@ -276,12 +578,22 @@ const TitleInput = styled.textarea`
|
|
|
276
578
|
const Question = styled.div`
|
|
277
579
|
max-width: 72ch;
|
|
278
580
|
color: var(--platform-colors-text-secondary, #6b665e);
|
|
279
|
-
font-size:
|
|
581
|
+
font-size: 15px;
|
|
280
582
|
line-height: 1.55;
|
|
281
583
|
`
|
|
282
584
|
|
|
585
|
+
const PromptLabel = styled.span`
|
|
586
|
+
color: var(--platform-colors-text-secondary, #8f897f);
|
|
587
|
+
font-family: var(--dossier-mono);
|
|
588
|
+
font-size: 11px;
|
|
589
|
+
font-weight: 800;
|
|
590
|
+
letter-spacing: 0.06em;
|
|
591
|
+
text-transform: uppercase;
|
|
592
|
+
`
|
|
593
|
+
|
|
283
594
|
const UpdatedStamp = styled.div`
|
|
284
595
|
color: var(--platform-colors-text-secondary, #8f897f);
|
|
596
|
+
font-family: var(--dossier-mono);
|
|
285
597
|
font-size: 11px;
|
|
286
598
|
font-weight: 750;
|
|
287
599
|
letter-spacing: 0.05em;
|
|
@@ -289,25 +601,80 @@ const UpdatedStamp = styled.div`
|
|
|
289
601
|
text-transform: uppercase;
|
|
290
602
|
`
|
|
291
603
|
|
|
604
|
+
const HeaderMeta = styled.div`
|
|
605
|
+
display: flex;
|
|
606
|
+
flex-wrap: wrap;
|
|
607
|
+
gap: 12px;
|
|
608
|
+
align-items: center;
|
|
609
|
+
`
|
|
610
|
+
|
|
611
|
+
const UpdateButton = styled.button`
|
|
612
|
+
border: 1px solid var(--platform-colors-border, rgb(46 51 56 / 14%));
|
|
613
|
+
border-radius: var(--platform-radius-sm);
|
|
614
|
+
background: transparent;
|
|
615
|
+
padding: 6px 9px;
|
|
616
|
+
color: var(--platform-colors-text, #20242a);
|
|
617
|
+
font-family: var(--dossier-sans);
|
|
618
|
+
font-size: 11px;
|
|
619
|
+
font-weight: 850;
|
|
620
|
+
letter-spacing: 0.06em;
|
|
621
|
+
line-height: 1;
|
|
622
|
+
text-transform: uppercase;
|
|
623
|
+
cursor: pointer;
|
|
624
|
+
|
|
625
|
+
&:hover,
|
|
626
|
+
&:focus-visible {
|
|
627
|
+
outline: none;
|
|
628
|
+
border-color: currentColor;
|
|
629
|
+
}
|
|
630
|
+
`
|
|
631
|
+
|
|
292
632
|
const Narrative = styled.section`
|
|
293
633
|
display: grid;
|
|
294
|
-
gap:
|
|
295
|
-
max-width:
|
|
296
|
-
|
|
634
|
+
gap: 20px;
|
|
635
|
+
max-width: min(100%, 1180px);
|
|
636
|
+
border-top: 1px solid var(--platform-colors-divider, rgb(46 51 56 / 10%));
|
|
637
|
+
border-bottom: 1px solid var(--platform-colors-divider, rgb(46 51 56 / 10%));
|
|
638
|
+
padding: 24px 0 28px;
|
|
297
639
|
`
|
|
298
640
|
|
|
299
641
|
const Lead = styled.div`
|
|
642
|
+
max-width: 72ch;
|
|
300
643
|
margin: 0;
|
|
301
644
|
color: var(--platform-colors-text, #1c1a17);
|
|
302
|
-
font-size: clamp(
|
|
303
|
-
line-height: 1.
|
|
645
|
+
font-size: clamp(17px, 1.12vw, 20px);
|
|
646
|
+
line-height: 1.64;
|
|
304
647
|
`
|
|
305
648
|
|
|
306
|
-
const
|
|
649
|
+
const ExecutiveList = styled.ul`
|
|
650
|
+
display: grid;
|
|
651
|
+
gap: 15px;
|
|
307
652
|
margin: 0;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
653
|
+
padding: 0;
|
|
654
|
+
list-style: none;
|
|
655
|
+
`
|
|
656
|
+
|
|
657
|
+
const ExecutiveItem = styled.li`
|
|
658
|
+
display: grid;
|
|
659
|
+
grid-template-columns: 26px minmax(0, 1fr);
|
|
660
|
+
gap: 12px;
|
|
661
|
+
max-width: 72ch;
|
|
662
|
+
color: var(--platform-colors-text, #20242a);
|
|
663
|
+
font-size: 16px;
|
|
664
|
+
line-height: 1.62;
|
|
665
|
+
`
|
|
666
|
+
|
|
667
|
+
const ExecutiveText = styled.div`
|
|
668
|
+
margin: 0;
|
|
669
|
+
overflow-wrap: anywhere;
|
|
670
|
+
`
|
|
671
|
+
|
|
672
|
+
const ExecutiveBullet = styled.span`
|
|
673
|
+
color: var(--platform-colors-text-secondary, #8f897f);
|
|
674
|
+
font-family: var(--dossier-mono);
|
|
675
|
+
font-size: 12px;
|
|
676
|
+
font-weight: 850;
|
|
677
|
+
letter-spacing: 0.04em;
|
|
311
678
|
`
|
|
312
679
|
|
|
313
680
|
const Findings = styled.section`
|
|
@@ -318,21 +685,24 @@ const Findings = styled.section`
|
|
|
318
685
|
const Finding = styled.article`
|
|
319
686
|
display: grid;
|
|
320
687
|
grid-template-columns: 34px minmax(0, 1fr);
|
|
321
|
-
gap:
|
|
322
|
-
padding:
|
|
688
|
+
gap: 22px;
|
|
689
|
+
padding: 24px 0 28px;
|
|
323
690
|
border-top: 1px solid var(--platform-colors-divider, rgb(46 51 56 / 10%));
|
|
324
691
|
`
|
|
325
692
|
|
|
326
693
|
const FindingNumber = styled.div`
|
|
694
|
+
padding-top: 0.38em;
|
|
327
695
|
color: var(--platform-colors-text-secondary, #8f897f);
|
|
696
|
+
font-family: var(--dossier-mono);
|
|
328
697
|
font-size: 13px;
|
|
329
698
|
font-weight: 800;
|
|
330
699
|
letter-spacing: 0.05em;
|
|
700
|
+
line-height: 1;
|
|
331
701
|
`
|
|
332
702
|
|
|
333
703
|
const FindingBody = styled.div`
|
|
334
704
|
display: grid;
|
|
335
|
-
gap:
|
|
705
|
+
gap: 12px;
|
|
336
706
|
min-width: 0;
|
|
337
707
|
`
|
|
338
708
|
|
|
@@ -358,24 +728,72 @@ const FindingTitleGroup = styled.div`
|
|
|
358
728
|
`
|
|
359
729
|
|
|
360
730
|
const FindingTitle = styled.h2`
|
|
731
|
+
flex: 1 1 auto;
|
|
732
|
+
min-width: 0;
|
|
361
733
|
margin: 0;
|
|
362
734
|
color: var(--platform-colors-text, #20242a);
|
|
363
|
-
font-
|
|
364
|
-
font-
|
|
735
|
+
font-family: var(--dossier-serif);
|
|
736
|
+
font-size: clamp(21px, 1.8vw, 30px);
|
|
737
|
+
font-weight: 600;
|
|
365
738
|
letter-spacing: 0;
|
|
366
|
-
line-height: 1.
|
|
739
|
+
line-height: 1.18;
|
|
367
740
|
overflow-wrap: anywhere;
|
|
368
741
|
`
|
|
369
742
|
|
|
743
|
+
const FindingBadges = styled.div`
|
|
744
|
+
display: flex;
|
|
745
|
+
flex-wrap: wrap;
|
|
746
|
+
gap: 7px;
|
|
747
|
+
align-items: center;
|
|
748
|
+
margin-top: 2px;
|
|
749
|
+
`
|
|
750
|
+
|
|
751
|
+
const FindingBadge = styled.span<{ $tone?: EvidenceStrength | 'fresh' | 'stale' | 'undated' }>`
|
|
752
|
+
display: inline-flex;
|
|
753
|
+
align-items: center;
|
|
754
|
+
min-height: 24px;
|
|
755
|
+
border: 1px solid
|
|
756
|
+
${({ $tone }) =>
|
|
757
|
+
$tone === 'strong' || $tone === 'fresh'
|
|
758
|
+
? 'rgb(61 107 74 / 24%)'
|
|
759
|
+
: $tone === 'weak' || $tone === 'stale'
|
|
760
|
+
? 'rgb(154 105 79 / 26%)'
|
|
761
|
+
: 'var(--platform-colors-border, rgb(46 51 56 / 12%))'};
|
|
762
|
+
border-radius: 999px;
|
|
763
|
+
background: ${({ $tone }) =>
|
|
764
|
+
$tone === 'strong' || $tone === 'fresh'
|
|
765
|
+
? 'rgb(61 107 74 / 6%)'
|
|
766
|
+
: $tone === 'weak' || $tone === 'stale'
|
|
767
|
+
? 'rgb(154 105 79 / 7%)'
|
|
768
|
+
: 'rgb(46 51 56 / 3%)'};
|
|
769
|
+
padding: 0 9px;
|
|
770
|
+
color: ${({ $tone }) =>
|
|
771
|
+
$tone === 'strong' || $tone === 'fresh'
|
|
772
|
+
? '#3d6b4a'
|
|
773
|
+
: $tone === 'weak' || $tone === 'stale'
|
|
774
|
+
? '#8a5f49'
|
|
775
|
+
: 'var(--platform-colors-text-secondary, #6b665e)'};
|
|
776
|
+
font-size: 11px;
|
|
777
|
+
font-family: var(--dossier-mono);
|
|
778
|
+
font-weight: 800;
|
|
779
|
+
letter-spacing: 0.04em;
|
|
780
|
+
line-height: 1;
|
|
781
|
+
text-transform: uppercase;
|
|
782
|
+
`
|
|
783
|
+
|
|
370
784
|
const DeleteAction = styled.button`
|
|
785
|
+
flex: 0 0 auto;
|
|
371
786
|
padding: 0;
|
|
372
787
|
border: 0;
|
|
373
788
|
background: transparent;
|
|
374
789
|
color: var(--platform-colors-text-secondary, #8f897f);
|
|
375
|
-
font:
|
|
790
|
+
font-family: var(--dossier-sans);
|
|
376
791
|
font-size: 11px;
|
|
377
792
|
font-weight: 800;
|
|
378
793
|
letter-spacing: 0.05em;
|
|
794
|
+
line-height: 1;
|
|
795
|
+
white-space: nowrap;
|
|
796
|
+
word-break: keep-all;
|
|
379
797
|
text-transform: uppercase;
|
|
380
798
|
opacity: 0;
|
|
381
799
|
cursor: pointer;
|
|
@@ -394,6 +812,7 @@ const DeleteAction = styled.button`
|
|
|
394
812
|
|
|
395
813
|
const Confidence = styled.span<{ $strength: EvidenceStrength }>`
|
|
396
814
|
flex: 0 0 auto;
|
|
815
|
+
min-width: 0;
|
|
397
816
|
color: ${({ $strength }) =>
|
|
398
817
|
$strength === 'strong'
|
|
399
818
|
? '#3d6b4a'
|
|
@@ -401,38 +820,228 @@ const Confidence = styled.span<{ $strength: EvidenceStrength }>`
|
|
|
401
820
|
? '#9a694f'
|
|
402
821
|
: 'var(--platform-colors-text-secondary, #6b665e)'};
|
|
403
822
|
font-size: 11px;
|
|
823
|
+
font-family: var(--dossier-mono);
|
|
404
824
|
font-weight: 800;
|
|
405
825
|
letter-spacing: 0.05em;
|
|
406
826
|
text-transform: uppercase;
|
|
827
|
+
overflow-wrap: anywhere;
|
|
407
828
|
`
|
|
408
829
|
|
|
409
|
-
const Claim = styled.
|
|
410
|
-
max-width:
|
|
830
|
+
const Claim = styled.div`
|
|
831
|
+
max-width: min(100%, 72ch);
|
|
411
832
|
margin: 0;
|
|
412
833
|
color: var(--platform-colors-text, #1c1a17);
|
|
413
|
-
font-size:
|
|
414
|
-
line-height: 1.
|
|
834
|
+
font-size: 16px;
|
|
835
|
+
line-height: 1.64;
|
|
415
836
|
`
|
|
416
837
|
|
|
417
838
|
const ClaimLine = styled.div`
|
|
418
|
-
display:
|
|
839
|
+
display: block;
|
|
840
|
+
`
|
|
841
|
+
|
|
842
|
+
const SourceSupportLine = styled.div`
|
|
843
|
+
display: flex;
|
|
844
|
+
flex-wrap: wrap;
|
|
845
|
+
gap: 6px 7px;
|
|
846
|
+
align-items: center;
|
|
847
|
+
margin-top: 10px;
|
|
848
|
+
max-width: 100%;
|
|
849
|
+
color: var(--platform-colors-text-secondary, #6b665e);
|
|
850
|
+
font-size: 13px;
|
|
851
|
+
line-height: 1.35;
|
|
852
|
+
`
|
|
853
|
+
|
|
854
|
+
const SourceSupportLabel = styled.span`
|
|
855
|
+
color: var(--platform-colors-text-secondary, #8f98a4);
|
|
856
|
+
font-family: var(--dossier-mono);
|
|
857
|
+
font-size: 10px;
|
|
858
|
+
font-weight: 800;
|
|
859
|
+
letter-spacing: 0.05em;
|
|
860
|
+
text-transform: uppercase;
|
|
861
|
+
`
|
|
862
|
+
|
|
863
|
+
const SourceSupportLink = styled.a`
|
|
864
|
+
display: inline-block;
|
|
865
|
+
max-width: min(30ch, 42vw);
|
|
866
|
+
overflow: hidden;
|
|
867
|
+
color: inherit;
|
|
868
|
+
font-family: var(--dossier-serif);
|
|
869
|
+
text-decoration: underline;
|
|
870
|
+
text-decoration-color: var(--platform-colors-border-strong, rgb(46 51 56 / 24%));
|
|
871
|
+
text-decoration-thickness: 1px;
|
|
872
|
+
text-underline-offset: 3px;
|
|
873
|
+
text-overflow: ellipsis;
|
|
874
|
+
white-space: nowrap;
|
|
875
|
+
|
|
876
|
+
&:hover,
|
|
877
|
+
&:focus-visible {
|
|
878
|
+
color: var(--platform-colors-text, #1c1a17);
|
|
879
|
+
outline: none;
|
|
880
|
+
text-decoration-color: currentColor;
|
|
881
|
+
}
|
|
882
|
+
`
|
|
883
|
+
|
|
884
|
+
const SourceSupportChip = styled.span`
|
|
885
|
+
display: inline-flex;
|
|
886
|
+
align-items: center;
|
|
887
|
+
max-width: min(32ch, 44vw);
|
|
888
|
+
border: 0;
|
|
889
|
+
background: transparent;
|
|
890
|
+
padding: 0;
|
|
891
|
+
overflow: hidden;
|
|
892
|
+
color: var(--platform-colors-text-secondary, #5f6670);
|
|
893
|
+
`
|
|
894
|
+
|
|
895
|
+
const ClaimContent = styled.div`
|
|
896
|
+
display: grid;
|
|
897
|
+
gap: 12px;
|
|
898
|
+
max-width: 100%;
|
|
899
|
+
min-width: 0;
|
|
900
|
+
`
|
|
901
|
+
|
|
902
|
+
const ClaimParagraph = styled.p`
|
|
903
|
+
margin: 0;
|
|
904
|
+
|
|
905
|
+
& + & {
|
|
906
|
+
margin-top: 0.72em;
|
|
907
|
+
}
|
|
908
|
+
`
|
|
909
|
+
|
|
910
|
+
const TableScroll = styled.div`
|
|
911
|
+
max-width: 100%;
|
|
912
|
+
overflow-x: auto;
|
|
913
|
+
`
|
|
914
|
+
|
|
915
|
+
const ClaimList = styled.ul`
|
|
916
|
+
margin: 0;
|
|
917
|
+
padding-left: 20px;
|
|
918
|
+
display: grid;
|
|
919
|
+
gap: 6px;
|
|
920
|
+
|
|
921
|
+
li {
|
|
922
|
+
margin: 0;
|
|
923
|
+
}
|
|
924
|
+
`
|
|
925
|
+
|
|
926
|
+
const ClaimHeading = styled.h4`
|
|
927
|
+
margin: 10px 0 0;
|
|
928
|
+
font-family: var(--dossier-sans);
|
|
929
|
+
font-size: 14px;
|
|
930
|
+
font-weight: 750;
|
|
931
|
+
letter-spacing: -0.01em;
|
|
932
|
+
color: var(--platform-colors-text, #1c1a17);
|
|
933
|
+
`
|
|
934
|
+
|
|
935
|
+
/** Mirrors the editor's comment-mark styling: accent underline, hover tint,
|
|
936
|
+
* with the explanation surfaced as a floating note. The note is a real
|
|
937
|
+
* element (not a CSS tooltip) so an authoritative explainer link stays
|
|
938
|
+
* clickable; it opens on hover and stays open while hovered or focused. */
|
|
939
|
+
const TermNoteMark = styled.span`
|
|
940
|
+
position: relative;
|
|
941
|
+
border-bottom: 1px solid
|
|
942
|
+
rgb(from var(--platform-colors-accent, #0067b8) r g b / 46%);
|
|
943
|
+
border-radius: 2px;
|
|
944
|
+
box-decoration-break: clone;
|
|
945
|
+
cursor: help;
|
|
946
|
+
padding: 0 1px;
|
|
947
|
+
|
|
948
|
+
&:hover,
|
|
949
|
+
&:focus-within {
|
|
950
|
+
background: rgb(from var(--platform-colors-accent, #0067b8) r g b / 12%);
|
|
951
|
+
outline: none;
|
|
952
|
+
}
|
|
953
|
+
`
|
|
954
|
+
|
|
955
|
+
const TermNotePopover = styled.span`
|
|
956
|
+
display: none;
|
|
957
|
+
position: absolute;
|
|
958
|
+
bottom: 100%;
|
|
959
|
+
left: 0;
|
|
960
|
+
z-index: 12;
|
|
961
|
+
/* Transparent bridge keeps hover alive between term and note. */
|
|
962
|
+
padding-bottom: 6px;
|
|
963
|
+
width: max-content;
|
|
964
|
+
max-width: min(340px, 72vw);
|
|
965
|
+
|
|
966
|
+
${TermNoteMark}:hover &,
|
|
967
|
+
${TermNoteMark}:focus-within & {
|
|
968
|
+
display: block;
|
|
969
|
+
}
|
|
970
|
+
`
|
|
971
|
+
|
|
972
|
+
const TermNoteBubble = styled.span`
|
|
973
|
+
display: block;
|
|
974
|
+
padding: 8px 10px;
|
|
975
|
+
border: 1px solid var(--platform-colors-border, rgb(46 51 56 / 16%));
|
|
976
|
+
border-radius: var(--platform-radius-sm, 4px);
|
|
977
|
+
background: var(--platform-colors-surface, #fff);
|
|
978
|
+
box-shadow: 0 6px 20px rgb(0 0 0 / 14%);
|
|
979
|
+
color: var(--platform-colors-text, #1c1a17);
|
|
980
|
+
font-family: var(--dossier-sans);
|
|
981
|
+
font-size: 12.5px;
|
|
982
|
+
font-weight: 400;
|
|
983
|
+
line-height: 1.45;
|
|
984
|
+
white-space: normal;
|
|
985
|
+
`
|
|
986
|
+
|
|
987
|
+
const TermNoteLink = styled.a`
|
|
988
|
+
display: inline-block;
|
|
989
|
+
margin-top: 4px;
|
|
990
|
+
color: var(--platform-colors-accent, #0067b8);
|
|
991
|
+
font-family: var(--dossier-mono);
|
|
992
|
+
font-size: 11px;
|
|
993
|
+
letter-spacing: 0.02em;
|
|
994
|
+
text-decoration: none;
|
|
995
|
+
|
|
996
|
+
&:hover,
|
|
997
|
+
&:focus-visible {
|
|
998
|
+
text-decoration: underline;
|
|
999
|
+
}
|
|
1000
|
+
`
|
|
1001
|
+
|
|
1002
|
+
const ClaimTable = styled.table`
|
|
1003
|
+
width: 100%;
|
|
1004
|
+
min-width: 560px;
|
|
1005
|
+
border-collapse: collapse;
|
|
1006
|
+
color: var(--platform-colors-text, #1c1a17);
|
|
1007
|
+
font-family: var(--dossier-sans);
|
|
1008
|
+
font-size: 12.5px;
|
|
1009
|
+
line-height: 1.45;
|
|
1010
|
+
|
|
1011
|
+
th,
|
|
1012
|
+
td {
|
|
1013
|
+
border: 1px solid var(--platform-colors-border, rgb(46 51 56 / 12%));
|
|
1014
|
+
padding: 7px 8px;
|
|
1015
|
+
text-align: left;
|
|
1016
|
+
vertical-align: top;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
th {
|
|
1020
|
+
background: var(--platform-colors-surface-hover, rgb(46 51 56 / 5%));
|
|
1021
|
+
font-family: var(--dossier-mono);
|
|
1022
|
+
font-size: 11px;
|
|
1023
|
+
font-weight: 800;
|
|
1024
|
+
letter-spacing: 0.04em;
|
|
1025
|
+
text-transform: uppercase;
|
|
1026
|
+
}
|
|
419
1027
|
`
|
|
420
1028
|
|
|
421
1029
|
const Annotation = styled.aside`
|
|
422
|
-
max-width:
|
|
1030
|
+
max-width: min(100%, 88ch);
|
|
423
1031
|
border-left: 3px solid rgb(82 90 101 / 36%);
|
|
424
1032
|
padding: 4px 0 4px 14px;
|
|
425
1033
|
color: var(--platform-colors-text-secondary, #6b665e);
|
|
426
|
-
font-
|
|
427
|
-
|
|
1034
|
+
font-family: var(--dossier-serif);
|
|
1035
|
+
font-size: 14px;
|
|
1036
|
+
line-height: 1.6;
|
|
428
1037
|
`
|
|
429
1038
|
|
|
430
1039
|
const EvidenceHandle = styled.button<{ $strength: EvidenceStrength }>`
|
|
431
1040
|
display: inline;
|
|
432
1041
|
align-items: center;
|
|
433
1042
|
min-height: 0;
|
|
434
|
-
margin-left:
|
|
435
|
-
transform:
|
|
1043
|
+
margin-left: 0;
|
|
1044
|
+
transform: none;
|
|
436
1045
|
border: 0;
|
|
437
1046
|
border-bottom: 1px solid
|
|
438
1047
|
${({ $strength }) =>
|
|
@@ -450,7 +1059,7 @@ const EvidenceHandle = styled.button<{ $strength: EvidenceStrength }>`
|
|
|
450
1059
|
: $strength === 'weak'
|
|
451
1060
|
? '#9a694f'
|
|
452
1061
|
: 'var(--platform-colors-text-secondary, #6b665e)'};
|
|
453
|
-
font:
|
|
1062
|
+
font-family: var(--dossier-mono);
|
|
454
1063
|
font-size: 0.78em;
|
|
455
1064
|
font-weight: 750;
|
|
456
1065
|
letter-spacing: 0.02em;
|
|
@@ -467,22 +1076,33 @@ const EvidenceHandle = styled.button<{ $strength: EvidenceStrength }>`
|
|
|
467
1076
|
const EvidenceDrawer = styled.aside`
|
|
468
1077
|
display: grid;
|
|
469
1078
|
gap: 10px;
|
|
470
|
-
max-width:
|
|
471
|
-
margin-top:
|
|
1079
|
+
max-width: min(100%, 1180px);
|
|
1080
|
+
margin-top: 10px;
|
|
472
1081
|
border-left: 2px solid var(--platform-colors-border, rgb(46 51 56 / 18%));
|
|
473
1082
|
padding: 2px 0 2px 14px;
|
|
474
1083
|
`
|
|
475
1084
|
|
|
476
1085
|
const EvidenceGrid = styled.div`
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
1086
|
+
column-count: 3;
|
|
1087
|
+
column-gap: 10px;
|
|
1088
|
+
min-width: 0;
|
|
1089
|
+
max-width: 100%;
|
|
480
1090
|
margin-top: 2px;
|
|
1091
|
+
|
|
1092
|
+
@media (max-width: 980px) {
|
|
1093
|
+
column-count: 2;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
@media (max-width: 640px) {
|
|
1097
|
+
column-count: 1;
|
|
1098
|
+
}
|
|
481
1099
|
`
|
|
482
1100
|
|
|
483
1101
|
const AuditList = styled.section`
|
|
484
1102
|
display: grid;
|
|
485
1103
|
gap: 14px;
|
|
1104
|
+
min-width: 0;
|
|
1105
|
+
max-width: 100%;
|
|
486
1106
|
border-top: 1px solid var(--platform-colors-divider, rgb(46 51 56 / 10%));
|
|
487
1107
|
padding-top: 18px;
|
|
488
1108
|
`
|
|
@@ -490,53 +1110,85 @@ const AuditList = styled.section`
|
|
|
490
1110
|
const AuditIntro = styled.div`
|
|
491
1111
|
max-width: 70ch;
|
|
492
1112
|
color: var(--platform-colors-text-secondary, #6b665e);
|
|
493
|
-
font-size:
|
|
1113
|
+
font-size: 15px;
|
|
494
1114
|
line-height: 1.55;
|
|
495
1115
|
`
|
|
496
1116
|
|
|
1117
|
+
const MarkdownPane = styled.pre`
|
|
1118
|
+
max-width: min(100%, 1180px);
|
|
1119
|
+
margin: 0;
|
|
1120
|
+
overflow: auto;
|
|
1121
|
+
border: 1px solid var(--platform-colors-border, rgb(46 51 56 / 12%));
|
|
1122
|
+
border-radius: var(--platform-radius-md, 0px);
|
|
1123
|
+
background: var(--platform-colors-surface, rgb(255 255 255 / 58%));
|
|
1124
|
+
padding: 18px;
|
|
1125
|
+
color: var(--platform-colors-text, #1c1a17);
|
|
1126
|
+
font-family:
|
|
1127
|
+
var(--platform-typography-font-family-mono, ui-monospace, SFMono-Regular),
|
|
1128
|
+
monospace;
|
|
1129
|
+
font-size: 12.5px;
|
|
1130
|
+
line-height: 1.55;
|
|
1131
|
+
white-space: pre-wrap;
|
|
1132
|
+
word-break: break-word;
|
|
1133
|
+
`
|
|
1134
|
+
|
|
497
1135
|
const AuditFinding = styled.div`
|
|
498
1136
|
display: grid;
|
|
499
1137
|
gap: 9px;
|
|
1138
|
+
min-width: 0;
|
|
1139
|
+
max-width: 100%;
|
|
500
1140
|
`
|
|
501
1141
|
|
|
502
1142
|
const EvidenceCard = styled.div`
|
|
503
1143
|
display: grid;
|
|
1144
|
+
break-inside: avoid;
|
|
504
1145
|
gap: 8px;
|
|
505
1146
|
min-width: 0;
|
|
1147
|
+
max-width: 100%;
|
|
1148
|
+
margin: 0 0 10px;
|
|
1149
|
+
overflow: hidden;
|
|
506
1150
|
border: 1px solid var(--platform-colors-border, rgb(46 51 56 / 10%));
|
|
507
1151
|
border-radius: var(--platform-radius-md, 0px);
|
|
508
1152
|
background: var(--platform-colors-surface, rgb(255 255 255 / 56%));
|
|
509
1153
|
padding: 12px 13px;
|
|
1154
|
+
font-family: var(--dossier-sans);
|
|
510
1155
|
`
|
|
511
1156
|
|
|
512
1157
|
const EvidenceTop = styled.div`
|
|
513
1158
|
display: flex;
|
|
1159
|
+
flex-wrap: wrap;
|
|
514
1160
|
align-items: baseline;
|
|
515
1161
|
justify-content: space-between;
|
|
516
1162
|
gap: 10px;
|
|
1163
|
+
min-width: 0;
|
|
517
1164
|
`
|
|
518
1165
|
|
|
519
1166
|
const EvidenceLabel = styled.span`
|
|
1167
|
+
min-width: 0;
|
|
520
1168
|
color: var(--platform-colors-text-secondary, #8f98a4);
|
|
1169
|
+
font-family: var(--dossier-mono);
|
|
521
1170
|
font-size: 10px;
|
|
522
1171
|
font-weight: 850;
|
|
523
1172
|
letter-spacing: 0.08em;
|
|
524
1173
|
text-transform: uppercase;
|
|
1174
|
+
overflow-wrap: anywhere;
|
|
525
1175
|
`
|
|
526
1176
|
|
|
527
1177
|
const EvidenceTitle = styled.a`
|
|
528
1178
|
min-width: 0;
|
|
529
|
-
overflow: hidden;
|
|
530
1179
|
color: var(--platform-colors-text, #1c1a17);
|
|
1180
|
+
font-family: var(--dossier-serif);
|
|
531
1181
|
font-size: 14px;
|
|
532
|
-
font-weight:
|
|
1182
|
+
font-weight: 600;
|
|
533
1183
|
line-height: 1.3;
|
|
534
|
-
text-decoration:
|
|
535
|
-
text-
|
|
536
|
-
|
|
1184
|
+
text-decoration: underline;
|
|
1185
|
+
text-decoration-color: var(--platform-colors-border-strong, rgb(46 51 56 / 24%));
|
|
1186
|
+
text-decoration-thickness: 1px;
|
|
1187
|
+
text-underline-offset: 3px;
|
|
1188
|
+
overflow-wrap: anywhere;
|
|
1189
|
+
word-break: break-word;
|
|
537
1190
|
|
|
538
1191
|
&:hover {
|
|
539
|
-
text-decoration: underline;
|
|
540
1192
|
text-decoration-color: var(
|
|
541
1193
|
--platform-colors-border-strong,
|
|
542
1194
|
rgb(46 51 56 / 28%)
|
|
@@ -546,15 +1198,40 @@ const EvidenceTitle = styled.a`
|
|
|
546
1198
|
|
|
547
1199
|
const EvidenceNote = styled.div`
|
|
548
1200
|
color: var(--platform-colors-text-secondary, #6b665e);
|
|
1201
|
+
font-family: var(--dossier-serif);
|
|
549
1202
|
font-size: 13px;
|
|
550
1203
|
line-height: 1.45;
|
|
551
1204
|
`
|
|
552
1205
|
|
|
1206
|
+
const EvidenceMiniTable = styled.dl`
|
|
1207
|
+
display: grid;
|
|
1208
|
+
grid-template-columns: minmax(64px, auto) minmax(0, 1fr);
|
|
1209
|
+
gap: 4px 8px;
|
|
1210
|
+
margin: 0;
|
|
1211
|
+
color: var(--platform-colors-text-secondary, #6b665e);
|
|
1212
|
+
font-family: var(--dossier-sans);
|
|
1213
|
+
font-size: 12px;
|
|
1214
|
+
line-height: 1.35;
|
|
1215
|
+
|
|
1216
|
+
dt {
|
|
1217
|
+
color: var(--platform-colors-text, #20242a);
|
|
1218
|
+
font-family: var(--dossier-mono);
|
|
1219
|
+
font-weight: 750;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
dd {
|
|
1223
|
+
margin: 0;
|
|
1224
|
+
min-width: 0;
|
|
1225
|
+
overflow-wrap: anywhere;
|
|
1226
|
+
}
|
|
1227
|
+
`
|
|
1228
|
+
|
|
553
1229
|
const Quote = styled.blockquote`
|
|
554
1230
|
margin: 0;
|
|
555
1231
|
border-left: 2px solid var(--platform-colors-border, rgb(46 51 56 / 16%));
|
|
556
1232
|
padding-left: 10px;
|
|
557
1233
|
color: var(--platform-colors-text, #3a4047);
|
|
1234
|
+
font-family: var(--dossier-serif);
|
|
558
1235
|
font-size: 12.5px;
|
|
559
1236
|
line-height: 1.45;
|
|
560
1237
|
`
|
|
@@ -584,7 +1261,7 @@ const QuietListItems = styled.div`
|
|
|
584
1261
|
|
|
585
1262
|
const QuietListItem = styled.div`
|
|
586
1263
|
color: var(--platform-colors-text-secondary, #6b665e);
|
|
587
|
-
font-size:
|
|
1264
|
+
font-size: 15px;
|
|
588
1265
|
line-height: 1.55;
|
|
589
1266
|
`
|
|
590
1267
|
|
|
@@ -592,6 +1269,7 @@ const Footer = styled.footer`
|
|
|
592
1269
|
border-top: 1px solid var(--platform-colors-divider, rgb(46 51 56 / 10%));
|
|
593
1270
|
padding-top: 14px;
|
|
594
1271
|
color: var(--platform-colors-text-secondary, #8f98a4);
|
|
1272
|
+
font-family: var(--dossier-sans);
|
|
595
1273
|
font-size: 12px;
|
|
596
1274
|
line-height: 1.5;
|
|
597
1275
|
`
|
|
@@ -618,9 +1296,10 @@ function SourceTitle({
|
|
|
618
1296
|
function evidenceHandleLabel(finding: EvidenceFinding): string {
|
|
619
1297
|
const count = finding?.evidence?.length
|
|
620
1298
|
const sourceLabel = count === 1 ? 'source' : 'sources'
|
|
621
|
-
if (finding.confidence === '
|
|
622
|
-
|
|
623
|
-
|
|
1299
|
+
if (finding.confidence === 'weak') {
|
|
1300
|
+
return `Review sources · needs support · ${count} ${sourceLabel}`
|
|
1301
|
+
}
|
|
1302
|
+
return `Review sources · ${count} ${sourceLabel}`
|
|
624
1303
|
}
|
|
625
1304
|
|
|
626
1305
|
function evidenceHandleTitle(finding: EvidenceFinding): string {
|
|
@@ -629,15 +1308,107 @@ function evidenceHandleTitle(finding: EvidenceFinding): string {
|
|
|
629
1308
|
return `Show evidence: ${cleanDisplayText(strongest.title)}`
|
|
630
1309
|
}
|
|
631
1310
|
|
|
1311
|
+
function topEvidenceSources(finding: EvidenceFinding): EvidenceSource[] {
|
|
1312
|
+
return finding.evidence.slice(0, 3)
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function markdownEscape(value: string): string {
|
|
1316
|
+
return cleanDisplayText(value).replace(/\[/g, '\\[').replace(/\]/g, '\\]')
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function markdownLink(label: string, url?: string): string {
|
|
1320
|
+
const cleanedLabel = markdownEscape(label || url || 'Source')
|
|
1321
|
+
if (!url) return cleanedLabel
|
|
1322
|
+
return `[${cleanedLabel}](${url.replace(/\)/g, '%29')})`
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function markdownFreshnessLabel(
|
|
1326
|
+
freshness?: EvidenceSource['freshness'],
|
|
1327
|
+
): string {
|
|
1328
|
+
if (freshness === 'fresh') return 'fresh evidence'
|
|
1329
|
+
if (freshness === 'stale') return 'older evidence'
|
|
1330
|
+
return 'undated evidence'
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function findingMarkdown(finding: EvidenceFinding, index: number): string {
|
|
1334
|
+
const sources = topEvidenceSources(finding)
|
|
1335
|
+
const lines = [
|
|
1336
|
+
`## ${index + 1}. ${cleanDisplayText(finding.title)}`,
|
|
1337
|
+
'',
|
|
1338
|
+
cleanRichText(finding.claim),
|
|
1339
|
+
'',
|
|
1340
|
+
`Evidence: ${strengthLabels[finding.confidence]}; ${
|
|
1341
|
+
finding.evidence.length
|
|
1342
|
+
} source${finding.evidence.length === 1 ? '' : 's'}.`,
|
|
1343
|
+
]
|
|
1344
|
+
if (finding.annotation) {
|
|
1345
|
+
lines.push('', `Note: ${cleanDisplayText(finding.annotation)}`)
|
|
1346
|
+
}
|
|
1347
|
+
if (sources.length) {
|
|
1348
|
+
lines.push('', 'Top supporting sources:')
|
|
1349
|
+
sources.forEach(source => {
|
|
1350
|
+
const date = source.sourceDate ? `, ${source.sourceDate}` : ''
|
|
1351
|
+
const freshness = source.freshness
|
|
1352
|
+
? `, ${markdownFreshnessLabel(source.freshness)}`
|
|
1353
|
+
: ''
|
|
1354
|
+
const note = sourceNoteSummary(source.note)
|
|
1355
|
+
lines.push(
|
|
1356
|
+
`- ${markdownLink(source.title || source.url || 'Source', source.url)}${date}${freshness}${note ? ` — ${note}` : ''}`,
|
|
1357
|
+
)
|
|
1358
|
+
})
|
|
1359
|
+
}
|
|
1360
|
+
return lines.join('\n')
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
export function dossierMarkdown(input: {
|
|
1364
|
+
title: string
|
|
1365
|
+
question: string
|
|
1366
|
+
updatedLabel: string | null
|
|
1367
|
+
leadText: string
|
|
1368
|
+
findings: EvidenceFinding[]
|
|
1369
|
+
}): string {
|
|
1370
|
+
const lines = [
|
|
1371
|
+
`# ${cleanDisplayText(input.title) || 'Untitled dossier'}`,
|
|
1372
|
+
'',
|
|
1373
|
+
`Original prompt: ${displayPrompt(input.question) || 'Not provided.'}`,
|
|
1374
|
+
]
|
|
1375
|
+
if (input.updatedLabel) lines.push(`Updated: ${input.updatedLabel}`)
|
|
1376
|
+
lines.push(
|
|
1377
|
+
'',
|
|
1378
|
+
'## Executive summary',
|
|
1379
|
+
'',
|
|
1380
|
+
cleanRichText(input.leadText),
|
|
1381
|
+
'',
|
|
1382
|
+
'## Findings',
|
|
1383
|
+
'',
|
|
1384
|
+
...input.findings.map(findingMarkdown),
|
|
1385
|
+
)
|
|
1386
|
+
return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trim()}\n`
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function findingFreshnessBadge(finding: EvidenceFinding): {
|
|
1390
|
+
label: string
|
|
1391
|
+
tone: 'fresh' | 'stale' | 'undated'
|
|
1392
|
+
} {
|
|
1393
|
+
if (finding.freshnessMode === 'open') {
|
|
1394
|
+
return { label: 'sources checked', tone: 'undated' }
|
|
1395
|
+
}
|
|
1396
|
+
if (finding.evidence.some(source => source.freshness === 'fresh')) {
|
|
1397
|
+
return { label: 'fresh sources', tone: 'fresh' }
|
|
1398
|
+
}
|
|
1399
|
+
if (finding.evidence.some(source => source.freshness === 'stale')) {
|
|
1400
|
+
return { label: 'older sources', tone: 'stale' }
|
|
1401
|
+
}
|
|
1402
|
+
return { label: 'check dates', tone: 'undated' }
|
|
1403
|
+
}
|
|
1404
|
+
|
|
632
1405
|
function EvidenceCitation({
|
|
633
1406
|
finding,
|
|
634
1407
|
open,
|
|
635
|
-
onOpen,
|
|
636
1408
|
onToggle,
|
|
637
1409
|
}: {
|
|
638
1410
|
finding: EvidenceFinding
|
|
639
1411
|
open: boolean
|
|
640
|
-
onOpen: () => void
|
|
641
1412
|
onToggle: () => void
|
|
642
1413
|
}): React.ReactElement | null {
|
|
643
1414
|
if (!finding?.evidence?.length) return null
|
|
@@ -645,17 +1416,56 @@ function EvidenceCitation({
|
|
|
645
1416
|
<EvidenceHandle
|
|
646
1417
|
type="button"
|
|
647
1418
|
$strength={finding.confidence}
|
|
648
|
-
onMouseEnter={onOpen}
|
|
649
|
-
onFocus={onOpen}
|
|
650
1419
|
onClick={onToggle}
|
|
651
1420
|
aria-expanded={open}
|
|
652
1421
|
title={evidenceHandleTitle(finding)}
|
|
653
1422
|
>
|
|
654
|
-
{evidenceHandleLabel(finding)}
|
|
1423
|
+
{open ? 'Hide sources' : evidenceHandleLabel(finding)}
|
|
655
1424
|
</EvidenceHandle>
|
|
656
1425
|
)
|
|
657
1426
|
}
|
|
658
1427
|
|
|
1428
|
+
function SourceSupport({
|
|
1429
|
+
finding,
|
|
1430
|
+
open,
|
|
1431
|
+
onToggle,
|
|
1432
|
+
}: {
|
|
1433
|
+
finding: EvidenceFinding
|
|
1434
|
+
open: boolean
|
|
1435
|
+
onToggle: () => void
|
|
1436
|
+
}): React.ReactElement | null {
|
|
1437
|
+
const sources = topEvidenceSources(finding)
|
|
1438
|
+
if (!finding.evidence.length) return null
|
|
1439
|
+
return (
|
|
1440
|
+
<SourceSupportLine>
|
|
1441
|
+
<EvidenceCitation
|
|
1442
|
+
finding={finding}
|
|
1443
|
+
open={open}
|
|
1444
|
+
onToggle={onToggle}
|
|
1445
|
+
/>
|
|
1446
|
+
{sources.length ? <SourceSupportLabel>Top sources</SourceSupportLabel> : null}
|
|
1447
|
+
{sources.map((source, index) => {
|
|
1448
|
+
const label =
|
|
1449
|
+
cleanDisplayText(source.title) ||
|
|
1450
|
+
cleanDisplayText(source.url) ||
|
|
1451
|
+
`Source ${index + 1}`
|
|
1452
|
+
const href = sourceUrlWithTextFragment(source)
|
|
1453
|
+
return (
|
|
1454
|
+
<SourceSupportChip key={source.id}>
|
|
1455
|
+
{href ? (
|
|
1456
|
+
<SourceSupportLink href={href} target="_blank" rel="noreferrer">
|
|
1457
|
+
{label}
|
|
1458
|
+
</SourceSupportLink>
|
|
1459
|
+
) : (
|
|
1460
|
+
label
|
|
1461
|
+
)}
|
|
1462
|
+
</SourceSupportChip>
|
|
1463
|
+
)
|
|
1464
|
+
})}
|
|
1465
|
+
</SourceSupportLine>
|
|
1466
|
+
)
|
|
1467
|
+
}
|
|
1468
|
+
|
|
659
1469
|
function EvidenceCards({
|
|
660
1470
|
sources,
|
|
661
1471
|
}: {
|
|
@@ -667,6 +1477,7 @@ function EvidenceCards({
|
|
|
667
1477
|
const showQuote = Boolean(
|
|
668
1478
|
source.quote && source.label !== 'Web evidence',
|
|
669
1479
|
)
|
|
1480
|
+
const rows = pipeSummaryRows(source.note)
|
|
670
1481
|
return (
|
|
671
1482
|
<EvidenceCard key={source.id}>
|
|
672
1483
|
<EvidenceTop>
|
|
@@ -676,7 +1487,19 @@ function EvidenceCards({
|
|
|
676
1487
|
</Confidence>
|
|
677
1488
|
</EvidenceTop>
|
|
678
1489
|
<SourceTitle source={source} />
|
|
679
|
-
<EvidenceNote>
|
|
1490
|
+
<EvidenceNote>
|
|
1491
|
+
<LinkifiedText text={sourceNoteSummary(source.note)} />
|
|
1492
|
+
</EvidenceNote>
|
|
1493
|
+
{rows.length ? (
|
|
1494
|
+
<EvidenceMiniTable>
|
|
1495
|
+
{rows.map(([label, value], index) => (
|
|
1496
|
+
<Fragment key={`${label}-${index}`}>
|
|
1497
|
+
<dt>{label}</dt>
|
|
1498
|
+
<dd>{value}</dd>
|
|
1499
|
+
</Fragment>
|
|
1500
|
+
))}
|
|
1501
|
+
</EvidenceMiniTable>
|
|
1502
|
+
) : null}
|
|
680
1503
|
{showQuote ? (
|
|
681
1504
|
<Quote>{cleanDisplayText(source.quote ?? '')}</Quote>
|
|
682
1505
|
) : null}
|
|
@@ -687,17 +1510,239 @@ function EvidenceCards({
|
|
|
687
1510
|
)
|
|
688
1511
|
}
|
|
689
1512
|
|
|
1513
|
+
/**
|
|
1514
|
+
* Repair markdown tables whose newlines were collapsed upstream (older
|
|
1515
|
+
* sanitizers flattened `\n` to spaces, leaving pipe rows glued into prose).
|
|
1516
|
+
* Only runs when a `|---|` separator run is present; row boundaries in
|
|
1517
|
+
* flattened text appear as `| |` (closing pipe, space, opening pipe).
|
|
1518
|
+
*/
|
|
1519
|
+
function inflateFlattenedTables(value: string): string {
|
|
1520
|
+
if (!/\|[ \t]*:?-{3,}/.test(value)) return value
|
|
1521
|
+
return value.replace(/\|[ \t]+\|/g, '|\n|')
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Heading markers that survive mid-line come from legacy flattened text where
|
|
1526
|
+
* the heading's line boundary is unrecoverable — drop the marker and its
|
|
1527
|
+
* section number ("### 2. " leaves a dangling "2." otherwise), keep the
|
|
1528
|
+
* words. Line-start headings are left intact for ClaimText to render.
|
|
1529
|
+
*/
|
|
1530
|
+
function stripInlineHeadingMarkers(value: string): string {
|
|
1531
|
+
return value.replace(
|
|
1532
|
+
/([^\n#])[ \t]+#{1,6}[ \t]+(?:\d{1,2}[.)][ \t]+)?/g,
|
|
1533
|
+
'$1 ',
|
|
1534
|
+
)
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function escapeRegExp(value: string): string {
|
|
1538
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/** Wrap glossary-term occurrences in an annotated mark with the explanation. */
|
|
1542
|
+
function annotateTerms(
|
|
1543
|
+
text: string,
|
|
1544
|
+
glossary: EvidenceTermNote[] | undefined,
|
|
1545
|
+
): ReactNode {
|
|
1546
|
+
const terms = (glossary ?? []).filter(
|
|
1547
|
+
item => item.term.trim() && item.explanation.trim(),
|
|
1548
|
+
)
|
|
1549
|
+
if (!terms.length) return text
|
|
1550
|
+
const byTerm = new Map(terms.map(item => [item.term.toLowerCase(), item]))
|
|
1551
|
+
const pattern = new RegExp(
|
|
1552
|
+
`(${[...terms]
|
|
1553
|
+
.sort((left, right) => right.term.length - left.term.length)
|
|
1554
|
+
.map(item => escapeRegExp(item.term))
|
|
1555
|
+
.join('|')})`,
|
|
1556
|
+
'gi',
|
|
1557
|
+
)
|
|
1558
|
+
const parts = text.split(pattern)
|
|
1559
|
+
if (parts.length === 1) return text
|
|
1560
|
+
return parts.map((part, partIndex) => {
|
|
1561
|
+
const note = byTerm.get(part.toLowerCase())
|
|
1562
|
+
if (!note) return part
|
|
1563
|
+
return (
|
|
1564
|
+
<TermNoteMark
|
|
1565
|
+
key={`term-${partIndex}`}
|
|
1566
|
+
tabIndex={0}
|
|
1567
|
+
data-explanation={note.explanation}
|
|
1568
|
+
aria-label={`${part}: ${note.explanation}`}
|
|
1569
|
+
>
|
|
1570
|
+
{part}
|
|
1571
|
+
<TermNotePopover role="note">
|
|
1572
|
+
<TermNoteBubble>
|
|
1573
|
+
{note.expansion ? <strong>{note.expansion} — </strong> : null}
|
|
1574
|
+
{note.explanation}
|
|
1575
|
+
{note.url ? (
|
|
1576
|
+
<TermNoteLink
|
|
1577
|
+
href={note.url}
|
|
1578
|
+
target="_blank"
|
|
1579
|
+
rel="noreferrer"
|
|
1580
|
+
aria-label={`Authoritative explanation of ${part}`}
|
|
1581
|
+
>
|
|
1582
|
+
{explainerLinkLabel(note.url)} ↗
|
|
1583
|
+
</TermNoteLink>
|
|
1584
|
+
) : null}
|
|
1585
|
+
</TermNoteBubble>
|
|
1586
|
+
</TermNotePopover>
|
|
1587
|
+
</TermNoteMark>
|
|
1588
|
+
)
|
|
1589
|
+
})
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function explainerLinkLabel(url: string): string {
|
|
1593
|
+
try {
|
|
1594
|
+
return new URL(url).hostname.replace(/^www\./, '')
|
|
1595
|
+
} catch {
|
|
1596
|
+
return 'Learn more'
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function ClaimText({
|
|
1601
|
+
value,
|
|
1602
|
+
glossary,
|
|
1603
|
+
}: {
|
|
1604
|
+
value: string
|
|
1605
|
+
glossary?: EvidenceTermNote[]
|
|
1606
|
+
}): React.ReactElement {
|
|
1607
|
+
const lines = stripInlineHeadingMarkers(
|
|
1608
|
+
inflateFlattenedTables(cleanRichText(value)),
|
|
1609
|
+
)
|
|
1610
|
+
.split('\n')
|
|
1611
|
+
.map(line => line.trim())
|
|
1612
|
+
const nodes: ReactNode[] = []
|
|
1613
|
+
let index = 0
|
|
1614
|
+
let paragraph: string[] = []
|
|
1615
|
+
const flushParagraph = (): void => {
|
|
1616
|
+
readableParagraphs(paragraph.join(' ')).forEach(text => {
|
|
1617
|
+
nodes.push(
|
|
1618
|
+
<ClaimParagraph key={`p-${nodes.length}`}>
|
|
1619
|
+
{annotateTerms(text, glossary)}
|
|
1620
|
+
</ClaimParagraph>,
|
|
1621
|
+
)
|
|
1622
|
+
})
|
|
1623
|
+
paragraph = []
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
while (index < lines.length) {
|
|
1627
|
+
// Detach prose glued to a table header ("…workflows: | Harness | …")
|
|
1628
|
+
// so the header row is recognized when the next line is a separator.
|
|
1629
|
+
const glued = lines[index]
|
|
1630
|
+
const nextLine = lines[index + 1]
|
|
1631
|
+
if (
|
|
1632
|
+
glued &&
|
|
1633
|
+
!glued.startsWith('|') &&
|
|
1634
|
+
glued.includes('|') &&
|
|
1635
|
+
glued.endsWith('|') &&
|
|
1636
|
+
nextLine &&
|
|
1637
|
+
isTableSeparator(nextLine)
|
|
1638
|
+
) {
|
|
1639
|
+
const pipeAt = glued.indexOf('|')
|
|
1640
|
+
const prose = glued.slice(0, pipeAt).trim()
|
|
1641
|
+
if (prose) paragraph.push(prose)
|
|
1642
|
+
lines[index] = glued.slice(pipeAt).trim()
|
|
1643
|
+
}
|
|
1644
|
+
const header = lines[index]
|
|
1645
|
+
const separator = lines[index + 1]
|
|
1646
|
+
if (header?.startsWith('|') && separator && isTableSeparator(separator)) {
|
|
1647
|
+
const headers = tableCells(header)
|
|
1648
|
+
const rows: string[][] = []
|
|
1649
|
+
index += 2
|
|
1650
|
+
while (index < lines.length && lines[index]?.startsWith('|')) {
|
|
1651
|
+
let rowLine = lines[index]
|
|
1652
|
+
// A final row may have trailing prose glued after its closing pipe
|
|
1653
|
+
// (flattened upstream); keep the row and requeue the prose.
|
|
1654
|
+
if (!rowLine.endsWith('|')) {
|
|
1655
|
+
const lastPipe = rowLine.lastIndexOf('|')
|
|
1656
|
+
if (lastPipe <= 0) break
|
|
1657
|
+
const trailing = rowLine.slice(lastPipe + 1).trim()
|
|
1658
|
+
rowLine = rowLine.slice(0, lastPipe + 1)
|
|
1659
|
+
if (trailing) lines.splice(index + 1, 0, trailing)
|
|
1660
|
+
}
|
|
1661
|
+
const cells = tableCells(rowLine)
|
|
1662
|
+
if (!cells.length) break
|
|
1663
|
+
rows.push(cells)
|
|
1664
|
+
index += 1
|
|
1665
|
+
}
|
|
1666
|
+
flushParagraph()
|
|
1667
|
+
nodes.push(
|
|
1668
|
+
<TableScroll key={`table-${nodes.length}`}>
|
|
1669
|
+
<ClaimTable>
|
|
1670
|
+
<thead>
|
|
1671
|
+
<tr>
|
|
1672
|
+
{headers.map((cell, cellIndex) => (
|
|
1673
|
+
<th key={`${cell}-${cellIndex}`}>{cell}</th>
|
|
1674
|
+
))}
|
|
1675
|
+
</tr>
|
|
1676
|
+
</thead>
|
|
1677
|
+
<tbody>
|
|
1678
|
+
{rows.map((row, rowIndex) => (
|
|
1679
|
+
<tr key={`row-${rowIndex}`}>
|
|
1680
|
+
{headers.map((_, cellIndex) => (
|
|
1681
|
+
<td key={`cell-${rowIndex}-${cellIndex}`}>
|
|
1682
|
+
{row[cellIndex] ?? ''}
|
|
1683
|
+
</td>
|
|
1684
|
+
))}
|
|
1685
|
+
</tr>
|
|
1686
|
+
))}
|
|
1687
|
+
</tbody>
|
|
1688
|
+
</ClaimTable>
|
|
1689
|
+
</TableScroll>,
|
|
1690
|
+
)
|
|
1691
|
+
continue
|
|
1692
|
+
}
|
|
1693
|
+
const headingMatch = header?.match(/^#{1,6}\s+(.+?)\s*#*$/)
|
|
1694
|
+
if (headingMatch && headingMatch[1].length <= 120) {
|
|
1695
|
+
flushParagraph()
|
|
1696
|
+
nodes.push(
|
|
1697
|
+
<ClaimHeading key={`heading-${nodes.length}`}>
|
|
1698
|
+
{headingMatch[1]}
|
|
1699
|
+
</ClaimHeading>,
|
|
1700
|
+
)
|
|
1701
|
+
index += 1
|
|
1702
|
+
continue
|
|
1703
|
+
}
|
|
1704
|
+
if (headingMatch) {
|
|
1705
|
+
// Over-long "heading" is legacy flattened text with prose glued after
|
|
1706
|
+
// the title — drop the marker and enumeration, reprocess as paragraph.
|
|
1707
|
+
lines[index] = headingMatch[1].replace(/^\d{1,2}[.)]\s+/, '')
|
|
1708
|
+
continue
|
|
1709
|
+
}
|
|
1710
|
+
if (header && /^[*•-]\s+/.test(header)) {
|
|
1711
|
+
flushParagraph()
|
|
1712
|
+
const items: string[] = []
|
|
1713
|
+
while (index < lines.length && /^[*•-]\s+/.test(lines[index])) {
|
|
1714
|
+
items.push(lines[index].replace(/^[*•-]\s+/, ''))
|
|
1715
|
+
index += 1
|
|
1716
|
+
}
|
|
1717
|
+
nodes.push(
|
|
1718
|
+
<ClaimList key={`list-${nodes.length}`}>
|
|
1719
|
+
{items.map((item, itemIndex) => (
|
|
1720
|
+
<li key={`item-${itemIndex}`}>{annotateTerms(item, glossary)}</li>
|
|
1721
|
+
))}
|
|
1722
|
+
</ClaimList>,
|
|
1723
|
+
)
|
|
1724
|
+
continue
|
|
1725
|
+
}
|
|
1726
|
+
if (header) paragraph.push(header)
|
|
1727
|
+
else flushParagraph()
|
|
1728
|
+
index += 1
|
|
1729
|
+
}
|
|
1730
|
+
flushParagraph()
|
|
1731
|
+
|
|
1732
|
+
return <ClaimContent>{nodes}</ClaimContent>
|
|
1733
|
+
}
|
|
1734
|
+
|
|
690
1735
|
export function EvidenceDossier({
|
|
691
1736
|
title,
|
|
692
1737
|
question,
|
|
693
1738
|
answer,
|
|
694
1739
|
updatedAt,
|
|
695
|
-
narrative,
|
|
696
1740
|
findings,
|
|
697
1741
|
uncertainties,
|
|
698
1742
|
nextSteps,
|
|
699
1743
|
onTitleChange,
|
|
700
1744
|
onDeleteFinding,
|
|
1745
|
+
onUpdate,
|
|
701
1746
|
footer,
|
|
702
1747
|
className,
|
|
703
1748
|
}: EvidenceDossierProps): React.ReactElement {
|
|
@@ -707,7 +1752,6 @@ export function EvidenceDossier({
|
|
|
707
1752
|
const safeFindings = Array.isArray(findings)
|
|
708
1753
|
? findings.map(normalizeEvidenceFinding).filter(isEvidenceFinding)
|
|
709
1754
|
: []
|
|
710
|
-
const safeNarrative = Array.isArray(narrative) ? narrative : []
|
|
711
1755
|
const safeUncertainties = Array.isArray(uncertainties) ? uncertainties : []
|
|
712
1756
|
const safeNextSteps = Array.isArray(nextSteps) ? nextSteps : []
|
|
713
1757
|
const activeEvidence =
|
|
@@ -718,14 +1762,23 @@ export function EvidenceDossier({
|
|
|
718
1762
|
setOpenEvidenceId(current => (current === findingId ? null : findingId))
|
|
719
1763
|
}
|
|
720
1764
|
const leadEvidence = safeFindings[0]
|
|
721
|
-
const
|
|
722
|
-
|
|
1765
|
+
const leadText =
|
|
1766
|
+
leadEvidence?.claim ||
|
|
1767
|
+
cleanDisplayText(answer) ||
|
|
1768
|
+
'No executive summary available yet.'
|
|
1769
|
+
const markdownText = dossierMarkdown({
|
|
1770
|
+
title,
|
|
1771
|
+
question,
|
|
1772
|
+
updatedLabel,
|
|
1773
|
+
leadText,
|
|
1774
|
+
findings: safeFindings,
|
|
1775
|
+
})
|
|
723
1776
|
|
|
724
1777
|
return (
|
|
725
1778
|
<Dossier className={className}>
|
|
726
1779
|
<ModeDock>
|
|
727
1780
|
<ModeSwitch aria-label="Dossier reading density">
|
|
728
|
-
{(['read', 'review', 'audit'] as DossierRenderMode[]).map(item => (
|
|
1781
|
+
{(['read', 'review', 'audit', 'markdown'] as DossierRenderMode[]).map(item => (
|
|
729
1782
|
<ModeButton
|
|
730
1783
|
key={item}
|
|
731
1784
|
type="button"
|
|
@@ -754,60 +1807,76 @@ export function EvidenceDossier({
|
|
|
754
1807
|
) : (
|
|
755
1808
|
<Title>{cleanDisplayText(title)}</Title>
|
|
756
1809
|
)}
|
|
757
|
-
<Question>
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1810
|
+
<Question>
|
|
1811
|
+
<PromptLabel>Original prompt: </PromptLabel>
|
|
1812
|
+
{displayPrompt(question)}
|
|
1813
|
+
</Question>
|
|
1814
|
+
<HeaderMeta>
|
|
1815
|
+
{updatedLabel ? (
|
|
1816
|
+
<UpdatedStamp>Updated {updatedLabel}</UpdatedStamp>
|
|
1817
|
+
) : null}
|
|
1818
|
+
{onUpdate ? (
|
|
1819
|
+
<UpdateButton type="button" onClick={onUpdate}>
|
|
1820
|
+
Update
|
|
1821
|
+
</UpdateButton>
|
|
1822
|
+
) : null}
|
|
1823
|
+
</HeaderMeta>
|
|
761
1824
|
</Header>
|
|
762
1825
|
|
|
763
|
-
{mode
|
|
1826
|
+
{mode === 'markdown' ? (
|
|
1827
|
+
<MarkdownPane aria-label="Markdown dossier handoff">
|
|
1828
|
+
{markdownText}
|
|
1829
|
+
</MarkdownPane>
|
|
1830
|
+
) : null}
|
|
1831
|
+
|
|
1832
|
+
{mode !== 'audit' && mode !== 'markdown' ? (
|
|
764
1833
|
<Narrative aria-label="Dossier narrative">
|
|
765
|
-
<Kicker>
|
|
1834
|
+
<Kicker>Executive summary</Kicker>
|
|
766
1835
|
<Lead>
|
|
767
|
-
{
|
|
1836
|
+
<ClaimText value={leadText} glossary={leadEvidence?.glossary} />
|
|
768
1837
|
{leadEvidence ? (
|
|
769
|
-
<
|
|
1838
|
+
<SourceSupport
|
|
770
1839
|
finding={leadEvidence}
|
|
771
1840
|
open={openEvidenceId === leadEvidence.id}
|
|
772
|
-
onOpen={() => setOpenEvidenceId(leadEvidence.id)}
|
|
773
1841
|
onToggle={() => toggleEvidence(leadEvidence.id)}
|
|
774
1842
|
/>
|
|
775
1843
|
) : null}
|
|
1844
|
+
{activeEvidence &&
|
|
1845
|
+
leadEvidence &&
|
|
1846
|
+
activeEvidence.id === leadEvidence.id ? (
|
|
1847
|
+
<EvidenceDrawer>
|
|
1848
|
+
<EvidenceCards sources={activeEvidence?.evidence} />
|
|
1849
|
+
</EvidenceDrawer>
|
|
1850
|
+
) : null}
|
|
776
1851
|
</Lead>
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
<EvidenceCards sources={activeEvidence?.evidence} />
|
|
782
|
-
</EvidenceDrawer>
|
|
783
|
-
) : null}
|
|
784
|
-
{safeNarrative.map((item, index) => {
|
|
785
|
-
const relatedFinding = narrativeEvidence(index)
|
|
786
|
-
const paragraphEvidence =
|
|
787
|
-
activeEvidence &&
|
|
788
|
-
relatedFinding &&
|
|
789
|
-
activeEvidence.id === relatedFinding.id
|
|
790
|
-
? activeEvidence
|
|
791
|
-
: null
|
|
1852
|
+
<ExecutiveList aria-label="Executive summary points">
|
|
1853
|
+
{safeFindings.map((finding, index) => {
|
|
1854
|
+
const paragraphEvidence =
|
|
1855
|
+
activeEvidence?.id === finding.id ? activeEvidence : null
|
|
792
1856
|
return (
|
|
793
|
-
<
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1857
|
+
<ExecutiveItem key={finding.id}>
|
|
1858
|
+
<ExecutiveBullet>
|
|
1859
|
+
{String(index + 1).padStart(2, '0')}
|
|
1860
|
+
</ExecutiveBullet>
|
|
1861
|
+
<div>
|
|
1862
|
+
<ExecutiveText>
|
|
1863
|
+
{summarySentence(finding.claim, finding.title)}
|
|
1864
|
+
</ExecutiveText>
|
|
1865
|
+
<SourceSupport
|
|
1866
|
+
finding={finding}
|
|
1867
|
+
open={openEvidenceId === finding.id}
|
|
1868
|
+
onToggle={() => toggleEvidence(finding.id)}
|
|
801
1869
|
/>
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
</
|
|
1870
|
+
{paragraphEvidence ? (
|
|
1871
|
+
<EvidenceDrawer>
|
|
1872
|
+
<EvidenceCards sources={paragraphEvidence.evidence} />
|
|
1873
|
+
</EvidenceDrawer>
|
|
1874
|
+
) : null}
|
|
1875
|
+
</div>
|
|
1876
|
+
</ExecutiveItem>
|
|
809
1877
|
)
|
|
810
|
-
|
|
1878
|
+
})}
|
|
1879
|
+
</ExecutiveList>
|
|
811
1880
|
</Narrative>
|
|
812
1881
|
) : null}
|
|
813
1882
|
|
|
@@ -821,12 +1890,14 @@ export function EvidenceDossier({
|
|
|
821
1890
|
{safeFindings.map(finding => (
|
|
822
1891
|
<AuditFinding key={finding.id}>
|
|
823
1892
|
<FindingTitle>{cleanDisplayText(finding.title)}</FindingTitle>
|
|
824
|
-
<Claim>
|
|
1893
|
+
<Claim>
|
|
1894
|
+
<ClaimText value={finding.claim} glossary={finding.glossary} />
|
|
1895
|
+
</Claim>
|
|
825
1896
|
<EvidenceCards sources={finding.evidence} />
|
|
826
1897
|
</AuditFinding>
|
|
827
1898
|
))}
|
|
828
1899
|
</AuditList>
|
|
829
|
-
) : (
|
|
1900
|
+
) : mode !== 'markdown' ? (
|
|
830
1901
|
<Findings aria-label="Evidence-backed positions">
|
|
831
1902
|
<Kicker>{mode === 'read' ? 'Argument' : 'Claim review'}</Kicker>
|
|
832
1903
|
{safeFindings.map((finding, index) => (
|
|
@@ -850,20 +1921,33 @@ export function EvidenceDossier({
|
|
|
850
1921
|
</DeleteAction>
|
|
851
1922
|
) : null}
|
|
852
1923
|
</FindingTitleGroup>
|
|
853
|
-
{mode === 'review' ? (
|
|
854
|
-
<Confidence $strength={finding.confidence}>
|
|
855
|
-
{strengthLabels[finding.confidence]}
|
|
856
|
-
</Confidence>
|
|
857
|
-
) : null}
|
|
858
1924
|
</FindingHeader>
|
|
1925
|
+
<FindingBadges aria-label="Finding review signals">
|
|
1926
|
+
<FindingBadge $tone={finding.confidence}>
|
|
1927
|
+
{strengthLabels[finding.confidence]}
|
|
1928
|
+
</FindingBadge>
|
|
1929
|
+
<FindingBadge $tone={findingFreshnessBadge(finding).tone}>
|
|
1930
|
+
{findingFreshnessBadge(finding).label}
|
|
1931
|
+
</FindingBadge>
|
|
1932
|
+
<FindingBadge>
|
|
1933
|
+
{finding.evidence.length} source
|
|
1934
|
+
{finding.evidence.length === 1 ? '' : 's'}
|
|
1935
|
+
</FindingBadge>
|
|
1936
|
+
</FindingBadges>
|
|
859
1937
|
<Claim>
|
|
860
|
-
<ClaimLine>
|
|
861
|
-
|
|
1938
|
+
<ClaimLine>
|
|
1939
|
+
<ClaimText value={finding.claim} glossary={finding.glossary} />
|
|
1940
|
+
</ClaimLine>
|
|
1941
|
+
<SourceSupport
|
|
862
1942
|
finding={finding}
|
|
863
1943
|
open={openEvidenceId === finding.id}
|
|
864
|
-
onOpen={() => setOpenEvidenceId(finding.id)}
|
|
865
1944
|
onToggle={() => toggleEvidence(finding.id)}
|
|
866
1945
|
/>
|
|
1946
|
+
{activeEvidence?.id === finding.id && mode !== 'review' ? (
|
|
1947
|
+
<EvidenceDrawer>
|
|
1948
|
+
<EvidenceCards sources={activeEvidence.evidence} />
|
|
1949
|
+
</EvidenceDrawer>
|
|
1950
|
+
) : null}
|
|
867
1951
|
</Claim>
|
|
868
1952
|
{finding.annotation ? (
|
|
869
1953
|
<Annotation>
|
|
@@ -872,35 +1956,33 @@ export function EvidenceDossier({
|
|
|
872
1956
|
) : null}
|
|
873
1957
|
{mode === 'review' ? (
|
|
874
1958
|
<EvidenceCards sources={finding.evidence} />
|
|
875
|
-
) : activeEvidence?.id === finding.id ? (
|
|
876
|
-
<EvidenceDrawer>
|
|
877
|
-
<EvidenceCards sources={activeEvidence.evidence} />
|
|
878
|
-
</EvidenceDrawer>
|
|
879
1959
|
) : null}
|
|
880
1960
|
</FindingBody>
|
|
881
1961
|
</Finding>
|
|
882
1962
|
))}
|
|
883
1963
|
</Findings>
|
|
884
|
-
)}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
<
|
|
888
|
-
<
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
<
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1964
|
+
) : null}
|
|
1965
|
+
|
|
1966
|
+
{mode !== 'markdown' ? (
|
|
1967
|
+
<BottomGrid>
|
|
1968
|
+
<QuietList>
|
|
1969
|
+
<Kicker>Uncertainty</Kicker>
|
|
1970
|
+
<QuietListItems>
|
|
1971
|
+
{safeUncertainties.map(item => (
|
|
1972
|
+
<QuietListItem key={item}>{cleanDisplayText(item)}</QuietListItem>
|
|
1973
|
+
))}
|
|
1974
|
+
</QuietListItems>
|
|
1975
|
+
</QuietList>
|
|
1976
|
+
<QuietList>
|
|
1977
|
+
<Kicker>Next</Kicker>
|
|
1978
|
+
<QuietListItems>
|
|
1979
|
+
{safeNextSteps.map(item => (
|
|
1980
|
+
<QuietListItem key={item}>{cleanDisplayText(item)}</QuietListItem>
|
|
1981
|
+
))}
|
|
1982
|
+
</QuietListItems>
|
|
1983
|
+
</QuietList>
|
|
1984
|
+
</BottomGrid>
|
|
1985
|
+
) : null}
|
|
904
1986
|
|
|
905
1987
|
{footer ? <Footer>{footer}</Footer> : null}
|
|
906
1988
|
</Dossier>
|