@webmcp-auto-ui/ui 2.5.31 → 2.5.33

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 (80) hide show
  1. package/package.json +15 -2
  2. package/src/agent/DiagnosticModal.svelte +126 -50
  3. package/src/agent/EphemeralBubble.svelte +13 -3
  4. package/src/agent/MCPserversList.svelte +147 -0
  5. package/src/agent/McpConnector.svelte +10 -1
  6. package/src/agent/RecipeBrowser.svelte +384 -0
  7. package/src/agent/RemoteMCPserversDemo.svelte +5 -121
  8. package/src/agent/ToolBrowser.svelte +133 -0
  9. package/src/agent/WebMCPserversList.svelte +2 -0
  10. package/src/agent/useAgentLoop.svelte.ts +396 -0
  11. package/src/base/chat-inline.svelte +64 -0
  12. package/src/base/dialog-content.svelte +3 -1
  13. package/src/components/HeaderControls.svelte +78 -0
  14. package/src/index.ts +13 -35
  15. package/src/stores/canvas.svelte.ts +0 -6
  16. package/src/widgets/SafeImage.svelte +67 -0
  17. package/src/widgets/WidgetRenderer.svelte +153 -78
  18. package/src/widgets/notebook/executors/index.ts +0 -1
  19. package/src/widgets/notebook/executors/sql.ts +32 -182
  20. package/src/widgets/notebook/import-modal-api.ts +237 -0
  21. package/src/widgets/notebook/import-modal.svelte +738 -0
  22. package/src/widgets/notebook/left-pane.ts +1 -1
  23. package/src/widgets/notebook/notebook.svelte +75 -0
  24. package/src/widgets/notebook/notebook.ts +38 -73
  25. package/src/widgets/notebook/prose.ts +6 -3
  26. package/src/widgets/notebook/shared.ts +68 -49
  27. package/src/widgets/rich/cards.svelte +74 -0
  28. package/src/widgets/rich/carousel.svelte +126 -0
  29. package/src/widgets/rich/chart-rich.svelte +221 -0
  30. package/src/widgets/rich/chat-input.svelte +52 -0
  31. package/src/widgets/rich/data-table.svelte +132 -0
  32. package/src/widgets/rich/gallery.svelte +115 -0
  33. package/src/widgets/rich/grid-data.svelte +85 -0
  34. package/src/widgets/rich/hemicycle.svelte +95 -0
  35. package/src/widgets/rich/js-sandbox.svelte +67 -0
  36. package/src/widgets/rich/json-viewer.svelte +82 -0
  37. package/src/widgets/rich/log.svelte +62 -0
  38. package/src/widgets/rich/profile.svelte +91 -0
  39. package/src/widgets/rich/sankey.svelte +73 -0
  40. package/src/widgets/rich/stat-card.svelte +60 -0
  41. package/src/widgets/rich/timeline.svelte +95 -0
  42. package/src/widgets/rich/trombinoscope.svelte +87 -0
  43. package/src/widgets/simple/actions.svelte +36 -0
  44. package/src/widgets/simple/alert.svelte +52 -0
  45. package/src/widgets/simple/chart.svelte +38 -0
  46. package/src/widgets/simple/code.svelte +30 -0
  47. package/src/widgets/simple/kv.svelte +31 -0
  48. package/src/widgets/simple/list.svelte +35 -0
  49. package/src/widgets/simple/stat.svelte +36 -0
  50. package/src/widgets/simple/tags.svelte +34 -0
  51. package/src/widgets/simple/text.svelte +130 -0
  52. package/src/widgets/helpers/safe-image.ts +0 -78
  53. package/src/widgets/notebook/import-modals.ts +0 -560
  54. package/src/widgets/notebook/recipe-browser.ts +0 -350
  55. package/src/widgets/rich/cards.ts +0 -181
  56. package/src/widgets/rich/carousel.ts +0 -319
  57. package/src/widgets/rich/chart-rich.ts +0 -386
  58. package/src/widgets/rich/d3.ts +0 -503
  59. package/src/widgets/rich/data-table.ts +0 -342
  60. package/src/widgets/rich/gallery.ts +0 -350
  61. package/src/widgets/rich/grid-data.ts +0 -173
  62. package/src/widgets/rich/hemicycle.ts +0 -313
  63. package/src/widgets/rich/js-sandbox.ts +0 -122
  64. package/src/widgets/rich/json-viewer.ts +0 -202
  65. package/src/widgets/rich/log.ts +0 -143
  66. package/src/widgets/rich/map.ts +0 -218
  67. package/src/widgets/rich/profile.ts +0 -256
  68. package/src/widgets/rich/sankey.ts +0 -257
  69. package/src/widgets/rich/stat-card.ts +0 -125
  70. package/src/widgets/rich/timeline.ts +0 -179
  71. package/src/widgets/rich/trombinoscope.ts +0 -246
  72. package/src/widgets/simple/actions.ts +0 -89
  73. package/src/widgets/simple/alert.ts +0 -100
  74. package/src/widgets/simple/chart.ts +0 -189
  75. package/src/widgets/simple/code.ts +0 -79
  76. package/src/widgets/simple/kv.ts +0 -68
  77. package/src/widgets/simple/list.ts +0 -89
  78. package/src/widgets/simple/stat.ts +0 -58
  79. package/src/widgets/simple/tags.ts +0 -125
  80. package/src/widgets/simple/text.ts +0 -198
