@poodle64/librarian 2026.9.5 → 2026.9.7

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.
Files changed (34) hide show
  1. package/README.md +143 -44
  2. package/dist/attachments.d.ts +30 -0
  3. package/dist/attachments.js +80 -0
  4. package/dist/citations.d.ts +63 -0
  5. package/dist/citations.js +89 -0
  6. package/dist/client.d.ts +5 -0
  7. package/dist/client.js +39 -13
  8. package/dist/components/activity-group/activity-group.svelte +36 -44
  9. package/dist/components/activity-group/activity-group.svelte.d.ts +1 -1
  10. package/dist/components/agent-transcript/agent-transcript.svelte +166 -26
  11. package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +8 -0
  12. package/dist/components/composer/composer.svelte +212 -59
  13. package/dist/components/composer/composer.svelte.d.ts +5 -1
  14. package/dist/components/conversation/conversation.svelte +179 -0
  15. package/dist/components/conversation/conversation.svelte.d.ts +26 -0
  16. package/dist/components/conversation/index.d.ts +2 -0
  17. package/dist/components/conversation/index.js +2 -0
  18. package/dist/components/document-pane/document-pane.svelte +177 -0
  19. package/dist/components/document-pane/document-pane.svelte.d.ts +9 -0
  20. package/dist/components/document-pane/index.d.ts +2 -0
  21. package/dist/components/document-pane/index.js +2 -0
  22. package/dist/components/markdown/index.d.ts +1 -1
  23. package/dist/components/markdown/index.js +1 -1
  24. package/dist/components/markdown/markdown.d.ts +13 -0
  25. package/dist/components/markdown/markdown.js +49 -1
  26. package/dist/components/markdown/markdown.svelte +85 -3
  27. package/dist/components/markdown/markdown.svelte.d.ts +6 -0
  28. package/dist/components/tool-row/tool-row.svelte +5 -11
  29. package/dist/components/working/working.svelte +1 -9
  30. package/dist/follow-scroll.svelte.d.ts +34 -0
  31. package/dist/follow-scroll.svelte.js +46 -0
  32. package/dist/transcript.svelte.d.ts +36 -4
  33. package/dist/transcript.svelte.js +50 -26
  34. package/package.json +16 -2
@@ -1,18 +1,27 @@
1
1
  <!--
2
- The composer: one rounded box holding the input AND a footer row.
2
+ The composer: one rounded box holding attachments, the input AND a footer row.
3
3
 
4
- Two things carry it: the chips sit BELOW the input inside the same box
5
- rather than beside it, and the trailing control is a circular filled accent
6
- button. The empty→typing transition lives almost entirely in that control —
7
- the box chrome does not resize.
4
+ Two things carry it: the chips sit BELOW the input inside the same box rather
5
+ than beside it, and the trailing control is a circular filled accent button.
6
+ The empty→typing transition lives almost entirely in that control — the box
7
+ chrome grows with the text and nothing else moves.
8
8
 
9
- The composer stays enabled while the agent works. Queuing the next message
10
- is not built yet; disabling the box would be a different and worse
11
- behaviour than the one being copied, so it is not disabled.
9
+ While Milton is answering the box is disabled and the send button becomes a
10
+ stop button. Queuing the next message is not built, and a box that takes
11
+ typing it will then discard is worse than one that plainly waits.
12
12
  -->
13
13
  <script lang="ts">
14
14
  import ArrowUpIcon from '@lucide/svelte/icons/arrow-up';
15
+ import PaperclipIcon from '@lucide/svelte/icons/paperclip';
15
16
  import SquareIcon from '@lucide/svelte/icons/square';
17
+ import XIcon from '@lucide/svelte/icons/x';
18
+ import {
19
+ ACCEPT_ATTRIBUTE,
20
+ acceptFiles,
21
+ formatSize,
22
+ MAX_FILES,
23
+ rejectionMessage
24
+ } from '../../attachments';
16
25
 
17
26
  export type Scope = 'document' | 'collection' | 'library';
18
27
 
@@ -20,9 +29,13 @@
20
29
  value: string;
21
30
  running: boolean;
22
31
  scope: Scope;
32
+ /** Files to send with the next question. Cleared by the caller on send. */
33
+ files?: File[];
23
34
  /** Names for the two narrower scopes; absent means that scope is unavailable. */
24
35
  documentName?: string;
25
36
  collectionName?: string;
