@webmcp-auto-ui/ui 2.5.27 → 2.5.28

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 (82) hide show
  1. package/package.json +15 -3
  2. package/src/agent/DataServersPanel.svelte +164 -0
  3. package/src/agent/LLMSelector.svelte +11 -3
  4. package/src/agent/ModelCacheManager.svelte +359 -0
  5. package/src/index.ts +42 -30
  6. package/src/widgets/WidgetRenderer.svelte +114 -104
  7. package/src/widgets/export-widget.ts +28 -1
  8. package/src/widgets/helpers/safe-image.ts +78 -0
  9. package/src/widgets/notebook/.gitkeep +0 -0
  10. package/src/widgets/notebook/chart-renderer.ts +63 -0
  11. package/src/widgets/notebook/compact.ts +823 -0
  12. package/src/widgets/notebook/document.ts +1065 -0
  13. package/src/widgets/notebook/editorial.ts +936 -0
  14. package/src/widgets/notebook/executors/.gitkeep +1 -0
  15. package/src/widgets/notebook/executors/index.ts +4 -0
  16. package/src/widgets/notebook/executors/js-worker.ts +269 -0
  17. package/src/widgets/notebook/executors/sql.ts +206 -0
  18. package/src/widgets/notebook/import-modals.ts +553 -0
  19. package/src/widgets/notebook/left-pane.ts +249 -0
  20. package/src/widgets/notebook/prose.ts +280 -0
  21. package/src/widgets/notebook/recipe-browser.ts +350 -0
  22. package/src/widgets/notebook/recipes/compact.md +124 -0
  23. package/src/widgets/notebook/recipes/document.md +139 -0
  24. package/src/widgets/notebook/recipes/editorial.md +120 -0
  25. package/src/widgets/notebook/recipes/workspace.md +119 -0
  26. package/src/widgets/notebook/resource-extractor.ts +162 -0
  27. package/src/widgets/notebook/share-handlers.ts +222 -0
  28. package/src/widgets/notebook/shared.ts +1592 -0
  29. package/src/widgets/notebook/workspace.ts +852 -0
  30. package/src/widgets/rich/cards.ts +181 -0
  31. package/src/widgets/rich/carousel.ts +319 -0
  32. package/src/widgets/rich/chart-rich.ts +386 -0
  33. package/src/widgets/rich/d3.ts +503 -0
  34. package/src/widgets/rich/data-table.ts +342 -0
  35. package/src/widgets/rich/gallery.ts +350 -0
  36. package/src/widgets/rich/grid-data.ts +173 -0
  37. package/src/widgets/rich/hemicycle.ts +313 -0
  38. package/src/widgets/rich/js-sandbox.ts +106 -0
  39. package/src/widgets/rich/json-viewer.ts +202 -0
  40. package/src/widgets/rich/log.ts +143 -0
  41. package/src/widgets/rich/map.ts +218 -0
  42. package/src/widgets/rich/profile.ts +256 -0
  43. package/src/widgets/rich/sankey.ts +262 -0
  44. package/src/widgets/rich/stat-card.ts +125 -0
  45. package/src/widgets/rich/timeline.ts +179 -0
  46. package/src/widgets/rich/trombinoscope.ts +246 -0
  47. package/src/widgets/simple/actions.ts +89 -0
  48. package/src/widgets/simple/alert.ts +100 -0
  49. package/src/widgets/simple/chart.ts +189 -0
  50. package/src/widgets/simple/code.ts +79 -0
  51. package/src/widgets/simple/kv.ts +68 -0
  52. package/src/widgets/simple/list.ts +89 -0
  53. package/src/widgets/simple/stat.ts +58 -0
  54. package/src/widgets/simple/tags.ts +125 -0
  55. package/src/widgets/simple/text.ts +198 -0
  56. package/src/widgets/SafeImage.svelte +0 -76
  57. package/src/widgets/rich/Cards.svelte +0 -39
  58. package/src/widgets/rich/Carousel.svelte +0 -88
  59. package/src/widgets/rich/Chart.svelte +0 -142
  60. package/src/widgets/rich/D3Widget.svelte +0 -378
  61. package/src/widgets/rich/DataTable.svelte +0 -62
  62. package/src/widgets/rich/Gallery.svelte +0 -94
  63. package/src/widgets/rich/GridData.svelte +0 -44
  64. package/src/widgets/rich/Hemicycle.svelte +0 -78
  65. package/src/widgets/rich/JsSandbox.svelte +0 -51
  66. package/src/widgets/rich/JsonViewer.svelte +0 -42
  67. package/src/widgets/rich/LogViewer.svelte +0 -24
  68. package/src/widgets/rich/MapView.svelte +0 -140
  69. package/src/widgets/rich/ProfileCard.svelte +0 -59
  70. package/src/widgets/rich/Sankey.svelte +0 -56
  71. package/src/widgets/rich/StatCard.svelte +0 -35
  72. package/src/widgets/rich/Timeline.svelte +0 -43
  73. package/src/widgets/rich/Trombinoscope.svelte +0 -48
  74. package/src/widgets/simple/ActionsBlock.svelte +0 -15
  75. package/src/widgets/simple/AlertBlock.svelte +0 -11
  76. package/src/widgets/simple/ChartBlock.svelte +0 -21
  77. package/src/widgets/simple/CodeBlock.svelte +0 -11
  78. package/src/widgets/simple/KVBlock.svelte +0 -16
  79. package/src/widgets/simple/ListBlock.svelte +0 -17
  80. package/src/widgets/simple/StatBlock.svelte +0 -14
  81. package/src/widgets/simple/TagsBlock.svelte +0 -15
  82. package/src/widgets/simple/TextBlock.svelte +0 -122