@@ -1,78 +0,0 @@
1
- /**
2
- * SafeImage (vanilla) — URL validation, error fallback, placeholder rendering.
3
- * Replaces SafeImage.svelte. Used by Profile/Cards/Carousel/Gallery/Trombinoscope renderers.
4
- */
5
-
6
- export interface SafeImageOptions {
7
- src: string | undefined | null;
8
- alt?: string;
9
- className?: string;
10
- style?: string;
11
- loading?: 'lazy' | 'eager';
12
- fallbackText?: string;
13
- hideOnError?: boolean;
14
- }
15
-
16
- const VALID_PREFIXES = ['http://', 'https://', 'data:', '/'];
17
-
18
- function isValidSrc(src: unknown): src is string {
19
- return typeof src === 'string' && src.length > 0 && VALID_PREFIXES.some((p) => (src as string).startsWith(p));
20
- }
21
-
22
- function escapeHtml(s: string): string {
23
- return s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]!));
24
- }
25
-
26
- function buildPlaceholder(label: string, opts: SafeImageOptions): HTMLElement {
27
- const div = document.createElement('div');
28
- div.className = `flex items-center justify-center bg-surface2 text-text2 text-xs ${opts.className ?? ''}`.trim();
29
- if (opts.style) div.setAttribute('style', opts.style);
30
- div.setAttribute('role', 'img');
31
- div.setAttribute('aria-label', label);
32
- div.innerHTML =
33
- '<svg class="w-5 h-5 opacity-40 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">' +
34
- '<rect x="3" y="3" width="18" height="18" rx="2"/>' +
35
- '<circle cx="8.5" cy="8.5" r="1.5"/>' +
36
- '<path d="M21 15l-5-5L5 21"/></svg>' +
37
- `<span class="truncate max-w-[80%]">${escapeHtml(label)}</span>`;
38
- return div;
39
- }
40
-
41
- function buildHidden(): HTMLElement {
42
- const el = document.createElement('div');
43
- el.style.display = 'none';
44
- el.setAttribute('aria-hidden', 'true');
45
- return el;
46
- }
47
-
48
- /**
49
- * Returns an HTMLElement for the given image source.
50
- * - Invalid/missing URL → placeholder (or hidden element if hideOnError).
51
- * - Valid URL → <img> that self-replaces with placeholder on load error.
52
- */
53
- export function createSafeImage(opts: SafeImageOptions): HTMLElement {
54
- const label = opts.fallbackText ?? opts.alt ?? 'Image';
55
-
56
- if (!isValidSrc(opts.src)) {
57
- return opts.hideOnError ? buildHidden() : buildPlaceholder(label, opts);
58
- }
59
-
60
- const img = document.createElement('img');
61
- img.src = opts.src;
62
- img.alt = opts.alt ?? '';
63
- if (opts.className) img.className = opts.className;
64
- if (opts.style) img.setAttribute('style', opts.style);
65
- img.loading = opts.loading ?? 'lazy';
66
- img.referrerPolicy = 'no-referrer';
67
-
68
- img.addEventListener(
69
- 'error',
70
- () => {
71
- const replacement = opts.hideOnError ? buildHidden() : buildPlaceholder(label, opts);
72
- img.replaceWith(replacement);
73
- },
74
- { once: true },
75
- );
76
-
77
- return img;
78
- }
@@ -1,560 +0,0 @@
1
- // @ts-nocheck
2
- // ---------------------------------------------------------------------------
3
- // Import modals for notebook widgets:
4
- // - openAddMdModal: new / file / url
5
- // - openAddRecipeModal: browser (WebMCP built-in + MCP server) / file / url
6
- // - openRecipeViewerModal: md rendered with ↳ inject buttons per fence
7
- // - openToolViewerModal: name + description + schema + inject button
8
- // ---------------------------------------------------------------------------
9
-
10
- import { callToolViaPostMessage } from '@webmcp-auto-ui/core';
11
- // NOTE: imports from @webmcp-auto-ui/agent create an ESM cycle ui <-> agent.
12
- // Safe: agent's autoui-server imports from ui/widgets/notebook for renderers,
13
- // while these recipe helpers are only invoked inside functions (no top-level eval).
14
- import { filterRecipes, sortRecipes, WEBMCP_RECIPES } from '@webmcp-auto-ui/agent';
15
- import { renderMarkdownWithInjectButtons } from './prose.js';
16
- import { extractCellsFromRecipe, extractCellsFromTool, extractCellFromMarkdown, extractCellFromFence } from './resource-extractor.js';
17
- import type { NotebookCell } from './shared.js';
18
- import type { McpToolLike } from './resource-extractor.js';
19
-
20
- // ---------------------------------------------------------------------------
21
- // Types
22
- // ---------------------------------------------------------------------------
23
-
24
- export interface ImportedRecipe {
25
- name: string;
26
- description?: string;
27
- body?: string;
28
- serverName?: string;
29
- serverUrl?: string;
30
- originalName?: string;
31
- id?: string;
32
- }
33
-
34
- export type MdSource = { kind: 'new' } | { kind: 'content'; content: string };
35
-
36
- // ---------------------------------------------------------------------------
37
- // Shared modal shell
38
- // ---------------------------------------------------------------------------
39
-
40
- function ensureImportOverlay(): HTMLElement {
41
- let ov = document.getElementById('nb-import-overlay') as HTMLElement | null;
42
- if (ov) return ov;
43
- ov = document.createElement('div');
44
- ov.id = 'nb-import-overlay';
45
- ov.className = 'nb-import-overlay';
46
- document.body.appendChild(ov);
47
- ov.addEventListener('click', (e) => {
48
- if (e.target === ov) closeImportModal();
49
- });
50
- injectImportStyles();
51
- return ov;
52
- }
53
-
54
- export function closeImportModal(): void {
55
- const ov = document.getElementById('nb-import-overlay');
56
- if (ov) ov.classList.remove('open');
57
- }
58
-
59
- function openWith(html: string): HTMLElement {
60
- const ov = ensureImportOverlay();
61
- ov.innerHTML = `<div class="nb-import-modal">${html}</div>`;
62
- ov.classList.add('open');
63
- return ov;
64
- }
65
-
66
- function tabButton(id: string, label: string, active: boolean): string {
67
- return `<button type="button" class="nb-imp-tab${active ? ' nb-imp-tab-active' : ''}" data-tab="${id}">${label}</button>`;
68
- }
69
-
70
- // ---------------------------------------------------------------------------
71
- // openAddMdModal — 3 tabs: New / File / URL → returns a string of content
72
- // ---------------------------------------------------------------------------
73
-
74
- export function openAddMdModal(onPick: (content: string) => void): void {
75
- const ov = openWith(`
76
- <header class="nb-imp-head">
77
- <span class="nb-imp-title">Add markdown</span>
78
- <button type="button" class="nb-imp-close">×</button>
79
- </header>
80
- <nav class="nb-imp-tabs">
81
- ${tabButton('new', 'New', true)}
82
- ${tabButton('file', 'File', false)}
83
- ${tabButton('url', 'URL', false)}
84
- </nav>
85
- <section class="nb-imp-body" data-active="new">
86
- <div class="nb-imp-panel" data-panel="new">
87
- <p class="nb-imp-hint">Paste markdown below, or leave empty to create a blank cell you can edit in place.</p>
88
- <textarea class="nb-imp-md-textarea" placeholder="### Heading\n\nParagraph text…"
89
- rows="10" spellcheck="true"></textarea>
90
- <div class="nb-imp-md-actions">
91
- <button type="button" class="nb-imp-btn nb-imp-primary" data-act="insert-md">Insert</button>
92
- </div>
93
- </div>
94
- <div class="nb-imp-panel" data-panel="file" hidden>
95
- <p class="nb-imp-hint">Pick a .md file from your computer.</p>
96
- <input type="file" accept=".md,.markdown,text/markdown,text/plain" class="nb-imp-file" />
97
- </div>
98
- <div class="nb-imp-panel" data-panel="url" hidden>
99
- <p class="nb-imp-hint">Fetch a markdown URL (routed through /api/proxy to avoid CORS).</p>
100
- <input type="url" placeholder="https://..." class="nb-imp-url" />
101
- <button type="button" class="nb-imp-btn nb-imp-primary" data-act="fetch-url">Fetch</button>
102
- <div class="nb-imp-error" data-role="error" hidden></div>
103
- </div>
104
- </section>
105
- `);
106
-
107
- bindCloseAndTabs(ov);
108
-
109
- const mdTextarea = ov.querySelector('.nb-imp-md-textarea') as HTMLTextAreaElement | null;
110
- const insertMd = () => {
111
- const val = mdTextarea?.value ?? '';
112
- const content = val.trim() === '' ? '### new section\n\nwrite here…' : val;
113
- onPick(content);
114
- closeImportModal();
115
- };
116
- ov.querySelector('[data-act="insert-md"]')!.addEventListener('click', insertMd);
117
- mdTextarea?.addEventListener('keydown', (e: KeyboardEvent) => {
118
- if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
119
- e.preventDefault();
120
- insertMd();
121
- }
122
- });
123
-
124
- (ov.querySelector('.nb-imp-file') as HTMLInputElement).addEventListener('change', async (e) => {
125
- const file = (e.target as HTMLInputElement).files?.[0];
126
- if (!file) return;
127
- const text = await file.text();
128
- onPick(text);
129
- closeImportModal();
130
- });
131
-
132
- ov.querySelector('[data-act="fetch-url"]')!.addEventListener('click', async () => {
133
- const input = ov.querySelector('.nb-imp-url') as HTMLInputElement;
134
- const err = ov.querySelector('[data-role="error"]') as HTMLElement;
135
- err.hidden = true;
136
- const url = input.value.trim();
137
- if (!url) return;
138
- try {
139
- const text = await fetchViaProxy(url);
140
- onPick(text);
141
- closeImportModal();
142
- } catch (e: any) {
143
- err.textContent = 'Fetch failed: ' + (e?.message ?? e);
144
- err.hidden = false;
145
- }
146
- });
147
- }
148
-
149
- // ---------------------------------------------------------------------------
150
- // openAddRecipeModal — 3 tabs: Browser / File / URL
151
- // ---------------------------------------------------------------------------
152
-
153
- export interface AddRecipeModalOptions {
154
- /** Connected MCP data servers; if any, their recipes are merged in the Browser tab. */
155
- mcpServers?: Array<{ name: string; url?: string }>;
156
- /** If 'data', hide built-in WEBMCP_RECIPES and list only MCP data servers recipes. Default 'all'. */
157
- scope?: 'data' | 'all';
158
- onPick: (recipe: ImportedRecipe) => void;
159
- }
160
-
161
- export function openAddRecipeModal(opts: AddRecipeModalOptions): void {
162
- const ov = openWith(`
163
- <header class="nb-imp-head">
164
- <span class="nb-imp-title">Add recipe</span>
165
- <button type="button" class="nb-imp-close">×</button>
166
- </header>
167
- <nav class="nb-imp-tabs">
168
- ${tabButton('browser', 'Browser', true)}
169
- ${tabButton('file', 'File', false)}
170
- ${tabButton('url', 'URL', false)}
171
- </nav>
172
- <section class="nb-imp-body" data-active="browser">
173
- <div class="nb-imp-panel" data-panel="browser">
174
- <input type="search" placeholder="Search recipes..." class="nb-imp-search" />
175
- <div class="nb-imp-recipes" data-role="list"></div>
176
- </div>
177
- <div class="nb-imp-panel" data-panel="file" hidden>
178
- <p class="nb-imp-hint">Pick a .md recipe file.</p>
179
- <input type="file" accept=".md,.markdown,text/markdown,text/plain" class="nb-imp-file" />
180
- </div>
181
- <div class="nb-imp-panel" data-panel="url" hidden>
182
- <p class="nb-imp-hint">Fetch a recipe URL (routed through /api/proxy).</p>
183
- <input type="url" placeholder="https://..." class="nb-imp-url" />
184
- <button type="button" class="nb-imp-btn nb-imp-primary" data-act="fetch-url">Fetch</button>
185
- <div class="nb-imp-error" data-role="error" hidden></div>
186
- </div>
187
- </section>
188
- `);
189
-
190
- bindCloseAndTabs(ov);
191
-
192
- const list = ov.querySelector('[data-role="list"]') as HTMLElement;
193
- const search = ov.querySelector('.nb-imp-search') as HTMLInputElement;
194
-
195
- // Load built-in recipes immediately + MCP recipes lazily (list_recipes)
196
- const includeBuiltin = opts.scope !== 'data';
197
- const builtin = includeBuiltin
198
- ? WEBMCP_RECIPES.map((r) => ({
199
- name: r.name,
200
- description: r.description,
201
- body: r.body,
202
- serverName: 'webmcp',
203
- }))
204
- : [];
205
-
206
- let all: ImportedRecipe[] = [...builtin];
207
- const hasServers = !!(opts.mcpServers && opts.mcpServers.length > 0);
208
- if (!includeBuiltin && !hasServers) {
209
- list.innerHTML = '<div class="nb-imp-empty">No data servers connected. Connect one to see recipes.</div>';
210
- } else if (!includeBuiltin && hasServers) {
211
- // No built-ins to show yet → display a transient loading state while we
212
- // wait for list_recipes to resolve. Avoids a flash of empty UI.
213
- list.innerHTML = '<div class="nb-imp-empty">Loading recipes…</div>';
214
- } else {
215
- renderList(list, all, onPickRecipe);
216
- }
217
-
218
- // Fetch MCP server recipes in parallel
219
- if (opts.mcpServers && opts.mcpServers.length > 0) {
220
- let pending = opts.mcpServers.length;
221
- for (const srv of opts.mcpServers) {
222
- callToolViaPostMessage(`${srv.name}_list_recipes`, {}).then((res: any) => {
223
- const items = extractRecipeItems(res, srv);
224
- if (items.length) {
225
- all = all.concat(items);
226
- }
227
- }).catch(() => { /* ignore: some servers may not expose list_recipes */ })
228
- .finally(() => {
229
- pending--;
230
- // Re-render now (showing whatever we have so far) and once more on
231
- // the final settle so the loading state is replaced even if no
232
- // server returned any recipes.
233
- if (all.length > 0 || pending === 0) {
234
- renderList(list, filterRecipes(all, search.value), onPickRecipe);
235
- }
236
- });
237
- }
238
- }
239
-
240
- search.addEventListener('input', () => renderList(list, filterRecipes(all, search.value), onPickRecipe));
241
-
242
- async function onPickRecipe(r: ImportedRecipe) {
243
- if (!r.body && r.serverName && r.serverName !== 'webmcp') {
244
- try {
245
- const res: any = await callToolViaPostMessage(`${r.serverName}_get_recipe`, { name: r.originalName ?? r.name, id: r.id ?? r.name });
246
- r.body = extractRecipeBody(res) ?? '';
247
- } catch { /* keep empty body */ }
248
- }
249
- opts.onPick(r);
250
- closeImportModal();
251
- }
252
-
253
- // File / URL tabs
254
- (ov.querySelector('.nb-imp-file') as HTMLInputElement).addEventListener('change', async (e) => {
255
- const file = (e.target as HTMLInputElement).files?.[0];
256
- if (!file) return;
257
- const text = await file.text();
258
- opts.onPick({ name: file.name.replace(/\.md$/, ''), body: text });
259
- closeImportModal();
260
- });
261
- ov.querySelector('[data-act="fetch-url"]')!.addEventListener('click', async () => {
262
- const input = ov.querySelector('.nb-imp-url') as HTMLInputElement;
263
- const err = ov.querySelector('[data-role="error"]') as HTMLElement;
264
- err.hidden = true;
265
- const url = input.value.trim();
266
- if (!url) return;
267
- try {
268
- const text = await fetchViaProxy(url);
269
- opts.onPick({ name: new URL(url).pathname.split('/').pop() || 'recipe', body: text });
270
- closeImportModal();
271
- } catch (e: any) {
272
- err.textContent = 'Fetch failed: ' + (e?.message ?? e);
273
- err.hidden = false;
274
- }
275
- });
276
- }
277
-
278
- function renderList(list: HTMLElement, recipes: ImportedRecipe[], onPick: (r: ImportedRecipe) => void) {
279
- const sorted = sortRecipes(recipes);
280
- list.innerHTML = '';
281
- if (!sorted.length) {
282
- list.innerHTML = '<div class="nb-imp-empty">No recipes.</div>';
283
- return;
284
- }
285
- for (const r of sorted) {
286
- const row = document.createElement('div');
287
- row.className = 'nb-imp-recipe';
288
- row.innerHTML = `
289
- <div class="nb-imp-recipe-name">${escapeHtml(r.name)}</div>
290
- ${r.description ? `<div class="nb-imp-recipe-desc">${escapeHtml(r.description)}</div>` : ''}
291
- ${r.serverName ? `<div class="nb-imp-recipe-srv">${escapeHtml(r.serverName)}</div>` : ''}
292
- `;
293
- row.addEventListener('click', () => onPick(r));
294
- list.appendChild(row);
295
- }
296
- }
297
-
298
- function extractRecipeItems(res: any, srv: { name: string; url?: string }): ImportedRecipe[] {
299
- const text = res?.content?.find?.((c: any) => c.type === 'text')?.text;
300
- if (!text) return [];
301
- let parsed: any;
302
- try { parsed = JSON.parse(text); } catch { return []; }
303
- const items = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.recipes) ? parsed.recipes : []);
304
- return items.map((it: any) => ({
305
- name: String(it?.name ?? it?.id ?? 'unnamed'),
306
- description: it?.description,
307
- originalName: it?.name,
308
- id: it?.id,
309
- serverName: srv.name,
310
- serverUrl: srv.url,
311
- }));
312
- }
313
-
314
- function extractRecipeBody(res: any): string | null {
315
- const text = res?.content?.find?.((c: any) => c.type === 'text')?.text;
316
- if (!text) return null;
317
- try {
318
- const parsed = JSON.parse(text);
319
- // Recipe servers return either { body: "..." } (autoui-style),
320
- // { content: "..." } (legacy), or { markdown: "..." }. Pick whichever
321
- // string field carries the markdown body, in priority order.
322
- if (parsed && typeof parsed === 'object') {
323
- if (typeof parsed.body === 'string') return parsed.body;
324
- if (typeof parsed.content === 'string') return parsed.content;
325
- if (typeof parsed.markdown === 'string') return parsed.markdown;
326
- }
327
- } catch { /* not JSON */ }
328
- return text;
329
- }
330
-
331
- // ---------------------------------------------------------------------------
332
- // openRecipeViewerModal — markdown rendered + ↳ inject on each code fence
333
- // ---------------------------------------------------------------------------
334
-
335
- export function openRecipeViewerModal(
336
- recipe: ImportedRecipe,
337
- onInjectCell: (cell: NotebookCell) => void,
338
- ): void {
339
- const body = recipe.body || '';
340
- const ov = openWith(`
341
- <header class="nb-imp-head">
342
- <span class="nb-imp-title">${escapeHtml(recipe.name)}</span>
343
- <button type="button" class="nb-imp-close">×</button>
344
- </header>
345
- <div class="nb-imp-recipe-meta">
346
- ${recipe.description ? `<p>${escapeHtml(recipe.description)}</p>` : ''}
347
- ${recipe.serverName ? `<span class="nb-imp-recipe-srv">${escapeHtml(recipe.serverName)}</span>` : ''}
348
- </div>
349
- <section class="nb-imp-body nb-imp-body-recipe" data-role="render"></section>
350
- <footer class="nb-imp-foot">
351
- <button type="button" class="nb-imp-btn" data-act="inject-all">Inject all cells</button>
352
- </footer>
353
- `);
354
- bindCloseAndTabs(ov);
355
-
356
- const target = ov.querySelector('[data-role="render"]') as HTMLElement;
357
- const { root } = renderMarkdownWithInjectButtons(body, ({ lang, content }) => {
358
- const cell = fenceToCell(lang, content);
359
- onInjectCell(cell);
360
- });
361
- target.appendChild(root);
362
-
363
- ov.querySelector('[data-act="inject-all"]')!.addEventListener('click', () => {
364
- const cells = extractCellsFromRecipe(body, { title: recipe.name, description: recipe.description });
365
- for (const c of cells) onInjectCell(c);
366
- closeImportModal();
367
- });
368
- }
369
-
370
- function fenceToCell(lang: string, content: string): NotebookCell {
371
- return extractCellFromFence(lang, content);
372
- }
373
-
374
- // ---------------------------------------------------------------------------
375
- // openToolViewerModal — show tool meta + inject button
376
- // ---------------------------------------------------------------------------
377
-
378
- export function openToolViewerModal(
379
- tool: McpToolLike,
380
- onInjectCells: (cells: NotebookCell[]) => void,
381
- ): void {
382
- const schema = tool.inputSchema ?? tool.schema ?? {};
383
- const ov = openWith(`
384
- <header class="nb-imp-head">
385
- <span class="nb-imp-title">${escapeHtml(tool.name)}${tool.serverName ? ` <span class="nb-imp-recipe-srv">${escapeHtml(tool.serverName)}</span>` : ''}</span>
386
- <button type="button" class="nb-imp-close">×</button>
387
- </header>
388
- <section class="nb-imp-body nb-imp-body-tool">
389
- ${tool.description ? `<p class="nb-imp-tool-desc">${escapeHtml(tool.description)}</p>` : ''}
390
- <div class="nb-imp-tool-schema">
391
- <div class="nb-imp-hint">input schema</div>
392
- <pre><code>${escapeHtml(JSON.stringify(schema, null, 2))}</code></pre>
393
- </div>
394
- </section>
395
- <footer class="nb-imp-foot">
396
- <button type="button" class="nb-imp-btn nb-imp-primary" data-act="inject">↳ inject as cell</button>
397
- </footer>
398
- `);
399
- bindCloseAndTabs(ov);
400
- ov.querySelector('[data-act="inject"]')!.addEventListener('click', () => {
401
- onInjectCells(extractCellsFromTool(tool));
402
- closeImportModal();
403
- });
404
- }
405
-
406
- // ---------------------------------------------------------------------------
407
- // Helpers
408
- // ---------------------------------------------------------------------------
409
-
410
- function bindCloseAndTabs(ov: HTMLElement) {
411
- ov.querySelector('.nb-imp-close')?.addEventListener('click', closeImportModal);
412
- const tabs = ov.querySelectorAll<HTMLElement>('.nb-imp-tab');
413
- const panels = ov.querySelectorAll<HTMLElement>('.nb-imp-panel');
414
- tabs.forEach((t) => {
415
- t.addEventListener('click', () => {
416
- tabs.forEach((x) => x.classList.remove('nb-imp-tab-active'));
417
- t.classList.add('nb-imp-tab-active');
418
- const id = t.dataset.tab;
419
- panels.forEach((p) => { p.hidden = p.dataset.panel !== id; });
420
- });
421
- });
422
- }
423
-
424
- async function fetchViaProxy(url: string): Promise<string> {
425
- // Use the local proxy endpoint if available, else direct fetch.
426
- // Agent H wires /api/proxy.
427
- try {
428
- const prox = `/api/proxy?url=${encodeURIComponent(url)}`;
429
- const res = await fetch(prox);
430
- if (res.ok) return await res.text();
431
- } catch { /* fallback to direct */ }
432
- const res = await fetch(url);
433
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
434
- return await res.text();
435
- }
436
-
437
- function escapeHtml(s: string): string {
438
- return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
439
- }
440
-
441
- // ---------------------------------------------------------------------------
442
- // Styles (injected once)
443
- // ---------------------------------------------------------------------------
444
-
445
- function injectImportStyles() {
446
- if (document.getElementById('nb-import-styles')) return;
447
- const s = document.createElement('style');
448
- s.id = 'nb-import-styles';
449
- s.textContent = `
450
- .nb-import-overlay {
451
- position: fixed; inset: 0; z-index: 2000;
452
- background: rgba(0,0,0,0.5);
453
- display: none; align-items: center; justify-content: center;
454
- }
455
- .nb-import-overlay.open { display: flex; }
456
- .nb-import-modal {
457
- width: min(680px, 92vw); max-height: 84vh;
458
- background: var(--color-surface, #fff); color: var(--color-text1, #111);
459
- border-radius: 14px; box-shadow: 0 20px 40px rgba(0,0,0,0.2);
460
- display: flex; flex-direction: column; overflow: hidden;
461
- font-family: var(--font-sans, system-ui);
462
- }
463
- .nb-imp-head {
464
- display: flex; align-items: center; padding: 14px 18px;
465
- border-bottom: 1px solid var(--color-border, #eee);
466
- }
467
- .nb-imp-title { flex: 1; font-weight: 600; font-size: 14px; }
468
- .nb-imp-recipe-srv {
469
- font-family: monospace; font-size: 11px; color: var(--color-text2, #666);
470
- margin-left: 6px;
471
- }
472
- .nb-imp-close { background: none; border: none; cursor: pointer; font-size: 20px; line-height: 1; color: var(--color-text2, #666); }
473
- .nb-imp-tabs { display: flex; padding: 0 14px; border-bottom: 1px solid var(--color-border, #eee); }
474
- .nb-imp-tab {
475
- background: none; border: none; cursor: pointer; padding: 10px 14px;
476
- font-size: 12px; color: var(--color-text2, #666);
477
- border-bottom: 2px solid transparent;
478
- }
479
- .nb-imp-tab-active { color: var(--color-text1, #111); border-bottom-color: var(--color-accent, #6a55ff); }
480
- .nb-imp-body { padding: 16px 18px; overflow-y: auto; flex: 1; }
481
- .nb-imp-body-recipe, .nb-imp-body-tool { font-size: 13px; }
482
- .nb-imp-panel[hidden] { display: none; }
483
- .nb-imp-hint { font-size: 12px; color: var(--color-text2, #666); margin: 0 0 10px 0; }
484
- .nb-imp-btn {
485
- background: var(--color-surface2, #f4f4f5); border: 1px solid var(--color-border, #e4e4e7);
486
- border-radius: 6px; padding: 8px 14px; font-size: 12px; cursor: pointer;
487
- }
488
- .nb-imp-btn:hover { background: var(--color-surface3, #eeeef0); }
489
- .nb-imp-primary { background: var(--color-accent, #6a55ff); color: #fff; border: 0; }
490
- .nb-imp-primary:hover { filter: brightness(1.1); color: #fff; }
491
- .nb-imp-search, .nb-imp-url {
492
- width: 100%; padding: 8px 10px; border: 1px solid var(--color-border, #e4e4e7);
493
- border-radius: 6px; font-size: 12px; margin-bottom: 10px;
494
- background: var(--color-surface, #fff); color: var(--color-text1, #111);
495
- }
496
- .nb-imp-file { font-size: 12px; }
497
- .nb-imp-md-textarea {
498
- width: 100%; min-height: 180px;
499
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
500
- font-size: 13px; line-height: 1.5;
501
- padding: 10px 12px;
502
- border: 1px solid var(--color-border, #e4e4e7);
503
- border-radius: 6px;
504
- background: var(--color-bg, #fff);
505
- color: var(--color-text1, #111);
506
- resize: vertical;
507
- outline: none;
508
- box-sizing: border-box;
509
- }
510
- .nb-imp-md-textarea:focus { border-color: var(--color-accent, #6a55ff); }
511
- .nb-imp-md-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
512
- .nb-imp-recipes { display: flex; flex-direction: column; gap: 6px; max-height: 46vh; overflow-y: auto; }
513
- .nb-imp-recipe {
514
- padding: 10px 12px; border-radius: 8px; cursor: pointer;
515
- background: var(--color-surface2, #f4f4f5);
516
- transition: background 0.12s;
517
- }
518
- .nb-imp-recipe:hover { background: var(--color-surface3, #eeeef0); }
519
- .nb-imp-recipe-name { font-weight: 500; font-size: 13px; }
520
- .nb-imp-recipe-desc { font-size: 11.5px; color: var(--color-text2, #666); margin-top: 3px; }
521
- .nb-imp-recipe-meta { padding: 0 18px 10px; border-bottom: 1px solid var(--color-border, #eee); }
522
- .nb-imp-empty { color: var(--color-text2, #666); font-size: 12px; padding: 14px; text-align: center; }
523
- .nb-imp-foot { padding: 12px 18px; border-top: 1px solid var(--color-border, #eee); display: flex; justify-content: flex-end; gap: 8px; }
524
- .nb-imp-error { color: #c2323a; font-size: 12px; margin-top: 8px; }
525
-
526
- .nb-md-render { font-size: 13px; line-height: 1.5; }
527
- .nb-md-render h1, .nb-md-render h2, .nb-md-render h3 { margin: 12px 0 6px; }
528
- .nb-md-render p { margin: 6px 0; }
529
- .nb-md-render ul, .nb-md-render ol { margin: 6px 0 6px 20px; }
530
- .nb-md-render pre {
531
- background: var(--color-surface2, #f4f4f5); padding: 10px 12px;
532
- border-radius: 6px; overflow-x: auto; font-size: 12px;
533
- }
534
- .nb-md-render code { font-family: var(--font-mono, monospace); font-size: 12px; }
535
- .nb-md-fence {
536
- border: 1px solid var(--color-border, #e4e4e7); border-radius: 8px;
537
- margin: 10px 0; overflow: hidden;
538
- }
539
- .nb-md-fence-head {
540
- display: flex; align-items: center; gap: 10px;
541
- padding: 6px 10px; background: var(--color-surface2, #f4f4f5);
542
- font-size: 11px; color: var(--color-text2, #666);
543
- border-bottom: 1px solid var(--color-border, #e4e4e7);
544
- }
545
- .nb-md-fence-lang { font-family: monospace; flex: 1; }
546
- .nb-md-fence-inject {
547
- background: var(--color-accent, #6a55ff); color: #fff; border: 0;
548
- border-radius: 4px; padding: 3px 9px; font-size: 11px; cursor: pointer;
549
- }
550
- .nb-md-fence-inject:hover { filter: brightness(1.08); }
551
- .nb-md-fence pre { margin: 0; border-radius: 0; background: transparent; }
552
-
553
- .nb-imp-tool-desc { font-size: 13px; margin: 0 0 10px; }
554
- .nb-imp-tool-schema pre {
555
- background: var(--color-surface2, #f4f4f5); padding: 10px;
556
- border-radius: 6px; max-height: 40vh; overflow: auto; font-size: 11.5px;
557
- }
558
- `;
559
- document.head.appendChild(s);
560
- }