37
+ /** Hides the paperclip for a host whose ask route takes no files. */
38
+ attachments?: boolean;
26
39
  onscope: (scope: Scope) => void;
27
40
  onsubmit: () => void;
28
41
  onstop: () => void;
@@ -32,23 +45,25 @@
32
45
  value = $bindable(),
33
46
  running,
34
47
  scope,
48
+ files = $bindable([]),
35
49
  documentName,
36
50
  collectionName,
51
+ attachments = true,
37
52
  onscope,
38
53
  onsubmit,
39
54
  onstop
40
55
  }: Props = $props();
41
56
 
42
- // What the agent is being asked about — chat with this document, this
43
- // collection, or the whole library. Callers that have no narrower scope
44
- // pass neither name, leaving "All collections" the sole entry — and a
45
- // sole entry is not a choice, so `hasChoice` below hides it rather than
46
- // rendering a chip with nothing to switch to.
57
+ // What Milton is being asked about — this document, this collection, or
58
+ // the whole library. Callers that have no narrower scope pass neither
59
+ // name, leaving "The whole library" the sole entry — and a sole entry is
60
+ // not a choice, so `hasChoice` below hides it rather than rendering a
61
+ // chip with nothing to switch to.
47
62
  const choices = $derived(
48
63
  [
49
64
  documentName ? { id: 'document' as const, label: documentName } : null,
50
65
  collectionName ? { id: 'collection' as const, label: collectionName } : null,
51
- { id: 'library' as const, label: 'All collections' }
66
+ { id: 'library' as const, label: 'The whole library' }
52
67
  ].filter((c) => c !== null)
53
68
  );
54
69
 
@@ -62,64 +77,202 @@
62
77
  // ("defence-s…", "All colle…"). Wrapping beats truncating: a chip a reader
63
78
  // cannot finish reading is not a control, it is decoration.
64
79
 
80
+ let textarea = $state<HTMLTextAreaElement | null>(null);
81
+ let picker = $state<HTMLInputElement | null>(null);
82
+ let rejected = $state('');
83
+
84
+ /** Six lines, then it scrolls. A `rows` attribute cannot express that —
85
+ * it sets the floor, not the ceiling — so the height is measured from the
86
+ * content and capped. `height: auto` first, or `scrollHeight` reports the
87
+ * height the box already has and the box can only ever grow. */
88
+ function grow() {
89
+ const el = textarea;
90
+ if (!el) return;
91
+ el.style.height = 'auto';
92
+ const line = parseFloat(getComputedStyle(el).lineHeight) || 24;
93
+ el.style.height = `${Math.min(el.scrollHeight, line * 6 + 20)}px`;
94
+ }
95
+
96
+ $effect(() => {
97
+ // Read `value` so a programmatic change (an example question tapped in
98
+ // the empty state) resizes the box too, not only typing.
99
+ void value;
100
+ grow();
101
+ });
102
+
103
+ // Focus comes back once Milton is done, so a reader can keep going without
104
+ // reaching for the mouse. Focusing at send is pointless — the box is
105
+ // disabled a frame later — so the transition out of `running` is the hook.
106
+ // Plain, not `$state`: read and written in the same effect, which would
107
+ // otherwise re-trigger itself forever.
108
+ let wasRunning = false;
109
+ $effect(() => {
110
+ if (wasRunning && !running) textarea?.focus();
111
+ wasRunning = running;
112
+ });
113
+
114
+ function submit() {
115
+ if (!value.trim() || running) return;
116
+ onsubmit();
117
+ textarea?.focus();
118
+ }
119
+
65
120
  function keydown(event: KeyboardEvent) {
66
121
  // Enter sends, Shift+Enter breaks the line — the convention in every
67
122
  // reference and the one a user will try first.
68
123
  if (event.key === 'Enter' && !event.shiftKey) {
69
124
  event.preventDefault();
70
- if (value.trim()) onsubmit();
125
+ submit();
71
126
  }
72
127
  }
