@poodle64/librarian 2026.9.13 → 2026.9.15
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 +77 -19
- package/dist/client.d.ts +16 -1
- package/dist/client.js +55 -8
- package/dist/components/activity-group/activity-group.svelte +76 -8
- package/dist/components/agent-transcript/agent-transcript.svelte +346 -150
- package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +7 -0
- package/dist/components/artefact-card/artefact-card.svelte +30 -2
- package/dist/components/artefact-pane/artefact-pane.svelte +141 -50
- package/dist/components/composer/composer.svelte +255 -38
- package/dist/components/composer/composer.svelte.d.ts +6 -0
- package/dist/components/conversation/conversation.svelte +254 -81
- package/dist/components/conversation/conversation.svelte.d.ts +7 -0
- package/dist/components/document-pane/document-pane.svelte +216 -33
- package/dist/components/markdown/markdown.svelte +73 -24
- package/dist/components/scope-statement/scope-statement.svelte +79 -12
- package/dist/components/source-list/index.d.ts +2 -0
- package/dist/components/source-list/index.js +2 -0
- package/dist/components/source-list/source-list.svelte +137 -0
- package/dist/components/source-list/source-list.svelte.d.ts +13 -0
- package/dist/components/thinking-row/thinking-row.svelte +69 -18
- package/dist/components/tool-row/tool-row.svelte +132 -26
- package/dist/components/working/working.svelte +51 -7
- package/dist/components/working/working.svelte.d.ts +3 -0
- package/dist/copy.d.ts +45 -10
- package/dist/copy.js +77 -32
- package/dist/transcript.svelte.d.ts +10 -0
- package/dist/transcript.svelte.js +4 -1
- package/package.json +4 -2
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
What an answer cited, as a list a reader can open.
|
|
3
|
+
|
|
4
|
+
One component rather than the same twenty lines in the transcript and again
|
|
5
|
+
in the artefact pane: the trust mark, the number chip and the disabled state
|
|
6
|
+
of a citation with no document behind it are one decision, and two copies of
|
|
7
|
+
it is two places for them to drift apart.
|
|
8
|
+
-->
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import { trustMark, type Citation } from '../../citations';
|
|
11
|
+
import type { LibrarianCopy } from '../../copy';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
sources: Citation[];
|
|
15
|
+
words: LibrarianCopy;
|
|
16
|
+
/** Opens the document. Omit and the rows list but do not open. */
|
|
17
|
+
oncite?: (citation: Citation) => void;
|
|
18
|
+
/** `h2` in a transcript turn, `h3` under the artefact's own title. */
|
|
19
|
+
level?: 2 | 3;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { sources, words, oncite, level = 2 }: Props = $props();
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<section class="ds-lib-sources">
|
|
26
|
+
{#if level === 2}
|
|
27
|
+
<h2 class="ds-lib-sources-heading">{words.sources}</h2>
|
|
28
|
+
{:else}
|
|
29
|
+
<h3 class="ds-lib-sources-heading">{words.sources}</h3>
|
|
30
|
+
{/if}
|
|
31
|
+
<ol class="ds-lib-sources-list">
|
|
32
|
+
{#each sources as source (source.n)}
|
|
33
|
+
{@const mark = trustMark(source, words)}
|
|
34
|
+
<li>
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
class="ds-lib-source"
|
|
38
|
+
onclick={() => oncite?.(source)}
|
|
39
|
+
disabled={!source.document_id || !oncite}
|
|
40
|
+
>
|
|
41
|
+
<span class="ds-lib-source-n">{source.n}</span>
|
|
42
|
+
<span class="ds-lib-source-text">
|
|
43
|
+
<span class="ds-lib-source-title">{source.title}</span>
|
|
44
|
+
{#if source.section}<span class="ds-lib-source-section"> · {source.section}</span>{/if}
|
|
45
|
+
<!-- Same muted register as the section, deliberately. An
|
|
46
|
+
unverified source is not an error and must not be dressed
|
|
47
|
+
as one; the words carry the difference, and a red one
|
|
48
|
+
would have a colleague discount a document that is
|
|
49
|
+
simply new. -->
|
|
50
|
+
{#if mark}<span class="ds-lib-source-mark"> · {mark}</span>{/if}
|
|
51
|
+
</span>
|
|
52
|
+
</button>
|
|
53
|
+
</li>
|
|
54
|
+
{/each}
|
|
55
|
+
</ol>
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
<style>
|
|
59
|
+
.ds-lib-sources-heading {
|
|
60
|
+
margin: 0 0 0.375rem;
|
|
61
|
+
color: var(--ds-color-muted-foreground);
|
|
62
|
+
font-family: inherit;
|
|
63
|
+
font-size: var(--ds-text-2xs);
|
|
64
|
+
font-weight: 500;
|
|
65
|
+
letter-spacing: 0.05em;
|
|
66
|
+
text-transform: uppercase;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.ds-lib-sources-list {
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
gap: 0.25rem;
|
|
73
|
+
margin: 0;
|
|
74
|
+
padding: 0;
|
|
75
|
+
list-style: none;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ds-lib-source {
|
|
79
|
+
display: flex;
|
|
80
|
+
width: 100%;
|
|
81
|
+
align-items: baseline;
|
|
82
|
+
gap: 0.5rem;
|
|
83
|
+
border: 1px solid var(--ds-color-border);
|
|
84
|
+
border-radius: var(--ds-radius-lg);
|
|
85
|
+
background: none;
|
|
86
|
+
padding: 0.375rem 0.625rem;
|
|
87
|
+
color: inherit;
|
|
88
|
+
font: inherit;
|
|
89
|
+
font-size: 0.875rem;
|
|
90
|
+
text-align: start;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
transition:
|
|
93
|
+
background-color 150ms ease,
|
|
94
|
+
border-color 150ms ease;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.ds-lib-source:hover:not(:disabled) {
|
|
98
|
+
border-color: var(--ds-color-border-strong);
|
|
99
|
+
background: var(--ds-color-surface-2);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.ds-lib-source:focus-visible {
|
|
103
|
+
outline: 2px solid var(--ds-color-ring);
|
|
104
|
+
outline-offset: 2px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.ds-lib-source:disabled {
|
|
108
|
+
cursor: default;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.ds-lib-source-n {
|
|
112
|
+
flex: none;
|
|
113
|
+
border-radius: var(--ds-radius-sm);
|
|
114
|
+
background: color-mix(in oklab, var(--ds-color-primary) 15%, transparent);
|
|
115
|
+
padding-inline: 0.375rem;
|
|
116
|
+
color: var(--ds-color-foreground);
|
|
117
|
+
font-family: var(--ds-font-code);
|
|
118
|
+
font-size: var(--ds-text-2xs);
|
|
119
|
+
font-variant-numeric: tabular-nums;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.ds-lib-source-text {
|
|
123
|
+
min-width: 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.ds-lib-source-title {
|
|
127
|
+
color: var(--ds-color-foreground);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.ds-lib-source-section {
|
|
131
|
+
color: var(--ds-color-muted-foreground);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.ds-lib-source-mark {
|
|
135
|
+
color: color-mix(in oklab, var(--ds-color-muted-foreground) 80%, transparent);
|
|
136
|
+
}
|
|
137
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Citation } from '../../citations';
|
|
2
|
+
import type { LibrarianCopy } from '../../copy';
|
|
3
|
+
interface Props {
|
|
4
|
+
sources: Citation[];
|
|
5
|
+
words: LibrarianCopy;
|
|
6
|
+
/** Opens the document. Omit and the rows list but do not open. */
|
|
7
|
+
oncite?: (citation: Citation) => void;
|
|
8
|
+
/** `h2` in a transcript turn, `h3` under the artefact's own title. */
|
|
9
|
+
level?: 2 | 3;
|
|
10
|
+
}
|
|
11
|
+
declare const SourceList: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type SourceList = ReturnType<typeof SourceList>;
|
|
13
|
+
export default SourceList;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
One line of
|
|
2
|
+
One line of the persona's working-out, behind a chevron.
|
|
3
3
|
|
|
4
4
|
Two different blocks render as this row and deliberately read the same. A
|
|
5
|
-
real `thinking` block is one; the other is a `text` block
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
real `thinking` block is one; the other is a `text` block written BETWEEN two
|
|
6
|
+
tool calls — "Let me also check whether…" — which the caller stream gives no
|
|
7
|
+
way to tell from the answer except by position, and which read as answer
|
|
8
|
+
prose on production until `segment()` started re-homing it here. To a reader
|
|
9
|
+
both are the same thing: what was being worked through, not what was
|
|
10
|
+
concluded. So both fold, and both fold by default.
|
|
11
11
|
-->
|
|
12
12
|
<script lang="ts">
|
|
13
13
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
|
@@ -22,25 +22,76 @@
|
|
|
22
22
|
let open = $state(false);
|
|
23
23
|
</script>
|
|
24
24
|
|
|
25
|
-
<div class="
|
|
25
|
+
<div class="ds-lib-thinking">
|
|
26
26
|
<button
|
|
27
27
|
type="button"
|
|
28
|
-
class="
|
|
28
|
+
class="ds-lib-thinking-row"
|
|
29
29
|
onclick={() => (open = !open)}
|
|
30
30
|
aria-expanded={open}
|
|
31
31
|
>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<span class=
|
|
32
|
+
<span class="ds-lib-thinking-chevron" class:is-open={open}>
|
|
33
|
+
<ChevronRightIcon size={12} />
|
|
34
|
+
</span>
|
|
35
|
+
<span class="ds-lib-thinking-label" class:is-active={active}>
|
|
36
36
|
{active ? 'Thinking…' : 'Thought'}
|
|
37
37
|
</span>
|
|
38
38
|
</button>
|
|
39
39
|
{#if open}
|
|
40
|
-
<p
|
|
41
|
-
class="text-muted-foreground border-border mt-1 ml-1.25 border-l pl-3 text-sm whitespace-pre-wrap"
|
|
42
|
-
>
|
|
43
|
-
{block.text}
|
|
44
|
-
</p>
|
|
40
|
+
<p class="ds-lib-thinking-text">{block.text}</p>
|
|
45
41
|
{/if}
|
|
46
42
|
</div>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
.ds-lib-thinking {
|
|
46
|
+
font-size: 0.875rem;
|
|
47
|
+
line-height: 1.25rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.ds-lib-thinking-row {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 0.5rem;
|
|
54
|
+
padding-block: 0.125rem;
|
|
55
|
+
border: 0;
|
|
56
|
+
background: none;
|
|
57
|
+
color: inherit;
|
|
58
|
+
font: inherit;
|
|
59
|
+
text-align: start;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ds-lib-thinking-chevron {
|
|
64
|
+
display: flex;
|
|
65
|
+
flex: none;
|
|
66
|
+
color: var(--ds-color-muted-foreground);
|
|
67
|
+
transition: transform 150ms ease;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.ds-lib-thinking-chevron.is-open {
|
|
71
|
+
transform: rotate(90deg);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ds-lib-thinking-label {
|
|
75
|
+
color: var(--ds-color-muted-foreground);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ds-lib-thinking-label.is-active {
|
|
79
|
+
color: var(--ds-color-foreground);
|
|
80
|
+
font-weight: 500;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.ds-lib-thinking-text {
|
|
84
|
+
margin: 0.25rem 0 0;
|
|
85
|
+
margin-inline-start: 0.3125rem;
|
|
86
|
+
border-inline-start: 1px solid var(--ds-color-border);
|
|
87
|
+
padding-inline-start: 0.75rem;
|
|
88
|
+
color: var(--ds-color-muted-foreground);
|
|
89
|
+
white-space: pre-wrap;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@media (prefers-reduced-motion: reduce) {
|
|
93
|
+
.ds-lib-thinking-chevron {
|
|
94
|
+
transition: none;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
</style>
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
dimmed sub-line. Borders and badge pills make a run of five calls read as
|
|
6
6
|
five separate events rather than one train of thought.
|
|
7
7
|
|
|
8
|
-
The row says what
|
|
9
|
-
name or the raw command it ran. Expanding a settled row shows what
|
|
10
|
-
back, never what was typed.
|
|
8
|
+
The row says what the persona DID, in a reader's own words — never the
|
|
9
|
+
tool's name or the raw command it ran. Expanding a settled row shows what
|
|
10
|
+
came back, never what was typed.
|
|
11
11
|
-->
|
|
12
12
|
<script lang="ts">
|
|
13
13
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
|
@@ -26,46 +26,152 @@
|
|
|
26
26
|
|
|
27
27
|
const settled = $derived(block.result !== undefined);
|
|
28
28
|
const said = $derived(describe(block));
|
|
29
|
-
const tone = $derived(
|
|
30
|
-
block.isError ? 'bg-status-error' : settled ? 'bg-status-success' : 'bg-status-info'
|
|
31
|
-
);
|
|
29
|
+
const tone = $derived(block.isError ? 'error' : settled ? 'success' : 'info');
|
|
32
30
|
const lines = $derived(block.result ? block.result.split('\n').length : 0);
|
|
33
31
|
</script>
|
|
34
32
|
|
|
35
|
-
<div class="
|
|
33
|
+
<div class="ds-lib-tool">
|
|
36
34
|
<button
|
|
37
35
|
type="button"
|
|
38
|
-
class="
|
|
36
|
+
class="ds-lib-tool-row"
|
|
39
37
|
onclick={() => (open = !open)}
|
|
40
38
|
disabled={!settled}
|
|
41
39
|
aria-expanded={open}
|
|
42
40
|
>
|
|
43
|
-
<span class="
|
|
44
|
-
{#if settled}
|
|
45
|
-
<ChevronRightIcon
|
|
46
|
-
class="text-muted-foreground size-3 transition-transform {open ? 'rotate-90' : ''}"
|
|
47
|
-
/>
|
|
48
|
-
{/if}
|
|
41
|
+
<span class="ds-lib-tool-chevron" class:is-open={open}>
|
|
42
|
+
{#if settled}<ChevronRightIcon size={12} />{/if}
|
|
49
43
|
</span>
|
|
50
|
-
<span class="
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<span class="
|
|
54
|
-
{#if
|
|
55
|
-
{#if repeats > 1}<span class="text-muted-foreground">· {repeats} pages</span>{/if}
|
|
44
|
+
<span class="ds-lib-tool-dot" data-tone={tone} class:is-live={!settled && running}></span>
|
|
45
|
+
<span class="ds-lib-tool-said">
|
|
46
|
+
<span class="ds-lib-tool-verb">{said.verb}</span>
|
|
47
|
+
{#if said.object}<span class="ds-lib-tool-object">{said.object}</span>{/if}
|
|
48
|
+
{#if repeats > 1}<span>· {repeats} pages</span>{/if}
|
|
56
49
|
</span>
|
|
57
50
|
</button>
|
|
58
51
|
|
|
59
52
|
{#if open}
|
|
60
|
-
<div class="
|
|
53
|
+
<div class="ds-lib-tool-body">
|
|
61
54
|
{#if block.result}
|
|
62
|
-
<pre
|
|
63
|
-
class="text-muted-foreground mt-1 max-h-72 overflow-auto font-mono text-xs whitespace-pre-wrap">{block.result}</pre>
|
|
55
|
+
<pre class="ds-lib-tool-result">{block.result}</pre>
|
|
64
56
|
{/if}
|
|
65
57
|
</div>
|
|
66
58
|
{:else if settled && lines > 0}
|
|
67
|
-
<p class="
|
|
68
|
-
{lines === 1 ? '1 line' : `${lines} lines`}
|
|
69
|
-
</p>
|
|
59
|
+
<p class="ds-lib-tool-lines">{lines === 1 ? '1 line' : `${lines} lines`}</p>
|
|
70
60
|
{/if}
|
|
71
61
|
</div>
|
|
62
|
+
|
|
63
|
+
<style>
|
|
64
|
+
.ds-lib-tool {
|
|
65
|
+
font-size: 0.875rem;
|
|
66
|
+
line-height: 1.25rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.ds-lib-tool-row {
|
|
70
|
+
display: flex;
|
|
71
|
+
width: 100%;
|
|
72
|
+
align-items: center;
|
|
73
|
+
gap: 0.5rem;
|
|
74
|
+
padding-block: 0.125rem;
|
|
75
|
+
border: 0;
|
|
76
|
+
background: none;
|
|
77
|
+
color: inherit;
|
|
78
|
+
font: inherit;
|
|
79
|
+
text-align: start;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.ds-lib-tool-row:disabled {
|
|
84
|
+
cursor: default;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ds-lib-tool-chevron {
|
|
88
|
+
display: flex;
|
|
89
|
+
width: 0.75rem;
|
|
90
|
+
flex: none;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
color: var(--ds-color-muted-foreground);
|
|
94
|
+
transition: transform 150ms ease;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.ds-lib-tool-chevron.is-open {
|
|
98
|
+
transform: rotate(90deg);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.ds-lib-tool-dot {
|
|
102
|
+
width: 0.375rem;
|
|
103
|
+
height: 0.375rem;
|
|
104
|
+
flex: none;
|
|
105
|
+
border-radius: var(--ds-radius-full);
|
|
106
|
+
background: var(--ds-color-status-info);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.ds-lib-tool-dot[data-tone='error'] {
|
|
110
|
+
background: var(--ds-color-status-error);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.ds-lib-tool-dot[data-tone='success'] {
|
|
114
|
+
background: var(--ds-color-status-success);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.ds-lib-tool-dot.is-live {
|
|
118
|
+
animation: ds-lib-tool-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.ds-lib-tool-said {
|
|
122
|
+
min-width: 0;
|
|
123
|
+
overflow: hidden;
|
|
124
|
+
text-overflow: ellipsis;
|
|
125
|
+
white-space: nowrap;
|
|
126
|
+
color: var(--ds-color-muted-foreground);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.ds-lib-tool-verb {
|
|
130
|
+
color: var(--ds-color-foreground);
|
|
131
|
+
font-weight: 500;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.ds-lib-tool-object {
|
|
135
|
+
color: color-mix(in oklab, var(--ds-color-foreground) 80%, transparent);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.ds-lib-tool-body {
|
|
139
|
+
margin-top: 0.25rem;
|
|
140
|
+
margin-inline-start: 1.375rem;
|
|
141
|
+
border-inline-start: 1px solid var(--ds-color-border);
|
|
142
|
+
padding-inline-start: 0.75rem;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.ds-lib-tool-result {
|
|
146
|
+
margin-top: 0.25rem;
|
|
147
|
+
max-height: 18rem;
|
|
148
|
+
overflow: auto;
|
|
149
|
+
color: var(--ds-color-muted-foreground);
|
|
150
|
+
font-family: var(--ds-font-code);
|
|
151
|
+
font-size: var(--ds-text-2xs);
|
|
152
|
+
white-space: pre-wrap;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.ds-lib-tool-lines {
|
|
156
|
+
margin: 0;
|
|
157
|
+
padding-inline-start: 1.375rem;
|
|
158
|
+
color: var(--ds-color-muted-foreground);
|
|
159
|
+
font-size: var(--ds-text-2xs);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@keyframes ds-lib-tool-pulse {
|
|
163
|
+
50% {
|
|
164
|
+
opacity: 0.4;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@media (prefers-reduced-motion: reduce) {
|
|
169
|
+
.ds-lib-tool-dot.is-live {
|
|
170
|
+
animation: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.ds-lib-tool-chevron {
|
|
174
|
+
transition: none;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
</style>
|
|
@@ -8,17 +8,22 @@
|
|
|
8
8
|
|
|
9
9
|
Two moving parts, deliberately: a word that changes so the page is evidently
|
|
10
10
|
alive, and a clock that only goes up so a long wait still reads as progress
|
|
11
|
-
rather than as a hang.
|
|
11
|
+
rather than as a hang. The words name whoever is answering — they came from
|
|
12
|
+
`copy`, which the host's `name` composed, so this component knows no persona.
|
|
12
13
|
-->
|
|
13
14
|
<script lang="ts">
|
|
15
|
+
import { resolveCopy, type LibrarianCopy } from '../../copy';
|
|
16
|
+
|
|
14
17
|
interface Props {
|
|
15
18
|
/** Shown instead of the cycling word once real work is identifiable. */
|
|
16
19
|
label?: string;
|
|
20
|
+
/** The persona's words. Resolved from `name` upstream. */
|
|
21
|
+
copy?: Partial<LibrarianCopy>;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
let { label }: Props = $props();
|
|
24
|
+
let { label, copy }: Props = $props();
|
|
20
25
|
|
|
21
|
-
const
|
|
26
|
+
const words = $derived(resolveCopy(copy));
|
|
22
27
|
|
|
23
28
|
let tick = $state(0);
|
|
24
29
|
let elapsed = $state(0);
|
|
@@ -32,11 +37,50 @@
|
|
|
32
37
|
};
|
|
33
38
|
});
|
|
34
39
|
|
|
35
|
-
const word = $derived(label ??
|
|
40
|
+
const word = $derived(label ?? words.working[tick % words.working.length]);
|
|
36
41
|
</script>
|
|
37
42
|
|
|
38
|
-
<div class="
|
|
39
|
-
<span class="
|
|
43
|
+
<div class="ds-lib-working">
|
|
44
|
+
<span class="ds-lib-working-dot"></span>
|
|
40
45
|
<span>{word}…</span>
|
|
41
|
-
<span class="
|
|
46
|
+
<span class="ds-lib-working-clock">{elapsed}s</span>
|
|
42
47
|
</div>
|
|
48
|
+
|
|
49
|
+
<style>
|
|
50
|
+
.ds-lib-working {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 0.5rem;
|
|
54
|
+
color: var(--ds-color-muted-foreground);
|
|
55
|
+
font-size: 0.875rem;
|
|
56
|
+
line-height: 1.25rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.ds-lib-working-dot {
|
|
60
|
+
width: 0.375rem;
|
|
61
|
+
height: 0.375rem;
|
|
62
|
+
flex: none;
|
|
63
|
+
border-radius: var(--ds-radius-full);
|
|
64
|
+
background: var(--ds-color-status-info);
|
|
65
|
+
animation: ds-lib-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.ds-lib-working-clock {
|
|
69
|
+
font-family: var(--ds-font-code);
|
|
70
|
+
font-size: var(--ds-text-2xs);
|
|
71
|
+
font-variant-numeric: tabular-nums;
|
|
72
|
+
opacity: 0.7;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@keyframes ds-lib-pulse {
|
|
76
|
+
50% {
|
|
77
|
+
opacity: 0.4;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@media (prefers-reduced-motion: reduce) {
|
|
82
|
+
.ds-lib-working-dot {
|
|
83
|
+
animation: none;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
</style>
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { type LibrarianCopy } from '../../copy';
|
|
1
2
|
interface Props {
|
|
2
3
|
/** Shown instead of the cycling word once real work is identifiable. */
|
|
3
4
|
label?: string;
|
|
5
|
+
/** The persona's words. Resolved from `name` upstream. */
|
|
6
|
+
copy?: Partial<LibrarianCopy>;
|
|
4
7
|
}
|
|
5
8
|
declare const Working: import("svelte").Component<Props, {}, "">;
|
|
6
9
|
type Working = ReturnType<typeof Working>;
|
package/dist/copy.d.ts
CHANGED
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
* Every word this package puts in front of a reader, in one place.
|
|
3
3
|
*
|
|
4
4
|
* Here rather than inline in each component because these sentences are the
|
|
5
|
-
* package's opinion about how
|
|
6
|
-
* needs a different register — a
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* package's opinion about how an agent talks to a colleague, and an app that
|
|
6
|
+
* needs a different register — a room whose boundary is worded by its own
|
|
7
|
+
* subject-matter owner — must be able to change one string without forking a
|
|
8
|
+
* component.
|
|
9
|
+
*
|
|
10
|
+
* NO STRING HERE NAMES A PERSONA. The name is an argument: a consuming app
|
|
11
|
+
* says who is speaking once, and every sentence the package renders is built
|
|
12
|
+
* from that. Milton is the library's own persona and therefore the default,
|
|
13
|
+
* which is a different thing from being baked in — a package that hardcodes
|
|
14
|
+
* one app's persona is a package that lies in every other app, and did:
|
|
15
|
+
* Pebblestone's Penny introduced herself as Milton for a fortnight.
|
|
9
16
|
*
|
|
10
17
|
* Every component takes `copy` as a PARTIAL and resolves it itself, so a host
|
|
11
|
-
* overriding one line does not have to restate the
|
|
12
|
-
*
|
|
13
|
-
* `Conversation`.
|
|
18
|
+
* overriding one line does not have to restate the others, and a component
|
|
19
|
+
* used directly behaves the same as one reached through `Conversation`.
|
|
14
20
|
*/
|
|
21
|
+
/** The library's own persona: the default wherever a consumer names none. */
|
|
22
|
+
export declare const DEFAULT_PERSONA = "Milton";
|
|
15
23
|
export interface LibrarianCopy {
|
|
16
24
|
/** Prefixes the date on a verified source: "verified 23 Jun 2026". */
|
|
17
25
|
verified: string;
|
|
@@ -21,12 +29,23 @@ export interface LibrarianCopy {
|
|
|
21
29
|
notHeld: string;
|
|
22
30
|
/** Shown when the run itself ended in error and said nothing about why. */
|
|
23
31
|
answerFailed: string;
|
|
32
|
+
/** Shown when the stream never opened, or died mid-answer. */
|
|
33
|
+
unreachable: string;
|
|
24
34
|
/** Heading over the list of what an answer cited. */
|
|
25
35
|
sources: string;
|
|
26
|
-
/** Heading over the follow-up questions the
|
|
36
|
+
/** Heading over the follow-up questions the persona offered. */
|
|
27
37
|
suggestions: string;
|
|
28
38
|
/** The collapsed scope statement's own label. */
|
|
29
39
|
scope: string;
|
|
40
|
+
/** The empty transcript's opening line. */
|
|
41
|
+
welcome: string;
|
|
42
|
+
/** The composer's placeholder, idle and while an answer streams. */
|
|
43
|
+
askPlaceholder: string;
|
|
44
|
+
answeringPlaceholder: string;
|
|
45
|
+
/** The pre-first-token words, cycled one every 2.6s. */
|
|
46
|
+
working: string[];
|
|
47
|
+
/** The scroll region's accessible name. */
|
|
48
|
+
conversationLabel: string;
|
|
30
49
|
copyAnswer: string;
|
|
31
50
|
copiedAnswer: string;
|
|
32
51
|
askAgain: string;
|
|
@@ -34,13 +53,29 @@ export interface LibrarianCopy {
|
|
|
34
53
|
closeSource: string;
|
|
35
54
|
documentUnavailable: string;
|
|
36
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The display name for a persona a host identified by its slug.
|
|
58
|
+
*
|
|
59
|
+
* A route parameter is `penny`, and a colleague reading the screen is talking
|
|
60
|
+
* to Penny. A host that already wrote the name the way it should read — any
|
|
61
|
+
* capital at all — is left alone, so "McTavish" and "eight" both survive.
|
|
62
|
+
*/
|
|
63
|
+
export declare function personaName(name: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* The package's words, spoken by one named persona.
|
|
66
|
+
*
|
|
67
|
+
* Deliberately free of pronouns: the persona is whoever the consuming app
|
|
68
|
+
* says it is, and a sentence that guesses is wrong for half of them.
|
|
69
|
+
*/
|
|
70
|
+
export declare function copyFor(name?: string): LibrarianCopy;
|
|
71
|
+
/** The library's own words, for a host that names no persona. */
|
|
37
72
|
export declare const DEFAULT_COPY: LibrarianCopy;
|
|
38
73
|
/**
|
|
39
|
-
*
|
|
74
|
+
* One persona's words with a host's overrides on top.
|
|
40
75
|
*
|
|
41
76
|
* An explicitly `undefined` key is dropped rather than spread, because
|
|
42
77
|
* `{...defaults, ...{ sources: undefined }}` leaves a component rendering
|
|
43
78
|
* nothing at all — and that is exactly the shape a host produces by passing a
|
|
44
79
|
* value it computed from state that has not loaded yet.
|
|
45
80
|
*/
|
|
46
|
-
export declare function resolveCopy(overrides?: Partial<LibrarianCopy
|
|
81
|
+
export declare function resolveCopy(overrides?: Partial<LibrarianCopy>, name?: string): LibrarianCopy;
|