@webmcp-auto-ui/ui 2.5.37 → 2.5.39
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/package.json +3 -1
- package/src/base/chat-inline.svelte +81 -9
- package/src/widgets/WidgetRenderer.svelte +6 -5
- package/src/widgets/notebook/left-pane.ts +20 -0
- package/src/widgets/notebook/notebook.ts +487 -47
- package/src/widgets/notebook/resource-extractor.ts +16 -1
- package/src/widgets/notebook/share-handlers.ts +24 -3
- package/src/widgets/notebook/shared.ts +187 -27
- package/src/widgets/rich/cards.svelte +3 -1
- package/src/widgets/rich/chart-rich.svelte +73 -7
- package/src/widgets/rich/data-table.svelte +28 -7
- package/src/widgets/rich/map.svelte +392 -0
- package/src/widgets/rich/stat-card.svelte +119 -20
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Consumed by import-modals.ts and left-pane.ts.
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
|
|
7
|
-
import { parseBody } from '@webmcp-auto-ui/sdk';
|
|
7
|
+
import { parseBody, parseWidgetDisplayCall } from '@webmcp-auto-ui/sdk';
|
|
8
8
|
import { uid, defaultCellContent } from './shared.js';
|
|
9
9
|
import type { NotebookCell, CellType } from './shared.js';
|
|
10
10
|
|
|
@@ -80,6 +80,21 @@ export function extractCellFromFence(lang: string, content: string): NotebookCel
|
|
|
80
80
|
return { id: uid(), type: 'sql', content: sql.trim(), hideSource: false, hideResult: false };
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
// widget_display is a local WebMCP tool (autoui), not a remote MCP tool —
|
|
84
|
+
// route it to the JS sandbox `widget()` helper which captures and renders
|
|
85
|
+
// inline. callTool() would fail with "No MCP server exposes tool".
|
|
86
|
+
if (/^(?:[A-Za-z_]\w*_)?widget_display$/.test(name)) {
|
|
87
|
+
const parsed = parseWidgetDisplayCall(trimmed);
|
|
88
|
+
if (parsed) {
|
|
89
|
+
const paramsLiteral = JSON.stringify(parsed.params, null, 2);
|
|
90
|
+
return {
|
|
91
|
+
id: uid(),
|
|
92
|
+
type: 'js',
|
|
93
|
+
content: `return widget('${parsed.name}', ${paramsLiteral})`,
|
|
94
|
+
hideSource: false, hideResult: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
83
98
|
return {
|
|
84
99
|
id: uid(),
|
|
85
100
|
type: 'js',
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 4 formats: JSON, Markdown, Hyperskill link (+ short), PNG snapshot.
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
|
|
7
|
-
import { encode, buildShortUrl } from '@webmcp-auto-ui/sdk';
|
|
7
|
+
import { encode, buildShortUrl, pickFence } from '@webmcp-auto-ui/sdk';
|
|
8
8
|
import { canvasVanilla } from '@webmcp-auto-ui/sdk/canvas-vanilla';
|
|
9
9
|
import type { NotebookState, NotebookCell } from './shared.js';
|
|
10
10
|
|
|
@@ -56,7 +56,9 @@ export function serializeToMarkdown(state: NotebookState): string {
|
|
|
56
56
|
const metaLine = cell.args && Object.keys(cell.args).length > 0
|
|
57
57
|
? `${commentPrefix} @meta ${JSON.stringify(cell.args)}\n`
|
|
58
58
|
: '';
|
|
59
|
-
|
|
59
|
+
const body = metaLine + cell.content.trim();
|
|
60
|
+
const fence = pickFence(body);
|
|
61
|
+
parts.push(fence + lang + varname, body, fence, '');
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
return parts.join('\n').trim() + '\n';
|
|
@@ -71,7 +73,8 @@ function buildFrontmatter(state: NotebookState): string {
|
|
|
71
73
|
const title = (state.title || '').trim();
|
|
72
74
|
const description = extractDescription(state);
|
|
73
75
|
const servers = collectEnabledServers();
|
|
74
|
-
|
|
76
|
+
const webmcpServers = collectEnabledWebmcpServers();
|
|
77
|
+
if (!title && !description && servers.length === 0 && webmcpServers.length === 0) return '';
|
|
75
78
|
|
|
76
79
|
const lines: string[] = ['---'];
|
|
77
80
|
if (title) lines.push(`title: ${yamlQuote(title)}`);
|
|
@@ -83,10 +86,28 @@ function buildFrontmatter(state: NotebookState): string {
|
|
|
83
86
|
lines.push(` url: ${yamlQuote(s.url)}`);
|
|
84
87
|
}
|
|
85
88
|
}
|
|
89
|
+
if (webmcpServers.length > 0) {
|
|
90
|
+
// YAML flow-style for compactness (registry ids, no spaces).
|
|
91
|
+
lines.push(`webmcp_servers: [${webmcpServers.map(yamlQuote).join(', ')}]`);
|
|
92
|
+
}
|
|
86
93
|
lines.push('---', '');
|
|
87
94
|
return lines.join('\n');
|
|
88
95
|
}
|
|
89
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Read canvas.enabledServerIds — the registry ids (e.g. 'autoui', 'd3',
|
|
99
|
+
* 'observable-plot') of bundled WebMCP servers active in this notebook.
|
|
100
|
+
* The viewer re-instantiates them from @webmcp-auto-ui/servers on load.
|
|
101
|
+
*/
|
|
102
|
+
function collectEnabledWebmcpServers(): string[] {
|
|
103
|
+
try {
|
|
104
|
+
const ids = (canvasVanilla as { enabledServerIds?: string[] }).enabledServerIds ?? [];
|
|
105
|
+
return ids.filter((id): id is string => typeof id === 'string' && id.length > 0);
|
|
106
|
+
} catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
90
111
|
function extractDescription(state: NotebookState): string {
|
|
91
112
|
for (const cell of state.cells) {
|
|
92
113
|
if (cell.type !== 'md') continue;
|
|
@@ -25,6 +25,7 @@ export type CellResult =
|
|
|
25
25
|
| { ok: true; kind: 'table'; rows: Record<string, unknown>[]; columns: string[]; rowCount: number; truncated?: boolean; durationMs: number; logs?: string[] }
|
|
26
26
|
| { ok: true; kind: 'value'; value: unknown; durationMs: number; logs?: string[] }
|
|
27
27
|
| { ok: true; kind: 'chart'; spec: unknown; durationMs: number; logs?: string[] }
|
|
28
|
+
| { ok: true; kind: 'widget'; widgets: Array<{ name: string; params: Record<string, unknown> }>; durationMs: number; logs?: string[] }
|
|
28
29
|
| { ok: true; kind: 'empty'; durationMs: number; logs?: string[] }
|
|
29
30
|
| { ok: false; error: string; errorKind?: 'syntax' | 'runtime' | 'timeout' | 'schema'; durationMs: number; logs?: string[] };
|
|
30
31
|
|
|
@@ -46,6 +47,8 @@ export interface NotebookCell {
|
|
|
46
47
|
varname?: string; // named output (compact)
|
|
47
48
|
hideSource?: boolean;
|
|
48
49
|
hideResult?: boolean;
|
|
50
|
+
/** view-mode: collapse the console logs panel under the head bar. Default true in view mode. */
|
|
51
|
+
hideLogs?: boolean;
|
|
49
52
|
runState?: RunState;
|
|
50
53
|
lastMs?: number;
|
|
51
54
|
status?: 'fresh' | 'stale';
|
|
@@ -71,6 +74,16 @@ export interface NotebookState {
|
|
|
71
74
|
* a RuntimeOverlay consumed at render. Default false (frozen snapshots).
|
|
72
75
|
*/
|
|
73
76
|
autoRun?: boolean;
|
|
77
|
+
/** Bundled WebMCP server instances active in this notebook. Populated from
|
|
78
|
+
* `data.webmcpServers` at mount; used by the widget picker (+widget) and as
|
|
79
|
+
* fallback for `mountWidget` when a custom-element tag isn't defined. */
|
|
80
|
+
webmcpServers?: import('@webmcp-auto-ui/core').WebMcpServer[];
|
|
81
|
+
/** Host-supplied LLM proxy endpoint. The notebook agent posts to this URL.
|
|
82
|
+
* Defaults to '/api/chat' when absent — flex sets '/flex/api/chat' to
|
|
83
|
+
* match its base path; notebook-viewer keeps the default. */
|
|
84
|
+
chatApiBase?: string;
|
|
85
|
+
/** Suppress the published-slug chip in the header (notebook-viewer use). */
|
|
86
|
+
hidePublishBadge?: boolean;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
export interface HistoryEntry {
|
|
@@ -189,6 +202,9 @@ export function createState(initial?: Partial<NotebookState>): NotebookState {
|
|
|
189
202
|
autoRun: initial?.autoRun ?? false,
|
|
190
203
|
publishedSlug: initial?.publishedSlug,
|
|
191
204
|
publishedToken: initial?.publishedToken,
|
|
205
|
+
webmcpServers: initial?.webmcpServers,
|
|
206
|
+
chatApiBase: initial?.chatApiBase,
|
|
207
|
+
hidePublishBadge: initial?.hidePublishBadge,
|
|
192
208
|
};
|
|
193
209
|
}
|
|
194
210
|
|
|
@@ -308,17 +324,18 @@ export async function runAutoRefresh(opts: AutoRefreshOptions): Promise<AutoRefr
|
|
|
308
324
|
onTick?.(overlay);
|
|
309
325
|
|
|
310
326
|
const summary: AutoRefreshSummary = { rerun: 0, frozen: 0, stale: 0, failed: 0 };
|
|
327
|
+
const sharedSignal = signal ?? new AbortController().signal;
|
|
311
328
|
|
|
312
|
-
|
|
313
|
-
if (
|
|
314
|
-
if (!isReRunnable(cell)) { summary.frozen++;
|
|
329
|
+
const tasks = state.cells.map(async (cell) => {
|
|
330
|
+
if (sharedSignal.aborted) return;
|
|
331
|
+
if (!isReRunnable(cell)) { summary.frozen++; return; }
|
|
315
332
|
|
|
316
333
|
overlay.status.set(cell.id, 'running');
|
|
317
334
|
onCellChange?.(cell.id);
|
|
318
335
|
onTick?.(overlay);
|
|
319
336
|
|
|
320
337
|
try {
|
|
321
|
-
const result = await runner(cell,
|
|
338
|
+
const result = await runner(cell, sharedSignal);
|
|
322
339
|
if (result.ok) {
|
|
323
340
|
overlay.outputs.set(cell.id, { result, refreshedAt: Date.now() });
|
|
324
341
|
overlay.status.set(cell.id, 'fresh');
|
|
@@ -334,7 +351,9 @@ export async function runAutoRefresh(opts: AutoRefreshOptions): Promise<AutoRefr
|
|
|
334
351
|
}
|
|
335
352
|
onCellChange?.(cell.id);
|
|
336
353
|
onTick?.(overlay);
|
|
337
|
-
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
await Promise.all(tasks);
|
|
338
357
|
|
|
339
358
|
overlay.finishedAt = Date.now();
|
|
340
359
|
onTick?.(overlay);
|
|
@@ -683,7 +702,8 @@ export interface DataServerDescriptor {
|
|
|
683
702
|
/** Real server name from MCP handshake (initResult.serverInfo.name, aliased). */
|
|
684
703
|
serverName?: string;
|
|
685
704
|
url?: string;
|
|
686
|
-
|
|
705
|
+
/** 'data' = remote MCP via canvas.dataServers, 'webmcp' = bundled JS server. */
|
|
706
|
+
kind?: 'data' | 'webmcp' | string;
|
|
687
707
|
tools?: DataServerTool[];
|
|
688
708
|
recipes?: DataServerRecipe[];
|
|
689
709
|
}
|
|
@@ -734,6 +754,30 @@ export function collectDataServers(data: Record<string, unknown>): DataServerDes
|
|
|
734
754
|
}));
|
|
735
755
|
}
|
|
736
756
|
|
|
757
|
+
/**
|
|
758
|
+
* Project bundled WebMCP server instances into the same descriptor shape used
|
|
759
|
+
* by the left pane, so they render alongside remote data servers in a separate
|
|
760
|
+
* "Widgets" group. Recipe bodies are inlined (no get_recipe roundtrip needed).
|
|
761
|
+
*/
|
|
762
|
+
export function collectWebmcpServers(
|
|
763
|
+
servers: import('@webmcp-auto-ui/core').WebMcpServer[] | undefined,
|
|
764
|
+
): DataServerDescriptor[] {
|
|
765
|
+
if (!servers?.length) return [];
|
|
766
|
+
return servers.map((s) => {
|
|
767
|
+
const layer = (() => { try { return s.layer(); } catch { return { recipes: [], tools: [] }; } })();
|
|
768
|
+
return {
|
|
769
|
+
name: s.name,
|
|
770
|
+
label: s.name,
|
|
771
|
+
kind: 'webmcp' as const,
|
|
772
|
+
recipes: (layer.recipes ?? []).map((r: any) => ({ name: r.name, description: r.description, body: r.body })),
|
|
773
|
+
// Tools of bundled WebMCP servers (search_recipes, widget_display, canvas, …)
|
|
774
|
+
// are agent-facing primitives, not callable from a JS cell via callTool().
|
|
775
|
+
// Hide them from the Resources pane to avoid generating broken cells.
|
|
776
|
+
tools: [],
|
|
777
|
+
};
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
737
781
|
|
|
738
782
|
// ---------------------------------------------------------------------------
|
|
739
783
|
// Publish controls (button + optional badge + optional footer)
|
|
@@ -765,16 +809,25 @@ function publishUrlFor(slug: string): string {
|
|
|
765
809
|
return `${NB_PUBLISH_HOST}/p/${slug}`;
|
|
766
810
|
}
|
|
767
811
|
|
|
812
|
+
function publishMode(state: NotebookState): 'publish' | 'save' | 'fork' {
|
|
813
|
+
if (!state.publishedSlug) return 'publish';
|
|
814
|
+
return state.publishedToken ? 'save' : 'fork';
|
|
815
|
+
}
|
|
816
|
+
|
|
768
817
|
function publishBtnLabel(state: NotebookState): string {
|
|
769
|
-
|
|
818
|
+
const m = publishMode(state);
|
|
819
|
+
return m === 'save' ? '💾 save' : m === 'fork' ? '🍴 fork' : '📤 publish';
|
|
770
820
|
}
|
|
771
821
|
|
|
772
822
|
function refreshPublishControls(state: NotebookState, controls: PublishControlsHandles): void {
|
|
773
823
|
const { btn, badge, footer } = controls;
|
|
774
824
|
btn.textContent = publishBtnLabel(state);
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
825
|
+
const mode = publishMode(state);
|
|
826
|
+
btn.dataset.state = mode === 'publish' ? 'draft' : mode === 'fork' ? 'fork' : 'published';
|
|
827
|
+
if (mode === 'save') {
|
|
828
|
+
btn.title = `Save changes to ${publishUrlFor(state.publishedSlug!)}`;
|
|
829
|
+
} else if (mode === 'fork') {
|
|
830
|
+
btn.title = 'Fork this notebook — publish a copy you own';
|
|
778
831
|
} else {
|
|
779
832
|
btn.title = 'Publish this notebook';
|
|
780
833
|
}
|
|
@@ -790,8 +843,18 @@ function refreshPublishControls(state: NotebookState, controls: PublishControlsH
|
|
|
790
843
|
}
|
|
791
844
|
if (footer) {
|
|
792
845
|
if (state.publishedSlug) {
|
|
793
|
-
const
|
|
794
|
-
|
|
846
|
+
const publicUrl = publishUrlFor(state.publishedSlug);
|
|
847
|
+
if (state.publishedToken) {
|
|
848
|
+
// Author view — show the author URL (with ?t=) so the author keeps their
|
|
849
|
+
// edit access even after the clipboard is overwritten. The public URL is
|
|
850
|
+
// exposed via a copy button next to it for sharing.
|
|
851
|
+
const authorUrl = `${publicUrl}?t=${encodeURIComponent(state.publishedToken)}`;
|
|
852
|
+
footer.innerHTML =
|
|
853
|
+
`Published at <a href="${escapeHtml(authorUrl)}" target="_blank" rel="noopener">${escapeHtml(publicUrl)}</a>` +
|
|
854
|
+
` <button type="button" class="nb-copy-public-link" title="Copy public link (no edit token)" data-public-url="${escapeHtml(publicUrl)}">copy public link</button>`;
|
|
855
|
+
} else {
|
|
856
|
+
footer.innerHTML = `Published at <a href="${escapeHtml(publicUrl)}" target="_blank" rel="noopener">${escapeHtml(publicUrl)}</a>`;
|
|
857
|
+
}
|
|
795
858
|
footer.style.display = '';
|
|
796
859
|
} else {
|
|
797
860
|
footer.innerHTML = '';
|
|
@@ -847,20 +910,24 @@ export function createPublishControls(state: NotebookState, opts: PublishControl
|
|
|
847
910
|
|
|
848
911
|
const onClick = async () => {
|
|
849
912
|
const prevLabel = btn.textContent ?? '';
|
|
913
|
+
const mode = publishMode(state);
|
|
850
914
|
btn.disabled = true;
|
|
851
|
-
btn.textContent =
|
|
915
|
+
btn.textContent = mode === 'save' ? '… saving' : mode === 'fork' ? '… forking' : '… publishing';
|
|
852
916
|
try {
|
|
853
917
|
// HyperSkill standalone markdown — frontmatter (title/description/servers)
|
|
854
918
|
// + body with fenced cells. Re-parsable via parseFrontmatter + parseBody.
|
|
855
919
|
const markdown = serializeToMarkdown(state);
|
|
920
|
+
// Fork = publish a fresh copy (no slug/token) — the server allocates a new
|
|
921
|
+
// slug and token, and the visitor becomes the author of the copy.
|
|
922
|
+
const payload: Record<string, unknown> = { markdown };
|
|
923
|
+
if (mode === 'save') {
|
|
924
|
+
payload.slug = state.publishedSlug;
|
|
925
|
+
payload.token = state.publishedToken;
|
|
926
|
+
}
|
|
856
927
|
const res = await fetch(`${NB_PUBLISH_HOST}/api/publish`, {
|
|
857
928
|
method: 'POST',
|
|
858
929
|
headers: { 'content-type': 'application/json' },
|
|
859
|
-
body: JSON.stringify(
|
|
860
|
-
markdown,
|
|
861
|
-
slug: state.publishedSlug,
|
|
862
|
-
token: state.publishedToken,
|
|
863
|
-
}),
|
|
930
|
+
body: JSON.stringify(payload),
|
|
864
931
|
});
|
|
865
932
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
866
933
|
const reply: any = await res.json();
|
|
@@ -873,15 +940,20 @@ export function createPublishControls(state: NotebookState, opts: PublishControl
|
|
|
873
940
|
// URL). Public visitors share the bare URL without ?t.
|
|
874
941
|
const authorUrl = `${baseUrl}?t=${encodeURIComponent(String(reply.token))}`;
|
|
875
942
|
try { await navigator.clipboard?.writeText?.(authorUrl); } catch { /* ignore */ }
|
|
943
|
+
// On fork, also rewrite the browser URL so the visitor sees the new slug
|
|
944
|
+
// and so a refresh keeps them as author (token persisted in localStorage).
|
|
945
|
+
if (mode === 'fork' && typeof window !== 'undefined') {
|
|
946
|
+
try {
|
|
947
|
+
window.localStorage.setItem(`nb-token-${reply.slug}`, String(reply.token));
|
|
948
|
+
} catch {}
|
|
949
|
+
try { window.history.replaceState({}, '', baseUrl); } catch {}
|
|
950
|
+
}
|
|
876
951
|
const updated = Boolean(reply.updated);
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
? `saved · ${baseUrl.replace(/^https?:\/\//, '')} (author link copied)`
|
|
880
|
-
: `published · ${baseUrl.replace(/^https?:\/\//, '')} (author link copied)`
|
|
881
|
-
);
|
|
952
|
+
const action = mode === 'fork' ? 'forked' : updated ? 'saved' : 'published';
|
|
953
|
+
toast(`${action} · ${baseUrl.replace(/^https?:\/\//, '')} (author link copied)`);
|
|
882
954
|
opts.onPublished?.({ slug: String(reply.slug), url: baseUrl, updated });
|
|
883
955
|
} catch (err: any) {
|
|
884
|
-
toast(
|
|
956
|
+
toast(`${mode} failed · ${String(err?.message ?? err)}`, true);
|
|
885
957
|
btn.textContent = prevLabel;
|
|
886
958
|
} finally {
|
|
887
959
|
btn.disabled = false;
|
|
@@ -891,8 +963,26 @@ export function createPublishControls(state: NotebookState, opts: PublishControl
|
|
|
891
963
|
|
|
892
964
|
btn.addEventListener('click', onClick);
|
|
893
965
|
|
|
966
|
+
// Delegated handler for the "copy public link" button rendered in the footer
|
|
967
|
+
// when the author has a token. We can't bind directly because the footer
|
|
968
|
+
// innerHTML is rebuilt by refreshPublishControls.
|
|
969
|
+
const onFooterClick = (e: Event) => {
|
|
970
|
+
const target = e.target as HTMLElement | null;
|
|
971
|
+
const copyBtn = target?.closest('.nb-copy-public-link') as HTMLElement | null;
|
|
972
|
+
if (!copyBtn) return;
|
|
973
|
+
e.preventDefault();
|
|
974
|
+
const url = copyBtn.dataset.publicUrl ?? '';
|
|
975
|
+
if (!url) return;
|
|
976
|
+
void (async () => {
|
|
977
|
+
try { await navigator.clipboard?.writeText?.(url); toast(`public link copied · ${url.replace(/^https?:\/\//, '')}`); }
|
|
978
|
+
catch { toast('copy failed', true); }
|
|
979
|
+
})();
|
|
980
|
+
};
|
|
981
|
+
if (footer) footer.addEventListener('click', onFooterClick);
|
|
982
|
+
|
|
894
983
|
return () => {
|
|
895
984
|
btn.removeEventListener('click', onClick);
|
|
985
|
+
if (footer) footer.removeEventListener('click', onFooterClick);
|
|
896
986
|
btn.parentNode?.removeChild(btn);
|
|
897
987
|
badge?.parentNode?.removeChild(badge);
|
|
898
988
|
footer?.parentNode?.removeChild(footer);
|
|
@@ -1160,9 +1250,79 @@ const NOTEBOOK_STYLES = `
|
|
|
1160
1250
|
|
|
1161
1251
|
.nb-root.nb-view-mode .nb-drag-handle,
|
|
1162
1252
|
.nb-root.nb-view-mode .nb-icon-btn.nb-danger,
|
|
1163
|
-
.nb-root.nb-view-mode .nb-
|
|
1164
|
-
.nb-root.nb-view-mode .
|
|
1165
|
-
|
|
1253
|
+
.nb-root.nb-view-mode .nb-add-cell,
|
|
1254
|
+
.nb-root.nb-view-mode .nbe-cell-actionbar { display: none !important; }
|
|
1255
|
+
|
|
1256
|
+
/* Per-cell action bar — slim row of insert/agent shortcuts shown under each cell in edit mode. */
|
|
1257
|
+
.nbe-cell-actionbar {
|
|
1258
|
+
display: flex; gap: 4px; align-items: center;
|
|
1259
|
+
margin: 4px 0 14px 0; padding-left: 28px;
|
|
1260
|
+
opacity: 0.55; transition: opacity 120ms ease;
|
|
1261
|
+
}
|
|
1262
|
+
.nbe-cell-actionbar:hover { opacity: 1; }
|
|
1263
|
+
.nb-cellbar-btn {
|
|
1264
|
+
font-size: 11px; padding: 2px 8px; line-height: 1.4;
|
|
1265
|
+
background: transparent; border: 1px dashed var(--color-border, #c8c2b4);
|
|
1266
|
+
color: var(--color-text2, #645d4f); border-radius: 6px; cursor: pointer;
|
|
1267
|
+
}
|
|
1268
|
+
.nb-cellbar-btn:hover { background: var(--color-bg2, #f4efdf); color: var(--color-text1, #1c1a16); border-style: solid; }
|
|
1269
|
+
|
|
1270
|
+
/* Widget picker overlay (+widget action) */
|
|
1271
|
+
.nbe-picker-overlay {
|
|
1272
|
+
position: fixed; inset: 0; background: rgba(20, 18, 14, 0.55);
|
|
1273
|
+
display: flex; align-items: flex-start; justify-content: center;
|
|
1274
|
+
padding-top: 80px; z-index: 9999;
|
|
1275
|
+
font-family: var(--font-sans, system-ui);
|
|
1276
|
+
}
|
|
1277
|
+
.nbe-picker {
|
|
1278
|
+
background: var(--color-bg1, #faf6e6); color: var(--color-text1, #1c1a16);
|
|
1279
|
+
border-radius: 12px; box-shadow: 0 24px 64px rgba(0,0,0,0.18);
|
|
1280
|
+
width: min(620px, calc(100vw - 32px)); max-height: 70vh;
|
|
1281
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
1282
|
+
}
|
|
1283
|
+
.nbe-picker > header {
|
|
1284
|
+
display: flex; gap: 8px; align-items: center;
|
|
1285
|
+
padding: 10px 12px; border-bottom: 1px solid var(--color-border, #c8c2b4);
|
|
1286
|
+
}
|
|
1287
|
+
.nbe-picker-q {
|
|
1288
|
+
flex: 1; font-family: inherit; font-size: 14px;
|
|
1289
|
+
padding: 6px 10px; border: 1px solid var(--color-border, #c8c2b4);
|
|
1290
|
+
background: var(--color-bg0, #ffffff); border-radius: 6px;
|
|
1291
|
+
}
|
|
1292
|
+
.nbe-picker-close {
|
|
1293
|
+
background: transparent; border: none; cursor: pointer;
|
|
1294
|
+
font-size: 16px; color: var(--color-text2, #645d4f); padding: 4px 8px;
|
|
1295
|
+
}
|
|
1296
|
+
.nbe-picker-list {
|
|
1297
|
+
list-style: none; margin: 0; padding: 4px 0;
|
|
1298
|
+
overflow-y: auto; flex: 1;
|
|
1299
|
+
}
|
|
1300
|
+
.nbe-picker-list li {
|
|
1301
|
+
display: grid; grid-template-columns: minmax(120px, auto) auto 1fr;
|
|
1302
|
+
gap: 12px; align-items: baseline;
|
|
1303
|
+
padding: 8px 14px; cursor: pointer;
|
|
1304
|
+
border-bottom: 1px solid var(--color-bg2, #f0eada);
|
|
1305
|
+
}
|
|
1306
|
+
.nbe-picker-list li:hover { background: var(--color-bg2, #f4efdf); }
|
|
1307
|
+
.nbe-picker-name { font-family: var(--font-mono, monospace); font-size: 13px; color: var(--color-text1, #1c1a16); }
|
|
1308
|
+
.nbe-picker-server { font-size: 11px; color: var(--color-accent, #1d6f5f); text-transform: lowercase; }
|
|
1309
|
+
.nbe-picker-desc { font-size: 12px; color: var(--color-text2, #645d4f); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1310
|
+
|
|
1311
|
+
/* Agent bar (+agent) */
|
|
1312
|
+
.nbe-agent-bar {
|
|
1313
|
+
margin: 0 0 16px 28px;
|
|
1314
|
+
padding: 8px 10px;
|
|
1315
|
+
background: var(--color-bg2, #f4efdf);
|
|
1316
|
+
border-radius: 8px;
|
|
1317
|
+
border: 1px solid var(--color-border, #c8c2b4);
|
|
1318
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
1319
|
+
}
|
|
1320
|
+
.nbe-agent-bar auto-chat-input { display: block; }
|
|
1321
|
+
.nbe-agent-status {
|
|
1322
|
+
font-family: var(--font-mono, monospace); font-size: 11px;
|
|
1323
|
+
color: var(--color-text2, #645d4f);
|
|
1324
|
+
padding: 0 4px;
|
|
1325
|
+
}
|
|
1166
1326
|
/* Run controls (.nb-ctl-pill) remain active in view mode — users can execute cells
|
|
1167
1327
|
* even when the notebook is read-only; only editing the source is locked. */
|
|
1168
1328
|
.nb-root.nb-view-mode textarea,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
export interface CardsData {
|
|
16
16
|
title?: string;
|
|
17
17
|
cards?: CardItem[];
|
|
18
|
+
items?: CardItem[];
|
|
18
19
|
minCardWidth?: string;
|
|
19
20
|
gap?: string;
|
|
20
21
|
emptyMessage?: string;
|
|
@@ -28,7 +29,8 @@
|
|
|
28
29
|
let { data = {}, oncardclick }: Props = $props();
|
|
29
30
|
|
|
30
31
|
const cards = $derived.by<CardItem[]>(() => {
|
|
31
|
-
|
|
32
|
+
const list = data?.cards ?? data?.items;
|
|
33
|
+
if (Array.isArray(list) && (list as unknown[]).length) return list as CardItem[];
|
|
32
34
|
return [];
|
|
33
35
|
});
|
|
34
36
|
</script>
|
|
@@ -41,20 +41,86 @@
|
|
|
41
41
|
return typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Normalize a single series item:
|
|
46
|
+
* - `values: number[]` → kept as-is
|
|
47
|
+
* - `value: number` → wrapped as `[value]` (tolerance #1)
|
|
48
|
+
*/
|
|
49
|
+
function normalizeItem(o: Record<string, unknown>): ChartDataset | null {
|
|
50
|
+
// Prefer explicit `values` array; fall back to scalar `value`
|
|
51
|
+
let values: number[];
|
|
52
|
+
if (Array.isArray(o.values)) {
|
|
53
|
+
values = coerceValues(o.values);
|
|
54
|
+
} else if (typeof o.value === 'number' && Number.isFinite(o.value)) {
|
|
55
|
+
values = [o.value];
|
|
56
|
+
} else if (typeof o.value === 'string') {
|
|
57
|
+
const n = Number(o.value);
|
|
58
|
+
values = Number.isFinite(n) ? [n] : [];
|
|
59
|
+
} else {
|
|
60
|
+
values = [];
|
|
61
|
+
}
|
|
62
|
+
if (!values.length) return null;
|
|
63
|
+
return {
|
|
64
|
+
label: isPrimitiveLabel(o.label) ? String(o.label) : undefined,
|
|
65
|
+
values,
|
|
66
|
+
color: typeof o.color === 'string' ? o.color : undefined,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Detect categorical shorthand: [{label?, value}, ...] — one point per item, single series. */
|
|
71
|
+
function isCategoricalArray(raw: unknown): boolean {
|
|
72
|
+
if (!Array.isArray(raw) || raw.length === 0) return false;
|
|
73
|
+
return raw.every(item =>
|
|
74
|
+
item && typeof item === 'object'
|
|
75
|
+
&& !Array.isArray((item as Record<string, unknown>).values)
|
|
76
|
+
&& (item as Record<string, unknown>).value !== undefined
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
44
80
|
function normalizeDatasets(raw: unknown): ChartDataset[] {
|
|
45
81
|
if (!Array.isArray(raw)) return [];
|
|
82
|
+
// Categorical shorthand: collapse N items into ONE series of N points.
|
|
83
|
+
// Without this, recipes that send `data: [{label, value}, ...]` produce N
|
|
84
|
+
// datasets of 1 point each, all rendering at x=0 (all dots stack vertically).
|
|
85
|
+
if (isCategoricalArray(raw)) {
|
|
86
|
+
const values = raw.map(item => {
|
|
87
|
+
const v = (item as Record<string, unknown>).value;
|
|
88
|
+
const n = typeof v === 'number' ? v : Number(v as string);
|
|
89
|
+
return Number.isFinite(n) ? n : 0;
|
|
90
|
+
});
|
|
91
|
+
return [{ values }];
|
|
92
|
+
}
|
|
46
93
|
return raw.flatMap(item => {
|
|
47
94
|
if (!item || typeof item !== 'object') return [];
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
const values = coerceValues(o.values);
|
|
51
|
-
if (!values.length) return [];
|
|
52
|
-
return [{ label: isPrimitiveLabel(o.label) ? String(o.label) : undefined, values, color: typeof o.color === 'string' ? o.color : undefined }];
|
|
95
|
+
const result = normalizeItem(item as Record<string, unknown>);
|
|
96
|
+
return result ? [result] : [];
|
|
53
97
|
});
|
|
54
98
|
}
|
|
55
99
|
|
|
56
|
-
|
|
57
|
-
|
|
100
|
+
function extractCategoricalLabels(raw: unknown): string[] {
|
|
101
|
+
if (!isCategoricalArray(raw)) return [];
|
|
102
|
+
return (raw as unknown[]).map(item => {
|
|
103
|
+
const l = (item as Record<string, unknown>).label;
|
|
104
|
+
return typeof l === 'string' || typeof l === 'number' ? String(l) : '';
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve datasets from `data` prop, tolerating two shapes:
|
|
110
|
+
* - `{ datasets: [...] }` — canonical
|
|
111
|
+
* - `{ data: [...] }` — agent shorthand (schema uses `data` key)
|
|
112
|
+
*/
|
|
113
|
+
const rawArray = $derived(
|
|
114
|
+
Array.isArray((data as Record<string, unknown> | undefined)?.['datasets'])
|
|
115
|
+
? (data as Record<string, unknown>)['datasets']
|
|
116
|
+
: (data as Record<string, unknown> | undefined)?.['data'],
|
|
117
|
+
);
|
|
118
|
+
const datasets = $derived<ChartDataset[]>(normalizeDatasets(rawArray));
|
|
119
|
+
const labels = $derived<string[]>(
|
|
120
|
+
Array.isArray(data?.labels)
|
|
121
|
+
? (data!.labels! as unknown[]).filter(isPrimitiveLabel).map(String)
|
|
122
|
+
: extractCategoricalLabels(rawArray),
|
|
123
|
+
);
|
|
58
124
|
const type = $derived(data?.type ?? 'bar');
|
|
59
125
|
const isPie = $derived(type === 'pie' || type === 'donut');
|
|
60
126
|
const allVals = $derived(datasets.flatMap(d => d.values));
|
|
@@ -23,15 +23,36 @@
|
|
|
23
23
|
let sortCol = $state<string|null>(null);
|
|
24
24
|
let sortAsc = $state(true);
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
// ── Tolerance 1: alias `headers` → `columns` ─────────────────────────────
|
|
27
|
+
// Some callers pass `headers` (array of strings or column objects) instead of
|
|
28
|
+
// `columns`. Normalise so downstream logic only sees `columns`.
|
|
29
|
+
const resolvedColumnDefs = $derived.by<DataTableColumn[] | null>(() => {
|
|
30
|
+
const src = (data as DataTableData & { headers?: unknown }).headers ?? data?.columns;
|
|
31
|
+
if (!Array.isArray(src) || !src.length) return null;
|
|
32
|
+
// headers can be plain strings ["A","B"] or full column objects [{key,label}]
|
|
33
|
+
return (src as unknown[]).map((h) =>
|
|
34
|
+
typeof h === 'string' ? { key: h, label: h } : h as DataTableColumn
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ── Tolerance 2: rows as array-of-arrays ─────────────────────────────────
|
|
39
|
+
// If the first row is an array, map each row to an object using `resolvedColumnDefs`
|
|
40
|
+
// (falling back to positional keys "0","1",… when no column defs are available).
|
|
41
|
+
const rows = $derived.by<Record<string,unknown>[]>(() => {
|
|
42
|
+
if (!Array.isArray(data?.rows) || !(data!.rows as unknown[]).length) return [];
|
|
43
|
+
const raw = data!.rows as unknown[];
|
|
44
|
+
if (!Array.isArray(raw[0])) return raw as Record<string,unknown>[];
|
|
45
|
+
// array-of-arrays: convert to array-of-objects
|
|
46
|
+
const keys = resolvedColumnDefs?.map(c => c.key)
|
|
47
|
+
?? (raw[0] as unknown[]).map((_, i) => String(i));
|
|
48
|
+
return (raw as unknown[][]).map(arr =>
|
|
49
|
+
Object.fromEntries(keys.map((k, i) => [k, arr[i]]))
|
|
50
|
+
);
|
|
51
|
+
});
|
|
31
52
|
|
|
32
53
|
const columns = $derived<DataTableColumn[]>(
|
|
33
|
-
|
|
34
|
-
?
|
|
54
|
+
resolvedColumnDefs && resolvedColumnDefs.length
|
|
55
|
+
? resolvedColumnDefs
|
|
35
56
|
: rows.length > 0
|
|
36
57
|
? Object.keys(rows[0] as object).map(k => ({ key: k, label: k }))
|
|
37
58
|
: []
|