@poodle64/librarian 2026.9.11 → 2026.9.12
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 +5 -3
- package/dist/client.d.ts +5 -0
- package/dist/client.js +4 -1
- package/dist/components/agent-transcript/agent-transcript.svelte +24 -6
- package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +8 -0
- package/dist/components/artefact-card/artefact-card.svelte +40 -0
- package/dist/components/artefact-card/artefact-card.svelte.d.ts +8 -0
- package/dist/components/artefact-card/index.d.ts +1 -0
- package/dist/components/artefact-card/index.js +1 -0
- package/dist/components/artefact-pane/artefact-pane.svelte +120 -0
- package/dist/components/artefact-pane/artefact-pane.svelte.d.ts +15 -0
- package/dist/components/artefact-pane/index.d.ts +1 -0
- package/dist/components/artefact-pane/index.js +1 -0
- package/dist/components/composer/composer.svelte +20 -1
- package/dist/components/composer/composer.svelte.d.ts +4 -0
- package/dist/components/conversation/conversation.svelte +26 -5
- package/dist/transcript.svelte.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,8 @@ src/lib/
|
|
|
24
24
|
scope-statement/ what this room answers from, and what it does not hold
|
|
25
25
|
agent-transcript/ one question and everything Milton did answering it
|
|
26
26
|
document-pane/ the cited document, open at the cited passage
|
|
27
|
+
artefact-card/ a study artefact's card in the transcript
|
|
28
|
+
artefact-pane/ a study artefact, open in the reading column
|
|
27
29
|
composer/ the input box: attachments, scope chips, send/stop
|
|
28
30
|
markdown/ sanitised, streaming-safe markdown + highlighting
|
|
29
31
|
activity-group/ a whole investigation, as one quiet line
|
|
@@ -153,14 +155,14 @@ on it fires once and never again.
|
|
|
153
155
|
|
|
154
156
|
| Concern | How |
|
|
155
157
|
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
156
|
-
| The ask route | `ask({ endpoint })`: a room's `/api/rooms/{id}/ask`, a caller's `/api/caller/ask`
|
|
158
|
+
| The ask route | `ask({ endpoint })`: a room's `/api/rooms/{id}/ask`, a caller's `/api/caller/ask` |
|
|
157
159
|
| Attachments | nothing: `ask()` posts multipart (`question`, `resume`, `collections[]`, `files[]`) whenever `files` is non-empty, and JSON when it is not. The route must accept both |
|
|
158
160
|
| Reading a cited document | `loadDocument(document_id) => Promise<{title, sections: [{anchor, heading, text}]}>`, proxied through the app's own authenticated route (cadmus: `GET /api/sources/documents/{id}/content`) |
|
|
159
|
-
| Asking again | `onregenerate`: re-send the last question as a NEW turn; the package exposes the action and never re-asks by itself
|
|
161
|
+
| Asking again | `onregenerate`: re-send the last question as a NEW turn; the package exposes the action and never re-asks by itself |
|
|
160
162
|
| Asking a follow-up | `onsuggest(question)`: ask it as a NEW turn. Without the handler the chips do not render at all — a chip that does nothing is worse than no chip |
|
|
161
163
|
| The empty state | `welcome` and up to three `examples` |
|
|
162
164
|
| What this surface covers | `scope`: one statement per room, in the host's own words. Rendered above the first turn and folded to a line once the conversation starts |
|
|
163
|
-
| The words themselves | `copy`: a partial of `LibrarianCopy`. Every component resolves it itself, so overriding one line does not mean restating the rest
|
|
165
|
+
| The words themselves | `copy`: a partial of `LibrarianCopy`. Every component resolves it itself, so overriding one line does not mean restating the rest |
|
|
164
166
|
|
|
165
167
|
Nothing here fetches on its own behalf. The library's document read is
|
|
166
168
|
authenticated, and a package that called it directly would be reaching past
|
package/dist/client.d.ts
CHANGED
|
@@ -57,6 +57,11 @@ export interface AskOptions {
|
|
|
57
57
|
collections?: string[];
|
|
58
58
|
/** Files Milton reads for this question. Switches the request to multipart. */
|
|
59
59
|
files?: File[];
|
|
60
|
+
/** What kind of turn this ask should become. Absent means an ordinary
|
|
61
|
+
* question; a host that offers a study artefact sends its own kind
|
|
62
|
+
* (`'briefing'`) and interprets what comes back accordingly — the wire
|
|
63
|
+
* vocabulary is the host's own, never a fixed set here. */
|
|
64
|
+
kind?: string;
|
|
60
65
|
signal?: AbortSignal;
|
|
61
66
|
/** Where the ask lands. Each app mounts its own ask route. */
|
|
62
67
|
endpoint?: string;
|
package/dist/client.js
CHANGED
|
@@ -29,7 +29,8 @@ function requestInit(options, signal) {
|
|
|
29
29
|
question: options.question,
|
|
30
30
|
resume: options.resume ?? null,
|
|
31
31
|
subtree: options.subtree ?? '',
|
|
32
|
-
collections: options.collections ?? []
|
|
32
|
+
collections: options.collections ?? [],
|
|
33
|
+
kind: options.kind
|
|
33
34
|
}),
|
|
34
35
|
signal
|
|
35
36
|
};
|
|
@@ -42,6 +43,8 @@ function requestInit(options, signal) {
|
|
|
42
43
|
form.append('collections[]', collection);
|
|
43
44
|
for (const file of options.files)
|
|
44
45
|
form.append('files[]', file, file.name);
|
|
46
|
+
if (options.kind)
|
|
47
|
+
form.append('kind', options.kind);
|
|
45
48
|
return { method: 'POST', credentials: 'include', body: form, signal };
|
|
46
49
|
}
|
|
47
50
|
/** Async-iterate the events of one question. */
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { resolveCopy, type LibrarianCopy } from '../../copy';
|
|
29
29
|
import Working from '../working/working.svelte';
|
|
30
30
|
import ActivityGroup from '../activity-group/activity-group.svelte';
|
|
31
|
+
import ArtefactCard from '../artefact-card/artefact-card.svelte';
|
|
31
32
|
import Markdown from '../markdown/markdown.svelte';
|
|
32
33
|
|
|
33
34
|
interface Props {
|
|
@@ -51,6 +52,14 @@
|
|
|
51
52
|
* no chip. */
|
|
52
53
|
onsuggest?: (question: string) => void;
|
|
53
54
|
copy?: Partial<LibrarianCopy>;
|
|
55
|
+
/** A study artefact rather than an ordinary answer: once settled, this
|
|
56
|
+
* renders as a card instead of prose. */
|
|
57
|
+
kind?: 'answer' | 'artefact';
|
|
58
|
+
/** The artefact's own name, for the card. */
|
|
59
|
+
title?: string;
|
|
60
|
+
/** Opens the artefact in the reading column. Required wherever `kind`
|
|
61
|
+
* is `'artefact'`. */
|
|
62
|
+
onopenartefact?: () => void;
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
let {
|
|
@@ -64,9 +73,14 @@
|
|
|
64
73
|
oncite,
|
|
65
74
|
onregenerate,
|
|
66
75
|
onsuggest,
|
|
67
|
-
copy
|
|
76
|
+
copy,
|
|
77
|
+
kind = 'answer',
|
|
78
|
+
title,
|
|
79
|
+
onopenartefact
|
|
68
80
|
}: Props = $props();
|
|
69
81
|
|
|
82
|
+
const isArtefact = $derived(kind === 'artefact');
|
|
83
|
+
|
|
70
84
|
const words = $derived(resolveCopy(copy));
|
|
71
85
|
|
|
72
86
|
const asked = $derived(readerQuestion(question));
|
|
@@ -181,7 +195,7 @@
|
|
|
181
195
|
{#each segments as seg (seg.index)}
|
|
182
196
|
{#if seg.kind === 'activity'}
|
|
183
197
|
<ActivityGroup group={seg} live={running && seg.index === lastIndex} />
|
|
184
|
-
{:else}
|
|
198
|
+
{:else if !isArtefact}
|
|
185
199
|
<div class="max-w-[72ch] min-w-0">
|
|
186
200
|
<Markdown
|
|
187
201
|
content={seg.index === lastTextIndex && split.citations.length > 0
|
|
@@ -199,7 +213,11 @@
|
|
|
199
213
|
{/if}
|
|
200
214
|
{/each}
|
|
201
215
|
|
|
202
|
-
{#if
|
|
216
|
+
{#if isArtefact && settled}
|
|
217
|
+
<ArtefactCard title={title ?? 'Briefing'} citationCount={sources.length} onopen={() => onopenartefact?.()} />
|
|
218
|
+
{/if}
|
|
219
|
+
|
|
220
|
+
{#if !isArtefact && sources.length > 0}
|
|
203
221
|
<section class="max-w-[72ch]">
|
|
204
222
|
<h2 class="text-muted-foreground mb-1.5 text-xs font-medium tracking-wide uppercase">
|
|
205
223
|
{words.sources}
|
|
@@ -237,11 +255,11 @@
|
|
|
237
255
|
</section>
|
|
238
256
|
{/if}
|
|
239
257
|
|
|
240
|
-
{#if notHeld}
|
|
258
|
+
{#if !isArtefact && notHeld}
|
|
241
259
|
<p class="text-muted-foreground max-w-[72ch] text-sm">{words.notHeld}</p>
|
|
242
260
|
{/if}
|
|
243
261
|
|
|
244
|
-
{#if followUps.length > 0}
|
|
262
|
+
{#if !isArtefact && followUps.length > 0}
|
|
245
263
|
<section class="max-w-[72ch]">
|
|
246
264
|
<h2 class="text-muted-foreground mb-1.5 text-xs font-medium tracking-wide uppercase">
|
|
247
265
|
{words.suggestions}
|
|
@@ -276,7 +294,7 @@
|
|
|
276
294
|
</p>
|
|
277
295
|
{/if}
|
|
278
296
|
|
|
279
|
-
{#if settled}
|
|
297
|
+
{#if !isArtefact && settled}
|
|
280
298
|
<div class="text-muted-foreground -ml-1.5 flex items-center gap-1">
|
|
281
299
|
{#if hasAnswer}
|
|
282
300
|
<button
|
|
@@ -22,6 +22,14 @@ interface Props {
|
|
|
22
22
|
* no chip. */
|
|
23
23
|
onsuggest?: (question: string) => void;
|
|
24
24
|
copy?: Partial<LibrarianCopy>;
|
|
25
|
+
/** A study artefact rather than an ordinary answer: once settled, this
|
|
26
|
+
* renders as a card instead of prose. */
|
|
27
|
+
kind?: 'answer' | 'artefact';
|
|
28
|
+
/** The artefact's own name, for the card. */
|
|
29
|
+
title?: string;
|
|
30
|
+
/** Opens the artefact in the reading column. Required wherever `kind`
|
|
31
|
+
* is `'artefact'`. */
|
|
32
|
+
onopenartefact?: () => void;
|
|
25
33
|
}
|
|
26
34
|
declare const AgentTranscript: import("svelte").Component<Props, {}, "">;
|
|
27
35
|
type AgentTranscript = ReturnType<typeof AgentTranscript>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
A study artefact's card in the transcript: what a colleague taps to read it.
|
|
3
|
+
It is not the artefact itself — that lives in the reading column
|
|
4
|
+
(`ArtefactPane`), never stacked beside this card and never inline as prose,
|
|
5
|
+
which is the whole of the one-surface decision this package exists to keep
|
|
6
|
+
every consumer honest about.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import FileTextIcon from '@lucide/svelte/icons/file-text';
|
|
10
|
+
import Panel from '@poodle64/ui/panel';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
title: string;
|
|
14
|
+
citationCount: number;
|
|
15
|
+
onopen: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let { title, citationCount, onopen }: Props = $props();
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<Panel
|
|
22
|
+
icon={FileTextIcon}
|
|
23
|
+
{title}
|
|
24
|
+
subtitle="Briefing doc"
|
|
25
|
+
role="button"
|
|
26
|
+
tabindex={0}
|
|
27
|
+
onclick={onopen}
|
|
28
|
+
onkeydown={(event: KeyboardEvent) => {
|
|
29
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
onopen();
|
|
32
|
+
}}
|
|
33
|
+
class="hover:border-border-strong focus-visible:ring-ring w-full max-w-[28rem] cursor-pointer text-left transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
34
|
+
>
|
|
35
|
+
{#snippet children()}
|
|
36
|
+
<p class="text-muted-foreground text-xs">
|
|
37
|
+
{citationCount} source{citationCount === 1 ? '' : 's'}
|
|
38
|
+
</p>
|
|
39
|
+
{/snippet}
|
|
40
|
+
</Panel>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './artefact-card.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './artefact-card.svelte';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
A study artefact, open in the reading column.
|
|
3
|
+
|
|
4
|
+
The same shell shape `DocumentPane` uses (a column at `lg`, a bottom sheet
|
|
5
|
+
below it) because decision 2 of the ask-milton surface is that a colleague
|
|
6
|
+
never has to learn which of the two things is in the column before they
|
|
7
|
+
open it — only ever one, in the same place, closing whichever was there.
|
|
8
|
+
-->
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import XIcon from '@lucide/svelte/icons/x';
|
|
11
|
+
import { segment, type TextBlock, type Turn } from '../../transcript.svelte';
|
|
12
|
+
import { resolveCitations, splitSources, trustMark, type Citation } from '../../citations';
|
|
13
|
+
import { resolveCopy, type LibrarianCopy } from '../../copy';
|
|
14
|
+
import Markdown from '../markdown/markdown.svelte';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
turn: Turn;
|
|
18
|
+
onclose: () => void;
|
|
19
|
+
/** Opens the cited document instead — swapping the column back, never
|
|
20
|
+
* stacking it beside the artefact. Omit and the sources still list, but
|
|
21
|
+
* do not open. */
|
|
22
|
+
oncite?: (citation: Citation) => void;
|
|
23
|
+
copy?: Partial<LibrarianCopy>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let { turn, onclose, oncite, copy }: Props = $props();
|
|
27
|
+
|
|
28
|
+
const words = $derived(resolveCopy(copy));
|
|
29
|
+
|
|
30
|
+
const texts = $derived(segment(turn.blocks).filter((s): s is TextBlock => s.kind === 'text'));
|
|
31
|
+
const rawAnswer = $derived(texts.map((t) => t.text).join('\n\n'));
|
|
32
|
+
const split = $derived(splitSources(rawAnswer));
|
|
33
|
+
const sources = $derived(resolveCitations(turn.citations ?? [], split.citations));
|
|
34
|
+
|
|
35
|
+
let closeButton = $state<HTMLButtonElement | null>(null);
|
|
36
|
+
|
|
37
|
+
// Opening a pane a reader reached with the keyboard must move focus into
|
|
38
|
+
// it, or Escape and Tab both act on the transcript behind it.
|
|
39
|
+
$effect(() => {
|
|
40
|
+
const returnTo = globalThis.document?.activeElement as HTMLElement | null;
|
|
41
|
+
closeButton?.focus();
|
|
42
|
+
return () => returnTo?.focus?.();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function keydown(event: KeyboardEvent) {
|
|
46
|
+
if (event.key === 'Escape') {
|
|
47
|
+
event.stopPropagation();
|
|
48
|
+
onclose();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<svelte:window onkeydown={keydown} />
|
|
54
|
+
|
|
55
|
+
<aside
|
|
56
|
+
aria-label="Study artefact"
|
|
57
|
+
class="bg-surface-1 border-border fixed inset-x-0 bottom-0 z-40 flex h-[80svh] flex-col rounded-t-2xl border-t shadow-2xl lg:relative lg:h-auto lg:w-[560px] lg:shrink-0 lg:rounded-none lg:border-t-0 lg:border-l lg:shadow-none"
|
|
58
|
+
>
|
|
59
|
+
<!-- The sheet's grabber. Below `lg` this is an overlay a reader has to be
|
|
60
|
+
able to see the top edge of; on a desktop it is a column, so there is
|
|
61
|
+
nothing here to grab. -->
|
|
62
|
+
<div class="flex justify-center pt-2 pb-1 lg:hidden" aria-hidden="true">
|
|
63
|
+
<span class="bg-border h-1 w-9 rounded-full"></span>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<header class="border-border flex items-start gap-2 border-b px-4 py-3 lg:pt-3">
|
|
67
|
+
<div class="min-w-0 flex-1">
|
|
68
|
+
<h2 class="text-foreground truncate text-sm font-semibold">{turn.title ?? 'Briefing'}</h2>
|
|
69
|
+
<p class="text-muted-foreground truncate text-xs">
|
|
70
|
+
Briefing doc · {sources.length} source{sources.length === 1 ? '' : 's'}
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
<button
|
|
74
|
+
bind:this={closeButton}
|
|
75
|
+
type="button"
|
|
76
|
+
onclick={onclose}
|
|
77
|
+
aria-label={words.closeSource}
|
|
78
|
+
class="text-muted-foreground hover:text-foreground hover:bg-surface-2 focus-visible:ring-ring -mt-1 flex size-8 shrink-0 items-center justify-center rounded-lg transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
79
|
+
>
|
|
80
|
+
<XIcon class="size-4" />
|
|
81
|
+
</button>
|
|
82
|
+
</header>
|
|
83
|
+
|
|
84
|
+
<div class="min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-3">
|
|
85
|
+
<Markdown content={split.citations.length > 0 ? split.body : rawAnswer} />
|
|
86
|
+
|
|
87
|
+
{#if sources.length > 0}
|
|
88
|
+
<section class="mt-6">
|
|
89
|
+
<h3 class="text-muted-foreground mb-1.5 text-xs font-medium tracking-wide uppercase">
|
|
90
|
+
{words.sources}
|
|
91
|
+
</h3>
|
|
92
|
+
<ol class="flex flex-col gap-1">
|
|
93
|
+
{#each sources as source (source.n)}
|
|
94
|
+
{@const mark = trustMark(source, words)}
|
|
95
|
+
<li>
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onclick={() => oncite?.(source)}
|
|
99
|
+
disabled={!source.document_id || !oncite}
|
|
100
|
+
class="border-border hover:border-border-strong hover:bg-surface-2 focus-visible:ring-ring flex w-full items-baseline gap-2 rounded-lg border px-2.5 py-1.5 text-left text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:cursor-default disabled:hover:bg-transparent"
|
|
101
|
+
>
|
|
102
|
+
<span
|
|
103
|
+
class="bg-primary/15 text-foreground shrink-0 rounded px-1.5 font-mono text-xs tabular-nums"
|
|
104
|
+
>{source.n}</span
|
|
105
|
+
>
|
|
106
|
+
<span class="min-w-0">
|
|
107
|
+
<span class="text-foreground">{source.title}</span>
|
|
108
|
+
{#if source.section}<span class="text-muted-foreground">
|
|
109
|
+
· {source.section}</span
|
|
110
|
+
>{/if}
|
|
111
|
+
{#if mark}<span class="text-muted-foreground/80"> · {mark}</span>{/if}
|
|
112
|
+
</span>
|
|
113
|
+
</button>
|
|
114
|
+
</li>
|
|
115
|
+
{/each}
|
|
116
|
+
</ol>
|
|
117
|
+
</section>
|
|
118
|
+
{/if}
|
|
119
|
+
</div>
|
|
120
|
+
</aside>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Turn } from '../../transcript.svelte';
|
|
2
|
+
import { type Citation } from '../../citations';
|
|
3
|
+
import { type LibrarianCopy } from '../../copy';
|
|
4
|
+
interface Props {
|
|
5
|
+
turn: Turn;
|
|
6
|
+
onclose: () => void;
|
|
7
|
+
/** Opens the cited document instead — swapping the column back, never
|
|
8
|
+
* stacking it beside the artefact. Omit and the sources still list, but
|
|
9
|
+
* do not open. */
|
|
10
|
+
oncite?: (citation: Citation) => void;
|
|
11
|
+
copy?: Partial<LibrarianCopy>;
|
|
12
|
+
}
|
|
13
|
+
declare const ArtefactPane: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type ArtefactPane = ReturnType<typeof ArtefactPane>;
|
|
15
|
+
export default ArtefactPane;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './artefact-pane.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './artefact-pane.svelte';
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
-->
|
|
13
13
|
<script lang="ts">
|
|
14
14
|
import ArrowUpIcon from '@lucide/svelte/icons/arrow-up';
|
|
15
|
+
import FileTextIcon from '@lucide/svelte/icons/file-text';
|
|
15
16
|
import PaperclipIcon from '@lucide/svelte/icons/paperclip';
|
|
16
17
|
import SquareIcon from '@lucide/svelte/icons/square';
|
|
17
18
|
import XIcon from '@lucide/svelte/icons/x';
|
|
@@ -39,6 +40,10 @@
|
|
|
39
40
|
onscope: (scope: Scope) => void;
|
|
40
41
|
onsubmit: () => void;
|
|
41
42
|
onstop: () => void;
|
|
43
|
+
/** Asks for a study artefact instead of an ordinary answer. Omit and no
|
|
44
|
+
* control renders — studying is a mode of asking, never a tab, so a
|
|
45
|
+
* host with nothing to build offers none rather than a disabled one. */
|
|
46
|
+
onbriefing?: () => void;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
let {
|
|
@@ -51,7 +56,8 @@
|
|
|
51
56
|
attachments = true,
|
|
52
57
|
onscope,
|
|
53
58
|
onsubmit,
|
|
54
|
-
onstop
|
|
59
|
+
onstop,
|
|
60
|
+
onbriefing
|
|
55
61
|
}: Props = $props();
|
|
56
62
|
|
|
57
63
|
// What Milton is being asked about — this document, this collection, or
|
|
@@ -235,6 +241,19 @@
|
|
|
235
241
|
</button>
|
|
236
242
|
{/if}
|
|
237
243
|
|
|
244
|
+
{#if onbriefing}
|
|
245
|
+
<button
|
|
246
|
+
type="button"
|
|
247
|
+
onclick={onbriefing}
|
|
248
|
+
disabled={running}
|
|
249
|
+
aria-label="Ask for a briefing"
|
|
250
|
+
title="Ask for a briefing"
|
|
251
|
+
class="text-muted-foreground hover:text-foreground hover:bg-surface-2 focus-visible:ring-ring flex size-8 shrink-0 items-center justify-center rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:opacity-30"
|
|
252
|
+
>
|
|
253
|
+
<FileTextIcon class="size-4" />
|
|
254
|
+
</button>
|
|
255
|
+
{/if}
|
|
256
|
+
|
|
238
257
|
{#if hasChoice}
|
|
239
258
|
<div class="flex min-w-0 flex-wrap items-center gap-1">
|
|
240
259
|
{#each choices as choice (choice.id)}
|
|
@@ -13,6 +13,10 @@ interface Props {
|
|
|
13
13
|
onscope: (scope: Scope) => void;
|
|
14
14
|
onsubmit: () => void;
|
|
15
15
|
onstop: () => void;
|
|
16
|
+
/** Asks for a study artefact instead of an ordinary answer. Omit and no
|
|
17
|
+
* control renders — studying is a mode of asking, never a tab, so a
|
|
18
|
+
* host with nothing to build offers none rather than a disabled one. */
|
|
19
|
+
onbriefing?: () => void;
|
|
16
20
|
}
|
|
17
21
|
declare const Composer: import("svelte").Component<Props, {}, "value" | "files">;
|
|
18
22
|
type Composer = ReturnType<typeof Composer>;
|
|
@@ -16,9 +16,15 @@
|
|
|
16
16
|
import { resolveCopy, type LibrarianCopy } from '../../copy';
|
|
17
17
|
import { FollowScroll } from '../../follow-scroll.svelte';
|
|
18
18
|
import AgentTranscript from '../agent-transcript/agent-transcript.svelte';
|
|
19
|
+
import ArtefactPane from '../artefact-pane/artefact-pane.svelte';
|
|
19
20
|
import DocumentPane from '../document-pane/document-pane.svelte';
|
|
20
21
|
import ScopeStatement from '../scope-statement/scope-statement.svelte';
|
|
21
22
|
|
|
23
|
+
/** What the reading column holds — a cited document or a study artefact,
|
|
24
|
+
* never both and never two panes: whichever the reader last opened
|
|
25
|
+
* replaces whatever was there. */
|
|
26
|
+
type Column = { kind: 'document'; citation: Citation } | { kind: 'artefact'; turn: Turn } | null;
|
|
27
|
+
|
|
22
28
|
interface Props {
|
|
23
29
|
turns: Turn[];
|
|
24
30
|
running: boolean;
|
|
@@ -68,7 +74,7 @@
|
|
|
68
74
|
const words = $derived(resolveCopy(copy));
|
|
69
75
|
|
|
70
76
|
let viewport = $state<HTMLElement | null>(null);
|
|
71
|
-
let
|
|
77
|
+
let column = $state<Column>(null);
|
|
72
78
|
const follow = new FollowScroll();
|
|
73
79
|
|
|
74
80
|
function metrics(el: HTMLElement) {
|
|
@@ -113,8 +119,13 @@
|
|
|
113
119
|
|
|
114
120
|
function cite(citation: Citation) {
|
|
115
121
|
// A citation derived from the prose has no id to read, so its chip is
|
|
116
|
-
// legible but inert rather than opening an empty pane.
|
|
117
|
-
|
|
122
|
+
// legible but inert rather than opening an empty pane. Replaces
|
|
123
|
+
// whatever the column held, artefact included — never a second pane.
|
|
124
|
+
if (loadDocument && citation.document_id) column = { kind: 'document', citation };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function openArtefact(turn: Turn) {
|
|
128
|
+
column = { kind: 'artefact', turn };
|
|
118
129
|
}
|
|
119
130
|
</script>
|
|
120
131
|
|
|
@@ -173,6 +184,9 @@
|
|
|
173
184
|
suggestions={turn.suggestions ?? []}
|
|
174
185
|
{collectionNames}
|
|
175
186
|
{copy}
|
|
187
|
+
kind={turn.kind}
|
|
188
|
+
title={turn.title}
|
|
189
|
+
onopenartefact={() => openArtefact(turn)}
|
|
176
190
|
oncite={cite}
|
|
177
191
|
onregenerate={index === turns.length - 1 && !running ? onregenerate : undefined}
|
|
178
192
|
onsuggest={index === turns.length - 1 && !running ? onsuggest : undefined}
|
|
@@ -201,7 +215,14 @@
|
|
|
201
215
|
|
|
202
216
|
</div>
|
|
203
217
|
|
|
204
|
-
{#if
|
|
205
|
-
<DocumentPane citation={
|
|
218
|
+
{#if column?.kind === 'document' && loadDocument}
|
|
219
|
+
<DocumentPane citation={column.citation} {loadDocument} {copy} onclose={() => (column = null)} />
|
|
220
|
+
{:else if column?.kind === 'artefact'}
|
|
221
|
+
<ArtefactPane
|
|
222
|
+
turn={column.turn}
|
|
223
|
+
{copy}
|
|
224
|
+
onclose={() => (column = null)}
|
|
225
|
+
oncite={(citation) => cite(citation)}
|
|
226
|
+
/>
|
|
206
227
|
{/if}
|
|
207
228
|
</div>
|
|
@@ -123,6 +123,12 @@ export interface Turn {
|
|
|
123
123
|
/** Follow-ups offered after this answer. Absent on a turn read back from
|
|
124
124
|
* history: they belonged to the moment it was asked. */
|
|
125
125
|
suggestions?: string[];
|
|
126
|
+
/** A study artefact rather than an ordinary answer: renders as a card once
|
|
127
|
+
* settled, never as prose, and opens the reading column instead of a
|
|
128
|
+
* citation. Absent (or `'answer'`) is every ordinary turn. */
|
|
129
|
+
kind?: 'answer' | 'artefact';
|
|
130
|
+
/** The artefact's own name. Only meaningful when `kind` is `'artefact'`. */
|
|
131
|
+
title?: string;
|
|
126
132
|
}
|
|
127
133
|
/**
|
|
128
134
|
* Fold a turn's flat block list into what a reader should actually see.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/librarian",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.12",
|
|
4
4
|
"description": "Milton's conversation surface as a consumable Svelte 5 package: the stream client, transcript state and chat components (transcript, composer, markdown) every household app renders instead of rebuilding.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|