@poodle64/librarian 2026.9.28 → 2026.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -116
- package/dist/attachments.d.ts +2 -2
- package/dist/attachments.js +3 -3
- package/dist/chat.svelte.d.ts +22 -23
- package/dist/chat.svelte.js +41 -64
- package/dist/citations.d.ts +7 -7
- package/dist/citations.js +6 -6
- package/dist/client.d.ts +16 -45
- package/dist/client.js +15 -23
- package/dist/components/activity-group/activity-group.svelte +1 -1
- package/dist/components/agent-transcript/agent-transcript.svelte +9 -110
- package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +4 -16
- package/dist/components/answer-mark/answer-mark.svelte +2 -2
- package/dist/components/answer-mark/answer-mark.svelte.d.ts +2 -2
- package/dist/components/answer-share/answer-share.svelte +2 -2
- package/dist/components/answer-share/answer-share.svelte.d.ts +2 -2
- package/dist/components/artefact-card/artefact-card.svelte +2 -2
- package/dist/components/artefact-card/artefact-card.svelte.d.ts +2 -2
- package/dist/components/artefact-pane/artefact-pane.svelte +2 -2
- package/dist/components/artefact-pane/artefact-pane.svelte.d.ts +2 -2
- package/dist/components/composer/composer.svelte +39 -112
- package/dist/components/composer/composer.svelte.d.ts +18 -18
- package/dist/components/composer/index.d.ts +1 -2
- package/dist/components/conversation/conversation.svelte +7 -18
- package/dist/components/conversation/conversation.svelte.d.ts +7 -12
- package/dist/components/conversation-list/conversation-list.svelte +2 -2
- package/dist/components/conversation-list/conversation-list.svelte.d.ts +2 -2
- package/dist/components/document-pane/document-pane.svelte +2 -2
- package/dist/components/document-pane/document-pane.svelte.d.ts +2 -2
- package/dist/components/fair-use-notice/fair-use-notice.svelte +2 -2
- package/dist/components/fair-use-notice/fair-use-notice.svelte.d.ts +2 -2
- package/dist/components/scope-statement/scope-statement.svelte +5 -5
- package/dist/components/scope-statement/scope-statement.svelte.d.ts +2 -2
- package/dist/components/source-list/source-list.svelte +2 -2
- package/dist/components/source-list/source-list.svelte.d.ts +2 -2
- package/dist/components/working/working.svelte +2 -2
- package/dist/components/working/working.svelte.d.ts +2 -2
- package/dist/copy.d.ts +16 -24
- package/dist/copy.js +10 -16
- package/dist/session.svelte.d.ts +1 -1
- package/dist/session.svelte.js +6 -7
- package/dist/transcript.svelte.d.ts +18 -32
- package/dist/transcript.svelte.js +19 -40
- package/package.json +1 -5
- package/dist/history.svelte.d.ts +0 -37
- package/dist/history.svelte.js +0 -59
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
type Artefact,
|
|
25
25
|
type Block,
|
|
26
26
|
type DescribeTool,
|
|
27
|
-
type Depth,
|
|
28
27
|
type Outcome,
|
|
29
28
|
type TextBlock
|
|
30
29
|
} from '../../transcript.svelte';
|
|
@@ -34,7 +33,7 @@
|
|
|
34
33
|
splitSources,
|
|
35
34
|
type Citation
|
|
36
35
|
} from '../../citations';
|
|
37
|
-
import { DEFAULT_PERSONA, personaName, resolveCopy, type
|
|
36
|
+
import { DEFAULT_PERSONA, personaName, resolveCopy, type ChatCopy } from '../../copy';
|
|
38
37
|
import type { Verdict } from '../../chat.svelte';
|
|
39
38
|
import AnswerMark from '../answer-mark/answer-mark.svelte';
|
|
40
39
|
import AnswerShare from '../answer-share/answer-share.svelte';
|
|
@@ -51,22 +50,16 @@
|
|
|
51
50
|
blocks: Block[];
|
|
52
51
|
outcome: Outcome | null;
|
|
53
52
|
running: boolean;
|
|
54
|
-
/** Sources from the
|
|
53
|
+
/** Sources from the app's `citations` frame. Absent falls back to
|
|
55
54
|
* whatever a "## Sources" block in the prose says. */
|
|
56
55
|
citations?: Citation[];
|
|
57
56
|
/** Forwarded to Markdown; omit if the caller has no collections to chip. */
|
|
58
57
|
collectionNames?: Set<string>;
|
|
59
|
-
/** Follow-ups from the library's `suggestions` frame. */
|
|
60
|
-
suggestions?: string[];
|
|
61
58
|
/** Opens the document pane. Omit and chips render but do not open. */
|
|
62
59
|
oncite?: (citation: Citation) => void;
|
|
63
60
|
/** Offered on the LAST answer only — re-asks the same question. */
|
|
64
61
|
onregenerate?: () => void;
|
|
65
|
-
|
|
66
|
-
* chips do not render, because a chip that does nothing is worse than
|
|
67
|
-
* no chip. */
|
|
68
|
-
onsuggest?: (question: string) => void;
|
|
69
|
-
copy?: Partial<LibrarianCopy>;
|
|
62
|
+
copy?: Partial<ChatCopy>;
|
|
70
63
|
/** The persona's display name, signed on the answer card. */
|
|
71
64
|
name?: string;
|
|
72
65
|
/** When the question was asked, epoch ms. Rendered as a clock time —
|
|
@@ -83,8 +76,6 @@
|
|
|
83
76
|
onopenartefact?: () => void;
|
|
84
77
|
/** The persona's own words for its tools, and what they read. */
|
|
85
78
|
describeTool?: DescribeTool;
|
|
86
|
-
/** The question waits behind somebody else's. */
|
|
87
|
-
waiting?: boolean;
|
|
88
79
|
/** When an answer this page did not start began, epoch ms: it is still
|
|
89
80
|
* being written, and the clock counts from then. */
|
|
90
81
|
answering?: number;
|
|
@@ -98,10 +89,6 @@
|
|
|
98
89
|
* true once recorded. Omit and nothing about sharing renders: nothing
|
|
99
90
|
* is shared unless the reader asks. */
|
|
100
91
|
onshare?: (share: boolean) => Promise<boolean>;
|
|
101
|
-
/** How hard the persona worked on this one, for the footer's cost line
|
|
102
|
-
* (`Quick · 56 s · about A$0.34`). Absent renders the footer exactly
|
|
103
|
-
* as it always has: the duration alone. */
|
|
104
|
-
depth?: Depth;
|
|
105
92
|
/** Prices `outcome.costUsd` in the host's own currency and words
|
|
106
93
|
* ("about A$0.34"). The package never converts currency itself,
|
|
107
94
|
* and a zero-cost run (a local model) shows no cost regardless. */
|
|
@@ -115,10 +102,8 @@
|
|
|
115
102
|
running,
|
|
116
103
|
citations = [],
|
|
117
104
|
collectionNames = new Set(),
|
|
118
|
-
suggestions = [],
|
|
119
105
|
oncite,
|
|
120
106
|
onregenerate,
|
|
121
|
-
onsuggest,
|
|
122
107
|
copy,
|
|
123
108
|
name = DEFAULT_PERSONA,
|
|
124
109
|
at,
|
|
@@ -126,12 +111,10 @@
|
|
|
126
111
|
artefactOpen = false,
|
|
127
112
|
onopenartefact,
|
|
128
113
|
describeTool,
|
|
129
|
-
waiting = false,
|
|
130
114
|
answering,
|
|
131
115
|
onmark,
|
|
132
116
|
shared,
|
|
133
117
|
onshare,
|
|
134
|
-
depth,
|
|
135
118
|
formatCost
|
|
136
119
|
}: AgentTranscriptProps = $props();
|
|
137
120
|
|
|
@@ -214,7 +197,7 @@
|
|
|
214
197
|
*
|
|
215
198
|
* Reading none of this left a failed turn rendering as a clean, complete
|
|
216
199
|
* answer: no banner, a duration badge, and — worse — the "answered without
|
|
217
|
-
* a source" line, which is a claim about the
|
|
200
|
+
* a source" line, which is a claim about the sources made off a run that
|
|
218
201
|
* never finished looking.
|
|
219
202
|
*/
|
|
220
203
|
const failure = $derived(
|
|
@@ -248,13 +231,6 @@
|
|
|
248
231
|
settled && hasAnswer && sources.length === 0 && Boolean(outcome) && !failure
|
|
249
232
|
);
|
|
250
233
|
|
|
251
|
-
// A follow-up is offered once. Clicking it asks the question, which puts a
|
|
252
|
-
// new turn below this one — and leaving the row up for the moment before
|
|
253
|
-
// that arrives invites a second click on a third suggestion, asking two
|
|
254
|
-
// questions the reader only meant to ask one of.
|
|
255
|
-
let used = $state(false);
|
|
256
|
-
const followUps = $derived(!used && onsuggest ? suggestions : []);
|
|
257
|
-
|
|
258
234
|
let copied = $state(false);
|
|
259
235
|
// Not `copy`: the prop of that name is the package's own words, and a
|
|
260
236
|
// function shadowing it here would be a redeclaration, not a shadow.
|
|
@@ -296,11 +272,7 @@
|
|
|
296
272
|
{#if running && segments.length === 0}
|
|
297
273
|
<Working
|
|
298
274
|
copy={words}
|
|
299
|
-
label={
|
|
300
|
-
? words.waiting
|
|
301
|
-
: answering !== undefined
|
|
302
|
-
? words.stillAnswering
|
|
303
|
-
: undefined}
|
|
275
|
+
label={answering !== undefined ? words.stillAnswering : undefined}
|
|
304
276
|
since={answering}
|
|
305
277
|
/>
|
|
306
278
|
{/if}
|
|
@@ -332,7 +304,7 @@
|
|
|
332
304
|
<SourceList {sources} {words} {oncite} />
|
|
333
305
|
{/if}
|
|
334
306
|
|
|
335
|
-
<!-- An empty `notHeld` is a host whose persona answers from no
|
|
307
|
+
<!-- An empty `notHeld` is a host whose persona answers from no sources,
|
|
336
308
|
where "without a source" would be a claim about nothing. -->
|
|
337
309
|
{#if unanswered && asked}
|
|
338
310
|
<p class="ds-lib-not-held">{words.noAnswer}</p>
|
|
@@ -342,30 +314,6 @@
|
|
|
342
314
|
<p class="ds-lib-not-held">{words.notHeld}</p>
|
|
343
315
|
{/if}
|
|
344
316
|
|
|
345
|
-
{#if !isArtefact && followUps.length > 0}
|
|
346
|
-
<section>
|
|
347
|
-
<h2 class="ds-lib-follow-heading">{words.suggestions}</h2>
|
|
348
|
-
<!-- Wrapping, not scrolling: at 390 a question is most of a line, so
|
|
349
|
-
a row of three would clip two of them to fragments. -->
|
|
350
|
-
<ul class="ds-lib-chips">
|
|
351
|
-
{#each followUps as followUp (followUp)}
|
|
352
|
-
<li>
|
|
353
|
-
<button
|
|
354
|
-
type="button"
|
|
355
|
-
class="ds-lib-chip"
|
|
356
|
-
onclick={() => {
|
|
357
|
-
used = true;
|
|
358
|
-
onsuggest?.(followUp);
|
|
359
|
-
}}
|
|
360
|
-
>
|
|
361
|
-
{followUp}
|
|
362
|
-
</button>
|
|
363
|
-
</li>
|
|
364
|
-
{/each}
|
|
365
|
-
</ul>
|
|
366
|
-
</section>
|
|
367
|
-
{/if}
|
|
368
|
-
|
|
369
317
|
{#if failure}
|
|
370
318
|
<p class="ds-lib-failure" role="alert">{failure}</p>
|
|
371
319
|
{/if}
|
|
@@ -402,10 +350,10 @@
|
|
|
402
350
|
{/if}
|
|
403
351
|
<!-- What it took, which is not the same fact as when it was
|
|
404
352
|
asked; both are on the card and neither stands in for the
|
|
405
|
-
other.
|
|
406
|
-
second one, once the host offers
|
|
353
|
+
other. Cost folds into the SAME line, never a
|
|
354
|
+
second one, once the host offers it. -->
|
|
407
355
|
{#if outcome && !failure}
|
|
408
|
-
<span class="ds-lib-duration">{outcomeLine(outcome,
|
|
356
|
+
<span class="ds-lib-duration">{outcomeLine(outcome, formatCost)}</span>
|
|
409
357
|
{/if}
|
|
410
358
|
</div>
|
|
411
359
|
{/if}
|
|
@@ -515,55 +463,6 @@
|
|
|
515
463
|
font-size: 0.875rem;
|
|
516
464
|
}
|
|
517
465
|
|
|
518
|
-
.ds-lib-follow-heading {
|
|
519
|
-
margin: 0 0 0.375rem;
|
|
520
|
-
color: var(--ds-color-muted-foreground);
|
|
521
|
-
font-family: inherit;
|
|
522
|
-
font-size: var(--ds-text-2xs);
|
|
523
|
-
font-weight: 500;
|
|
524
|
-
letter-spacing: 0.05em;
|
|
525
|
-
text-transform: uppercase;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
.ds-lib-chips {
|
|
529
|
-
display: flex;
|
|
530
|
-
flex-wrap: wrap;
|
|
531
|
-
gap: 0.5rem;
|
|
532
|
-
margin: 0;
|
|
533
|
-
padding: 0;
|
|
534
|
-
list-style: none;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
.ds-lib-chips li {
|
|
538
|
-
max-width: 100%;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
.ds-lib-chip {
|
|
542
|
-
max-width: 100%;
|
|
543
|
-
border: 1px solid var(--ds-color-border);
|
|
544
|
-
border-radius: var(--ds-radius-full);
|
|
545
|
-
background: none;
|
|
546
|
-
padding: 0.5rem 0.875rem;
|
|
547
|
-
color: var(--ds-color-foreground);
|
|
548
|
-
font: inherit;
|
|
549
|
-
font-size: 0.875rem;
|
|
550
|
-
text-align: start;
|
|
551
|
-
cursor: pointer;
|
|
552
|
-
transition:
|
|
553
|
-
background-color 150ms ease,
|
|
554
|
-
border-color 150ms ease;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
.ds-lib-chip:hover {
|
|
558
|
-
border-color: var(--ds-color-border-strong);
|
|
559
|
-
background: var(--ds-color-surface-1);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
.ds-lib-chip:focus-visible {
|
|
563
|
-
outline: 2px solid var(--ds-color-ring);
|
|
564
|
-
outline-offset: 2px;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
466
|
.ds-lib-failure {
|
|
568
467
|
margin: 0;
|
|
569
468
|
border: 1px solid color-mix(in oklab, var(--ds-color-status-error) 40%, transparent);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type Artefact, type Block, type DescribeTool, type
|
|
1
|
+
import { type Artefact, type Block, type DescribeTool, type Outcome } from '../../transcript.svelte';
|
|
2
2
|
import { type Citation } from '../../citations';
|
|
3
|
-
import { type
|
|
3
|
+
import { type ChatCopy } from '../../copy';
|
|
4
4
|
import type { Verdict } from '../../chat.svelte';
|
|
5
5
|
/** Everything one turn renders from: what `Conversation` hands a host's
|
|
6
6
|
* own `turn` snippet, so it can render this component or its own. */
|
|
@@ -9,22 +9,16 @@ export interface AgentTranscriptProps {
|
|
|
9
9
|
blocks: Block[];
|
|
10
10
|
outcome: Outcome | null;
|
|
11
11
|
running: boolean;
|
|
12
|
-
/** Sources from the
|
|
12
|
+
/** Sources from the app's `citations` frame. Absent falls back to
|
|
13
13
|
* whatever a "## Sources" block in the prose says. */
|
|
14
14
|
citations?: Citation[];
|
|
15
15
|
/** Forwarded to Markdown; omit if the caller has no collections to chip. */
|
|
16
16
|
collectionNames?: Set<string>;
|
|
17
|
-
/** Follow-ups from the library's `suggestions` frame. */
|
|
18
|
-
suggestions?: string[];
|
|
19
17
|
/** Opens the document pane. Omit and chips render but do not open. */
|
|
20
18
|
oncite?: (citation: Citation) => void;
|
|
21
19
|
/** Offered on the LAST answer only — re-asks the same question. */
|
|
22
20
|
onregenerate?: () => void;
|
|
23
|
-
|
|
24
|
-
* chips do not render, because a chip that does nothing is worse than
|
|
25
|
-
* no chip. */
|
|
26
|
-
onsuggest?: (question: string) => void;
|
|
27
|
-
copy?: Partial<LibrarianCopy>;
|
|
21
|
+
copy?: Partial<ChatCopy>;
|
|
28
22
|
/** The persona's display name, signed on the answer card. */
|
|
29
23
|
name?: string;
|
|
30
24
|
/** When the question was asked, epoch ms. Rendered as a clock time —
|
|
@@ -41,8 +35,6 @@ export interface AgentTranscriptProps {
|
|
|
41
35
|
onopenartefact?: () => void;
|
|
42
36
|
/** The persona's own words for its tools, and what they read. */
|
|
43
37
|
describeTool?: DescribeTool;
|
|
44
|
-
/** The question waits behind somebody else's. */
|
|
45
|
-
waiting?: boolean;
|
|
46
38
|
/** When an answer this page did not start began, epoch ms: it is still
|
|
47
39
|
* being written, and the clock counts from then. */
|
|
48
40
|
answering?: number;
|
|
@@ -56,10 +48,6 @@ export interface AgentTranscriptProps {
|
|
|
56
48
|
* true once recorded. Omit and nothing about sharing renders: nothing
|
|
57
49
|
* is shared unless the reader asks. */
|
|
58
50
|
onshare?: (share: boolean) => Promise<boolean>;
|
|
59
|
-
/** How hard the persona worked on this one, for the footer's cost line
|
|
60
|
-
* (`Quick · 56 s · about A$0.34`). Absent renders the footer exactly
|
|
61
|
-
* as it always has: the duration alone. */
|
|
62
|
-
depth?: Depth;
|
|
63
51
|
/** Prices `outcome.costUsd` in the host's own currency and words
|
|
64
52
|
* ("about A$0.34"). The package never converts currency itself,
|
|
65
53
|
* and a zero-cost run (a local model) shows no cost regardless. */
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
import ThumbsDownIcon from '@lucide/svelte/icons/thumbs-down';
|
|
19
19
|
import ThumbsUpIcon from '@lucide/svelte/icons/thumbs-up';
|
|
20
20
|
import type { Verdict } from '../../chat.svelte';
|
|
21
|
-
import { resolveCopy, type
|
|
21
|
+
import { resolveCopy, type ChatCopy } from '../../copy';
|
|
22
22
|
|
|
23
23
|
interface Props {
|
|
24
24
|
/** Records a verdict, replacing any before it. Resolves true once it is
|
|
25
25
|
* recorded. */
|
|
26
26
|
onmark: (verdict: Verdict) => Promise<boolean>;
|
|
27
|
-
copy?: Partial<
|
|
27
|
+
copy?: Partial<ChatCopy>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
let { onmark, copy }: Props = $props();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Verdict } from '../../chat.svelte';
|
|
2
|
-
import { type
|
|
2
|
+
import { type ChatCopy } from '../../copy';
|
|
3
3
|
interface Props {
|
|
4
4
|
/** Records a verdict, replacing any before it. Resolves true once it is
|
|
5
5
|
* recorded. */
|
|
6
6
|
onmark: (verdict: Verdict) => Promise<boolean>;
|
|
7
|
-
copy?: Partial<
|
|
7
|
+
copy?: Partial<ChatCopy>;
|
|
8
8
|
}
|
|
9
9
|
declare const AnswerMark: import("svelte").Component<Props, {}, "">;
|
|
10
10
|
type AnswerMark = ReturnType<typeof AnswerMark>;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import CheckIcon from '@lucide/svelte/icons/check';
|
|
14
14
|
import LinkIcon from '@lucide/svelte/icons/link';
|
|
15
15
|
import Share2Icon from '@lucide/svelte/icons/share-2';
|
|
16
|
-
import { resolveCopy, type
|
|
16
|
+
import { resolveCopy, type ChatCopy } from '../../copy';
|
|
17
17
|
|
|
18
18
|
interface Props {
|
|
19
19
|
/** Shares the answer (true) or stops sharing it (false), resolving true
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
/** Where the read-only view lives while the answer is shared, as the
|
|
23
23
|
* host's own address. */
|
|
24
24
|
shared?: string;
|
|
25
|
-
copy?: Partial<
|
|
25
|
+
copy?: Partial<ChatCopy>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
let { onshare, shared, copy }: Props = $props();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ChatCopy } from '../../copy';
|
|
2
2
|
interface Props {
|
|
3
3
|
/** Shares the answer (true) or stops sharing it (false), resolving true
|
|
4
4
|
* once recorded. */
|
|
@@ -6,7 +6,7 @@ interface Props {
|
|
|
6
6
|
/** Where the read-only view lives while the answer is shared, as the
|
|
7
7
|
* host's own address. */
|
|
8
8
|
shared?: string;
|
|
9
|
-
copy?: Partial<
|
|
9
|
+
copy?: Partial<ChatCopy>;
|
|
10
10
|
}
|
|
11
11
|
declare const AnswerShare: import("svelte").Component<Props, {}, "">;
|
|
12
12
|
type AnswerShare = ReturnType<typeof AnswerShare>;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<script lang="ts">
|
|
13
13
|
import FileTextIcon from '@lucide/svelte/icons/file-text';
|
|
14
14
|
import type { Artefact } from '../../transcript.svelte';
|
|
15
|
-
import { resolveCopy, type
|
|
15
|
+
import { resolveCopy, type ChatCopy } from '../../copy';
|
|
16
16
|
|
|
17
17
|
interface Props {
|
|
18
18
|
artefact: Artefact;
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
/** Omit and the card informs but does not open: a host with nowhere
|
|
22
22
|
* to show the artefact offers no action rather than a dead one. */
|
|
23
23
|
onopen?: () => void;
|
|
24
|
-
copy?: Partial<
|
|
24
|
+
copy?: Partial<ChatCopy>;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
let { artefact, open = false, onopen, copy }: Props = $props();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Artefact } from '../../transcript.svelte';
|
|
2
|
-
import { type
|
|
2
|
+
import { type ChatCopy } from '../../copy';
|
|
3
3
|
interface Props {
|
|
4
4
|
artefact: Artefact;
|
|
5
5
|
/** The host is showing it now. */
|
|
@@ -7,7 +7,7 @@ interface Props {
|
|
|
7
7
|
/** Omit and the card informs but does not open: a host with nowhere
|
|
8
8
|
* to show the artefact offers no action rather than a dead one. */
|
|
9
9
|
onopen?: () => void;
|
|
10
|
-
copy?: Partial<
|
|
10
|
+
copy?: Partial<ChatCopy>;
|
|
11
11
|
}
|
|
12
12
|
declare const ArtefactCard: import("svelte").Component<Props, {}, "">;
|
|
13
13
|
type ArtefactCard = ReturnType<typeof ArtefactCard>;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import XIcon from '@lucide/svelte/icons/x';
|
|
11
11
|
import { segment, type TextBlock, type Turn } from '../../transcript.svelte';
|
|
12
12
|
import { resolveCitations, splitSources, type Citation } from '../../citations';
|
|
13
|
-
import { resolveCopy, type
|
|
13
|
+
import { resolveCopy, type ChatCopy } from '../../copy';
|
|
14
14
|
import Markdown from '../markdown/markdown.svelte';
|
|
15
15
|
import SourceList from '../source-list/source-list.svelte';
|
|
16
16
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* stacking it beside the artefact. Omit and the sources still list, but
|
|
22
22
|
* do not open. */
|
|
23
23
|
oncite?: (citation: Citation) => void;
|
|
24
|
-
copy?: Partial<
|
|
24
|
+
copy?: Partial<ChatCopy>;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
let { turn, onclose, oncite, copy }: Props = $props();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Turn } from '../../transcript.svelte';
|
|
2
2
|
import { type Citation } from '../../citations';
|
|
3
|
-
import { type
|
|
3
|
+
import { type ChatCopy } from '../../copy';
|
|
4
4
|
interface Props {
|
|
5
5
|
turn: Turn;
|
|
6
6
|
onclose: () => void;
|
|
@@ -8,7 +8,7 @@ interface Props {
|
|
|
8
8
|
* stacking it beside the artefact. Omit and the sources still list, but
|
|
9
9
|
* do not open. */
|
|
10
10
|
oncite?: (citation: Citation) => void;
|
|
11
|
-
copy?: Partial<
|
|
11
|
+
copy?: Partial<ChatCopy>;
|
|
12
12
|
}
|
|
13
13
|
declare const ArtefactPane: import("svelte").Component<Props, {}, "">;
|
|
14
14
|
type ArtefactPane = ReturnType<typeof ArtefactPane>;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
The empty→typing transition lives almost entirely in that control — the box
|
|
7
7
|
chrome grows with the text and nothing else moves.
|
|
8
8
|
|
|
9
|
-
While
|
|
9
|
+
While the persona is answering the box is disabled and the send button becomes a
|
|
10
10
|
stop button: an ask route takes one question per request, and a box that
|
|
11
11
|
takes typing it will then discard is worse than one that plainly waits. A
|
|
12
12
|
host whose session reads messages while it works (`sendWhileRunning`) keeps
|
|
@@ -26,30 +26,35 @@
|
|
|
26
26
|
MAX_FILES,
|
|
27
27
|
rejectionMessage
|
|
28
28
|
} from '../../attachments';
|
|
29
|
-
import type
|
|
30
|
-
import { DEFAULT_PERSONA, resolveCopy, type LibrarianCopy } from '../../copy';
|
|
29
|
+
import { DEFAULT_PERSONA, resolveCopy, type ChatCopy } from '../../copy';
|
|
31
30
|
|
|
32
31
|
/** Past this many turns, the long-conversation banner offers to start
|
|
33
32
|
* over: a follow-up re-reads the whole conversation, so each one costs
|
|
34
33
|
* more than the last (measured — the longest ran 2.05M cached tokens). */
|
|
35
34
|
const DEFAULT_TURN_LIMIT = 8;
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
/** A collection a person may read, as the app lists it: `name` is what a
|
|
37
|
+
* question is narrowed to, `summary` is what the picker shows. */
|
|
38
|
+
export interface CollectionChoice {
|
|
39
|
+
name: string;
|
|
40
|
+
summary: string;
|
|
41
|
+
}
|
|
38
42
|
|
|
39
43
|
interface Props {
|
|
40
44
|
value: string;
|
|
41
45
|
running: boolean;
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
44
|
-
|
|
46
|
+
/** The collections this person may read. The picker renders when there
|
|
47
|
+
* are two or more: with one, or none, there is nothing to choose. */
|
|
48
|
+
collections?: CollectionChoice[];
|
|
49
|
+
/** The collection the next question is narrowed to, by name; undefined
|
|
50
|
+
* asks across all of them. */
|
|
51
|
+
collection?: string;
|
|
45
52
|
/** Files to send with the next question. Cleared by the caller on send. */
|
|
46
53
|
files?: File[];
|
|
47
|
-
/** Names for the two narrower scopes; absent means that scope is unavailable. */
|
|
48
|
-
documentName?: string;
|
|
49
|
-
collectionName?: string;
|
|
50
54
|
/** Hides the paperclip for a host whose ask route takes no files. */
|
|
51
55
|
attachments?: boolean;
|
|
52
|
-
|
|
56
|
+
/** The person picked a collection by name, or all of them (undefined). */
|
|
57
|
+
oncollection?: (collection: string | undefined) => void;
|
|
53
58
|
onsubmit: () => void;
|
|
54
59
|
onstop: () => void;
|
|
55
60
|
/** Asks for a study artefact instead of an ordinary answer. Omit and no
|
|
@@ -60,21 +65,16 @@
|
|
|
60
65
|
* both placeholders are composed from it. */
|
|
61
66
|
name?: string;
|
|
62
67
|
/** Overrides for the package's own words. */
|
|
63
|
-
copy?: Partial<
|
|
68
|
+
copy?: Partial<ChatCopy>;
|
|
64
69
|
/** The session reads a message sent while it works — a job whose
|
|
65
70
|
* route writes it onto the running process — so the box stays open
|
|
66
71
|
* and Send sits beside Stop. Words only: the paperclip still waits for
|
|
67
72
|
* the run. Absent, a chat waits for its answer. */
|
|
68
73
|
sendWhileRunning?: boolean;
|
|
69
|
-
/** One quiet line in the box's footer, where
|
|
74
|
+
/** One quiet line in the box's footer, where the collection picker would
|
|
70
75
|
* sit, in the host's words: what sending does here ("It carries on
|
|
71
76
|
* from where it stopped."). */
|
|
72
77
|
note?: string;
|
|
73
|
-
/** Quick or Thorough for the next question, as a two-way switch with a
|
|
74
|
-
* one-line hint. Bind it and pass it to `ask()`; omit it and nothing
|
|
75
|
-
* renders. Once shown it is never disabled or hidden, and never names
|
|
76
|
-
* the model behind it. */
|
|
77
|
-
depth?: Depth;
|
|
78
78
|
/** How many turns are in the conversation so far. Past `turnLimit`
|
|
79
79
|
* (8 by default), a quiet banner appears above the box offering to
|
|
80
80
|
* start over. Omit and no banner ever renders — a host that does not
|
|
@@ -90,12 +90,11 @@
|
|
|
90
90
|
let {
|
|
91
91
|
value = $bindable(),
|
|
92
92
|
running,
|
|
93
|
-
|
|
93
|
+
collections = [],
|
|
94
|
+
collection,
|
|
94
95
|
files = $bindable([]),
|
|
95
|
-
documentName,
|
|
96
|
-
collectionName,
|
|
97
96
|
attachments = true,
|
|
98
|
-
|
|
97
|
+
oncollection,
|
|
99
98
|
onsubmit,
|
|
100
99
|
onstop,
|
|
101
100
|
onbriefing,
|
|
@@ -103,7 +102,6 @@
|
|
|
103
102
|
copy,
|
|
104
103
|
note,
|
|
105
104
|
sendWhileRunning = false,
|
|
106
|
-
depth = $bindable(),
|
|
107
105
|
turnCount,
|
|
108
106
|
turnLimit = DEFAULT_TURN_LIMIT,
|
|
109
107
|
onnewquestion
|
|
@@ -120,24 +118,11 @@
|
|
|
120
118
|
turnCount !== undefined && turnCount > turnLimit && Boolean(onnewquestion)
|
|
121
119
|
);
|
|
122
120
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
const choices = $derived(
|
|
129
|
-
[
|
|
130
|
-
documentName ? { id: 'document' as const, label: documentName } : null,
|
|
131
|
-
collectionName ? { id: 'collection' as const, label: collectionName } : null,
|
|
132
|
-
{ id: 'library' as const, label: 'The whole library' }
|
|
133
|
-
].filter((c) => c !== null)
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
// A room with one shelf set has one scope, fixed by the caller never
|
|
137
|
-
// passing a name — no pick to make, so the row renders nothing rather
|
|
138
|
-
// than a chip that only ever reselects itself (design-system, the
|
|
139
|
-
// fixed-scope Composer defect).
|
|
140
|
-
const hasChoice = $derived(scope !== undefined && choices.length > 1);
|
|
121
|
+
// Who the question is put to: every collection the person may read, or one
|
|
122
|
+
// of them. A person with one collection (or none) has nothing to pick, so
|
|
123
|
+
// the row renders nothing rather than a chip that only reselects itself.
|
|
124
|
+
// A collection is shown by its summary, the words a reader knows it by.
|
|
125
|
+
const hasChoice = $derived(collections.length > 1);
|
|
141
126
|
|
|
142
127
|
// Three chips in a narrow column clipped all three to fragments
|
|
143
128
|
// ("defence-s…", "All colle…"). Wrapping beats truncating: a chip a reader
|
|
@@ -166,7 +151,7 @@
|
|
|
166
151
|
grow();
|
|
167
152
|
});
|
|
168
153
|
|
|
169
|
-
// Focus comes back once
|
|
154
|
+
// Focus comes back once the persona is done, so a reader can keep going without
|
|
170
155
|
// reaching for the mouse. It has to be the transition OUT of `waiting`: a
|
|
171
156
|
// disabled element cannot hold focus, so the focus call in `submit()` below
|
|
172
157
|
// survives only for a box that never disables — measured, focus lands on
|
|
@@ -321,41 +306,26 @@
|
|
|
321
306
|
</button>
|
|
322
307
|
{/if}
|
|
323
308
|
|
|
324
|
-
{#if
|
|
325
|
-
<div role="group" aria-label={words.
|
|
326
|
-
<button
|
|
327
|
-
type="button"
|
|
328
|
-
aria-pressed={depth === 'quick'}
|
|
329
|
-
class="ds-lib-depth-side"
|
|
330
|
-
class:is-chosen={depth === 'quick'}
|
|
331
|
-
onclick={() => (depth = 'quick')}
|
|
332
|
-
>
|
|
333
|
-
{words.depthQuick}
|
|
334
|
-
</button>
|
|
309
|
+
{#if hasChoice}
|
|
310
|
+
<div role="group" aria-label={words.collectionsLabel} class="ds-lib-scopes">
|
|
335
311
|
<button
|
|
336
312
|
type="button"
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
class:is-chosen={
|
|
340
|
-
onclick={() => (
|
|
313
|
+
class="ds-lib-scope-chip"
|
|
314
|
+
aria-pressed={collection === undefined}
|
|
315
|
+
class:is-chosen={collection === undefined}
|
|
316
|
+
onclick={() => oncollection?.(undefined)}
|
|
341
317
|
>
|
|
342
|
-
{words.
|
|
318
|
+
{words.allCollections}
|
|
343
319
|
</button>
|
|
344
|
-
|
|
345
|
-
<span class="ds-lib-note">
|
|
346
|
-
{depth === 'quick' ? words.depthQuickHint : words.depthThoroughHint}
|
|
347
|
-
</span>
|
|
348
|
-
{/if}
|
|
349
|
-
{#if hasChoice}
|
|
350
|
-
<div class="ds-lib-scopes">
|
|
351
|
-
{#each choices as choice (choice.id)}
|
|
320
|
+
{#each collections as choice (choice.name)}
|
|
352
321
|
<button
|
|
353
322
|
type="button"
|
|
354
323
|
class="ds-lib-scope-chip"
|
|
355
|
-
|
|
356
|
-
|
|
324
|
+
aria-pressed={collection === choice.name}
|
|
325
|
+
class:is-chosen={collection === choice.name}
|
|
326
|
+
onclick={() => oncollection?.(choice.name)}
|
|
357
327
|
>
|
|
358
|
-
{choice.
|
|
328
|
+
{choice.summary || choice.name}
|
|
359
329
|
</button>
|
|
360
330
|
{/each}
|
|
361
331
|
</div>
|
|
@@ -607,49 +577,6 @@
|
|
|
607
577
|
font-size: var(--ds-text-2xs);
|
|
608
578
|
}
|
|
609
579
|
|
|
610
|
-
/* A labelled button group, never a select: two states a reader compares
|
|
611
|
-
at a glance, not a list to open. 44px touch targets throughout — the
|
|
612
|
-
one row here a colleague reaches for on every single question. */
|
|
613
|
-
.ds-lib-depth {
|
|
614
|
-
display: flex;
|
|
615
|
-
flex: none;
|
|
616
|
-
gap: 0.125rem;
|
|
617
|
-
border-radius: var(--ds-radius-lg);
|
|
618
|
-
background: var(--ds-color-surface-3);
|
|
619
|
-
padding: 0.1875rem;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
.ds-lib-depth-side {
|
|
623
|
-
min-width: 2.75rem;
|
|
624
|
-
min-height: 2.75rem;
|
|
625
|
-
border: 0;
|
|
626
|
-
border-radius: var(--ds-radius-md);
|
|
627
|
-
background: none;
|
|
628
|
-
padding: 0 0.75rem;
|
|
629
|
-
color: var(--ds-color-muted-foreground);
|
|
630
|
-
font: inherit;
|
|
631
|
-
font-size: var(--ds-text-2xs);
|
|
632
|
-
cursor: pointer;
|
|
633
|
-
transition:
|
|
634
|
-
color 150ms ease,
|
|
635
|
-
background-color 150ms ease;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
.ds-lib-depth-side:hover {
|
|
639
|
-
color: var(--ds-color-foreground);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
.ds-lib-depth-side.is-chosen {
|
|
643
|
-
background: var(--ds-color-primary);
|
|
644
|
-
color: var(--ds-color-primary-foreground);
|
|
645
|
-
font-weight: 600;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
.ds-lib-depth-side:focus-visible {
|
|
649
|
-
outline: 2px solid var(--ds-color-ring);
|
|
650
|
-
outline-offset: 2px;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
580
|
/* Above the box, never inside it: it is about the conversation so far,
|
|
654
581
|
not this one question. Warning-tinted, never error — nothing has gone
|
|
655
582
|
wrong, a follow-up just costs more than the last one did. */
|