128
+
129
+ function take(list: FileList | File[] | null) {
130
+ if (!list) return;
131
+ const check = acceptFiles(files, list);
132
+ files = check.accepted;
133
+ rejected = rejectionMessage(check.rejected);
134
+ }
135
+
136
+ function pick(event: Event) {
137
+ const input = event.currentTarget as HTMLInputElement;
138
+ take(input.files);
139
+ // Same file twice in a row fires no change event unless the value is
140
+ // cleared, so removing a chip and re-adding it would silently do nothing.
141
+ input.value = '';
142
+ }
143
+
144
+ function remove(index: number) {
145
+ files = files.filter((_, i) => i !== index);
146
+ rejected = '';
147
+ }
148
+
149
+ // The keyboard does not shrink the layout viewport on iOS, so a composer at
150
+ // the bottom of a full-height column sits UNDER it. The visual viewport is
151
+ // the one that moves; the overlap between the two is exactly how far the
152
+ // box must rise.
153
+ let lift = $state(0);
154
+ $effect(() => {
155
+ const viewport = globalThis.visualViewport;
156
+ if (!viewport) return;
157
+ const measure = () => {
158
+ lift = Math.max(0, window.innerHeight - viewport.height - viewport.offsetTop);
159
+ };
160
+ measure();
161
+ viewport.addEventListener('resize', measure);
162
+ viewport.addEventListener('scroll', measure);
163
+ return () => {
164
+ viewport.removeEventListener('resize', measure);
165
+ viewport.removeEventListener('scroll', measure);
166
+ };
167
+ });
73
168
  </script>
74
169
 
75
170
  <div
76
- class="border-border bg-surface-1 focus-within:border-primary/60 rounded-xl border transition-colors"
171
+ style="transform: translateY(-{lift}px); padding-bottom: {lift
172
+ ? '0px'
173
+ : 'env(safe-area-inset-bottom, 0px)'}"
174
+ class="transition-transform duration-150"
77
175
  >
