@webmcp-auto-ui/ui 2.5.26 → 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.
- package/README.md +2 -2
- package/package.json +15 -3
- package/src/agent/AgentConsole.svelte +1 -21
- package/src/agent/DataServersPanel.svelte +164 -0
- package/src/agent/LLMSelector.svelte +26 -8
- package/src/agent/ModelCacheManager.svelte +359 -0
- package/src/agent/{GemmaLoader.svelte → ModelLoader.svelte} +1 -1
- package/src/agent/SettingsPanel.svelte +16 -2
- package/src/index.ts +45 -31
- package/src/widgets/WidgetRenderer.svelte +118 -115
- package/src/widgets/export-widget.ts +28 -1
- package/src/widgets/helpers/safe-image.ts +78 -0
- package/src/widgets/notebook/.gitkeep +0 -0
- package/src/widgets/notebook/chart-renderer.ts +63 -0
- package/src/widgets/notebook/compact.ts +823 -0
- package/src/widgets/notebook/document.ts +1065 -0
- package/src/widgets/notebook/editorial.ts +936 -0
- package/src/widgets/notebook/executors/.gitkeep +1 -0
- package/src/widgets/notebook/executors/index.ts +4 -0
- package/src/widgets/notebook/executors/js-worker.ts +269 -0
- package/src/widgets/notebook/executors/sql.ts +206 -0
- package/src/widgets/notebook/import-modals.ts +553 -0
- package/src/widgets/notebook/left-pane.ts +249 -0
- package/src/widgets/notebook/prose.ts +280 -0
- package/src/widgets/notebook/recipe-browser.ts +350 -0
- package/src/widgets/notebook/recipes/compact.md +124 -0
- package/src/widgets/notebook/recipes/document.md +139 -0
- package/src/widgets/notebook/recipes/editorial.md +120 -0
- package/src/widgets/notebook/recipes/workspace.md +119 -0
- package/src/widgets/notebook/resource-extractor.ts +162 -0
- package/src/widgets/notebook/share-handlers.ts +222 -0
- package/src/widgets/notebook/shared.ts +1592 -0
- package/src/widgets/notebook/workspace.ts +852 -0
- package/src/widgets/rich/cards.ts +181 -0
- package/src/widgets/rich/carousel.ts +319 -0
- package/src/widgets/rich/chart-rich.ts +386 -0
- package/src/widgets/rich/d3.ts +503 -0
- package/src/widgets/rich/data-table.ts +342 -0
- package/src/widgets/rich/gallery.ts +350 -0
- package/src/widgets/rich/grid-data.ts +173 -0
- package/src/widgets/rich/hemicycle.ts +313 -0
- package/src/widgets/rich/js-sandbox.ts +106 -0
- package/src/widgets/rich/json-viewer.ts +202 -0
- package/src/widgets/rich/log.ts +143 -0
- package/src/widgets/rich/map.ts +218 -0
- package/src/widgets/rich/profile.ts +256 -0
- package/src/widgets/rich/sankey.ts +262 -0
- package/src/widgets/rich/stat-card.ts +125 -0
- package/src/widgets/rich/timeline.ts +179 -0
- package/src/widgets/rich/trombinoscope.ts +246 -0
- package/src/widgets/simple/actions.ts +89 -0
- package/src/widgets/simple/alert.ts +100 -0
- package/src/widgets/simple/chart.ts +189 -0
- package/src/widgets/simple/code.ts +79 -0
- package/src/widgets/simple/kv.ts +68 -0
- package/src/widgets/simple/list.ts +89 -0
- package/src/widgets/simple/stat.ts +58 -0
- package/src/widgets/simple/tags.ts +125 -0
- package/src/widgets/simple/text.ts +198 -0
- package/src/wm/FloatingLayout.svelte +2 -0
- package/src/wm/LinkIndicators.svelte +8 -15
- package/src/widgets/SafeImage.svelte +0 -76
- package/src/widgets/rich/Cards.svelte +0 -39
- package/src/widgets/rich/Carousel.svelte +0 -88
- package/src/widgets/rich/Chart.svelte +0 -142
- package/src/widgets/rich/D3Widget.svelte +0 -373
- package/src/widgets/rich/DataTable.svelte +0 -62
- package/src/widgets/rich/Gallery.svelte +0 -94
- package/src/widgets/rich/GridData.svelte +0 -44
- package/src/widgets/rich/Hemicycle.svelte +0 -78
- package/src/widgets/rich/JsSandbox.svelte +0 -51
- package/src/widgets/rich/JsonViewer.svelte +0 -42
- package/src/widgets/rich/LogViewer.svelte +0 -24
- package/src/widgets/rich/MapView.svelte +0 -140
- package/src/widgets/rich/ProfileCard.svelte +0 -59
- package/src/widgets/rich/Sankey.svelte +0 -38
- package/src/widgets/rich/StatCard.svelte +0 -35
- package/src/widgets/rich/Timeline.svelte +0 -43
- package/src/widgets/rich/Trombinoscope.svelte +0 -48
- package/src/widgets/simple/ActionsBlock.svelte +0 -15
- package/src/widgets/simple/AlertBlock.svelte +0 -11
- package/src/widgets/simple/ChartBlock.svelte +0 -21
- package/src/widgets/simple/CodeBlock.svelte +0 -11
- package/src/widgets/simple/KVBlock.svelte +0 -16
- package/src/widgets/simple/ListBlock.svelte +0 -17
- package/src/widgets/simple/StatBlock.svelte +0 -14
- package/src/widgets/simple/TagsBlock.svelte +0 -15
- 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
|
+
}
|