@@ -0,0 +1,120 @@
1
+ ---
2
+ widget: notebook-editorial
3
+ description: Publication-ready notebook with serif prose and inline cells, all drag-and-droppable in a single ordered flow. Inspired by Observable — cells can be prose paragraphs, sql queries, or js charts, mixed freely in any order to build an article-like narrative.
4
+ schema:
5
+ type: object
6
+ properties:
7
+ id:
8
+ type: string
9
+ title:
10
+ type: string
11
+ mode:
12
+ type: string
13
+ enum: [edit, view]
14
+ kicker:
15
+ type: string
16
+ description: Small uppercase label above the title (e.g. "analysis", "memo", "brief"). Editable inline. Defaults to "untitled".
17
+ cells:
18
+ type: array
19
+ description: Mixed flow of prose and code cells. All share the same ordering and can be reordered together.
20
+ items:
21
+ type: object
22
+ required: [type, content]
23
+ properties:
24
+ type:
25
+ type: string
26
+ enum: [md, sql, js]
27
+ description: md = prose paragraph (markdown rendered + sanitized), sql = query cell with table output, js = code cell with chart output
28
+ content:
29
+ type: string
30
+ hideSource:
31
+ type: boolean
32
+ hideResult:
33
+ type: boolean
34
+ ---
35
+
36
+ ## When to use
37
+
38
+ Use `notebook-editorial` when the notebook is meant to be published or shared as a finished artifact:
39
+ - Research memos with code appendices visible on demand
40
+ - Blog-style writeups mixing narrative and runnable code
41
+ - Final deliverables where prose leads and code supports
42
+
43
+ The distinguishing feature: prose paragraphs and code cells share a single ordered list, both drag-and-droppable with the same handle. This lets users rearrange the story freely without thinking about "sections".
44
+
45
+ ## How to use
46
+
47
+ 1. **Start with prose-first seed content** and intersperse code cells:
48
+ ```
49
+ widget_display({name: "notebook-editorial", params: {
50
+ title: "Q3 observations",
51
+ kicker: "memo",
52
+ cells: [
53
+ {type: "md", content: "This memo covers the highlights of last quarter."},
54
+ {type: "md", content: "We first look at revenue, then at churn."},
55
+ {type: "sql", content: "select * from source limit 10"},
56
+ {type: "md", content: "The table above suggests..."},
57
+ {type: "js", content: "// render a chart"}
58
+ ]
59
+ }})
60
+ ```
61
+
62
+ 2. **Use prose paragraphs as transitions** between code blocks. The layout emphasizes reading flow.
63
+
64
+ 3. **Code cells render their result in the editorial style**:
65
+ - SQL cells show a minimal mono-spaced table (live data from the connected MCP server's `*_query_sql` tool).
66
+ - JS cells run in an isolated Web Worker with upstream named outputs in scope.
67
+
68
+ 4. **Reorder cells freely** — the user can drag a prose paragraph from the bottom to the top, or swap a chart and its introduction, all via the same handle.
69
+
70
+ ## Notes
71
+
72
+ - The serif font (EB Garamond, with Georgia fallback) applies only to prose content inside this widget — it signals "publication" the moment the user sees it.
73
+ - The **kicker** above the title ("analysis", "memo", "internal") is editable inline — click to rename. Keep it short.
74
+ - Prose cells are rendered via an HTML-sanitizing markdown pipeline: markdown syntax is resolved, unsafe tags are stripped (XSS closed), `<mark>` and other editorial tags are preserved.
75
+ - The footer exposes a single `share` button.
76
+ - Run / Stop controls are at the left of each code cell's header, same as the other notebook layouts.
77
+ - Unlike the other widgets, `notebook-editorial` does not separate prose and code into different flows — they are the same flow in one list.
78
+
79
+ ## Left pane — resources from connected servers
80
+
81
+ A collapsible **left pane** (bookmark-bar styling, collapsed by default) lists recipes and tools exposed by connected MCP data servers. Clicking any recipe opens a viewer modal; fenced code blocks expose a `↳ inject` button that drops the snippet into the article flow as a new cell.
82
+
83
+ Two toolbar buttons flank this pane:
84
+
85
+ - **`+ md`** — 3-tab modal (New / File / URL) to insert a prose paragraph from scratch, a local `.md` file, or a URL.
86
+ - **`+ recipe`** — 3-tab modal (Browser / File / URL) to import a recipe from a connected server, a local `.recipe.md` file, or a URL.
87
+
88
+ ## Share
89
+
90
+ The `share` button in the footer offers **four formats**:
91
+
92
+ - **Hyperskill link** — copies both the canonical Hyperskill URL and a short domain-scoped URL (`?n=<token>`). The read-only public viewer lives at `nb.hyperskills.net`.
93
+ - **Markdown** — downloads a `.md` file.
94
+ - **PNG** — snapshots the rendered article.
95
+ - **JSON** — exports full widget state.
96
+
97
+ ## Integration with connected data servers
98
+
99
+ An editorial piece earns its weight when the prose is anchored to real material. If a MCP **data** server is connected (say `tricoteuses` or `metmuseum`), the agent should — before composing the memo — go and see what the server has to offer:
100
+
101
+ 1. Call `{server}_list_recipes()` or `{server}_search_recipes(query)` to find recipes that speak to the subject at hand.
102
+ 2. Call `{server}_list_tools()` to survey the available tables and endpoints.
103
+ 3. For each recipe or table worth citing, seed one cell that lets the reader touch the evidence — a modest SQL `SELECT ... LIMIT 10`, a `run_script` call, or a prose paragraph that introduces the figure to come. Let prose and code alternate; the editorial flow is built for exactly that.
104
+ 4. Pass the server metadata through the `servers:` param so the footer's share affordance, the left pane, and the connect modal reflect the provenance of the piece:
105
+
106
+ ```ts
107
+ widget_display({
108
+ name: 'notebook-editorial',
109
+ params: {
110
+ title: '...',
111
+ kicker: 'memo',
112
+ cells: [...],
113
+ servers: [{ name: 'tricoteuses', url: 'https://...', kind: 'data' }]
114
+ }
115
+ })
116
+ ```
117
+
118
+ When no data server is connected, seed a prose-first skeleton that stakes out the argument and gently invites the reader to connect an MCP server so the memo can take on flesh.
119
+
120
+ **Filter rule**: only MCP *data* servers (`kind: 'data'`) belong in `servers:`. WebMCP UI servers like `autoui` are kept out — they hold no queryable material and have no place in the editorial masthead.
@@ -0,0 +1,119 @@
1
+ ---
2
+ widget: notebook-workspace
3
+ description: Dense analyst workspace with a header bar (title, draft/published tag, run all, publish) and a left sidebar listing data sources and cells for navigation. Cells display their name, execution time, and row counts. Targets data-team workflows with a publishable deliverable.
4
+ schema:
5
+ type: object
6
+ properties:
7
+ id:
8
+ type: string
9
+ title:
10
+ type: string
11
+ mode:
12
+ type: string
13
+ enum: [edit, view]
14
+ cells:
15
+ type: array
16
+ items:
17
+ type: object
18
+ required: [type, content]
19
+ properties:
20
+ type:
21
+ type: string
22
+ enum: [md, sql, js]
23
+ content:
24
+ type: string
25
+ name:
26
+ type: string
27
+ description: Cell name shown in sidebar navigation (e.g. "intro", "sql_sales", "plot")
28
+ hideSource:
29
+ type: boolean
30
+ hideResult:
31
+ type: boolean
32
+ ---
33
+
34
+ ## When to use
35
+
36
+ Use `notebook-workspace` for analyst-oriented work that needs:
37
+ - Multi-cell analyses with named, navigable cells (sidebar)
38
+ - A clear separation between the editing context and a publishable "app" view
39
+ - A data source reference permanently visible (even as placeholder: "no source connected")
40
+ - Team workflows where "run all" and "publish" are distinct actions
41
+
42
+ This layout is more enterprise-flavoured than `notebook-compact`. Prefer it when the user is building something to hand off, not just exploring.
43
+
44
+ ## How to use
45
+
46
+ 1. **Create with named cells** so the sidebar navigation is meaningful:
47
+ ```
48
+ widget_display({name: "notebook-workspace", params: {
49
+ title: "My analysis",
50
+ cells: [
51
+ {type: "md", name: "intro", content: "What we are investigating."},
52
+ {type: "sql", name: "fetch_rows", content: "select * from source limit 100"},
53
+ {type: "js", name: "visualize", content: "// chart the rows"}
54
+ ]
55
+ }})
56
+ ```
57
+
58
+ 2. **The sidebar shows** data sources (when connected) and the numbered list of cells. Clicking a cell item scrolls to it and focuses its textarea for immediate editing.
59
+
60
+ 3. **The header has** `run all` / `share` / `publish` buttons.
61
+ - **`run all`** sequentially executes every non-markdown cell from top to bottom.
62
+ - **`publish`** is the primary (accent-coloured) CTA — it flips the notebook to `mode: 'view'`, tags it `published` (from `draft`), and emits a Hyperskill share link automatically.
63
+ - The **title** is editable inline in the header and persists across reloads via localStorage.
64
+
65
+ ## Notes
66
+
67
+ - Cells are added via buttons in the sidebar (not in the header).
68
+ - Run controls (green/red pill) sit at the left of each cell's header, right after the drag handle.
69
+ - Each cell head can be renamed inline in the sidebar (click the "N · name" item).
70
+ - The sidebar under "sources" shows a placeholder when no MCP source is connected. Each source row is clickable: a `connect via mcp…` entry opens the servers modal so the user can hook one up without leaving the notebook.
71
+ - **SQL cells** dispatch to the connected MCP server's `*_query_sql` tool (auto-detected).
72
+ - **JS cells** execute in an isolated Web Worker with upstream named outputs injected as scope.
73
+
74
+ ## Left pane — resources from connected servers
75
+
76
+ A collapsible **left pane** (bookmark-bar styling, collapsed by default) lists recipes and tools exposed by connected MCP data servers. Clicking any recipe opens a viewer modal; each fenced code block inside exposes a `↳ inject` button that drops the snippet into the notebook as a new cell.
77
+
78
+ Two toolbar buttons flank this pane:
79
+
80
+ - **`+ md`** — 3-tab modal (New / File / URL) to create a markdown cell from scratch, from a local `.md` file, or from a URL.
81
+ - **`+ recipe`** — 3-tab modal (Browser / File / URL) to import a recipe from a connected server, a local `.recipe.md` file, or a URL.
82
+
83
+ ## Share & publish
84
+
85
+ The `share` button offers **four formats**:
86
+
87
+ - **Hyperskill link** — copies both the canonical Hyperskill URL and a short domain-scoped URL (`?n=<token>`). The published view is served read-only at `nb.hyperskills.net`.
88
+ - **Markdown** — downloads a `.md` file with the notebook content.
89
+ - **PNG** — snapshots the rendered notebook.
90
+ - **JSON** — exports full widget state.
91
+
92
+ `publish` wraps these: it sets `mode: 'view'`, tags the notebook `published`, and copies the Hyperskill link in one gesture — the canonical way to hand off a finished analysis.
93
+
94
+ ## Integration with connected data servers
95
+
96
+ If a MCP **data** server is currently connected (the user has linked one, e.g. `tricoteuses`, `metmuseum`), BEFORE seeding cells the agent MUST:
97
+
98
+ 1. Call `{server}_list_recipes()` or `{server}_search_recipes(query)` to discover data recipes relevant to the user's intent.
99
+ 2. Call `{server}_list_tools()` to see available tables and endpoints.
100
+ 3. For each high-signal recipe or table, seed ONE named cell that demonstrates it — typical shapes:
101
+ - an SQL `SELECT ... LIMIT 10` with a meaningful cell name (e.g. `fetch_amendments`)
102
+ - a `run_script` invocation for recipes requiring code
103
+ - a short markdown cell describing the recipe and its parameters
104
+ 4. Pass the server metadata via the `servers:` param so the sidebar "sources" section, the left pane, and the server menu modal render correctly:
105
+
106
+ ```ts
107
+ widget_display({
108
+ name: 'notebook-workspace',
109
+ params: {
110
+ title: '...',
111
+ cells: [...],
112
+ servers: [{ name: 'tricoteuses', url: 'https://...', kind: 'data' }]
113
+ }
114
+ })
115
+ ```
116
+
117
+ If NO data server is connected, seed generic markdown-only cells that describe the intended analysis and invite the user to connect an MCP server — this matches the "no source connected" sidebar placeholder.
118
+
119
+ **Filter rule**: only MCP *data* servers (`kind: 'data'`) belong in `servers:`. Do NOT include WebMCP UI servers such as `autoui` — they expose no queryable data and would clutter the sources sidebar.
@@ -0,0 +1,162 @@
1
+ // @ts-nocheck
2
+ // ---------------------------------------------------------------------------
3
+ // Extract notebook cells from imported resources (recipe body, tool def, md).
4
+ // Consumed by import-modals.ts and left-pane.ts.
5
+ // ---------------------------------------------------------------------------
6
+
7
+ import { parseBody } from '@webmcp-auto-ui/sdk';
8
+ import { uid, defaultCellContent } from './shared.js';
9
+ import type { NotebookCell, CellType } from './shared.js';
10
+
11
+ // Languages we map directly to a code cell type
12
+ const LANG_TO_TYPE: Record<string, CellType> = {
13
+ sql: 'sql',
14
+ psql: 'sql',
15
+ mysql: 'sql',
16
+ sqlite: 'sql',
17
+ js: 'js',
18
+ javascript: 'js',
19
+ ts: 'js', // treat TS as js source (stripped at runtime is user's concern)
20
+ typescript: 'js',
21
+ node: 'js',
22
+ };
23
+
24
+ /** Languages that map to 'js' but use a syntax superset that will fail at
25
+ * runtime (interfaces, type annotations, `as` casts, etc.). We log a warning
26
+ * whenever such a fence is mapped so the user gets a hint. */
27
+ const TS_LIKE_LANGS = new Set(['ts', 'typescript']);
28
+
29
+ export function fenceLangToCellType(lang: string): CellType | null {
30
+ const key = (lang || '').toLowerCase().trim();
31
+ if (TS_LIKE_LANGS.has(key)) {
32
+ try {
33
+ console.warn(
34
+ `[notebook] Fence language "${key}" mapped to JS — TS-specific syntax (interfaces, type annotations, "as" casts) will fail at runtime.`
35
+ );
36
+ } catch { /* ignore */ }
37
+ }
38
+ return LANG_TO_TYPE[key] ?? null;
39
+ }
40
+
41
+ /**
42
+ * Build a notebook cell from a single fence (lang + content).
43
+ * Unsupported languages fall back to a markdown cell wrapping the fence.
44
+ */
45
+ export function extractCellFromFence(lang: string, content: string): NotebookCell {
46
+ const cellType = fenceLangToCellType(lang);
47
+ if (cellType) {
48
+ return { id: uid(), type: cellType, content: content.trim(), hideSource: false, hideResult: false };
49
+ }
50
+ // Detect pseudo-code MCP tool calls like: query_sql({sql: "..."})
51
+ // Only attempted when the fence language is unknown/text (cellType === null).
52
+ const trimmed = content.trim();
53
+ const callMatch = trimmed.match(/^([A-Za-z_][\w]*)\s*\(\s*(\{[\s\S]*\})\s*\)\s*;?\s*$/);
54
+ if (callMatch) {
55
+ const name = callMatch[1];
56
+ const argsRaw = callMatch[2];
57
+ if (name === 'query_sql') {
58
+ const sql = extractSqlFromLooseObject(argsRaw);
59
+ if (sql != null) {
60
+ return { id: uid(), type: 'sql', content: sql.trim(), hideSource: false, hideResult: false };
61
+ }
62
+ }
63
+ return {
64
+ id: uid(),
65
+ type: 'js',
66
+ content: `// MCP tool call: ${name}\nawait callTool('${name}', ${argsRaw});`,
67
+ hideSource: false,
68
+ hideResult: false,
69
+ };
70
+ }
71
+ // Preserve the original fence in markdown so users see it verbatim
72
+ return {
73
+ id: uid(),
74
+ type: 'md',
75
+ content: '```' + (lang || '') + '\n' + content.trim() + '\n```',
76
+ hideSource: false,
77
+ hideResult: false,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Best-effort extraction of the `sql` property value from a loose JS-object literal
83
+ * (unquoted keys, possibly single-quoted strings). Returns null if no sql key found.
84
+ */
85
+ function extractSqlFromLooseObject(argsRaw: string): string | null {
86
+ // Essai 1: JSON.parse direct (cheap, rarely works for loose objects)
87
+ try {
88
+ const parsed = JSON.parse(argsRaw);
89
+ if (parsed && typeof parsed === 'object' && typeof parsed.sql === 'string') {
90
+ return parsed.sql;
91
+ }
92
+ } catch {
93
+ /* fallthrough */
94
+ }
95
+ // Essai 2: regex extract sql: "..." (double-quoted)
96
+ const dq = argsRaw.match(/sql\s*:\s*"([\s\S]*?)"/);
97
+ if (dq) return dq[1];
98
+ // Essai 3: regex extract sql: '...' (single-quoted)
99
+ const sq = argsRaw.match(/sql\s*:\s*'([\s\S]*?)'/);
100
+ if (sq) return sq[1];
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Extract cells from a full recipe body (markdown with frontmatter already stripped).
106
+ * Returns: a single intro markdown cell (first prose block) + one cell per fenced block.
107
+ */
108
+ export function extractCellsFromRecipe(body: string, opts?: { title?: string; description?: string }): NotebookCell[] {
109
+ const cells: NotebookCell[] = [];
110
+ if (opts?.title || opts?.description) {
111
+ const md = ['# ' + (opts?.title ?? 'Imported recipe'), opts?.description ?? ''].filter(Boolean).join('\n\n');
112
+ cells.push({ id: uid(), type: 'md', content: md, hideSource: false, hideResult: false });
113
+ }
114
+ const segments = parseBody(body || '');
115
+ for (const seg of segments) {
116
+ if (seg.type === 'markdown') {
117
+ cells.push({ id: uid(), type: 'md', content: seg.content.trim(), hideSource: false, hideResult: false });
118
+ } else {
119
+ cells.push(extractCellFromFence(seg.lang || 'text', seg.content));
120
+ }
121
+ }
122
+ return cells;
123
+ }
124
+
125
+ /**
126
+ * Produce 2 cells for a tool: md (name + description + schema) + a starter call cell.
127
+ */
128
+ export interface McpToolLike {
129
+ name: string;
130
+ description?: string;
131
+ inputSchema?: unknown;
132
+ schema?: unknown;
133
+ serverName?: string;
134
+ }
135
+
136
+ export function extractCellsFromTool(tool: McpToolLike): NotebookCell[] {
137
+ const schema = tool.inputSchema ?? tool.schema ?? {};
138
+ const schemaStr = JSON.stringify(schema, null, 2);
139
+ const mdParts: string[] = [
140
+ `## ${tool.name}${tool.serverName ? ` · \`${tool.serverName}\`` : ''}`,
141
+ ];
142
+ if (tool.description) mdParts.push(tool.description);
143
+ mdParts.push('```json\n' + schemaStr + '\n```');
144
+
145
+ const isSql = /(_|^)query_sql$|(^|_)sql_query$/i.test(tool.name);
146
+ const cellType: CellType = isSql ? 'sql' : 'js';
147
+ const template = isSql
148
+ ? '-- call via MCP bridge: ' + tool.name + '\n' + defaultCellContent('sql')
149
+ : '// call via MCP bridge\nawait callTool(' + JSON.stringify(tool.name) + ', {});';
150
+
151
+ return [
152
+ { id: uid(), type: 'md', content: mdParts.join('\n\n'), hideSource: false, hideResult: false },
153
+ { id: uid(), type: cellType, content: template, hideSource: false, hideResult: false },
154
+ ];
155
+ }
156
+
157
+ /**
158
+ * Wrap raw markdown content as a single md cell.
159
+ */
160
+ export function extractCellFromMarkdown(md: string): NotebookCell {
161
+ return { id: uid(), type: 'md', content: (md || '').trim(), hideSource: false, hideResult: false };
162
+ }
@@ -0,0 +1,222 @@
1
+ // @ts-nocheck
2
+ // ---------------------------------------------------------------------------
3
+ // Share handlers — real implementations for notebook share modal.
4
+ // 4 formats: JSON, Markdown, Hyperskill link (+ short), PNG snapshot.
5
+ // ---------------------------------------------------------------------------
6
+
7
+ import { encode, buildShortUrl } from '@webmcp-auto-ui/sdk';
8
+ import type { NotebookState, NotebookCell } from './shared.js';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // JSON export
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export async function shareAsJson(state: NotebookState): Promise<void> {
15
+ const minimal = minify(state);
16
+ const blob = new Blob([JSON.stringify(minimal, null, 2)], { type: 'application/json' });
17
+ triggerDownload(blob, sanitizeFilename(state.title || 'notebook') + '.json');
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Markdown export
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export async function shareAsMarkdown(state: NotebookState): Promise<void> {
25
+ const md = serializeToMarkdown(state);
26
+ const blob = new Blob([md], { type: 'text/markdown' });
27
+ triggerDownload(blob, sanitizeFilename(state.title || 'notebook') + '.md');
28
+ }
29
+
30
+ function serializeToMarkdown(state: NotebookState): string {
31
+ const parts: string[] = [];
32
+ if (state.title) parts.push(`# ${state.title}`, '');
33
+ for (const cell of state.cells) {
34
+ if (cell.type === 'md') {
35
+ parts.push(stripHtml(cell.content).trim(), '');
36
+ } else {
37
+ const lang = cell.type === 'sql' ? 'sql' : 'js';
38
+ const varname = cell.varname ? ` // → ${cell.varname}` : '';
39
+ parts.push('```' + lang + varname, cell.content.trim(), '```', '');
40
+ }
41
+ }
42
+ return parts.join('\n').trim() + '\n';
43
+ }
44
+
45
+ function stripHtml(s: string): string {
46
+ if (typeof document === 'undefined') return s;
47
+ const d = document.createElement('div');
48
+ d.innerHTML = s;
49
+ return d.textContent || '';
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Hyperskill link (+ short URL)
54
+ // ---------------------------------------------------------------------------
55
+
56
+ export interface HyperskillShareResult {
57
+ fullUrl: string;
58
+ shortUrl: string;
59
+ }
60
+
61
+ export async function shareAsHyperskill(state: NotebookState): Promise<HyperskillShareResult> {
62
+ const origin = typeof window !== 'undefined' ? window.location.href.split('?')[0] : 'https://example.com';
63
+ const payload = JSON.stringify(minify(state));
64
+ const fullUrl = await encode(origin, payload);
65
+ const shortUrl = await buildShortUrl(origin, payload);
66
+ try {
67
+ await navigator.clipboard?.writeText(fullUrl);
68
+ } catch {
69
+ /* clipboard API can fail silently (focus, permission) */
70
+ }
71
+ return { fullUrl, shortUrl };
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // PNG snapshot — uses __exportPng widget hook (commit ded48c9) if present,
76
+ // falls back to a library-free DOM → SVG → PNG pipeline.
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export async function shareAsPng(state: NotebookState, container: HTMLElement): Promise<void> {
80
+ // Preferred: widget-level hook
81
+ const hook = (container as any).__exportPng as (() => Promise<Blob>) | undefined;
82
+ if (typeof hook === 'function') {
83
+ try {
84
+ const blob = await hook();
85
+ triggerDownload(blob, sanitizeFilename(state.title || 'notebook') + '.png');
86
+ return;
87
+ } catch {
88
+ /* fall through to fallback */
89
+ }
90
+ }
91
+ // Fallback: SVG foreignObject → canvas → PNG
92
+ const blob = await domToPngBlob(container);
93
+ triggerDownload(blob, sanitizeFilename(state.title || 'notebook') + '.png');
94
+ }
95
+
96
+ async function domToPngBlob(el: HTMLElement): Promise<Blob> {
97
+ const rect = el.getBoundingClientRect();
98
+ const w = Math.max(1, Math.ceil(rect.width));
99
+ const h = Math.max(1, Math.ceil(rect.height));
100
+ const serialized = new XMLSerializer().serializeToString(el);
101
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
102
+ <foreignObject width="100%" height="100%">
103
+ <div xmlns="http://www.w3.org/1999/xhtml">${serialized}</div>
104
+ </foreignObject>
105
+ </svg>`;
106
+ const svgBlob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
107
+ const url = URL.createObjectURL(svgBlob);
108
+ const img = new Image();
109
+ await new Promise<void>((resolve, reject) => {
110
+ img.onload = () => resolve();
111
+ img.onerror = (e) => reject(e);
112
+ img.src = url;
113
+ });
114
+ const canvas = document.createElement('canvas');
115
+ canvas.width = w;
116
+ canvas.height = h;
117
+ const ctx = canvas.getContext('2d')!;
118
+ ctx.fillStyle = 'white';
119
+ ctx.fillRect(0, 0, w, h);
120
+ ctx.drawImage(img, 0, 0);
121
+ URL.revokeObjectURL(url);
122
+ return new Promise<Blob>((resolve, reject) => {
123
+ canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png');
124
+ });
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Dispatcher used by shared.ts::openShareModal callback
129
+ // ---------------------------------------------------------------------------
130
+
131
+ export type ShareKind = 'hyperskill' | 'json' | 'markdown' | 'png';
132
+
133
+ export interface ShareResultInfo {
134
+ fmt: string;
135
+ kind: ShareKind | string;
136
+ message: string;
137
+ url?: string;
138
+ shortUrl?: string;
139
+ fullUrl?: string;
140
+ }
141
+
142
+ export interface ShareDispatchOptions {
143
+ container?: HTMLElement;
144
+ onResult?: (info: ShareResultInfo) => void;
145
+ }
146
+
147
+ export async function dispatchShare(
148
+ fmt: string,
149
+ state: NotebookState,
150
+ opts: ShareDispatchOptions = {},
151
+ ): Promise<void> {
152
+ try {
153
+ if (fmt === 'json') {
154
+ await shareAsJson(state);
155
+ opts.onResult?.({ fmt, kind: 'json', message: 'JSON downloaded' });
156
+ } else if (fmt === 'md' || fmt === 'markdown') {
157
+ await shareAsMarkdown(state);
158
+ opts.onResult?.({ fmt, kind: 'markdown', message: 'Markdown downloaded' });
159
+ } else if (fmt === 'hyperskill' || fmt === 'hs') {
160
+ const { fullUrl, shortUrl } = await shareAsHyperskill(state);
161
+ opts.onResult?.({
162
+ fmt,
163
+ kind: 'hyperskill',
164
+ message: 'URL copied',
165
+ url: shortUrl || fullUrl,
166
+ shortUrl,
167
+ fullUrl,
168
+ });
169
+ } else if (fmt === 'png') {
170
+ if (!opts.container) throw new Error('png export requires container');
171
+ await shareAsPng(state, opts.container);
172
+ opts.onResult?.({ fmt, kind: 'png', message: 'PNG downloaded' });
173
+ } else {
174
+ throw new Error(`Unknown share format: ${fmt}`);
175
+ }
176
+ } catch (err: any) {
177
+ opts.onResult?.({ fmt, kind: fmt, message: 'Error: ' + String(err?.message ?? err) });
178
+ throw err;
179
+ }
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Helpers
184
+ // ---------------------------------------------------------------------------
185
+
186
+ /**
187
+ * Strip non-serializable / transient fields from state for share/encode.
188
+ */
189
+ function minify(state: NotebookState): Record<string, unknown> {
190
+ return {
191
+ id: state.id,
192
+ title: state.title,
193
+ mode: state.mode,
194
+ kicker: state.kicker,
195
+ cells: state.cells.map((c: NotebookCell) => ({
196
+ id: c.id,
197
+ type: c.type,
198
+ content: c.content,
199
+ name: c.name,
200
+ varname: c.varname,
201
+ hideSource: c.hideSource,
202
+ hideResult: c.hideResult,
203
+ comment: c.comment ?? undefined,
204
+ // intentionally skip lastResult, runState, lastMs — transient
205
+ })),
206
+ };
207
+ }
208
+
209
+ function triggerDownload(blob: Blob, filename: string): void {
210
+ const url = URL.createObjectURL(blob);
211
+ const a = document.createElement('a');
212
+ a.href = url;
213
+ a.download = filename;
214
+ document.body.appendChild(a);
215
+ a.click();
216
+ document.body.removeChild(a);
217
+ URL.revokeObjectURL(url);
218
+ }
219
+
220
+ function sanitizeFilename(name: string): string {
221
+ return name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9._-]/g, '') || 'notebook';
222
+ }