78
- <textarea
79
- bind:value
80
- onkeydown={keydown}
81
- rows="2"
82
- placeholder={running ? 'Queue another message…' : 'Ask anything'}
83
- class="text-foreground placeholder:text-muted-foreground max-h-52 w-full resize-none bg-transparent px-4 pt-3 pb-2 text-base outline-none"
84
- ></textarea>
85
-
86
- <div class="flex items-center gap-2 px-3 pb-2.5">
87
- {#if hasChoice}
88
- <div class="flex min-w-0 flex-wrap items-center gap-1">
89
- {#each choices as choice (choice.id)}
90
- <button
91
- type="button"
92
- onclick={() => onscope(choice.id)}
93
- class="max-w-full truncate rounded-full px-2 py-0.5 text-xs transition-colors {scope ===
94
- choice.id
95
- ? 'bg-primary/15 text-foreground border-primary/40 border'
96
- : 'text-muted-foreground hover:text-foreground border border-transparent'}"
176
+ <div
177
+ class="border-border bg-surface-1 focus-within:border-primary/60 rounded-xl border transition-colors"
178
+ >
179
+ {#if files.length > 0}
180
+ <ul class="flex flex-wrap gap-1.5 px-3 pt-3">
181
+ {#each files as file, index (file.name + file.size)}
182
+ <li
183
+ class="border-border bg-surface-2 flex max-w-full items-center gap-1.5 rounded-lg border py-1 pr-1 pl-2 text-xs"
97
184
  >
98
- {choice.label}
99
- </button>
185
+ <span class="text-foreground truncate">{file.name}</span>
186
+ <span class="text-muted-foreground shrink-0 tabular-nums">{formatSize(file.size)}</span
187
+ >
188
+ <button
189
+ type="button"
190
+ onclick={() => remove(index)}
191
+ aria-label="Remove {file.name}"
192
+ class="text-muted-foreground hover:text-foreground hover:bg-surface-3 focus-visible:ring-ring flex size-5 shrink-0 items-center justify-center rounded transition-colors focus-visible:ring-2 focus-visible:outline-none"
193
+ >
194
+ <XIcon class="size-3" />
195
+ </button>
196
+ </li>
100
197
  {/each}
101
- </div>
102
- {/if}
103
- <span class="flex-1"></span>
104
- {#if running}
105
- <button
106
- type="button"
107
- onclick={onstop}
108
- aria-label="Stop"
109
- class="bg-foreground text-background hover:bg-foreground/90 flex size-8 items-center justify-center rounded-full transition-colors"
110
- >
111
- <SquareIcon class="size-3.5 fill-current" />
112
- </button>
113
- {:else}
114
- <button
115
- type="button"
116
- onclick={onsubmit}
117
- disabled={!value.trim()}
118
- aria-label="Send"
119
- class="bg-primary text-primary-foreground hover:bg-primary/90 disabled:hover:bg-primary flex size-8 items-center justify-center rounded-full transition-all disabled:opacity-30"
120
- >
121
- <ArrowUpIcon class="size-4" />
122
- </button>
198
+ </ul>
123
199
  {/if}
200
+
201
+ <textarea
202
+ bind:this={textarea}
203
+ bind:value
204
+ oninput={grow}
205
+ onkeydown={keydown}
206
+ rows="1"
207
+ disabled={running}
208
+ placeholder={running ? 'Milton is answering…' : 'Ask Milton…'}
209
+ class="text-foreground placeholder:text-muted-foreground w-full resize-none bg-transparent px-4 pt-3 pb-2 text-base outline-none disabled:opacity-60"
210
+ ></textarea>
211
+
212
+ <div class="flex items-center gap-2 px-3 pb-2.5">
213
+ {#if attachments}
214
+ <input
215
+ bind:this={picker}
216
+ type="file"
217
+ multiple
218
+ accept={ACCEPT_ATTRIBUTE}
219
+ onchange={pick}
220
+ class="hidden"
221
+ tabindex="-1"
222
+ aria-hidden="true"
223
+ />
224
+ <button
225
+ type="button"
226
+ onclick={() => picker?.click()}
227
+ disabled={running || files.length >= MAX_FILES}
228
+ aria-label="Attach a file"
229
+ 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"
230
+ >
231
+ <PaperclipIcon class="size-4" />
232
+ </button>
233
+ {/if}
234
+
235
+ {#if hasChoice}
236
+ <div class="flex min-w-0 flex-wrap items-center gap-1">
237
+ {#each choices as choice (choice.id)}
238
+ <button
239
+ type="button"
240
+ onclick={() => onscope(choice.id)}
241
+ class="max-w-full truncate rounded-full px-2 py-0.5 text-xs transition-colors {scope ===
242
+ choice.id
243
+ ? 'bg-primary/15 text-foreground border-primary/40 border'
244
+ : 'text-muted-foreground hover:text-foreground border border-transparent'}"
245
+ >
246
+ {choice.label}
247
+ </button>
248
+ {/each}
249
+ </div>
250
+ {/if}
251
+ <span class="flex-1"></span>
252
+ {#if running}
253
+ <button
254
+ type="button"
255
+ onclick={onstop}
256
+ aria-label="Stop"
257
+ class="bg-foreground text-background hover:bg-foreground/90 flex size-8 items-center justify-center rounded-full transition-colors"
258
+ >
259
+ <SquareIcon class="size-3.5 fill-current" />
260
+ </button>
261
+ {:else}
262
+ <button
263
+ type="button"
264
+ onclick={submit}
265
+ disabled={!value.trim()}
266
+ aria-label="Send"
267
+ class="bg-primary text-primary-foreground hover:bg-primary/90 disabled:hover:bg-primary flex size-8 items-center justify-center rounded-full transition-all disabled:opacity-30"
268
+ >
269
+ <ArrowUpIcon class="size-4" />
270
+ </button>
271
+ {/if}
272
+ </div>
124
273
  </div>
274
+
275
+ {#if rejected}
276
+ <p class="text-status-error px-1 pt-1.5 text-xs" role="status" aria-live="polite">{rejected}</p>
277
+ {/if}
125
278
  </div>
@@ -3,13 +3,17 @@ interface Props {
3
3
  value: string;
4
4
  running: boolean;
5
5
  scope: Scope;
6
+ /** Files to send with the next question. Cleared by the caller on send. */
7
+ files?: File[];
6
8
  /** Names for the two narrower scopes; absent means that scope is unavailable. */
7
9
  documentName?: string;
8
10
  collectionName?: string;
11
+ /** Hides the paperclip for a host whose ask route takes no files. */
12
+ attachments?: boolean;
9
13
  onscope: (scope: Scope) => void;
10
14
  onsubmit: () => void;
11
15
  onstop: () => void;
12
16
  }
13
- declare const Composer: import("svelte").Component<Props, {}, "value">;
17
+ declare const Composer: import("svelte").Component<Props, {}, "value" | "files">;
14
18
  type Composer = ReturnType<typeof Composer>;
15
19
  export default Composer;
@@ -0,0 +1,179 @@
1
+ <!--
2
+ The whole reading surface: every turn, the scroll that follows the stream,
3
+ and the source pane beside it.
4
+
5
+ This owns the SCROLL CONTAINER, which is the reason it exists rather than the
6
+ host composing `AgentTranscript` in a div of its own. Follow-scroll, the
7
+ jump-to-latest pill and a document pane that narrows the transcript instead
8
+ of covering it are all one question — where the overflow lives — and three
9
+ hosts answering it separately is three chances to answer it differently.
10
+ -->
11
+ <script lang="ts">
12
+ import type { Snippet } from 'svelte';
13
+ import ArrowDownIcon from '@lucide/svelte/icons/arrow-down';
14
+ import type { Turn } from '../../transcript.svelte';
15
+ import type { Citation, LoadDocument } from '../../citations';
16
+ import { FollowScroll } from '../../follow-scroll.svelte';
17
+ import AgentTranscript from '../agent-transcript/agent-transcript.svelte';
18
+ import DocumentPane from '../document-pane/document-pane.svelte';
19
+
20
+ interface Props {
21
+ turns: Turn[];
22
+ running: boolean;
23
+ /** `Transcript.version` — bumped per event. Text grows in place, so a
24
+ * count of turns is not enough to keep the scroll following. */
25
+ version?: number;
26
+ welcome?: string;
27
+ /** Up to three, shown in the empty state as tappable pills. */
28
+ examples?: string[];
29
+ onexample?: (question: string) => void;
30
+ /** Re-asks the last question. Offered on the last answer only. */
31
+ onregenerate?: () => void;
32
+ /** Enables the source pane. Without it, chips render but do not open. */
33
+ loadDocument?: LoadDocument;
34
+ collectionNames?: Set<string>;
35
+ /** The composer, rendered INSIDE the transcript column so the source
36
+ * pane narrows it too — a composer the host places outside slides
37
+ * under the pane the moment one opens. */
38
+ composer?: Snippet;
39
+ }
40
+
41
+ let {
42
+ turns,
43
+ running,
44
+ version = 0,
45
+ welcome = 'Ask Milton a question about the library.',
46
+ examples = [],
47
+ onexample,
48
+ onregenerate,
49
+ loadDocument,
50
+ collectionNames = new Set(),
51
+ composer
52
+ }: Props = $props();
53
+
54
+ let viewport = $state<HTMLElement | null>(null);
55
+ let open = $state<Citation | null>(null);
56
+ const follow = new FollowScroll();
57
+
58
+ function metrics(el: HTMLElement) {
59
+ return {
60
+ scrollTop: el.scrollTop,
61
+ clientHeight: el.clientHeight,
62
+ scrollHeight: el.scrollHeight
63
+ };
64
+ }
65
+
66
+ // A new question re-pins: the reader asked for the thing about to arrive,
67
+ // even if they were reading something further up when they asked.
68
+ //
69
+ // Deliberately NOT `$state`: an effect that both reads and writes one piece
70
+ // of reactive state re-triggers itself, which Svelte stops with
71
+ // `effect_update_depth_exceeded` — measured, on this component. Nothing
72
+ // renders this, so a plain variable is both correct and enough.
73
+ let seen = 0;
74
+ $effect(() => {
75
+ if (turns.length > seen) follow.pin();
76
+ seen = turns.length;
77
+ });
78
+
79
+ $effect(() => {
80
+ // Both dependencies are deliberate: `version` for text growing inside an
81
+ // existing block, `turns.length` for a whole new turn.
82
+ void version;
83
+ void turns.length;
84
+ if (!viewport || !follow.following) return;
85
+ viewport.scrollTop = viewport.scrollHeight;
86
+ });
87
+
88
+ function scrolled() {
89
+ if (viewport) follow.measure(metrics(viewport));
90
+ }
91
+
92
+ function jump() {
93
+ if (!viewport) return;
94
+ follow.pin();
95
+ viewport.scrollTo({ top: viewport.scrollHeight, behavior: 'smooth' });
96
+ }
97
+
98
+ function cite(citation: Citation) {
99
+ // A citation derived from the prose has no id to read, so its chip is
100
+ // legible but inert rather than opening an empty pane.
101
+ if (loadDocument && citation.document_id) open = citation;
102
+ }
103
+ </script>
104
+
105
+ <div class="flex min-h-0 flex-1">
106
+ <div class="flex min-h-0 min-w-0 flex-1 flex-col">
107
+ <!-- The pill is positioned against the SCROLL region, not the column: the
108
+ composer is in the column too, and a pill over the input is a pill in
109
+ the way. -->
110
+ <div class="relative flex min-h-0 flex-1 flex-col">
111
+ <div
112
+ bind:this={viewport}
113
+ onscroll={scrolled}
114
+ role="log"
115
+ aria-live="polite"
116
+ aria-label="Conversation with Milton"
117
+ class="min-h-0 flex-1 overflow-y-auto overscroll-contain"
118
+ >
119
+ <div class="mx-auto flex w-full max-w-[46rem] flex-col gap-8 px-4 py-6">
120
+ {#if turns.length === 0}
121
+ <div class="flex flex-col items-start gap-4 py-8">
122
+ <p class="text-foreground max-w-[46ch] text-lg leading-snug">{welcome}</p>
123
+ {#if examples.length > 0}
124
+ <ul class="flex flex-col items-start gap-2">
125
+ {#each examples.slice(0, 3) as example (example)}
126
+ <li class="max-w-full">
127
+ <button
128
+ type="button"
129
+ onclick={() => onexample?.(example)}
130
+ class="border-border hover:border-border-strong hover:bg-surface-2 focus-visible:ring-ring text-foreground max-w-full rounded-full border px-3.5 py-2 text-left text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none"
131
+ >
132
+ {example}
133
+ </button>
134
+ </li>
135
+ {/each}
136
+ </ul>
137
+ {/if}
138
+ </div>
139
+ {/if}
140
+
141
+ {#each turns as turn, index (turn.id)}
142
+ <AgentTranscript
143
+ question={turn.question}
144
+ blocks={turn.blocks}
145
+ outcome={turn.outcome}
146
+ running={running && index === turns.length - 1}
147
+ citations={turn.citations ?? []}
148
+ {collectionNames}
149
+ oncite={cite}
150
+ onregenerate={index === turns.length - 1 && !running ? onregenerate : undefined}
151
+ />
152
+ {/each}
153
+ </div>
154
+ </div>
155
+
156
+ {#if !follow.following && turns.length > 0}
157
+ <button
158
+ type="button"
159
+ onclick={jump}
160
+ class="border-border bg-surface-1 text-foreground hover:bg-surface-2 focus-visible:ring-ring absolute bottom-3 left-1/2 flex -translate-x-1/2 items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs shadow-lg transition-colors focus-visible:ring-2 focus-visible:outline-none"
161
+ >
162
+ <ArrowDownIcon class="size-3.5" />
163
+ Jump to latest
164
+ </button>
165
+ {/if}
166
+ </div>
167
+
168
+ {#if composer}
169
+ <div class="shrink-0 px-4 pb-3">
170
+ <div class="mx-auto w-full max-w-[46rem]">{@render composer()}</div>
171
+ </div>
172
+ {/if}
173
+
174
+ </div>
175
+
176
+ {#if open && loadDocument}
177
+ <DocumentPane citation={open} {loadDocument} onclose={() => (open = null)} />
178
+ {/if}
179
+ </div>
@@ -0,0 +1,26 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { Turn } from '../../transcript.svelte';
3
+ import type { LoadDocument } from '../../citations';
4
+ interface Props {
5
+ turns: Turn[];
6
+ running: boolean;
7
+ /** `Transcript.version` — bumped per event. Text grows in place, so a
8
+ * count of turns is not enough to keep the scroll following. */
9
+ version?: number;
10
+ welcome?: string;
11
+ /** Up to three, shown in the empty state as tappable pills. */
12
+ examples?: string[];
13
+ onexample?: (question: string) => void;
14
+ /** Re-asks the last question. Offered on the last answer only. */
15
+ onregenerate?: () => void;
16
+ /** Enables the source pane. Without it, chips render but do not open. */
17
+ loadDocument?: LoadDocument;
18
+ collectionNames?: Set<string>;
19
+ /** The composer, rendered INSIDE the transcript column so the source
20
+ * pane narrows it too — a composer the host places outside slides
21
+ * under the pane the moment one opens. */
22
+ composer?: Snippet;
23
+ }
24
+ declare const Conversation: import("svelte").Component<Props, {}, "">;
25
+ type Conversation = ReturnType<typeof Conversation>;
26
+ export default Conversation;
@@ -0,0 +1,2 @@
1
+ export { default as Conversation } from './conversation.svelte';
2
+ export { default } from './conversation.svelte';
@@ -0,0 +1,2 @@
1
+ export { default as Conversation } from './conversation.svelte';
2
+ export { default } from './conversation.svelte';