@poodle64/librarian 2026.10.5 → 2026.10.6
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 +2 -2
- package/dist/components/visual-block/chart-draw.svelte +26 -0
- package/dist/components/visual-block/chart-draw.svelte.d.ts +7 -0
- package/dist/components/visual-block/chart.svelte +73 -0
- package/dist/components/visual-block/chart.svelte.d.ts +8 -0
- package/dist/visuals.d.ts +9 -1
- package/dist/visuals.js +31 -29
- package/package.json +2 -4
- package/dist/components/visual-block/vega-chart.svelte +0 -110
- package/dist/components/visual-block/vega-chart.svelte.d.ts +0 -7
package/README.md
CHANGED
|
@@ -179,7 +179,7 @@ on it fires once and never again.
|
|
|
179
179
|
| Asking again | `onregenerate`: re-send the last question as a NEW turn; the package exposes the action and never re-asks by itself |
|
|
180
180
|
| Choosing a collection | `Composer`'s `collections` (`{ name, title?, description? }[]`, the ones the person may read) and `collection` (the chosen name; undefined is all of them), with `oncollection(name \| undefined)`. From two collections up it is ONE control naming the choice, opening a listbox (searchable from eight) where each shows by its title (the name made readable where there is none) over its description, never a retrieval summary |
|
|
181
181
|
| The empty state | `welcome` and up to three `examples`. With a composer, the opening and the composer sit together in the middle of the panel, the examples, the folded scope and `lead` under it; the first turn docks the composer at the foot |
|
|
182
|
-
| Conversation controls | `toolbar`: a snippet at the measure's trailing edge above the transcript, outside its scroll (the
|
|
182
|
+
| Conversation controls | `toolbar`: a snippet at the measure's trailing edge above the transcript, outside its scroll (the chat page's Past questions and New question) |
|
|
183
183
|
| What this surface covers | `scope`: one statement per persona, in the host's own words. Rendered above the first turn and folded to a line once the conversation starts |
|
|
184
184
|
| The words themselves | `copy`: a partial of `ChatCopy`. Every component resolves it itself, so overriding one line does not mean restating the rest |
|
|
185
185
|
| An answer its own way | `turn`: a snippet given each turn's `AgentTranscriptProps` in place of `AgentTranscript`. Render your own from the parts, or `AgentTranscript` inside your own to add to it |
|
|
@@ -405,7 +405,7 @@ end that way.
|
|
|
405
405
|
| Documents it wrote | `artefact` frames and a stored turn's `artefacts` (each with `title`, `kind`, `changes`, `pages`) become `turn.written`: the newest version of each is a card with Open and a download chip (Word where `formats` has it, else the source). The pane marks what a version changed since the one before: a "Changed since vN" line from `changes`, and a bar beside each changed block. `loadWritten(name, version)` and `writtenUrl(name, version, format)` (`Chat.loadArtefact` / `Chat.artefactUrl` over the transport) let the pane show a version: Markdown sanitised, HTML only in a `sandbox=""` frame |
|
|
406
406
|
| The turn's steps | One fold, "Read your attachment, searched the library and the web, and wrote a minute · 6 steps", composed from the steps (`stepsSummary`); it says what is happening now while the turn runs, and opens on one row per step (a search row counts what it found, a web search opens on the pages it found) |
|
|
407
407
|
| Web sources | Pages from the turn's supacrawl calls and the answer's Markdown links fold with the library's sources ("6 sources · 4 library · 2 web"), numbered on from the library's, and a link to one in the answer reads as that number's chip |
|
|
408
|
-
| Drawing a fence | `visuals` on `Conversation` is a registry keyed on a fence's language, `{ component, parse }` each; `defaultVisuals` (from `@poodle64/librarian/visuals`) draws `
|
|
408
|
+
| Drawing a fence | `visuals` on `Conversation` is a registry keyed on a fence's language, `{ component, parse }` each; `defaultVisuals` (from `@poodle64/librarian/visuals`) draws `chart` (`{title, type: bar | line | area, x, y: [fields], data}` on LayerChart, in the app's `--chart-1`…`--chart-5` tokens, loaded when an answer carries one), `timeline` and `card`. An app passes `{ ...defaultVisuals, openui }`. A block whose closing fence has not arrived is "Drawing…", one that does not parse is its code |
|
|
409
409
|
| "Answered without a source" | `copy.notHeld: ''` for a persona that answers from no sources |
|
|
410
410
|
| Saying something mid-run | `Composer`'s `sendWhileRunning`: while the session works the box stays open and Send sits beside Stop. For a route that reads a message while the run works; a chat leaves it off and waits for its answer |
|
|
411
411
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { AreaChart, BarChart, LineChart } from 'layerchart';
|
|
3
|
+
import type { ChartValue } from '../../visuals';
|
|
4
|
+
|
|
5
|
+
let { value }: { value: ChartValue } = $props();
|
|
6
|
+
|
|
7
|
+
// `--chart-1` to `--chart-5` are the app's own series colours.
|
|
8
|
+
const series = $derived(
|
|
9
|
+
value.y.map((key, index) => ({ key, label: key, color: `var(--chart-${index + 1})` }))
|
|
10
|
+
);
|
|
11
|
+
const legend = $derived(series.length > 1);
|
|
12
|
+
// A line or area over ISO dates is a time axis; anything else is the label it says.
|
|
13
|
+
const data = $derived(
|
|
14
|
+
value.type !== 'bar' && value.data.every((row) => /^\d{4}-\d{2}(-\d{2})?$/.test(String(row[value.x])))
|
|
15
|
+
? value.data.map((row) => ({ ...row, [value.x]: new Date(String(row[value.x])) }))
|
|
16
|
+
: value.data
|
|
17
|
+
);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
{#if value.type === 'bar'}
|
|
21
|
+
<BarChart {data} x={value.x} {series} {legend} seriesLayout="group" props={{ bars: { strokeWidth: 0, radius: 2 } }} />
|
|
22
|
+
{:else if value.type === 'line'}
|
|
23
|
+
<LineChart {data} x={value.x} {series} {legend} />
|
|
24
|
+
{:else}
|
|
25
|
+
<AreaChart {data} x={value.x} {series} {legend} />
|
|
26
|
+
{/if}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
One chart block, drawn with LayerChart in the app's chart tokens. The persona
|
|
3
|
+
writes the data and the kind; colour, type, axes and size come from here.
|
|
4
|
+
LayerChart is loaded when an answer carries a chart, never with the page.
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import type { Component } from 'svelte';
|
|
8
|
+
import type { ChartValue } from '../../visuals';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
value: unknown;
|
|
12
|
+
source: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { value, source }: Props = $props();
|
|
16
|
+
const chart = $derived(value as ChartValue);
|
|
17
|
+
let Draw = $state<Component<{ value: ChartValue }> | null>(null);
|
|
18
|
+
let failed = $state(false);
|
|
19
|
+
|
|
20
|
+
$effect(() => {
|
|
21
|
+
if (Draw || failed) return;
|
|
22
|
+
import('./chart-draw.svelte').then(
|
|
23
|
+
(module) => (Draw = module.default),
|
|
24
|
+
() => (failed = true)
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
{#if failed}
|
|
30
|
+
<pre data-highlighted="true"><code>{source}</code></pre>
|
|
31
|
+
{:else}
|
|
32
|
+
<figure class="ds-lib-chart">
|
|
33
|
+
{#if chart.title}<figcaption class="ds-lib-chart-title">{chart.title}</figcaption>{/if}
|
|
34
|
+
<div class="ds-lib-chart-host">
|
|
35
|
+
{#if Draw}<Draw value={chart} />{/if}
|
|
36
|
+
</div>
|
|
37
|
+
</figure>
|
|
38
|
+
{/if}
|
|
39
|
+
|
|
40
|
+
<style>
|
|
41
|
+
.ds-lib-chart {
|
|
42
|
+
margin: 0.75em 0;
|
|
43
|
+
border: 1px solid var(--ds-color-border);
|
|
44
|
+
border-radius: var(--ds-radius-lg);
|
|
45
|
+
background: var(--ds-color-surface-1);
|
|
46
|
+
padding: 0.75rem 0.875rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.ds-lib-chart-title {
|
|
50
|
+
margin-bottom: 0.5rem;
|
|
51
|
+
color: var(--ds-color-foreground);
|
|
52
|
+
font-size: 0.875rem;
|
|
53
|
+
font-weight: 600;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.ds-lib-chart-host {
|
|
57
|
+
width: 100%;
|
|
58
|
+
height: 16rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.ds-lib-chart-host :global(text) {
|
|
62
|
+
fill: var(--ds-color-muted-foreground);
|
|
63
|
+
stroke: none;
|
|
64
|
+
font-size: 12px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.ds-lib-chart-host :global(.lc-rule),
|
|
68
|
+
.ds-lib-chart-host :global(.lc-axis-tick),
|
|
69
|
+
.ds-lib-chart-host :global(.lc-grid-x-line),
|
|
70
|
+
.ds-lib-chart-host :global(.lc-grid-y-line) {
|
|
71
|
+
stroke: var(--ds-color-border-strong);
|
|
72
|
+
}
|
|
73
|
+
</style>
|
package/dist/visuals.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export interface Visual {
|
|
|
17
17
|
/** The block's text as the value its component draws; throws on a block that is invalid. */
|
|
18
18
|
parse: (source: string) => unknown;
|
|
19
19
|
}
|
|
20
|
+
export interface ChartValue {
|
|
21
|
+
title: string;
|
|
22
|
+
type: 'bar' | 'line' | 'area';
|
|
23
|
+
/** The field on the x axis; each y is a field drawn as a series. */
|
|
24
|
+
x: string;
|
|
25
|
+
y: string[];
|
|
26
|
+
data: Array<Record<string, string | number>>;
|
|
27
|
+
}
|
|
20
28
|
export interface TimelineValue {
|
|
21
29
|
title: string;
|
|
22
30
|
events: Array<{
|
|
@@ -34,5 +42,5 @@ export interface CardValue {
|
|
|
34
42
|
value: string;
|
|
35
43
|
}>;
|
|
36
44
|
}
|
|
37
|
-
/** A chart, a timeline and an entity card. An app adds to these or replaces one through `ChatExtensions.visuals`. */
|
|
45
|
+
/** A chart (LayerChart), a timeline and an entity card. An app adds to these or replaces one through `ChatExtensions.visuals`. */
|
|
38
46
|
export declare const defaultVisuals: Record<string, Visual>;
|
package/dist/visuals.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import EntityCard from './components/visual-block/entity-card.svelte';
|
|
12
12
|
import Timeline from './components/visual-block/timeline.svelte';
|
|
13
|
-
import
|
|
13
|
+
import Chart from './components/visual-block/chart.svelte';
|
|
14
14
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
15
|
const text = (value) => typeof value === 'string' ? value : typeof value === 'number' ? String(value) : '';
|
|
16
16
|
const object = (source) => {
|
|
@@ -19,32 +19,34 @@ const object = (source) => {
|
|
|
19
19
|
throw new Error('not an object');
|
|
20
20
|
return value;
|
|
21
21
|
};
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
22
|
+
const KINDS = ['bar', 'line', 'area'];
|
|
23
|
+
const MAX_SERIES = 5;
|
|
24
|
+
const MAX_ROWS = 500;
|
|
25
|
+
function parseChart(source) {
|
|
26
|
+
const value = object(source);
|
|
27
|
+
const type = KINDS.find((kind) => kind === value.type);
|
|
28
|
+
if (!type)
|
|
29
|
+
throw new Error('not a bar, line or area');
|
|
30
|
+
const x = text(value.x);
|
|
31
|
+
const y = Array.isArray(value.y) ? value.y.map(text) : [];
|
|
32
|
+
if (!x || y.length === 0 || y.length > MAX_SERIES || y.some((key) => !key))
|
|
33
|
+
throw new Error('no x or y');
|
|
34
|
+
const rows = Array.isArray(value.data) ? value.data : [];
|
|
35
|
+
if (rows.length === 0 || rows.length > MAX_ROWS)
|
|
36
|
+
throw new Error('no data');
|
|
37
|
+
const data = rows.map((row) => {
|
|
38
|
+
if (!isRecord(row) || !text(row[x]))
|
|
39
|
+
throw new Error('a row with no x');
|
|
40
|
+
const point = { [x]: typeof row[x] === 'number' ? row[x] : text(row[x]) };
|
|
41
|
+
for (const key of y) {
|
|
42
|
+
const number = row[key];
|
|
43
|
+
if (typeof number !== 'number' || !Number.isFinite(number))
|
|
44
|
+
throw new Error('a y that is not a number');
|
|
45
|
+
point[key] = number;
|
|
46
|
+
}
|
|
47
|
+
return point;
|
|
48
|
+
});
|
|
49
|
+
return { title: text(value.title), type, x, y, data };
|
|
48
50
|
}
|
|
49
51
|
function parseTimeline(source) {
|
|
50
52
|
const value = object(source);
|
|
@@ -74,9 +76,9 @@ function parseCard(source) {
|
|
|
74
76
|
.filter((fact) => fact.label || fact.value)
|
|
75
77
|
};
|
|
76
78
|
}
|
|
77
|
-
/** A chart, a timeline and an entity card. An app adds to these or replaces one through `ChatExtensions.visuals`. */
|
|
79
|
+
/** A chart (LayerChart), a timeline and an entity card. An app adds to these or replaces one through `ChatExtensions.visuals`. */
|
|
78
80
|
export const defaultVisuals = {
|
|
79
|
-
|
|
81
|
+
chart: { component: Chart, parse: parseChart },
|
|
80
82
|
timeline: { component: Timeline, parse: parseTimeline },
|
|
81
83
|
card: { component: EntityCard, parse: parseCard }
|
|
82
84
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/librarian",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.6",
|
|
4
4
|
"description": "An agent's conversation surface as a consumable Svelte 5 package: the stream client, transcript state and self-styling chat components (transcript, composer, markdown) every household app renders instead of rebuilding, speaking as whichever persona the app names.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -73,9 +73,7 @@
|
|
|
73
73
|
"shiki": "^4.4.3"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"
|
|
77
|
-
"vega-embed": "7.3.0",
|
|
78
|
-
"vega-lite": "6.4.3"
|
|
76
|
+
"layerchart": "2.6.0"
|
|
79
77
|
},
|
|
80
78
|
"devDependencies": {
|
|
81
79
|
"@lucide/svelte": "^1.7.0",
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
One Vega-Lite v5 spec, drawn with the app's own tokens. The persona writes
|
|
3
|
-
the data and the marks; colour, type, axes and size come from here and
|
|
4
|
-
override whatever the spec says. Loaded on demand: vega is not small.
|
|
5
|
-
-->
|
|
6
|
-
<script lang="ts">
|
|
7
|
-
import { onMount } from 'svelte';
|
|
8
|
-
|
|
9
|
-
interface Props {
|
|
10
|
-
value: unknown;
|
|
11
|
-
source: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
let { value, source }: Props = $props();
|
|
15
|
-
let host = $state<HTMLDivElement | null>(null);
|
|
16
|
-
let failed = $state(false);
|
|
17
|
-
|
|
18
|
-
const css = (name: string): string =>
|
|
19
|
-
getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
20
|
-
|
|
21
|
-
function config() {
|
|
22
|
-
const ink = css('--ds-color-foreground') || '#231f20';
|
|
23
|
-
const muted = css('--ds-color-muted-foreground') || '#57595c';
|
|
24
|
-
const line = css('--ds-color-border-strong') || '#00000038';
|
|
25
|
-
const font = getComputedStyle(document.body).fontFamily;
|
|
26
|
-
const series = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => css(`--chart-${i}`)).filter(Boolean);
|
|
27
|
-
return {
|
|
28
|
-
background: null,
|
|
29
|
-
font,
|
|
30
|
-
...(series.length > 0 ? { range: { category: series } } : {}),
|
|
31
|
-
mark: { color: series[0] ?? ink },
|
|
32
|
-
bar: { cornerRadius: 1 },
|
|
33
|
-
view: { stroke: null },
|
|
34
|
-
axis: {
|
|
35
|
-
labelColor: muted,
|
|
36
|
-
titleColor: muted,
|
|
37
|
-
domainColor: line,
|
|
38
|
-
tickColor: line,
|
|
39
|
-
gridColor: line,
|
|
40
|
-
gridOpacity: 0.5,
|
|
41
|
-
labelFont: font,
|
|
42
|
-
titleFont: font,
|
|
43
|
-
labelFontSize: 12,
|
|
44
|
-
titleFontSize: 12,
|
|
45
|
-
titleFontWeight: 600
|
|
46
|
-
},
|
|
47
|
-
legend: { labelColor: ink, titleColor: muted, labelFont: font, titleFont: font },
|
|
48
|
-
title: { color: ink, font, fontSize: 14, fontWeight: 600, anchor: 'start' }
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let latest = 0;
|
|
53
|
-
// The view drawn last, ended before the next is drawn and when the chart goes.
|
|
54
|
-
let drawn: { finalize: () => void } | undefined;
|
|
55
|
-
async function draw() {
|
|
56
|
-
if (!host) return;
|
|
57
|
-
const mine = ++latest;
|
|
58
|
-
try {
|
|
59
|
-
const { default: embed } = await import('vega-embed');
|
|
60
|
-
if (mine !== latest) return;
|
|
61
|
-
const { $schema: _schema, config: _config, usermeta: _usermeta, ...spec } = value as Record<string, unknown>;
|
|
62
|
-
drawn?.finalize();
|
|
63
|
-
drawn = await embed(
|
|
64
|
-
host,
|
|
65
|
-
{ ...spec, width: 'container', autosize: { type: 'fit', contains: 'padding' } } as never,
|
|
66
|
-
{ actions: false, renderer: 'svg', config: config() as never }
|
|
67
|
-
);
|
|
68
|
-
failed = false;
|
|
69
|
-
} catch {
|
|
70
|
-
if (mine === latest) failed = true;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
onMount(() => {
|
|
75
|
-
// The palette follows the theme, and the theme is a class on the root.
|
|
76
|
-
const watch = new MutationObserver(() => void draw());
|
|
77
|
-
watch.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
78
|
-
return () => {
|
|
79
|
-
watch.disconnect();
|
|
80
|
-
drawn?.finalize();
|
|
81
|
-
};
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
$effect(() => {
|
|
85
|
-
void value;
|
|
86
|
-
void host;
|
|
87
|
-
void draw();
|
|
88
|
-
});
|
|
89
|
-
</script>
|
|
90
|
-
|
|
91
|
-
{#if failed}
|
|
92
|
-
<pre data-highlighted="true"><code>{source}</code></pre>
|
|
93
|
-
{:else}
|
|
94
|
-
<figure class="ds-lib-chart"><div bind:this={host} class="ds-lib-chart-host"></div></figure>
|
|
95
|
-
{/if}
|
|
96
|
-
|
|
97
|
-
<style>
|
|
98
|
-
.ds-lib-chart {
|
|
99
|
-
margin: 0.75em 0;
|
|
100
|
-
border: 1px solid var(--ds-color-border);
|
|
101
|
-
border-radius: var(--ds-radius-lg);
|
|
102
|
-
background: var(--ds-color-surface-1);
|
|
103
|
-
padding: 0.75rem 0.875rem;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.ds-lib-chart-host {
|
|
107
|
-
width: 100%;
|
|
108
|
-
min-height: 6rem;
|
|
109
|
-
}
|
|
110
|
-
</style>
|