@poodle64/librarian 2026.10.3 → 2026.10.5
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/README.md +12 -4
- package/dist/chat.svelte.d.ts +40 -0
- package/dist/chat.svelte.js +57 -3
- package/dist/client.d.ts +3 -0
- package/dist/client.js +4 -1
- package/dist/components/activity-group/activity-group.svelte +72 -77
- package/dist/components/activity-group/activity-group.svelte.d.ts +6 -1
- package/dist/components/agent-transcript/agent-transcript.svelte +225 -9
- package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +19 -0
- package/dist/components/artefact-pane/artefact-pane.svelte +387 -43
- package/dist/components/artefact-pane/artefact-pane.svelte.d.ts +10 -1
- package/dist/components/conversation/conversation.svelte +74 -4
- package/dist/components/conversation/conversation.svelte.d.ts +15 -0
- package/dist/components/markdown/markdown.d.ts +30 -2
- package/dist/components/markdown/markdown.js +142 -5
- package/dist/components/markdown/markdown.svelte +130 -16
- package/dist/components/markdown/markdown.svelte.d.ts +12 -0
- package/dist/components/source-fold/index.d.ts +2 -0
- package/dist/components/source-fold/index.js +2 -0
- package/dist/components/source-fold/source-fold.svelte +225 -0
- package/dist/components/source-fold/source-fold.svelte.d.ts +17 -0
- package/dist/components/source-list/source-list.svelte +5 -3
- package/dist/components/source-list/source-list.svelte.d.ts +2 -0
- package/dist/components/thinking-row/thinking-row.svelte +38 -16
- package/dist/components/thinking-row/thinking-row.svelte.d.ts +3 -0
- package/dist/components/tool-row/tool-row.svelte +161 -49
- package/dist/components/tool-row/tool-row.svelte.d.ts +2 -3
- package/dist/components/visual-block/entity-card.svelte +94 -0
- package/dist/components/visual-block/entity-card.svelte.d.ts +7 -0
- package/dist/components/visual-block/timeline.svelte +99 -0
- package/dist/components/visual-block/timeline.svelte.d.ts +7 -0
- package/dist/components/visual-block/vega-chart.svelte +110 -0
- package/dist/components/visual-block/vega-chart.svelte.d.ts +7 -0
- package/dist/components/visual-block/visual-block.svelte +53 -0
- package/dist/components/visual-block/visual-block.svelte.d.ts +13 -0
- package/dist/components/written-card/index.d.ts +2 -0
- package/dist/components/written-card/index.js +2 -0
- package/dist/components/written-card/written-card.svelte +178 -0
- package/dist/components/written-card/written-card.svelte.d.ts +15 -0
- package/dist/copy.d.ts +73 -0
- package/dist/copy.js +56 -0
- package/dist/diff.d.ts +8 -0
- package/dist/diff.js +37 -0
- package/dist/steps.d.ts +28 -0
- package/dist/steps.js +168 -0
- package/dist/transcript.svelte.d.ts +19 -34
- package/dist/transcript.svelte.js +64 -56
- package/dist/visuals.d.ts +38 -0
- package/dist/visuals.js +82 -0
- package/dist/web-sources.d.ts +34 -0
- package/dist/web-sources.js +90 -0
- package/dist/written.d.ts +71 -0
- package/dist/written.js +83 -0
- package/package.json +14 -1
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
An artefact that is its answer's own prose,
|
|
2
|
+
An artefact that is its answer's own prose, or one version of a document the
|
|
3
|
+
persona wrote, open in the reading column.
|
|
4
|
+
|
|
5
|
+
A written document is shown from its source: Markdown through the package's
|
|
6
|
+
sanitising renderer, HTML only inside a sandboxed frame with no permissions
|
|
7
|
+
(no scripts, no same origin), never injected into the page.
|
|
3
8
|
|
|
4
9
|
The same shell shape `DocumentPane` uses (a column at 64rem, a bottom sheet
|
|
5
10
|
below it) because decision 2 of the agent surface is that a colleague never
|
|
@@ -7,15 +12,40 @@
|
|
|
7
12
|
only ever one, in the same place, closing whichever was there.
|
|
8
13
|
-->
|
|
9
14
|
<script lang="ts">
|
|
15
|
+
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
|
16
|
+
import CopyIcon from '@lucide/svelte/icons/copy';
|
|
17
|
+
import CheckIcon from '@lucide/svelte/icons/check';
|
|
18
|
+
import DownloadIcon from '@lucide/svelte/icons/download';
|
|
10
19
|
import XIcon from '@lucide/svelte/icons/x';
|
|
20
|
+
import { untrack } from 'svelte';
|
|
11
21
|
import { segment, type TextBlock, type Turn } from '../../transcript.svelte';
|
|
12
22
|
import { resolveCitations, splitSources, type Citation } from '../../citations';
|
|
13
|
-
import { resolveCopy, type ChatCopy } from '../../copy';
|
|
23
|
+
import { fill, resolveCopy, type ChatCopy } from '../../copy';
|
|
24
|
+
import {
|
|
25
|
+
downloads,
|
|
26
|
+
framed,
|
|
27
|
+
isHtml,
|
|
28
|
+
kindLabel,
|
|
29
|
+
stripFrontMatter,
|
|
30
|
+
writtenKey,
|
|
31
|
+
type LoadWritten,
|
|
32
|
+
type Written,
|
|
33
|
+
type WrittenUrl
|
|
34
|
+
} from '../../written';
|
|
14
35
|
import Markdown from '../markdown/markdown.svelte';
|
|
15
36
|
import SourceList from '../source-list/source-list.svelte';
|
|
16
37
|
|
|
38
|
+
|
|
17
39
|
interface Props {
|
|
18
|
-
turn
|
|
40
|
+
/** The turn whose answer is the artefact. Absent when `versions` is given. */
|
|
41
|
+
turn?: Turn;
|
|
42
|
+
/** Every version of one written document, oldest first: the pane shows
|
|
43
|
+
* `version` of them, with a switcher. */
|
|
44
|
+
versions?: Written[];
|
|
45
|
+
version?: number;
|
|
46
|
+
onversion?: (version: number) => void;
|
|
47
|
+
loadWritten?: LoadWritten;
|
|
48
|
+
writtenUrl?: WrittenUrl;
|
|
19
49
|
onclose: () => void;
|
|
20
50
|
/** Opens the cited document instead — swapping the column back, never
|
|
21
51
|
* stacking it beside the artefact. Omit and the sources still list, but
|
|
@@ -24,14 +54,99 @@
|
|
|
24
54
|
copy?: Partial<ChatCopy>;
|
|
25
55
|
}
|
|
26
56
|
|
|
27
|
-
let {
|
|
57
|
+
let {
|
|
58
|
+
turn,
|
|
59
|
+
versions = [],
|
|
60
|
+
version,
|
|
61
|
+
onversion,
|
|
62
|
+
loadWritten,
|
|
63
|
+
writtenUrl,
|
|
64
|
+
onclose,
|
|
65
|
+
oncite,
|
|
66
|
+
copy
|
|
67
|
+
}: Props = $props();
|
|
28
68
|
|
|
29
69
|
const words = $derived(resolveCopy(copy));
|
|
30
70
|
|
|
31
|
-
|
|
71
|
+
// The newest, until the reader picks another.
|
|
72
|
+
const current = $derived(versions.find((v) => v.version === version) ?? versions.at(-1));
|
|
73
|
+
const currentKey = $derived(current ? writtenKey(current) : '');
|
|
74
|
+
// The one before it, whose blocks this one is marked against.
|
|
75
|
+
const before = $derived(current ? versions.filter((v) => v.version < current.version).at(-1) : undefined);
|
|
76
|
+
const beforeKey = $derived(before ? writtenKey(before) : '');
|
|
77
|
+
let source = $state<{ text: string; mediaType: string } | null>(null);
|
|
78
|
+
let earlier = $state<string | null>(null);
|
|
79
|
+
let failed = $state(false);
|
|
80
|
+
|
|
81
|
+
// Keyed on the version alone: a new version of the same name arriving in
|
|
82
|
+
// the stream must not read this one again.
|
|
83
|
+
$effect(() => {
|
|
84
|
+
if (!currentKey) return;
|
|
85
|
+
const shown = untrack(() => current);
|
|
86
|
+
const load = untrack(() => loadWritten);
|
|
87
|
+
if (!shown || !load) return;
|
|
88
|
+
let live = true;
|
|
89
|
+
source = null;
|
|
90
|
+
failed = false;
|
|
91
|
+
load(shown.name, shown.version).then(
|
|
92
|
+
(read) => live && (source = read),
|
|
93
|
+
() => live && (failed = true)
|
|
94
|
+
);
|
|
95
|
+
return () => (live = false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
$effect(() => {
|
|
99
|
+
earlier = null;
|
|
100
|
+
if (!beforeKey) return;
|
|
101
|
+
const prior = untrack(() => before);
|
|
102
|
+
const load = untrack(() => loadWritten);
|
|
103
|
+
if (!prior || !load || isHtml(prior.media_type)) return;
|
|
104
|
+
let live = true;
|
|
105
|
+
// A version that cannot be read is no reason to mark nothing else.
|
|
106
|
+
load(prior.name, prior.version).then(
|
|
107
|
+
(read) => live && (earlier = stripFrontMatter(read.text)),
|
|
108
|
+
() => {}
|
|
109
|
+
);
|
|
110
|
+
return () => (live = false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const texts = $derived(
|
|
114
|
+
segment(turn?.blocks ?? []).filter((s): s is TextBlock => s.kind === 'text')
|
|
115
|
+
);
|
|
32
116
|
const rawAnswer = $derived(texts.map((t) => t.text).join('\n\n'));
|
|
33
117
|
const split = $derived(splitSources(rawAnswer));
|
|
34
|
-
const sources = $derived(resolveCitations(turn
|
|
118
|
+
const sources = $derived(resolveCitations(turn?.citations ?? [], split.citations));
|
|
119
|
+
|
|
120
|
+
const links = $derived(current && writtenUrl ? downloads(current, words) : []);
|
|
121
|
+
// The one download the bar offers directly: Word where there is one, else the source.
|
|
122
|
+
const primary = $derived(links.find((link) => link.format === 'docx') ?? links[0]);
|
|
123
|
+
const sub = $derived.by(() => {
|
|
124
|
+
if (!current) return '';
|
|
125
|
+
const fresh = current.at !== undefined && Date.now() - current.at < 120_000;
|
|
126
|
+
const when = fresh
|
|
127
|
+
? fill(current.version > 1 ? words.paneEdited : words.paneWritten, { when: words.justNow })
|
|
128
|
+
: '';
|
|
129
|
+
return [kindLabel(current, words), when].filter(Boolean).join(' · ');
|
|
130
|
+
});
|
|
131
|
+
const changed = $derived(
|
|
132
|
+
current?.changes && before
|
|
133
|
+
? fill(words.changedSince, { n: before.version, changes: current.changes })
|
|
134
|
+
: ''
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
let menu = $state(false);
|
|
138
|
+
let copied = $state(false);
|
|
139
|
+
|
|
140
|
+
async function copyDocument() {
|
|
141
|
+
if (!source) return;
|
|
142
|
+
try {
|
|
143
|
+
await navigator.clipboard.writeText(stripFrontMatter(source.text));
|
|
144
|
+
copied = true;
|
|
145
|
+
setTimeout(() => (copied = false), 1600);
|
|
146
|
+
} catch {
|
|
147
|
+
// A denied clipboard is not worth an error state on text the reader can select.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
35
150
|
|
|
36
151
|
let closeButton = $state<HTMLButtonElement | null>(null);
|
|
37
152
|
|
|
@@ -44,28 +159,22 @@
|
|
|
44
159
|
});
|
|
45
160
|
|
|
46
161
|
function keydown(event: KeyboardEvent) {
|
|
47
|
-
if (event.key
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
162
|
+
if (event.key !== 'Escape') return;
|
|
163
|
+
event.stopPropagation();
|
|
164
|
+
if (menu) menu = false;
|
|
165
|
+
else onclose();
|
|
51
166
|
}
|
|
52
167
|
</script>
|
|
53
168
|
|
|
54
169
|
<svelte:window onkeydown={keydown} />
|
|
55
170
|
|
|
56
|
-
<aside aria-label=
|
|
171
|
+
<aside aria-label={current?.title ?? 'Study artefact'} class="ds-lib-pane">
|
|
57
172
|
<!-- The sheet's grabber. On a phone this is an overlay a reader has to be
|
|
58
173
|
able to see the top edge of; on a desktop it is a column, so there is
|
|
59
174
|
nothing here to grab. -->
|
|
60
175
|
<div class="ds-lib-pane-grabber" aria-hidden="true"><span></span></div>
|
|
61
176
|
|
|
62
177
|
<header class="ds-lib-pane-header">
|
|
63
|
-
<div class="ds-lib-pane-heading">
|
|
64
|
-
<h2 class="ds-lib-pane-title">{turn.artefact?.title}</h2>
|
|
65
|
-
{#if turn.artefact?.summary}
|
|
66
|
-
<p class="ds-lib-pane-subtitle">{turn.artefact.summary}</p>
|
|
67
|
-
{/if}
|
|
68
|
-
</div>
|
|
69
178
|
<button
|
|
70
179
|
bind:this={closeButton}
|
|
71
180
|
type="button"
|
|
@@ -75,33 +184,112 @@
|
|
|
75
184
|
>
|
|
76
185
|
<XIcon size={16} />
|
|
77
186
|
</button>
|
|
187
|
+
<div class="ds-lib-pane-heading">
|
|
188
|
+
{#if current}
|
|
189
|
+
<h2 class="ds-lib-pane-title">{current.title}</h2>
|
|
190
|
+
{#if sub}<p class="ds-lib-pane-subtitle">{sub}</p>{/if}
|
|
191
|
+
{:else}
|
|
192
|
+
<h2 class="ds-lib-pane-title">{turn?.artefact?.title}</h2>
|
|
193
|
+
{#if turn?.artefact?.summary}
|
|
194
|
+
<p class="ds-lib-pane-subtitle">{turn.artefact.summary}</p>
|
|
195
|
+
{/if}
|
|
196
|
+
{/if}
|
|
197
|
+
</div>
|
|
198
|
+
{#if current}
|
|
199
|
+
<select
|
|
200
|
+
class="ds-lib-pane-versions"
|
|
201
|
+
aria-label={words.writtenVersions}
|
|
202
|
+
value={current.version}
|
|
203
|
+
onchange={(event) => onversion?.(Number(event.currentTarget.value))}
|
|
204
|
+
>
|
|
205
|
+
{#each versions as option (option.version)}
|
|
206
|
+
<option value={option.version}>{fill(words.writtenVersion, { n: option.version })}</option>
|
|
207
|
+
{/each}
|
|
208
|
+
</select>
|
|
209
|
+
{#if source && !failed}
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
class="ds-lib-pane-tool ds-lib-pane-copy"
|
|
213
|
+
onclick={copyDocument}
|
|
214
|
+
aria-label={copied ? words.copiedDocument : words.copyDocument}
|
|
215
|
+
>
|
|
216
|
+
{#if copied}<CheckIcon size={15} />{:else}<CopyIcon size={15} />{/if}
|
|
217
|
+
</button>
|
|
218
|
+
{/if}
|
|
219
|
+
{#if writtenUrl && links.length > 0}
|
|
220
|
+
<div class="ds-lib-pane-menu">
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
class="ds-lib-pane-tool"
|
|
224
|
+
aria-haspopup="menu"
|
|
225
|
+
aria-expanded={menu}
|
|
226
|
+
onclick={() => (menu = !menu)}
|
|
227
|
+
>
|
|
228
|
+
<DownloadIcon size={15} />{words.download}<ChevronDownIcon size={14} />
|
|
229
|
+
</button>
|
|
230
|
+
{#if menu}
|
|
231
|
+
<ul class="ds-lib-pane-links" role="menu" aria-label={words.download}>
|
|
232
|
+
{#each links as link (link.format)}
|
|
233
|
+
<li role="none">
|
|
234
|
+
<a
|
|
235
|
+
role="menuitem"
|
|
236
|
+
href={writtenUrl(current.name, current.version, link.format)}
|
|
237
|
+
onclick={() => (menu = false)}>{link.label}</a
|
|
238
|
+
>
|
|
239
|
+
</li>
|
|
240
|
+
{/each}
|
|
241
|
+
</ul>
|
|
242
|
+
{/if}
|
|
243
|
+
</div>
|
|
244
|
+
{#if primary}
|
|
245
|
+
<a
|
|
246
|
+
class="ds-lib-pane-tool ds-lib-pane-quick"
|
|
247
|
+
href={writtenUrl(current.name, current.version, primary.format)}
|
|
248
|
+
>
|
|
249
|
+
<DownloadIcon size={15} />{primary.label}
|
|
250
|
+
</a>
|
|
251
|
+
{/if}
|
|
252
|
+
{/if}
|
|
253
|
+
{/if}
|
|
78
254
|
</header>
|
|
79
255
|
|
|
80
|
-
|
|
81
|
-
<
|
|
256
|
+
{#if current}
|
|
257
|
+
{#if changed}<p class="ds-lib-pane-changed">{changed}</p>{/if}
|
|
258
|
+
{@const html = source !== null && isHtml(source.mediaType || current.media_type)}
|
|
259
|
+
<div class="ds-lib-pane-body" class:is-frame={html}>
|
|
260
|
+
{#if failed}
|
|
261
|
+
<p class="ds-lib-pane-note">{words.documentUnavailable}</p>
|
|
262
|
+
{:else if source && html}
|
|
263
|
+
<iframe class="ds-lib-pane-frame" sandbox="" srcdoc={framed(source.text)} title={current.title}
|
|
264
|
+
></iframe>
|
|
265
|
+
{:else if source}
|
|
266
|
+
<div class="ds-lib-pane-paper">
|
|
267
|
+
<Markdown content={stripFrontMatter(source.text)} previous={earlier ?? undefined} />
|
|
268
|
+
</div>
|
|
269
|
+
{/if}
|
|
270
|
+
</div>
|
|
271
|
+
{:else}
|
|
272
|
+
<div class="ds-lib-pane-body">
|
|
273
|
+
<Markdown content={split.citations.length > 0 ? split.body : rawAnswer} />
|
|
82
274
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
275
|
+
{#if sources.length > 0}
|
|
276
|
+
<div class="ds-lib-pane-sources">
|
|
277
|
+
<SourceList {sources} {words} {oncite} level={3} />
|
|
278
|
+
</div>
|
|
279
|
+
{/if}
|
|
280
|
+
</div>
|
|
281
|
+
{/if}
|
|
89
282
|
</aside>
|
|
90
283
|
|
|
91
284
|
<style>
|
|
92
285
|
.ds-lib-pane {
|
|
93
286
|
position: fixed;
|
|
94
|
-
inset
|
|
95
|
-
bottom: 0;
|
|
287
|
+
inset: 0;
|
|
96
288
|
z-index: 40;
|
|
97
289
|
display: flex;
|
|
98
|
-
height:
|
|
290
|
+
height: 100dvh;
|
|
99
291
|
flex-direction: column;
|
|
100
|
-
border-top: 1px solid var(--ds-color-border);
|
|
101
|
-
border-start-start-radius: var(--ds-radius-xl);
|
|
102
|
-
border-start-end-radius: var(--ds-radius-xl);
|
|
103
292
|
background: var(--ds-color-surface-1);
|
|
104
|
-
box-shadow: 0 -12px 40px rgb(0 0 0 / 0.25);
|
|
105
293
|
}
|
|
106
294
|
|
|
107
295
|
.ds-lib-pane-grabber {
|
|
@@ -118,17 +306,19 @@
|
|
|
118
306
|
background: var(--ds-color-border);
|
|
119
307
|
}
|
|
120
308
|
|
|
309
|
+
/* A phone's sheet leads with Close; the column ends with it. */
|
|
121
310
|
.ds-lib-pane-header {
|
|
122
311
|
display: flex;
|
|
123
|
-
align-items:
|
|
312
|
+
align-items: center;
|
|
124
313
|
gap: 0.5rem;
|
|
125
314
|
border-bottom: 1px solid var(--ds-color-border);
|
|
126
|
-
padding: 0.
|
|
315
|
+
padding: 0.5rem 1rem 0.75rem;
|
|
127
316
|
}
|
|
128
317
|
|
|
129
318
|
.ds-lib-pane-heading {
|
|
130
319
|
min-width: 0;
|
|
131
320
|
flex: 1;
|
|
321
|
+
order: 1;
|
|
132
322
|
}
|
|
133
323
|
|
|
134
324
|
.ds-lib-pane-title,
|
|
@@ -142,23 +332,24 @@
|
|
|
142
332
|
.ds-lib-pane-title {
|
|
143
333
|
color: var(--ds-color-foreground);
|
|
144
334
|
font-family: inherit;
|
|
145
|
-
font-size: 0.
|
|
335
|
+
font-size: 0.9375rem;
|
|
146
336
|
font-weight: 600;
|
|
147
337
|
}
|
|
148
338
|
|
|
149
339
|
.ds-lib-pane-subtitle {
|
|
340
|
+
display: none;
|
|
150
341
|
color: var(--ds-color-muted-foreground);
|
|
151
342
|
font-size: var(--ds-text-2xs);
|
|
152
343
|
}
|
|
153
344
|
|
|
154
345
|
.ds-lib-pane-close {
|
|
155
346
|
display: flex;
|
|
156
|
-
width:
|
|
157
|
-
height:
|
|
347
|
+
width: 2.5rem;
|
|
348
|
+
height: 2.5rem;
|
|
158
349
|
flex: none;
|
|
159
350
|
align-items: center;
|
|
160
351
|
justify-content: center;
|
|
161
|
-
|
|
352
|
+
order: 0;
|
|
162
353
|
border: 0;
|
|
163
354
|
border-radius: var(--ds-radius-lg);
|
|
164
355
|
background: none;
|
|
@@ -174,11 +365,109 @@
|
|
|
174
365
|
color: var(--ds-color-foreground);
|
|
175
366
|
}
|
|
176
367
|
|
|
177
|
-
.ds-lib-pane-close:focus-visible
|
|
368
|
+
.ds-lib-pane-close:focus-visible,
|
|
369
|
+
.ds-lib-pane-tool:focus-visible,
|
|
370
|
+
.ds-lib-pane-versions:focus-visible {
|
|
178
371
|
outline: 2px solid var(--ds-color-ring);
|
|
179
372
|
outline-offset: 2px;
|
|
180
373
|
}
|
|
181
374
|
|
|
375
|
+
.ds-lib-pane-versions {
|
|
376
|
+
order: 2;
|
|
377
|
+
border: 1px solid var(--ds-color-border);
|
|
378
|
+
border-radius: var(--ds-radius-md);
|
|
379
|
+
background: var(--ds-color-surface-1);
|
|
380
|
+
padding: 0.375rem 0.5rem;
|
|
381
|
+
color: var(--ds-color-foreground);
|
|
382
|
+
font: inherit;
|
|
383
|
+
font-size: 0.8125rem;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.ds-lib-pane-tool {
|
|
387
|
+
display: inline-flex;
|
|
388
|
+
min-height: 2.25rem;
|
|
389
|
+
align-items: center;
|
|
390
|
+
gap: 0.375rem;
|
|
391
|
+
border: 1px solid var(--ds-color-border);
|
|
392
|
+
border-radius: var(--ds-radius-md);
|
|
393
|
+
background: none;
|
|
394
|
+
padding: 0.25rem 0.625rem;
|
|
395
|
+
color: var(--ds-color-foreground);
|
|
396
|
+
font: inherit;
|
|
397
|
+
font-size: 0.8125rem;
|
|
398
|
+
text-decoration: none;
|
|
399
|
+
cursor: pointer;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.ds-lib-pane-tool:hover {
|
|
403
|
+
background: var(--ds-color-surface-2);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.ds-lib-pane-copy {
|
|
407
|
+
display: none;
|
|
408
|
+
order: 3;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.ds-lib-pane-menu {
|
|
412
|
+
position: relative;
|
|
413
|
+
display: none;
|
|
414
|
+
order: 4;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.ds-lib-pane-quick {
|
|
418
|
+
order: 3;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.ds-lib-pane-links {
|
|
422
|
+
position: absolute;
|
|
423
|
+
inset-block-start: calc(100% + 0.25rem);
|
|
424
|
+
inset-inline-end: 0;
|
|
425
|
+
z-index: 2;
|
|
426
|
+
display: flex;
|
|
427
|
+
min-width: 9rem;
|
|
428
|
+
flex-direction: column;
|
|
429
|
+
margin: 0;
|
|
430
|
+
border: 1px solid var(--ds-color-border);
|
|
431
|
+
border-radius: var(--ds-radius-md);
|
|
432
|
+
background: var(--ds-color-surface-1);
|
|
433
|
+
padding: 0.25rem;
|
|
434
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 0.25);
|
|
435
|
+
list-style: none;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.ds-lib-pane-links a {
|
|
439
|
+
display: block;
|
|
440
|
+
border-radius: var(--ds-radius-sm);
|
|
441
|
+
padding: 0.5rem 0.625rem;
|
|
442
|
+
color: var(--ds-color-foreground);
|
|
443
|
+
font-size: 0.8125rem;
|
|
444
|
+
text-decoration: none;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.ds-lib-pane-links a:hover,
|
|
448
|
+
.ds-lib-pane-links a:focus-visible {
|
|
449
|
+
background: var(--ds-color-surface-2);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.ds-lib-pane-changed {
|
|
453
|
+
display: flex;
|
|
454
|
+
align-items: center;
|
|
455
|
+
gap: 0.5rem;
|
|
456
|
+
margin: 0;
|
|
457
|
+
border-bottom: 1px solid var(--ds-color-border);
|
|
458
|
+
padding: 0.5rem 1rem;
|
|
459
|
+
color: var(--ds-color-muted-foreground);
|
|
460
|
+
font-size: 0.8125rem;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.ds-lib-pane-changed::before {
|
|
464
|
+
content: '';
|
|
465
|
+
width: 2px;
|
|
466
|
+
height: 0.875rem;
|
|
467
|
+
flex: none;
|
|
468
|
+
background: var(--ds-color-foreground);
|
|
469
|
+
}
|
|
470
|
+
|
|
182
471
|
.ds-lib-pane-body {
|
|
183
472
|
min-height: 0;
|
|
184
473
|
flex: 1;
|
|
@@ -187,6 +476,39 @@
|
|
|
187
476
|
padding: 0.75rem 1rem;
|
|
188
477
|
}
|
|
189
478
|
|
|
479
|
+
.ds-lib-pane-note {
|
|
480
|
+
margin: 0;
|
|
481
|
+
color: var(--ds-color-muted-foreground);
|
|
482
|
+
font-size: var(--ds-text-2xs);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* The document is paper, light in either theme: its own ink and rules
|
|
486
|
+
replace the app's, so a dark page does not paint it grey on grey. */
|
|
487
|
+
.ds-lib-pane-paper {
|
|
488
|
+
--ds-color-foreground: var(--ds-paper-ink, #1b1c1e);
|
|
489
|
+
--ds-color-muted-foreground: var(--ds-paper-muted, #5f6368);
|
|
490
|
+
--ds-color-border: var(--ds-paper-rule, #c4c7cc);
|
|
491
|
+
--ds-color-surface-1: var(--ds-paper-shade, #dcdee2);
|
|
492
|
+
--ds-color-primary-ink: var(--ds-paper-ink, #1b1c1e);
|
|
493
|
+
border-radius: var(--ds-radius-sm);
|
|
494
|
+
background: var(--ds-paper, #e9eaec);
|
|
495
|
+
padding: 1.25rem 1.5rem;
|
|
496
|
+
color: var(--ds-color-foreground);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.ds-lib-pane-body.is-frame {
|
|
500
|
+
overflow: hidden;
|
|
501
|
+
padding: 0;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.ds-lib-pane-frame {
|
|
505
|
+
display: block;
|
|
506
|
+
width: 100%;
|
|
507
|
+
height: 100%;
|
|
508
|
+
border: 0;
|
|
509
|
+
background: #fff;
|
|
510
|
+
}
|
|
511
|
+
|
|
190
512
|
.ds-lib-pane-sources {
|
|
191
513
|
margin-top: 1.5rem;
|
|
192
514
|
}
|
|
@@ -196,16 +518,38 @@
|
|
|
196
518
|
position: relative;
|
|
197
519
|
inset: auto;
|
|
198
520
|
height: auto;
|
|
199
|
-
width:
|
|
521
|
+
width: 42rem;
|
|
200
522
|
flex: none;
|
|
201
|
-
border-top: 0;
|
|
202
523
|
border-inline-start: 1px solid var(--ds-color-border);
|
|
203
|
-
border-radius: 0;
|
|
204
|
-
box-shadow: none;
|
|
205
524
|
}
|
|
206
525
|
|
|
207
526
|
.ds-lib-pane-grabber {
|
|
208
527
|
display: none;
|
|
209
528
|
}
|
|
529
|
+
|
|
530
|
+
.ds-lib-pane-header {
|
|
531
|
+
padding-top: 0.75rem;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.ds-lib-pane-heading {
|
|
535
|
+
order: 0;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.ds-lib-pane-subtitle {
|
|
539
|
+
display: block;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.ds-lib-pane-copy,
|
|
543
|
+
.ds-lib-pane-menu {
|
|
544
|
+
display: inline-flex;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.ds-lib-pane-quick {
|
|
548
|
+
display: none;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.ds-lib-pane-close {
|
|
552
|
+
order: 5;
|
|
553
|
+
}
|
|
210
554
|
}
|
|
211
555
|
</style>
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { type Turn } from '../../transcript.svelte';
|
|
2
2
|
import { type Citation } from '../../citations';
|
|
3
3
|
import { type ChatCopy } from '../../copy';
|
|
4
|
+
import { type LoadWritten, type Written, type WrittenUrl } from '../../written';
|
|
4
5
|
interface Props {
|
|
5
|
-
turn
|
|
6
|
+
/** The turn whose answer is the artefact. Absent when `versions` is given. */
|
|
7
|
+
turn?: Turn;
|
|
8
|
+
/** Every version of one written document, oldest first: the pane shows
|
|
9
|
+
* `version` of them, with a switcher. */
|
|
10
|
+
versions?: Written[];
|
|
11
|
+
version?: number;
|
|
12
|
+
onversion?: (version: number) => void;
|
|
13
|
+
loadWritten?: LoadWritten;
|
|
14
|
+
writtenUrl?: WrittenUrl;
|
|
6
15
|
onclose: () => void;
|
|
7
16
|
/** Opens the cited document instead — swapping the column back, never
|
|
8
17
|
* stacking it beside the artefact. Omit and the sources still list, but
|
|
@@ -20,19 +20,32 @@
|
|
|
20
20
|
import type { DescribeTool, Turn } from '../../transcript.svelte';
|
|
21
21
|
import type { Verdict } from '../../chat.svelte';
|
|
22
22
|
import type { Citation, LoadDocument } from '../../citations';
|
|
23
|
+
import {
|
|
24
|
+
writtenKey,
|
|
25
|
+
writtenVersions,
|
|
26
|
+
type LoadWritten,
|
|
27
|
+
type Written,
|
|
28
|
+
type WrittenUrl
|
|
29
|
+
} from '../../written';
|
|
23
30
|
import { DEFAULT_PERSONA, personaName, resolveCopy, type ChatCopy } from '../../copy';
|
|
24
31
|
import { FollowScroll } from '../../follow-scroll.svelte';
|
|
25
32
|
import AgentTranscript, {
|
|
26
33
|
type AgentTranscriptProps
|
|
27
34
|
} from '../agent-transcript/agent-transcript.svelte';
|
|
28
35
|
import ArtefactPane from '../artefact-pane/artefact-pane.svelte';
|
|
36
|
+
import type { Visual } from '../../visuals';
|
|
29
37
|
import DocumentPane from '../document-pane/document-pane.svelte';
|
|
30
38
|
import ScopeStatement from '../scope-statement/scope-statement.svelte';
|
|
31
39
|
|
|
32
|
-
/** What the reading column holds — a cited document
|
|
33
|
-
*
|
|
34
|
-
* replaces whatever was there.
|
|
35
|
-
|
|
40
|
+
/** What the reading column holds — a cited document, a study artefact or a
|
|
41
|
+
* document the persona wrote, never two at once and never two panes:
|
|
42
|
+
* whichever the reader last opened replaces whatever was there. A written
|
|
43
|
+
* document's `version` is null while the reader follows the newest. */
|
|
44
|
+
type Column =
|
|
45
|
+
| { kind: 'document'; citation: Citation }
|
|
46
|
+
| { kind: 'artefact'; turn: Turn }
|
|
47
|
+
| { kind: 'written'; name: string; version: number | null }
|
|
48
|
+
| null;
|
|
36
49
|
|
|
37
50
|
interface Props {
|
|
38
51
|
turns: Turn[];
|
|
@@ -86,6 +99,19 @@
|
|
|
86
99
|
showing?: string;
|
|
87
100
|
/** The persona's own words for its tools, and what they read. */
|
|
88
101
|
describeTool?: DescribeTool;
|
|
102
|
+
/** Replaces the question at `position` (its place among `turns`) with
|
|
103
|
+
* `text` and asks it again from there (`Chat.edit`). Offered on every
|
|
104
|
+
* plain question while no answer is being written; without it no turn
|
|
105
|
+
* can be edited. */
|
|
106
|
+
onedit?: (position: number, text: string) => Promise<boolean>;
|
|
107
|
+
/** Reads one version of a document the persona wrote, to show it in the
|
|
108
|
+
* column. Without it its cards link but do not open. */
|
|
109
|
+
loadWritten?: LoadWritten;
|
|
110
|
+
/** Where a version downloads. Without it no download links render. */
|
|
111
|
+
writtenUrl?: WrittenUrl;
|
|
112
|
+
/** Renderers by a fence's language (`defaultVisuals`, and the app's own):
|
|
113
|
+
* a block of one is drawn by its component rather than as code. */
|
|
114
|
+
visuals?: Record<string, Visual>;
|
|
89
115
|
/** Prices a turn's `outcome.costUsd` in the host's own currency and
|
|
90
116
|
* words ("about A$0.34"), for the footer's cost line beside a turn's
|
|
91
117
|
* duration. The package never converts currency itself; omitted, the
|
|
@@ -133,6 +159,10 @@
|
|
|
133
159
|
onopenartefact,
|
|
134
160
|
showing,
|
|
135
161
|
describeTool,
|
|
162
|
+
onedit,
|
|
163
|
+
loadWritten,
|
|
164
|
+
writtenUrl,
|
|
165
|
+
visuals,
|
|
136
166
|
collectionNames = new Set(),
|
|
137
167
|
composer,
|
|
138
168
|
turn: presentTurn,
|
|
@@ -252,6 +282,24 @@
|
|
|
252
282
|
return undefined;
|
|
253
283
|
}
|
|
254
284
|
|
|
285
|
+
/** Opens a written version, following the newest if it is the newest. */
|
|
286
|
+
function openWritten(written: Written) {
|
|
287
|
+
const newest = writtenVersions(turns, written.name).at(-1)?.version;
|
|
288
|
+
column = {
|
|
289
|
+
kind: 'written',
|
|
290
|
+
name: written.name,
|
|
291
|
+
version: written.version === newest ? null : written.version
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const shownWritten = $derived.by(() => {
|
|
296
|
+
const open = column;
|
|
297
|
+
if (open?.kind !== 'written') return undefined;
|
|
298
|
+
const versions = writtenVersions(turns, open.name);
|
|
299
|
+
const shown = versions.find((v) => v.version === open.version) ?? versions.at(-1);
|
|
300
|
+
return { versions, version: shown?.version, key: shown ? writtenKey(shown) : undefined };
|
|
301
|
+
});
|
|
302
|
+
|
|
255
303
|
function isShowing(turn: Turn): boolean {
|
|
256
304
|
if (onopenartefact) return showing === turn.id;
|
|
257
305
|
return column?.kind === 'artefact' && column.turn.id === turn.id;
|
|
@@ -333,6 +381,16 @@
|
|
|
333
381
|
artefactOpen: isShowing(turn),
|
|
334
382
|
onopenartefact: opener(turn),
|
|
335
383
|
describeTool,
|
|
384
|
+
files: turn.files,
|
|
385
|
+
written: turn.written,
|
|
386
|
+
showingWritten: shownWritten?.key,
|
|
387
|
+
onopenwritten: loadWritten ? openWritten : undefined,
|
|
388
|
+
writtenUrl,
|
|
389
|
+
visuals,
|
|
390
|
+
onedit:
|
|
391
|
+
onedit && !running && !turn.kind && turn.question
|
|
392
|
+
? (text: string) => onedit(index, text)
|
|
393
|
+
: undefined,
|
|
336
394
|
formatCost,
|
|
337
395
|
oncite: oncite || loadDocument ? cite : undefined,
|
|
338
396
|
onregenerate: last && !running ? onregenerate : undefined,
|
|
@@ -387,6 +445,18 @@
|
|
|
387
445
|
copy={words}
|
|
388
446
|
onclose={() => (column = null)}
|
|
389
447
|
/>
|
|
448
|
+
{:else if column?.kind === 'written' && shownWritten && shownWritten.versions.length > 0}
|
|
449
|
+
<ArtefactPane
|
|
450
|
+
versions={shownWritten.versions}
|
|
451
|
+
version={shownWritten.version}
|
|
452
|
+
onversion={(version) => {
|
|
453
|
+
if (column?.kind === 'written') column = { ...column, version };
|
|
454
|
+
}}
|
|
455
|
+
{loadWritten}
|
|
456
|
+
{writtenUrl}
|
|
457
|
+
copy={words}
|
|
458
|
+
onclose={() => (column = null)}
|
|
459
|
+
/>
|
|
390
460
|
{:else if column?.kind === 'artefact'}
|
|
391
461
|
<ArtefactPane
|
|
392
462
|
turn={column.turn}
|