@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,350 @@
1
+ // @ts-nocheck
2
+ // ---------------------------------------------------------------------------
3
+ // recipe-browser — vanilla widget renderer
4
+ // Interactive browser: search, filter by kind/tags, layout toggle, preview, pick/download.
5
+ // ---------------------------------------------------------------------------
6
+
7
+ // NOTE: this widget lives in @webmcp-auto-ui/ui but imports runtime helpers from
8
+ // @webmcp-auto-ui/agent, creating an ESM cycle ui <-> agent. The cycle is safe
9
+ // because these imports are only used inside functions (no top-level eval).
10
+ import { filterRecipes, sortRecipes, recipeToDownloadBlob, recipeToMarkdown } from '@webmcp-auto-ui/agent';
11
+ import type { Recipe } from '@webmcp-auto-ui/agent';
12
+
13
+ export interface RecipeBrowserData {
14
+ recipes: Recipe[];
15
+ filters?: { tags?: string[]; kind?: 'webmcp' | 'mcp' | 'all'; q?: string };
16
+ layout?: 'list' | 'grid';
17
+ onPick?: (recipe: Recipe) => void;
18
+ }
19
+
20
+ const STYLE_ID = 'rb-recipe-browser-styles';
21
+
22
+ function injectStyles(): void {
23
+ if (document.getElementById(STYLE_ID)) return;
24
+ const s = document.createElement('style');
25
+ s.id = STYLE_ID;
26
+ s.textContent = `
27
+ .recipe-browser-root {
28
+ display: flex; flex-direction: column; gap: 10px;
29
+ font-family: var(--font-sans, system-ui, sans-serif);
30
+ color: var(--color-text1, #111);
31
+ background: var(--color-surface, #fff);
32
+ border: 1px solid var(--color-border, #e4e4e7);
33
+ border-radius: 10px;
34
+ padding: 14px;
35
+ min-height: 360px;
36
+ box-sizing: border-box;
37
+ }
38
+ .recipe-browser-root * { box-sizing: border-box; }
39
+ .rb-head {
40
+ display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
41
+ padding-bottom: 10px;
42
+ border-bottom: 1px solid var(--color-border, #e4e4e7);
43
+ }
44
+ .rb-search {
45
+ flex: 1 1 200px; min-width: 160px;
46
+ padding: 7px 10px; font-size: 13px;
47
+ border: 1px solid var(--color-border, #e4e4e7);
48
+ border-radius: 6px;
49
+ background: var(--color-surface, #fff);
50
+ color: var(--color-text1, #111);
51
+ outline: none;
52
+ }
53
+ .rb-search:focus { border-color: var(--color-accent, #6a55ff); }
54
+ .rb-select {
55
+ padding: 7px 10px; font-size: 12px;
56
+ border: 1px solid var(--color-border, #e4e4e7);
57
+ border-radius: 6px;
58
+ background: var(--color-surface, #fff);
59
+ color: var(--color-text1, #111);
60
+ }
61
+ .rb-layout-toggle { display: inline-flex; border: 1px solid var(--color-border, #e4e4e7); border-radius: 6px; overflow: hidden; }
62
+ .rb-layout-btn {
63
+ background: var(--color-surface, #fff); color: var(--color-text2, #666);
64
+ border: 0; padding: 7px 10px; font-size: 12px; cursor: pointer;
65
+ }
66
+ .rb-layout-btn.rb-on { background: var(--color-accent, #6a55ff); color: #fff; }
67
+ .rb-body {
68
+ display: grid; grid-template-columns: minmax(220px, 320px) 1fr;
69
+ gap: 12px; min-height: 280px;
70
+ }
71
+ .rb-body[data-layout="grid"] { grid-template-columns: 1fr; }
72
+ .rb-body[data-layout="grid"] .rb-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px; max-height: none; }
73
+ .rb-list {
74
+ display: flex; flex-direction: column; gap: 6px;
75
+ max-height: 420px; overflow-y: auto;
76
+ padding-right: 4px;
77
+ }
78
+ .rb-item {
79
+ padding: 9px 11px; border-radius: 8px;
80
+ background: var(--color-surface2, #f4f4f5);
81
+ border: 1px solid transparent;
82
+ cursor: pointer; transition: background .12s, border-color .12s;
83
+ }
84
+ .rb-item:hover { background: var(--color-surface3, #eeeef0); }
85
+ .rb-item.rb-selected { border-color: var(--color-accent, #6a55ff); background: var(--color-surface3, #eeeef0); }
86
+ .rb-item-name { font-weight: 600; font-size: 13px; margin-bottom: 3px; }
87
+ .rb-item-desc { font-size: 11.5px; color: var(--color-text2, #666); margin-bottom: 6px; line-height: 1.35; }
88
+ .rb-item-foot { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
89
+ .rb-chip {
90
+ font-size: 10.5px; font-family: var(--font-mono, monospace);
91
+ padding: 1px 6px; border-radius: 999px;
92
+ background: var(--color-surface, #fff);
93
+ border: 1px solid var(--color-border, #e4e4e7);
94
+ color: var(--color-text2, #666);
95
+ }
96
+ .rb-chip.rb-srv { background: var(--color-accent, #6a55ff); color: #fff; border-color: transparent; }
97
+ .rb-preview {
98
+ display: flex; flex-direction: column; min-height: 260px;
99
+ border: 1px solid var(--color-border, #e4e4e7);
100
+ border-radius: 8px; overflow: hidden;
101
+ background: var(--color-surface, #fff);
102
+ }
103
+ .rb-preview-head {
104
+ display: flex; align-items: center; gap: 8px;
105
+ padding: 9px 12px;
106
+ border-bottom: 1px solid var(--color-border, #e4e4e7);
107
+ background: var(--color-surface2, #f4f4f5);
108
+ }
109
+ .rb-preview-title { flex: 1; font-weight: 600; font-size: 13px; }
110
+ .rb-preview-body {
111
+ flex: 1; padding: 12px; overflow-y: auto;
112
+ font-size: 12.5px; line-height: 1.5;
113
+ max-height: 420px;
114
+ }
115
+ .rb-preview-body pre {
116
+ background: var(--color-surface2, #f4f4f5);
117
+ padding: 10px; border-radius: 6px;
118
+ overflow-x: auto;
119
+ font-family: var(--font-mono, monospace);
120
+ font-size: 11.5px;
121
+ white-space: pre-wrap;
122
+ margin: 0;
123
+ }
124
+ .rb-btn {
125
+ background: var(--color-surface2, #f4f4f5);
126
+ border: 1px solid var(--color-border, #e4e4e7);
127
+ border-radius: 6px; padding: 6px 12px; font-size: 12px;
128
+ color: var(--color-text1, #111);
129
+ cursor: pointer;
130
+ }
131
+ .rb-btn:hover { background: var(--color-surface3, #eeeef0); }
132
+ .rb-btn-primary { background: var(--color-accent, #6a55ff); color: #fff; border-color: transparent; }
133
+ .rb-btn-primary:hover { filter: brightness(1.08); }
134
+ .rb-empty { padding: 24px; text-align: center; color: var(--color-text2, #666); font-size: 12px; }
135
+ .rb-placeholder { color: var(--color-text2, #666); font-size: 12px; padding: 24px; text-align: center; }
136
+ `;
137
+ document.head.appendChild(s);
138
+ }
139
+
140
+ function escapeHtml(s: unknown): string {
141
+ return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
142
+ }
143
+
144
+ function detectKind(r: Recipe): 'webmcp' | 'mcp' {
145
+ // Heuristic: if frontmatter 'servers' present and non-empty, treat as MCP-connected.
146
+ const srv = (r as any).server ?? (r as any).serverName;
147
+ if (typeof srv === 'string' && srv && srv !== 'webmcp') return 'mcp';
148
+ return 'webmcp';
149
+ }
150
+
151
+ function recipeTags(r: Recipe): string[] {
152
+ const out: string[] = [];
153
+ if (Array.isArray(r.servers)) out.push(...r.servers);
154
+ if (Array.isArray(r.components_used)) out.push(...r.components_used.slice(0, 3));
155
+ return out;
156
+ }
157
+
158
+ function serverLabel(r: Recipe): string {
159
+ const srv = (r as any).server ?? (r as any).serverName;
160
+ if (typeof srv === 'string' && srv) return srv;
161
+ if (Array.isArray(r.servers) && r.servers.length) return r.servers[0];
162
+ return 'webmcp';
163
+ }
164
+
165
+ function simpleMarkdown(body: string): string {
166
+ // Minimal safe rendering: escape + preserve paragraphs + wrap fences in <pre>.
167
+ const escaped = escapeHtml(body);
168
+ // Code fences
169
+ const withFences = escaped.replace(/```([a-zA-Z0-9_-]*)\n([\s\S]*?)```/g, (_m, _lang, code) => {
170
+ return `<pre><code>${code}</code></pre>`;
171
+ });
172
+ // Paragraphs (double newlines)
173
+ const paras = withFences.split(/\n{2,}/).map((p) => {
174
+ if (p.startsWith('<pre>')) return p;
175
+ return `<p>${p.replace(/\n/g, '<br/>')}</p>`;
176
+ });
177
+ return paras.join('\n');
178
+ }
179
+
180
+ export function render(container: HTMLElement, data: RecipeBrowserData): () => void {
181
+ injectStyles();
182
+
183
+ const all: Recipe[] = Array.isArray(data?.recipes) ? data.recipes : [];
184
+ if (!all.length) {
185
+ container.innerHTML = '<div class="recipe-browser-root"><div class="rb-empty">No recipes available.</div></div>';
186
+ return () => { container.innerHTML = ''; };
187
+ }
188
+
189
+ const initialFilters = data?.filters ?? {};
190
+ let q = initialFilters.q ?? '';
191
+ let kind: 'all' | 'webmcp' | 'mcp' = initialFilters.kind ?? 'all';
192
+ let layout: 'list' | 'grid' = data?.layout === 'grid' ? 'grid' : 'list';
193
+ let selectedId: string | null = null;
194
+
195
+ const root = document.createElement('div');
196
+ root.className = 'recipe-browser-root';
197
+ root.innerHTML = `
198
+ <div class="rb-head">
199
+ <input type="search" class="rb-search" placeholder="Search recipes..." value="${escapeHtml(q)}" />
200
+ <select class="rb-select" data-role="kind">
201
+ <option value="all"${kind === 'all' ? ' selected' : ''}>All kinds</option>
202
+ <option value="webmcp"${kind === 'webmcp' ? ' selected' : ''}>WebMCP</option>
203
+ <option value="mcp"${kind === 'mcp' ? ' selected' : ''}>MCP</option>
204
+ </select>
205
+ <div class="rb-layout-toggle">
206
+ <button type="button" class="rb-layout-btn${layout === 'list' ? ' rb-on' : ''}" data-layout="list">list</button>
207
+ <button type="button" class="rb-layout-btn${layout === 'grid' ? ' rb-on' : ''}" data-layout="grid">grid</button>
208
+ </div>
209
+ </div>
210
+ <div class="rb-body" data-layout="${layout}">
211
+ <div class="rb-list" data-role="list"></div>
212
+ <div class="rb-preview" data-role="preview">
213
+ <div class="rb-placeholder">Select a recipe to preview.</div>
214
+ </div>
215
+ </div>
216
+ `;
217
+ container.innerHTML = '';
218
+ container.appendChild(root);
219
+
220
+ const searchEl = root.querySelector('.rb-search') as HTMLInputElement;
221
+ const kindEl = root.querySelector('[data-role="kind"]') as HTMLSelectElement;
222
+ const bodyEl = root.querySelector('.rb-body') as HTMLElement;
223
+ const listEl = root.querySelector('[data-role="list"]') as HTMLElement;
224
+ const previewEl = root.querySelector('[data-role="preview"]') as HTMLElement;
225
+ const layoutBtns = Array.from(root.querySelectorAll('.rb-layout-btn')) as HTMLButtonElement[];
226
+
227
+ const listeners: Array<{ el: EventTarget; type: string; fn: EventListener }> = [];
228
+ function on<T extends EventTarget>(el: T, type: string, fn: EventListener) {
229
+ el.addEventListener(type, fn);
230
+ listeners.push({ el, type, fn });
231
+ }
232
+
233
+ function applyFilters(): Recipe[] {
234
+ let filtered = all.slice();
235
+ if (kind !== 'all') filtered = filtered.filter((r) => detectKind(r) === kind);
236
+ // Tag filter (optional): keep only recipes whose tags intersect data.filters.tags
237
+ const wantTags = initialFilters.tags;
238
+ if (Array.isArray(wantTags) && wantTags.length) {
239
+ const wanted = new Set(wantTags.map((t) => String(t).toLowerCase()));
240
+ filtered = filtered.filter((r) => recipeTags(r).some((t) => wanted.has(String(t).toLowerCase())));
241
+ }
242
+ filtered = filterRecipes(filtered, q);
243
+ return sortRecipes(filtered);
244
+ }
245
+
246
+ function renderList() {
247
+ const items = applyFilters();
248
+ listEl.innerHTML = '';
249
+ if (!items.length) {
250
+ const empty = document.createElement('div');
251
+ empty.className = 'rb-empty';
252
+ empty.textContent = 'No matches.';
253
+ listEl.appendChild(empty);
254
+ return;
255
+ }
256
+ for (const r of items) {
257
+ const id = r.id ?? r.name ?? '';
258
+ const node = document.createElement('div');
259
+ node.className = 'rb-item' + (id === selectedId ? ' rb-selected' : '');
260
+ const tags = recipeTags(r).slice(0, 4);
261
+ node.innerHTML = `
262
+ <div class="rb-item-name">${escapeHtml(r.name || 'Untitled')}</div>
263
+ <div class="rb-item-desc">${escapeHtml(r.description || 'No description')}</div>
264
+ <div class="rb-item-foot">
265
+ <span class="rb-chip rb-srv">${escapeHtml(serverLabel(r))}</span>
266
+ ${tags.map((t) => `<span class="rb-chip">${escapeHtml(t)}</span>`).join('')}
267
+ </div>
268
+ `;
269
+ on(node, 'click', () => {
270
+ selectedId = id;
271
+ renderList();
272
+ renderPreview(r);
273
+ });
274
+ listEl.appendChild(node);
275
+ }
276
+ }
277
+
278
+ function renderPreview(r: Recipe) {
279
+ const md = typeof r.body === 'string' && r.body.trim()
280
+ ? r.body
281
+ : (() => { try { return recipeToMarkdown(r as any); } catch { return ''; } })();
282
+ previewEl.innerHTML = `
283
+ <div class="rb-preview-head">
284
+ <span class="rb-preview-title">${escapeHtml(r.name || 'Untitled')}</span>
285
+ <button type="button" class="rb-btn" data-act="download">Download .md</button>
286
+ <button type="button" class="rb-btn rb-btn-primary" data-act="pick">Pick</button>
287
+ </div>
288
+ <div class="rb-preview-body">${md ? simpleMarkdown(md) : '<div class="rb-placeholder">No body.</div>'}</div>
289
+ `;
290
+
291
+ const pickBtn = previewEl.querySelector('[data-act="pick"]') as HTMLButtonElement | null;
292
+ const dlBtn = previewEl.querySelector('[data-act="download"]') as HTMLButtonElement | null;
293
+
294
+ if (pickBtn) {
295
+ on(pickBtn, 'click', () => {
296
+ if (typeof data.onPick === 'function') {
297
+ data.onPick(r);
298
+ } else {
299
+ container.dispatchEvent(new CustomEvent('widget:interact', {
300
+ bubbles: true,
301
+ detail: { action: 'pick', payload: r },
302
+ }));
303
+ }
304
+ });
305
+ }
306
+ if (dlBtn) {
307
+ on(dlBtn, 'click', () => {
308
+ try {
309
+ const { blob, filename } = recipeToDownloadBlob(r as any);
310
+ triggerDownload(blob, filename);
311
+ } catch {
312
+ // fallback
313
+ const body = typeof r.body === 'string' ? r.body : '';
314
+ const blob = new Blob([body], { type: 'text/markdown' });
315
+ const filename = (r.name || r.id || 'recipe').replace(/[^a-z0-9_.-]/gi, '-').toLowerCase() + '.md';
316
+ triggerDownload(blob, filename);
317
+ }
318
+ });
319
+ }
320
+ }
321
+
322
+ function triggerDownload(blob: Blob, filename: string) {
323
+ const url = URL.createObjectURL(blob);
324
+ const a = document.createElement('a');
325
+ a.href = url;
326
+ a.download = filename;
327
+ document.body.appendChild(a);
328
+ a.click();
329
+ a.remove();
330
+ setTimeout(() => URL.revokeObjectURL(url), 500);
331
+ }
332
+
333
+ on(searchEl, 'input', () => { q = searchEl.value; renderList(); });
334
+ on(kindEl, 'change', () => { kind = (kindEl.value as any); renderList(); });
335
+ for (const btn of layoutBtns) {
336
+ on(btn, 'click', () => {
337
+ layout = (btn.dataset.layout as 'list' | 'grid') || 'list';
338
+ bodyEl.setAttribute('data-layout', layout);
339
+ layoutBtns.forEach((b) => b.classList.toggle('rb-on', b === btn));
340
+ });
341
+ }
342
+
343
+ renderList();
344
+
345
+ return () => {
346
+ for (const { el, type, fn } of listeners) el.removeEventListener(type, fn);
347
+ listeners.length = 0;
348
+ container.innerHTML = '';
349
+ };
350
+ }
@@ -0,0 +1,124 @@
1
+ ---
2
+ widget: notebook-compact
3
+ description: Reactive minimalist notebook with a left gutter showing cell types and named outputs. Cells chain via variable names (→ rows, → df) and display fresh/stale status. Minimal chrome, ideal for quick data exploration.
4
+ schema:
5
+ type: object
6
+ properties:
7
+ id:
8
+ type: string
9
+ description: Unique notebook identifier (for save/share via hyperskill)
10
+ title:
11
+ type: string
12
+ description: Notebook title
13
+ mode:
14
+ type: string
15
+ enum: [edit, view]
16
+ description: edit allows run/edit/reorder; view hides all controls for read-only display
17
+ cells:
18
+ type: array
19
+ description: Ordered list of cells (markdown, sql, js)
20
+ items:
21
+ type: object
22
+ required: [type, content]
23
+ properties:
24
+ type:
25
+ type: string
26
+ enum: [md, sql, js]
27
+ content:
28
+ type: string
29
+ description: Cell source (markdown text or code)
30
+ name:
31
+ type: string
32
+ description: Optional cell name
33
+ varname:
34
+ type: string
35
+ description: Named output variable (code cells only), displayed as → varname
36
+ hideSource:
37
+ type: boolean
38
+ hideResult:
39
+ type: boolean
40
+ ---
41
+
42
+ ## When to use
43
+
44
+ Use `notebook-compact` when the user wants to explore data quickly with a minimal, reactive interface. Ideal for:
45
+ - Iterative SQL / JS exploration where output variables feed each other
46
+ - Quick prototyping where chrome should fade into the background
47
+ - Hackathon playgrounds where users expect a familiar "type, see, share" flow
48
+ - Situations where the reactive dataflow (fresh/stale status) helps the user reason about stale results
49
+
50
+ Prefer over `notebook-workspace` when there is no multi-source concern and no publish/dashboard end goal.
51
+ Prefer over `notebook-document` when collaboration and comments are not central.
52
+ Prefer over `notebook-editorial` when the result is a working session, not a publication.
53
+
54
+ ## How to use
55
+
56
+ 1. **Create the widget** with a title and 2–4 seed cells:
57
+ ```
58
+ widget_display({name: "notebook-compact", params: {
59
+ title: "Exploration",
60
+ cells: [
61
+ {type: "md", content: "### Quick look\n\nLet's inspect the data."},
62
+ {type: "sql", content: "select * from source limit 10", varname: "rows"},
63
+ {type: "js", content: "console.table(rows)"}
64
+ ]
65
+ }})
66
+ ```
67
+
68
+ 2. **Name outputs** (`varname`) on the SQL/JS cells so the gutter displays `→ varname` and downstream cells can reference them. Named outputs are injected into the scope of every JS cell executed after them; if a downstream cell's code mentions `varname` (word-boundary match), it is flagged **stale** whenever the upstream cell re-runs, so the user sees what needs replaying.
69
+
70
+ 3. **Start markdown cells with a heading** (`### Title`) — the first line renders larger and gives the cell an anchor.
71
+
72
+ 4. **Let the user iterate** — they can add cells with `+ md / + sql / + js`, reorder via the drag handle, toggle source/result, and run cells with the green pill button.
73
+
74
+ ## Notes
75
+
76
+ - The Run button lives at the left of each cell's title row, right after the type label.
77
+ - Stop (red pill) appears while running, with a live elapsed timer.
78
+ - After a run, Run becomes the replay button (same green pill, re-click to re-run).
79
+ - **SQL cells** are dispatched to the first matching `*_query_sql` tool on the connected MCP data server (auto-detected).
80
+ - **JS cells** execute on the main thread via `new Function(...)` with upstream named outputs injected as scope — `console.log` / `console.table` results are captured and rendered inline. Note: there is no worker isolation yet, so a `while (true)` loop or other synchronous blocker will freeze the UI thread.
81
+ - Deletions prompt a confirmation modal and are recorded in the history panel; they can be restored from there.
82
+ - `mode: "view"` hides all controls (run, delete, drag, add), making the notebook read-only.
83
+
84
+ ## Left pane — connected data servers
85
+
86
+ When one or more MCP data servers are connected, a collapsible **left pane** (bookmark-bar styling, collapsed by default) lists their recipes and tools. Clicking any recipe opens it in a viewer modal; each fenced code block inside the recipe has a `↳ inject` button that drops the snippet into the notebook as a new cell.
87
+
88
+ Two toolbar buttons flank this pane:
89
+
90
+ - **`+ md`** — 3-tab modal (New / File / URL) to create a markdown cell from scratch, a local `.md` file, or a remote URL.
91
+ - **`+ recipe`** — 3-tab modal (Browser / File / URL) to import a recipe from a connected server, a local `.recipe.md` file, or a URL.
92
+
93
+ ## Share & publish
94
+
95
+ The `share` button offers **four formats**:
96
+
97
+ - **Hyperskill link** — copies both the canonical Hyperskill URL and a short domain-scoped URL (`?n=<token>`). Opens at `nb.hyperskills.net` (read-only public viewer) when mode is `view`.
98
+ - **Markdown** — downloads a `.md` file with the notebook content.
99
+ - **PNG** — snapshots the rendered notebook to an image.
100
+ - **JSON** — exports the full widget state for programmatic reuse.
101
+
102
+ ## Integration with connected data servers
103
+
104
+ If a MCP **data** server is connected (e.g. `tricoteuses`, `metmuseum`), BEFORE seeding cells:
105
+
106
+ 1. Call `{server}_list_recipes()` or `{server}_search_recipes(query)` to find recipes that match the user's intent.
107
+ 2. Call `{server}_list_tools()` to see available tables/endpoints.
108
+ 3. For each high-signal recipe or table, seed ONE cell that demonstrates it: an SQL `SELECT ... LIMIT 10`, a `run_script` call, or a short markdown note.
109
+ 4. Pass the server metadata via the `servers:` param so the UI can render the server menu modal and populate the left pane:
110
+
111
+ ```ts
112
+ widget_display({
113
+ name: 'notebook-compact',
114
+ params: {
115
+ title: '...',
116
+ cells: [...],
117
+ servers: [{ name: 'tricoteuses', url: 'https://...', kind: 'data' }]
118
+ }
119
+ })
120
+ ```
121
+
122
+ If NO data server is connected, seed generic markdown-only cells that explain the notebook's purpose and invite the user to connect an MCP server.
123
+
124
+ **Filter rule**: only include MCP *data* servers (`kind: 'data'`) in `servers:`. Do NOT include WebMCP UI servers like `autoui` — they don't expose queryable data and would clutter the menu.
@@ -0,0 +1,139 @@
1
+ ---
2
+ widget: notebook-document
3
+ description: Collaborative notebook styled as a shared document. Optional editor avatars at the top (opt-in), inline highlights in prose, optional margin comments next to cells (editable + threaded replies). Minimal cell chrome, reads as a report with conversation around it.
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
+ presence:
15
+ type: array
16
+ description: Optional list of editors to display as avatars at the top (opt-in). When absent or empty, no presence indicator is rendered.
17
+ items:
18
+ type: object
19
+ properties:
20
+ name:
21
+ type: string
22
+ color:
23
+ type: string
24
+ cells:
25
+ type: array
26
+ items:
27
+ type: object
28
+ required: [type, content]
29
+ properties:
30
+ type:
31
+ type: string
32
+ enum: [md, sql, js]
33
+ content:
34
+ type: string
35
+ description: For md cells, markdown is rendered and sanitized (use <mark> for inline highlights).
36
+ hideSource:
37
+ type: boolean
38
+ hideResult:
39
+ type: boolean
40
+ comment:
41
+ type: object
42
+ description: Optional margin comment attached to a code cell. Editable after creation; supports replies.
43
+ properties:
44
+ who:
45
+ type: string
46
+ when:
47
+ type: string
48
+ description: ISO timestamp or relative label. Relative time is recomputed from lastEditAt at render.
49
+ body:
50
+ type: string
51
+ replies:
52
+ type: array
53
+ items:
54
+ type: object
55
+ properties:
56
+ who: {type: string}
57
+ when: {type: string}
58
+ body: {type: string}
59
+ ---
60
+
61
+ ## When to use
62
+
63
+ Use `notebook-document` when the notebook is meant to be read and discussed by a team:
64
+ - Shared analyses where colleagues leave margin comments on specific cells
65
+ - Onboarding guides, knowledge docs, incident retros
66
+ - Any context where the notebook should feel like a Google Doc / Notion page, not a developer tool
67
+
68
+ Prose cells can contain `<mark>` highlights inline to draw attention to a sentence, and code cells can carry an optional margin comment object showing who commented and when — comments are editable after creation and support threaded replies via a `+ reply` button.
69
+
70
+ ## How to use
71
+
72
+ 1. **Create with prose-heavy seed content** and inline highlights for emphasis:
73
+ ```
74
+ widget_display({name: "notebook-document", params: {
75
+ title: "Weekly review",
76
+ cells: [
77
+ {type: "md", content: "Here is this week's summary. <mark>Pay attention to this metric.</mark> More context follows."},
78
+ {type: "sql", content: "select ...", comment: {who: "reviewer", when: "2m", body: "Can we filter X here?"}},
79
+ {type: "js", content: "// visualization"}
80
+ ]
81
+ }})
82
+ ```
83
+
84
+ 2. **Attach comments to code cells** by adding a `comment` object. The cell row splits into two columns (cell + comment) only when a comment exists. Comments can be edited inline after creation, and replies can be threaded under them via `+ reply`.
85
+
86
+ 3. **Keep prose short** — the layout punishes walls of text. Break them into several markdown cells with their own handles.
87
+
88
+ ## Notes
89
+
90
+ - Every cell (prose or code) has its own drag handle for reordering.
91
+ - Prose cells are rendered via an HTML-sanitizing markdown pipeline — `<mark>` is preserved, script/style and other dangerous tags are stripped (XSS closed).
92
+ - `<mark>` inside markdown renders with an amber tint; use sparingly.
93
+ - **Presence is opt-in**: the row of editor avatars and the "X editors online" label appear **only** when the `presence` param is explicitly provided with at least one entry. Without `presence`, nothing is rendered — no fake collaborators.
94
+ - A timestamp under the title reads `edited Xs ago`, computed in real time from the notebook's last edit.
95
+ - The footer shows a single `share` link that opens the share modal. (Live invite/collaboration is not available in this build.)
96
+ - **SQL cells** dispatch to the connected MCP server's `*_query_sql` tool (auto-detected). **JS cells** run in an isolated Web Worker with upstream named outputs in scope.
97
+ - Like the other notebook widgets, `mode: "view"` removes all editing and running controls.
98
+
99
+ ## Left pane — resources from connected servers
100
+
101
+ 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 in as a new cell.
102
+
103
+ Two toolbar buttons flank this pane:
104
+
105
+ - **`+ md`** — 3-tab modal (New / File / URL) to create a markdown cell from scratch, a local `.md` file, or a URL.
106
+ - **`+ recipe`** — 3-tab modal (Browser / File / URL) to import a recipe from a connected server, a local `.recipe.md` file, or a URL.
107
+
108
+ ## Share
109
+
110
+ The `share` button offers **four formats**:
111
+
112
+ - **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`.
113
+ - **Markdown** — downloads a `.md` file.
114
+ - **PNG** — snapshots the rendered document.
115
+ - **JSON** — exports full widget state.
116
+
117
+ ## Integration with connected data servers
118
+
119
+ When a MCP **data** server is connected (for instance `tricoteuses` or `metmuseum`), the document should open on real material rather than empty placeholders. Before seeding cells, the agent takes a short discovery pass:
120
+
121
+ 1. It calls `{server}_list_recipes()` or `{server}_search_recipes(query)` to locate recipes aligned with the user's topic.
122
+ 2. It calls `{server}_list_tools()` to learn which tables and endpoints the server exposes.
123
+ 3. For each relevant recipe or table, it seeds a single cell that grounds the document — a short SQL `SELECT ... LIMIT 10`, a `run_script` call, or a prose cell that introduces what the data shows. A margin `comment` on a code cell is a natural way to flag an open question for collaborators.
124
+ 4. It passes the server metadata through the `servers:` param so the share affordance and the left pane reflect what is in play:
125
+
126
+ ```ts
127
+ widget_display({
128
+ name: 'notebook-document',
129
+ params: {
130
+ title: '...',
131
+ cells: [...],
132
+ servers: [{ name: 'tricoteuses', url: 'https://...', kind: 'data' }]
133
+ }
134
+ })
135
+ ```
136
+
137
+ When no data server is connected, seed a prose-only skeleton that frames the discussion and invites the reader (or a colleague) to link an MCP server.
138
+
139
+ **Filter rule**: only MCP *data* servers (`kind: 'data'`) appear in `servers:`. WebMCP UI servers such as `autoui` are excluded — they carry no queryable data and would clutter the collaborative view.