@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
|
@@ -2,8 +2,10 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
import type { DescribeTool, Turn } from '../../transcript.svelte';
|
|
3
3
|
import type { Verdict } from '../../chat.svelte';
|
|
4
4
|
import type { Citation, LoadDocument } from '../../citations';
|
|
5
|
+
import { type LoadWritten, type WrittenUrl } from '../../written';
|
|
5
6
|
import { type ChatCopy } from '../../copy';
|
|
6
7
|
import { type AgentTranscriptProps } from '../agent-transcript/agent-transcript.svelte';
|
|
8
|
+
import type { Visual } from '../../visuals';
|
|
7
9
|
interface Props {
|
|
8
10
|
turns: Turn[];
|
|
9
11
|
running: boolean;
|
|
@@ -56,6 +58,19 @@ interface Props {
|
|
|
56
58
|
showing?: string;
|
|
57
59
|
/** The persona's own words for its tools, and what they read. */
|
|
58
60
|
describeTool?: DescribeTool;
|
|
61
|
+
/** Replaces the question at `position` (its place among `turns`) with
|
|
62
|
+
* `text` and asks it again from there (`Chat.edit`). Offered on every
|
|
63
|
+
* plain question while no answer is being written; without it no turn
|
|
64
|
+
* can be edited. */
|
|
65
|
+
onedit?: (position: number, text: string) => Promise<boolean>;
|
|
66
|
+
/** Reads one version of a document the persona wrote, to show it in the
|
|
67
|
+
* column. Without it its cards link but do not open. */
|
|
68
|
+
loadWritten?: LoadWritten;
|
|
69
|
+
/** Where a version downloads. Without it no download links render. */
|
|
70
|
+
writtenUrl?: WrittenUrl;
|
|
71
|
+
/** Renderers by a fence's language (`defaultVisuals`, and the app's own):
|
|
72
|
+
* a block of one is drawn by its component rather than as code. */
|
|
73
|
+
visuals?: Record<string, Visual>;
|
|
59
74
|
/** Prices a turn's `outcome.costUsd` in the host's own currency and
|
|
60
75
|
* words ("about A$0.34"), for the footer's cost line beside a turn's
|
|
61
76
|
* duration. The package never converts currency itself; omitted, the
|
|
@@ -17,8 +17,13 @@
|
|
|
17
17
|
*/
|
|
18
18
|
/** Close anything the stream has opened but not yet finished. */
|
|
19
19
|
export declare function balance(markdown: string): string;
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* `markdown` as sanitised HTML. With `previous`, the version before it, each
|
|
22
|
+
* block it changed is marked on the sanitised DOM, before its one serialisation.
|
|
23
|
+
*/
|
|
24
|
+
export declare function render(markdown: string, { streaming, previous }?: {
|
|
25
|
+
streaming?: boolean;
|
|
26
|
+
previous?: string;
|
|
22
27
|
}): string;
|
|
23
28
|
/**
|
|
24
29
|
* Mark every inline `code` span that names a known collection.
|
|
@@ -42,6 +47,29 @@ export declare function markCollections(root: HTMLElement, collections: Set<stri
|
|
|
42
47
|
* button ROLE and a tab stop, and the component delegates the events.
|
|
43
48
|
*/
|
|
44
49
|
export declare function markCitations(root: HTMLElement, valid: Set<number>): void;
|
|
50
|
+
/** A turn's prose and the fenced blocks of a registered language, in order. */
|
|
51
|
+
export type Part = {
|
|
52
|
+
kind: 'md';
|
|
53
|
+
text: string;
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'visual';
|
|
56
|
+
lang: string;
|
|
57
|
+
source: string;
|
|
58
|
+
closed: boolean;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Cut `markdown` at each fenced block whose language `has` a renderer for.
|
|
62
|
+
* A fence of another language (or none) is ordinary code and stays in the
|
|
63
|
+
* prose, and so does anything inside one: an example of a visual's fence in a
|
|
64
|
+
* code block is text. A block still open at the end is `closed: false`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function splitVisuals(markdown: string, has: (lang: string) => boolean): Part[];
|
|
67
|
+
/**
|
|
68
|
+
* Turn each link to a numbered web page into its citation chip: the link's
|
|
69
|
+
* text stays, unlinked, and the chip after it carries the page's number and
|
|
70
|
+
* opens the page. Against the rendered DOM, for the reason `markCitations` is.
|
|
71
|
+
*/
|
|
72
|
+
export declare function markWebLinks(root: HTMLElement, numbers: Map<string, number>): void;
|
|
45
73
|
/**
|
|
46
74
|
* Replace every `<pre><code>` in a rendered fragment with a highlighted one.
|
|
47
75
|
* Runs against the DOM node rather than the HTML string so it can be applied
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import DOMPurify from 'isomorphic-dompurify';
|
|
19
19
|
import { marked } from 'marked';
|
|
20
|
+
import { markChanges } from '../../diff';
|
|
21
|
+
import { webKey } from '../../web-sources';
|
|
20
22
|
marked.setOptions({ gfm: true, breaks: false });
|
|
21
23
|
/** Close anything the stream has opened but not yet finished. */
|
|
22
24
|
export function balance(markdown) {
|
|
@@ -35,14 +37,60 @@ export function balance(markdown) {
|
|
|
35
37
|
}
|
|
36
38
|
return text;
|
|
37
39
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const FETCHERS = 'img, picture, source, video, audio, track, iframe, object, embed, link, style, image, use';
|
|
41
|
+
const URL_ATTRS = ['src', 'href', 'xlink:href', 'poster', 'data', 'srcset'];
|
|
42
|
+
/** Only what cannot leave the origin: a data URL, a same-origin path, a fragment. */
|
|
43
|
+
function stays(url) {
|
|
44
|
+
const u = url.trim();
|
|
45
|
+
return /^data:/i.test(u) || (u.startsWith('/') && !u.startsWith('//')) || u.startsWith('#');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A rendered answer must not fetch from anywhere: a remote image or url() in
|
|
49
|
+
* agent output is a beacon that carries the conversation out in its query.
|
|
50
|
+
* A remote image degrades to its alt text (or its address as a link).
|
|
51
|
+
*/
|
|
52
|
+
function dropFetchers(root) {
|
|
53
|
+
for (const el of root.querySelectorAll(FETCHERS)) {
|
|
54
|
+
const urls = URL_ATTRS.map((a) => el.getAttribute(a)).filter((v) => v !== null);
|
|
55
|
+
if (urls.every(stays))
|
|
56
|
+
continue;
|
|
57
|
+
if (el.tagName === 'IMG') {
|
|
58
|
+
const alt = el.getAttribute('alt')?.trim();
|
|
59
|
+
const src = el.getAttribute('src')?.trim() ?? '';
|
|
60
|
+
if (alt)
|
|
61
|
+
el.replaceWith(root.ownerDocument.createTextNode(alt));
|
|
62
|
+
else if (/^https?:\/\//i.test(src)) {
|
|
63
|
+
const a = root.ownerDocument.createElement('a');
|
|
64
|
+
a.setAttribute('href', src);
|
|
65
|
+
a.textContent = src;
|
|
66
|
+
el.replaceWith(a);
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
el.remove();
|
|
70
|
+
}
|
|
71
|
+
else
|
|
72
|
+
el.remove();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* `markdown` as sanitised HTML. With `previous`, the version before it, each
|
|
77
|
+
* block it changed is marked on the sanitised DOM, before its one serialisation.
|
|
78
|
+
*/
|
|
79
|
+
export function render(markdown, { streaming = false, previous } = {}) {
|
|
80
|
+
const body = sanitised(streaming ? balance(markdown) : markdown);
|
|
81
|
+
if (previous !== undefined && !streaming)
|
|
82
|
+
markChanges(body, sanitised(previous));
|
|
83
|
+
return body.innerHTML;
|
|
84
|
+
}
|
|
85
|
+
function sanitised(source) {
|
|
86
|
+
const body = DOMPurify.sanitize(marked.parse(source, { async: false }), {
|
|
42
87
|
ADD_ATTR: ['target', 'rel', 'data-cite', 'tabindex', 'role'],
|
|
43
88
|
FORBID_TAGS: ['style', 'form', 'input', 'button'],
|
|
44
|
-
FORBID_ATTR: ['style', 'onerror', 'onload']
|
|
89
|
+
FORBID_ATTR: ['style', 'srcset', 'onerror', 'onload'],
|
|
90
|
+
RETURN_DOM: true
|
|
45
91
|
});
|
|
92
|
+
dropFetchers(body);
|
|
93
|
+
return body;
|
|
46
94
|
}
|
|
47
95
|
let highlighterPromise = null;
|
|
48
96
|
/** Loaded once, lazily — shiki's engine and grammars are not small. */
|
|
@@ -137,6 +185,95 @@ export function markCitations(root, valid) {
|
|
|
137
185
|
text.replaceWith(fragment);
|
|
138
186
|
}
|
|
139
187
|
}
|
|
188
|
+
const OPEN = /^ {0,3}(`{3,}|~{3,})[ \t]*([^\s`]*)[^`]*$/;
|
|
189
|
+
const CLOSE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
|
|
190
|
+
/**
|
|
191
|
+
* Cut `markdown` at each fenced block whose language `has` a renderer for.
|
|
192
|
+
* A fence of another language (or none) is ordinary code and stays in the
|
|
193
|
+
* prose, and so does anything inside one: an example of a visual's fence in a
|
|
194
|
+
* code block is text. A block still open at the end is `closed: false`.
|
|
195
|
+
*/
|
|
196
|
+
export function splitVisuals(markdown, has) {
|
|
197
|
+
const parts = [];
|
|
198
|
+
let prose = [];
|
|
199
|
+
let code = null;
|
|
200
|
+
const flush = () => {
|
|
201
|
+
if (prose.join('').trim())
|
|
202
|
+
parts.push({ kind: 'md', text: prose.join('\n') });
|
|
203
|
+
prose = [];
|
|
204
|
+
};
|
|
205
|
+
for (const line of markdown.split('\n')) {
|
|
206
|
+
if (code) {
|
|
207
|
+
const close = CLOSE.exec(line);
|
|
208
|
+
if (close && close[1][0] === code.fence[0] && close[1].length >= code.fence.length) {
|
|
209
|
+
if (code.visual) {
|
|
210
|
+
parts.push({
|
|
211
|
+
kind: 'visual',
|
|
212
|
+
lang: code.visual.lang,
|
|
213
|
+
source: code.visual.lines.join('\n'),
|
|
214
|
+
closed: true
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else
|
|
218
|
+
prose.push(line);
|
|
219
|
+
code = null;
|
|
220
|
+
}
|
|
221
|
+
else if (code.visual)
|
|
222
|
+
code.visual.lines.push(line);
|
|
223
|
+
else
|
|
224
|
+
prose.push(line);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const open = OPEN.exec(line);
|
|
228
|
+
if (open) {
|
|
229
|
+
const lang = open[2];
|
|
230
|
+
if (lang && has(lang)) {
|
|
231
|
+
flush();
|
|
232
|
+
code = { fence: open[1], visual: { lang, lines: [] } };
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
code = { fence: open[1] };
|
|
236
|
+
prose.push(line);
|
|
237
|
+
}
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
prose.push(line);
|
|
241
|
+
}
|
|
242
|
+
if (code?.visual)
|
|
243
|
+
parts.push({
|
|
244
|
+
kind: 'visual',
|
|
245
|
+
lang: code.visual.lang,
|
|
246
|
+
source: code.visual.lines.join('\n'),
|
|
247
|
+
closed: false
|
|
248
|
+
});
|
|
249
|
+
else
|
|
250
|
+
flush();
|
|
251
|
+
return parts;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Turn each link to a numbered web page into its citation chip: the link's
|
|
255
|
+
* text stays, unlinked, and the chip after it carries the page's number and
|
|
256
|
+
* opens the page. Against the rendered DOM, for the reason `markCitations` is.
|
|
257
|
+
*/
|
|
258
|
+
export function markWebLinks(root, numbers) {
|
|
259
|
+
if (numbers.size === 0)
|
|
260
|
+
return;
|
|
261
|
+
for (const link of root.querySelectorAll('a[href]')) {
|
|
262
|
+
const href = link.getAttribute('href') ?? '';
|
|
263
|
+
const n = numbers.get(webKey(href));
|
|
264
|
+
if (n === undefined)
|
|
265
|
+
continue;
|
|
266
|
+
const chip = document.createElement('sup');
|
|
267
|
+
chip.className = 'ds-cite';
|
|
268
|
+
chip.dataset.cite = String(n);
|
|
269
|
+
chip.dataset.web = href;
|
|
270
|
+
chip.setAttribute('role', 'button');
|
|
271
|
+
chip.setAttribute('tabindex', '0');
|
|
272
|
+
chip.setAttribute('aria-label', `Web page ${n}`);
|
|
273
|
+
chip.textContent = String(n);
|
|
274
|
+
link.replaceWith(...link.childNodes, chip);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
140
277
|
/**
|
|
141
278
|
* Replace every `<pre><code>` in a rendered fragment with a highlighted one.
|
|
142
279
|
* Runs against the DOM node rather than the HTML string so it can be applied
|
|
@@ -7,7 +7,17 @@
|
|
|
7
7
|
consuming app.
|
|
8
8
|
-->
|
|
9
9
|
<script lang="ts">
|
|
10
|
-
import {
|
|
10
|
+
import { resolveCopy, type ChatCopy } from '../../copy';
|
|
11
|
+
import type { Visual } from '../../visuals';
|
|
12
|
+
import VisualBlock from '../visual-block/visual-block.svelte';
|
|
13
|
+
import {
|
|
14
|
+
highlight,
|
|
15
|
+
markCitations,
|
|
16
|
+
markCollections,
|
|
17
|
+
markWebLinks,
|
|
18
|
+
render,
|
|
19
|
+
splitVisuals
|
|
20
|
+
} from './markdown';
|
|
11
21
|
|
|
12
22
|
interface Props {
|
|
13
23
|
content: string;
|
|
@@ -16,10 +26,20 @@
|
|
|
16
26
|
collectionNames?: Set<string>;
|
|
17
27
|
/** `[n]` markers to turn into chips; omit and the markers stay as text. */
|
|
18
28
|
citationNumbers?: Set<number>;
|
|
29
|
+
/** Web pages by `webKey`, numbered after the library's: a link to one
|
|
30
|
+
* renders as its chip. */
|
|
31
|
+
webNumbers?: Map<string, number>;
|
|
19
32
|
oncite?: (n: number) => void;
|
|
20
33
|
/** A step down, for prose that is reference material beside an answer
|
|
21
34
|
* rather than the answer itself — the source pane's sections. */
|
|
22
35
|
dense?: boolean;
|
|
36
|
+
/** Renderers by a fence's language: a block of one is drawn by its
|
|
37
|
+
* component in place of the code. Absent, every fence is code. */
|
|
38
|
+
visuals?: Record<string, Visual>;
|
|
39
|
+
/** The same document's earlier version, as Markdown: what differs here
|
|
40
|
+
* is marked with a bar. */
|
|
41
|
+
previous?: string;
|
|
42
|
+
copy?: Partial<ChatCopy>;
|
|
23
43
|
}
|
|
24
44
|
|
|
25
45
|
let {
|
|
@@ -27,43 +47,68 @@
|
|
|
27
47
|
streaming = false,
|
|
28
48
|
collectionNames = new Set(),
|
|
29
49
|
citationNumbers = new Set(),
|
|
50
|
+
webNumbers = new Map(),
|
|
30
51
|
oncite,
|
|
31
|
-
dense = false
|
|
52
|
+
dense = false,
|
|
53
|
+
visuals,
|
|
54
|
+
previous,
|
|
55
|
+
copy
|
|
32
56
|
}: Props = $props();
|
|
33
57
|
let host = $state<HTMLElement | null>(null);
|
|
34
58
|
|
|
35
|
-
const
|
|
59
|
+
const words = $derived(resolveCopy(copy));
|
|
60
|
+
|
|
61
|
+
// A visual is its own component between chunks of prose, so a token that
|
|
62
|
+
// grows the last chunk leaves the charts above it standing.
|
|
63
|
+
const parts = $derived(
|
|
64
|
+
visuals
|
|
65
|
+
? splitVisuals(content, (lang) => Object.hasOwn(visuals, lang))
|
|
66
|
+
: [{ kind: 'md' as const, text: content }]
|
|
67
|
+
);
|
|
68
|
+
const chunked = $derived(parts.some((part) => part.kind === 'visual'));
|
|
69
|
+
const lastPart = $derived(parts.length - 1);
|
|
70
|
+
|
|
71
|
+
const prose = (text: string, last: boolean): string => render(text, { streaming: streaming && last, previous });
|
|
36
72
|
|
|
37
73
|
// Highlight only once the text has settled. Re-running per token would
|
|
38
74
|
// re-parse grammars for a block that changes again milliseconds later.
|
|
39
75
|
$effect(() => {
|
|
40
|
-
|
|
76
|
+
void parts;
|
|
77
|
+
if (!host) return;
|
|
41
78
|
// Chips are cheap and wanted DURING streaming; highlighting is not.
|
|
42
79
|
markCollections(host, collectionNames);
|
|
43
80
|
markCitations(host, citationNumbers);
|
|
81
|
+
markWebLinks(host, webNumbers);
|
|
44
82
|
if (streaming) return;
|
|
45
83
|
void highlight(host);
|
|
46
84
|
});
|
|
47
85
|
|
|
48
86
|
// Delegated, because the chips are created by the sanitiser's output rather
|
|
49
87
|
// than by this template — there is no element here to put a handler on.
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
88
|
+
function chipOf(target: EventTarget | null): HTMLElement | null {
|
|
89
|
+
return ((target as HTMLElement | null)?.closest?.('[data-cite]') as HTMLElement | null) ?? null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function press(chip: HTMLElement) {
|
|
93
|
+
if (chip.dataset.web) {
|
|
94
|
+
window.open(chip.dataset.web, '_blank', 'noopener,noreferrer');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const n = Number(chip.dataset.cite);
|
|
98
|
+
if (Number.isFinite(n) && n > 0) oncite?.(n);
|
|
54
99
|
}
|
|
55
100
|
|
|
56
101
|
function click(event: MouseEvent) {
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
102
|
+
const chip = chipOf(event.target);
|
|
103
|
+
if (chip) press(chip);
|
|
59
104
|
}
|
|
60
105
|
|
|
61
106
|
function keydown(event: KeyboardEvent) {
|
|
62
107
|
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
108
|
+
const chip = chipOf(event.target);
|
|
109
|
+
if (!chip) return;
|
|
65
110
|
event.preventDefault();
|
|
66
|
-
|
|
111
|
+
press(chip);
|
|
67
112
|
}
|
|
68
113
|
</script>
|
|
69
114
|
|
|
@@ -77,9 +122,30 @@
|
|
|
77
122
|
class="agent-prose"
|
|
78
123
|
class:is-dense={dense}
|
|
79
124
|
class:is-streaming={streaming}
|
|
125
|
+
class:is-chunked={chunked}
|
|
80
126
|
>
|
|
81
|
-
|
|
82
|
-
|
|
127
|
+
{#if !chunked}
|
|
128
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in render() -->
|
|
129
|
+
{@html prose(content, true)}
|
|
130
|
+
{:else}
|
|
131
|
+
{#each parts as part, i (i)}
|
|
132
|
+
{#if part.kind === 'md'}
|
|
133
|
+
<div class="ds-lib-chunk">
|
|
134
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in render() -->
|
|
135
|
+
{@html prose(part.text, i === lastPart)}
|
|
136
|
+
</div>
|
|
137
|
+
{:else if visuals}
|
|
138
|
+
<VisualBlock
|
|
139
|
+
lang={part.lang}
|
|
140
|
+
visual={visuals[part.lang]}
|
|
141
|
+
source={part.source}
|
|
142
|
+
closed={part.closed}
|
|
143
|
+
{streaming}
|
|
144
|
+
{words}
|
|
145
|
+
/>
|
|
146
|
+
{/if}
|
|
147
|
+
{/each}
|
|
148
|
+
{/if}
|
|
83
149
|
</div>
|
|
84
150
|
|
|
85
151
|
<style>
|
|
@@ -111,6 +177,31 @@
|
|
|
111
177
|
animation: ds-lib-caret 1.1s steps(1, end) infinite;
|
|
112
178
|
}
|
|
113
179
|
|
|
180
|
+
/* Chunked, the caret rides the last prose chunk's last block, not the chunk. */
|
|
181
|
+
.agent-prose.is-chunked.is-streaming :global(> .ds-lib-chunk:last-child::after) {
|
|
182
|
+
content: none;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.agent-prose.is-chunked.is-streaming :global(> .ds-lib-chunk:last-child > *:last-child::after) {
|
|
186
|
+
content: '';
|
|
187
|
+
display: inline-block;
|
|
188
|
+
width: 0.45em;
|
|
189
|
+
height: 1.05em;
|
|
190
|
+
margin-inline-start: 0.12em;
|
|
191
|
+
border-radius: 1px;
|
|
192
|
+
background: var(--ds-color-primary);
|
|
193
|
+
vertical-align: -0.15em;
|
|
194
|
+
animation: ds-lib-caret 1.1s steps(1, end) infinite;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.agent-prose :global(.ds-lib-chunk > *:first-child) {
|
|
198
|
+
margin-top: 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.agent-prose :global(.ds-lib-chunk > *:last-child) {
|
|
202
|
+
margin-bottom: 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
114
205
|
@keyframes ds-lib-caret {
|
|
115
206
|
50% {
|
|
116
207
|
opacity: 0;
|
|
@@ -118,7 +209,8 @@
|
|
|
118
209
|
}
|
|
119
210
|
|
|
120
211
|
@media (prefers-reduced-motion: reduce) {
|
|
121
|
-
.agent-prose.is-streaming :global(> *:last-child::after)
|
|
212
|
+
.agent-prose.is-streaming :global(> *:last-child::after),
|
|
213
|
+
.agent-prose.is-chunked.is-streaming :global(> .ds-lib-chunk:last-child > *:last-child::after) {
|
|
122
214
|
animation: none;
|
|
123
215
|
}
|
|
124
216
|
}
|
|
@@ -354,6 +446,28 @@
|
|
|
354
446
|
outline-offset: 2px;
|
|
355
447
|
}
|
|
356
448
|
|
|
449
|
+
/* What this version changed: a bar in the margin beside the block. */
|
|
450
|
+
.agent-prose :global([data-changed]) {
|
|
451
|
+
position: relative;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.agent-prose :global([data-changed]::before) {
|
|
455
|
+
content: '';
|
|
456
|
+
position: absolute;
|
|
457
|
+
inset-block: 0;
|
|
458
|
+
inset-inline-start: -0.75rem;
|
|
459
|
+
width: 2px;
|
|
460
|
+
background: var(--ds-color-foreground);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.agent-prose :global(tr[data-changed]::before) {
|
|
464
|
+
content: none;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.agent-prose :global(tr[data-changed] > td:first-child) {
|
|
468
|
+
box-shadow: inset 2px 0 0 var(--ds-color-foreground);
|
|
469
|
+
}
|
|
470
|
+
|
|
357
471
|
.agent-prose :global(hr) {
|
|
358
472
|
border: 0;
|
|
359
473
|
border-top: 1px solid var(--ds-color-border);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type ChatCopy } from '../../copy';
|
|
2
|
+
import type { Visual } from '../../visuals';
|
|
1
3
|
interface Props {
|
|
2
4
|
content: string;
|
|
3
5
|
streaming?: boolean;
|
|
@@ -5,10 +7,20 @@ interface Props {
|
|
|
5
7
|
collectionNames?: Set<string>;
|
|
6
8
|
/** `[n]` markers to turn into chips; omit and the markers stay as text. */
|
|
7
9
|
citationNumbers?: Set<number>;
|
|
10
|
+
/** Web pages by `webKey`, numbered after the library's: a link to one
|
|
11
|
+
* renders as its chip. */
|
|
12
|
+
webNumbers?: Map<string, number>;
|
|
8
13
|
oncite?: (n: number) => void;
|
|
9
14
|
/** A step down, for prose that is reference material beside an answer
|
|
10
15
|
* rather than the answer itself — the source pane's sections. */
|
|
11
16
|
dense?: boolean;
|
|
17
|
+
/** Renderers by a fence's language: a block of one is drawn by its
|
|
18
|
+
* component in place of the code. Absent, every fence is code. */
|
|
19
|
+
visuals?: Record<string, Visual>;
|
|
20
|
+
/** The same document's earlier version, as Markdown: what differs here
|
|
21
|
+
* is marked with a bar. */
|
|
22
|
+
previous?: string;
|
|
23
|
+
copy?: Partial<ChatCopy>;
|
|
12
24
|
}
|
|
13
25
|
declare const Markdown: import("svelte").Component<Props, {}, "">;
|
|
14
26
|
type Markdown = ReturnType<typeof Markdown>;
|