@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
@@ -0,0 +1,738 @@
1
+ <svelte:options customElement={{ tag: 'auto-import-modal', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ // ---------------------------------------------------------------------------
5
+ // <auto-import-modal> — Custom Element Svelte 5
6
+ // Replaces import-modals.ts (vanilla 560 l.)
7
+ //
8
+ // Four modal modes, controlled via `mode` prop:
9
+ // 'add-md' → 3 tabs: New / File / URL → emits widget:interact {action:'pick-md', payload:string}
10
+ // 'add-recipe' → 3 tabs: Browser / File / URL → emits widget:interact {action:'pick-recipe', payload:ImportedRecipe}
11
+ // 'recipe-viewer' → markdown + inject buttons → emits widget:interact {action:'inject-cell', payload:NotebookCell}
12
+ // 'tool-viewer' → schema display → emits widget:interact {action:'inject-cells', payload:NotebookCell[]}
13
+ //
14
+ // All modes emit widget:interact {action:'close'} on close (in addition to the host
15
+ // calling data.onClose or listening to the event).
16
+ //
17
+ // data.mcpServers, data.scope, data.recipe, data.tool are mode-specific payloads.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ import { filterRecipes, sortRecipes, WEBMCP_RECIPES } from '@webmcp-auto-ui/agent';
21
+ import { callToolViaPostMessage } from '@webmcp-auto-ui/core';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Types (inlined to avoid import-modals.ts cycle)
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export interface ImportedRecipe {
28
+ name: string;
29
+ description?: string;
30
+ body?: string;
31
+ serverName?: string;
32
+ serverUrl?: string;
33
+ originalName?: string;
34
+ id?: string;
35
+ }
36
+
37
+ export interface McpToolLike {
38
+ name: string;
39
+ description?: string;
40
+ inputSchema?: unknown;
41
+ schema?: unknown;
42
+ serverName?: string;
43
+ }
44
+
45
+ export interface NotebookCell {
46
+ type: string;
47
+ content?: string;
48
+ [k: string]: unknown;
49
+ }
50
+
51
+ export interface AddRecipeModalData {
52
+ mcpServers?: Array<{ name: string; url?: string }>;
53
+ scope?: 'data' | 'all';
54
+ }
55
+
56
+ export type ModalMode = 'add-md' | 'add-recipe' | 'recipe-viewer' | 'tool-viewer';
57
+
58
+ export interface ImportModalData {
59
+ mode?: ModalMode;
60
+ recipe?: ImportedRecipe;
61
+ tool?: McpToolLike;
62
+ mcpServers?: Array<{ name: string; url?: string }>;
63
+ scope?: 'data' | 'all';
64
+ }
65
+
66
+ interface Props {
67
+ data?: ImportModalData | null;
68
+ }
69
+
70
+ let { data = null }: Props = $props();
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // State
74
+ // ---------------------------------------------------------------------------
75
+
76
+ let open = $state(false);
77
+ let activeTab = $state('');
78
+ let mdContent = $state('');
79
+ let searchQuery = $state('');
80
+ let urlInput = $state('');
81
+ let fetchError = $state('');
82
+ let recipes = $state<ImportedRecipe[]>([]);
83
+ let filteredRecipes = $derived(filterRecipes(recipes, searchQuery));
84
+ let sortedRecipes = $derived(sortRecipes(filteredRecipes));
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Derived from data
88
+ // ---------------------------------------------------------------------------
89
+
90
+ const mode = $derived(data?.mode ?? 'add-md');
91
+ const recipe = $derived(data?.recipe);
92
+ const tool = $derived(data?.tool);
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Public API — called by wrapper functions
96
+ // ---------------------------------------------------------------------------
97
+
98
+ let hostEl: HTMLElement;
99
+
100
+ function emitInteract(action: string, payload?: unknown) {
101
+ hostEl?.dispatchEvent(new CustomEvent('widget:interact', {
102
+ detail: { action, payload },
103
+ bubbles: true,
104
+ }));
105
+ }
106
+
107
+ export function openModal(newData: ImportModalData) {
108
+ data = newData;
109
+ activeTab = defaultTab(newData.mode ?? 'add-md');
110
+ mdContent = '';
111
+ searchQuery = '';
112
+ urlInput = '';
113
+ fetchError = '';
114
+ recipes = [];
115
+
116
+ if (newData.mode === 'add-recipe') {
117
+ loadRecipes(newData);
118
+ }
119
+ open = true;
120
+ }
121
+
122
+ export function closeModal() {
123
+ open = false;
124
+ emitInteract('close');
125
+ }
126
+
127
+ function defaultTab(m: ModalMode): string {
128
+ if (m === 'add-md') return 'new';
129
+ if (m === 'add-recipe') return 'browser';
130
+ return '';
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Recipe loading (add-recipe mode)
135
+ // ---------------------------------------------------------------------------
136
+
137
+ async function loadRecipes(d: ImportModalData) {
138
+ const includeBuiltin = d.scope !== 'data';
139
+ const builtin: ImportedRecipe[] = includeBuiltin
140
+ ? WEBMCP_RECIPES.map((r: any) => ({ name: r.name, description: r.description, body: r.body, serverName: 'webmcp' }))
141
+ : [];
142
+
143
+ recipes = [...builtin];
144
+
145
+ if (!includeBuiltin && !(d.mcpServers?.length)) {
146
+ // Will show empty state in the template
147
+ return;
148
+ }
149
+
150
+ if (d.mcpServers?.length) {
151
+ const fetches = d.mcpServers.map(async (srv) => {
152
+ try {
153
+ const res: any = await callToolViaPostMessage(`${srv.name}_list_recipes`, {});
154
+ const items = extractRecipeItems(res, srv);
155
+ if (items.length) recipes = [...recipes, ...items];
156
+ } catch { /* ignore */ }
157
+ });
158
+ await Promise.allSettled(fetches);
159
+ }
160
+ }
161
+
162
+ function extractRecipeItems(res: any, srv: { name: string; url?: string }): ImportedRecipe[] {
163
+ const text = res?.content?.find?.((c: any) => c.type === 'text')?.text;
164
+ if (!text) return [];
165
+ let parsed: any;
166
+ try { parsed = JSON.parse(text); } catch { return []; }
167
+ const items = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.recipes) ? parsed.recipes : []);
168
+ return items.map((it: any) => ({
169
+ name: String(it?.name ?? it?.id ?? 'unnamed'),
170
+ description: it?.description,
171
+ originalName: it?.name,
172
+ id: it?.id,
173
+ serverName: srv.name,
174
+ serverUrl: srv.url,
175
+ }));
176
+ }
177
+
178
+ function extractRecipeBody(res: any): string | null {
179
+ const text = res?.content?.find?.((c: any) => c.type === 'text')?.text;
180
+ if (!text) return null;
181
+ try {
182
+ const parsed = JSON.parse(text);
183
+ if (parsed && typeof parsed === 'object') {
184
+ if (typeof parsed.body === 'string') return parsed.body;
185
+ if (typeof parsed.content === 'string') return parsed.content;
186
+ if (typeof parsed.markdown === 'string') return parsed.markdown;
187
+ }
188
+ } catch { /* not JSON */ }
189
+ return text;
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Handlers — add-md
194
+ // ---------------------------------------------------------------------------
195
+
196
+ function handleInsertMd() {
197
+ const content = mdContent.trim() === '' ? '### new section\n\nwrite here…' : mdContent;
198
+ emitInteract('pick-md', content);
199
+ closeModal();
200
+ }
201
+
202
+ async function handleMdFile(e: Event) {
203
+ const file = (e.target as HTMLInputElement).files?.[0];
204
+ if (!file) return;
205
+ const text = await file.text();
206
+ emitInteract('pick-md', text);
207
+ closeModal();
208
+ }
209
+
210
+ async function handleFetchUrl() {
211
+ fetchError = '';
212
+ if (!urlInput.trim()) return;
213
+ try {
214
+ const text = await fetchViaProxy(urlInput.trim());
215
+ if (mode === 'add-md') {
216
+ emitInteract('pick-md', text);
217
+ } else {
218
+ const name = new URL(urlInput.trim()).pathname.split('/').pop() || 'recipe';
219
+ emitInteract('pick-recipe', { name, body: text });
220
+ }
221
+ closeModal();
222
+ } catch (err: any) {
223
+ fetchError = 'Fetch failed: ' + (err?.message ?? err);
224
+ }
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Handlers — add-recipe
229
+ // ---------------------------------------------------------------------------
230
+
231
+ async function handlePickRecipe(r: ImportedRecipe) {
232
+ // Fetch body on demand if missing
233
+ if (!r.body && r.serverName && r.serverName !== 'webmcp') {
234
+ try {
235
+ const res: any = await callToolViaPostMessage(
236
+ `${r.serverName}_get_recipe`,
237
+ { name: r.originalName ?? r.name, id: r.id ?? r.name },
238
+ );
239
+ r = { ...r, body: extractRecipeBody(res) ?? '' };
240
+ } catch { /* keep empty body */ }
241
+ }
242
+ emitInteract('pick-recipe', r);
243
+ closeModal();
244
+ }
245
+
246
+ async function handleRecipeFile(e: Event) {
247
+ const file = (e.target as HTMLInputElement).files?.[0];
248
+ if (!file) return;
249
+ const text = await file.text();
250
+ emitInteract('pick-recipe', { name: file.name.replace(/\.md$/, ''), body: text });
251
+ closeModal();
252
+ }
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // Handlers — recipe-viewer
256
+ // ---------------------------------------------------------------------------
257
+
258
+ // prose rendering is done server-side for recipe-viewer; we re-use the
259
+ // renderMarkdownWithInjectButtons helper injected via slot or data.
260
+ // Since this is a CE, we render as raw HTML via {@html} after sanitizing.
261
+ // The inject buttons are wired via event delegation on the section.
262
+
263
+ function handleInjectAll() {
264
+ // extractCellsFromRecipe is a pure function from resource-extractor.ts —
265
+ // but import would create a cycle ui<->notebook. We emit the raw body
266
+ // and let the consumer (notebook.ts) call extractCellsFromRecipe.
267
+ emitInteract('inject-all', { recipe: recipe });
268
+ closeModal();
269
+ }
270
+
271
+ function handleInjectFence(e: Event) {
272
+ // Delegated click on .nb-md-fence-inject buttons rendered via {@html}
273
+ const btn = (e.target as HTMLElement).closest('[data-fence-inject]') as HTMLElement | null;
274
+ if (!btn) return;
275
+ const lang = btn.dataset.lang ?? '';
276
+ const content = btn.dataset.content ?? '';
277
+ emitInteract('inject-fence', { lang, content });
278
+ }
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // Handlers — tool-viewer
282
+ // ---------------------------------------------------------------------------
283
+
284
+ function handleInjectTool() {
285
+ emitInteract('inject-tool', { tool });
286
+ closeModal();
287
+ }
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // Helpers
291
+ // ---------------------------------------------------------------------------
292
+
293
+ async function fetchViaProxy(url: string): Promise<string> {
294
+ try {
295
+ const prox = `/api/proxy?url=${encodeURIComponent(url)}`;
296
+ const res = await fetch(prox);
297
+ if (res.ok) return await res.text();
298
+ } catch { /* fallback */ }
299
+ const res = await fetch(url);
300
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
301
+ return await res.text();
302
+ }
303
+
304
+ function escapeHtml(s: string): string {
305
+ return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
306
+ }
307
+
308
+ function toolSchema(): string {
309
+ const schema = tool?.inputSchema ?? (tool as any)?.schema ?? {};
310
+ return JSON.stringify(schema, null, 2);
311
+ }
312
+ </script>
313
+
314
+ <!-- Overlay -->
315
+ {#if open}
316
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
317
+ <div
318
+ class="nb-import-overlay"
319
+ bind:this={hostEl}
320
+ onclick={(e) => { if (e.target === e.currentTarget) closeModal(); }}
321
+ >
322
+ <div class="nb-import-modal">
323
+
324
+ <!-- ================================================================ -->
325
+ <!-- add-md -->
326
+ <!-- ================================================================ -->
327
+ {#if mode === 'add-md'}
328
+ <header class="nb-imp-head">
329
+ <span class="nb-imp-title">Add markdown</span>
330
+ <button type="button" class="nb-imp-close" onclick={closeModal}>×</button>
331
+ </header>
332
+ <nav class="nb-imp-tabs">
333
+ {#each ['new','file','url'] as tab}
334
+ <button
335
+ type="button"
336
+ class="nb-imp-tab"
337
+ class:nb-imp-tab-active={activeTab === tab}
338
+ onclick={() => activeTab = tab}
339
+ >{tab.charAt(0).toUpperCase() + tab.slice(1)}</button>
340
+ {/each}
341
+ </nav>
342
+ <section class="nb-imp-body">
343
+ {#if activeTab === 'new'}
344
+ <p class="nb-imp-hint">Paste markdown below, or leave empty to create a blank cell you can edit in place.</p>
345
+ <textarea
346
+ class="nb-imp-md-textarea"
347
+ placeholder={"### Heading\n\nParagraph text…"}
348
+ rows={10}
349
+ spellcheck={true}
350
+ bind:value={mdContent}
351
+ onkeydown={(e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); handleInsertMd(); } }}
352
+ ></textarea>
353
+ <div class="nb-imp-md-actions">
354
+ <button type="button" class="nb-imp-btn nb-imp-primary" onclick={handleInsertMd}>Insert</button>
355
+ </div>
356
+ {:else if activeTab === 'file'}
357
+ <p class="nb-imp-hint">Pick a .md file from your computer.</p>
358
+ <input type="file" accept=".md,.markdown,text/markdown,text/plain" class="nb-imp-file" onchange={handleMdFile} />
359
+ {:else if activeTab === 'url'}
360
+ <p class="nb-imp-hint">Fetch a markdown URL (routed through /api/proxy to avoid CORS).</p>
361
+ <input type="url" placeholder="https://..." class="nb-imp-url" bind:value={urlInput} />
362
+ <button type="button" class="nb-imp-btn nb-imp-primary" onclick={handleFetchUrl}>Fetch</button>
363
+ {#if fetchError}<div class="nb-imp-error">{fetchError}</div>{/if}
364
+ {/if}
365
+ </section>
366
+
367
+ <!-- ================================================================ -->
368
+ <!-- add-recipe -->
369
+ <!-- ================================================================ -->
370
+ {:else if mode === 'add-recipe'}
371
+ <header class="nb-imp-head">
372
+ <span class="nb-imp-title">Add recipe</span>
373
+ <button type="button" class="nb-imp-close" onclick={closeModal}>×</button>
374
+ </header>
375
+ <nav class="nb-imp-tabs">
376
+ {#each ['browser','file','url'] as tab}
377
+ <button
378
+ type="button"
379
+ class="nb-imp-tab"
380
+ class:nb-imp-tab-active={activeTab === tab}
381
+ onclick={() => activeTab = tab}
382
+ >{tab.charAt(0).toUpperCase() + tab.slice(1)}</button>
383
+ {/each}
384
+ </nav>
385
+ <section class="nb-imp-body">
386
+ {#if activeTab === 'browser'}
387
+ <input type="search" placeholder="Search recipes..." class="nb-imp-search" bind:value={searchQuery} />
388
+ <div class="nb-imp-recipes">
389
+ {#if sortedRecipes.length === 0}
390
+ {#if !data?.mcpServers?.length && data?.scope === 'data'}
391
+ <div class="nb-imp-empty">No data servers connected. Connect one to see recipes.</div>
392
+ {:else}
393
+ <div class="nb-imp-empty">No recipes.</div>
394
+ {/if}
395
+ {:else}
396
+ {#each sortedRecipes as r}
397
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
398
+ <div class="nb-imp-recipe" onclick={() => handlePickRecipe(r)}>
399
+ <div class="nb-imp-recipe-name">{r.name}</div>
400
+ {#if r.description}<div class="nb-imp-recipe-desc">{r.description}</div>{/if}
401
+ {#if r.serverName}<div class="nb-imp-recipe-srv">{r.serverName}</div>{/if}
402
+ </div>
403
+ {/each}
404
+ {/if}
405
+ </div>
406
+ {:else if activeTab === 'file'}
407
+ <p class="nb-imp-hint">Pick a .md recipe file.</p>
408
+ <input type="file" accept=".md,.markdown,text/markdown,text/plain" class="nb-imp-file" onchange={handleRecipeFile} />
409
+ {:else if activeTab === 'url'}
410
+ <p class="nb-imp-hint">Fetch a recipe URL (routed through /api/proxy).</p>
411
+ <input type="url" placeholder="https://..." class="nb-imp-url" bind:value={urlInput} />
412
+ <button type="button" class="nb-imp-btn nb-imp-primary" onclick={handleFetchUrl}>Fetch</button>
413
+ {#if fetchError}<div class="nb-imp-error">{fetchError}</div>{/if}
414
+ {/if}
415
+ </section>
416
+
417
+ <!-- ================================================================ -->
418
+ <!-- recipe-viewer -->
419
+ <!-- ================================================================ -->
420
+ {:else if mode === 'recipe-viewer'}
421
+ <header class="nb-imp-head">
422
+ <span class="nb-imp-title">{recipe?.name ?? ''}</span>
423
+ <button type="button" class="nb-imp-close" onclick={closeModal}>×</button>
424
+ </header>
425
+ <div class="nb-imp-recipe-meta">
426
+ {#if recipe?.description}<p>{recipe.description}</p>{/if}
427
+ {#if recipe?.serverName}<span class="nb-imp-recipe-srv">{recipe.serverName}</span>{/if}
428
+ </div>
429
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
430
+ <section class="nb-imp-body nb-imp-body-recipe" data-role="render" onclick={handleInjectFence}>
431
+ <!-- Content injected imperatively by notebook.ts via renderMarkdownWithInjectButtons -->
432
+ </section>
433
+ <footer class="nb-imp-foot">
434
+ <button type="button" class="nb-imp-btn" onclick={handleInjectAll}>Inject all cells</button>
435
+ </footer>
436
+
437
+ <!-- ================================================================ -->
438
+ <!-- tool-viewer -->
439
+ <!-- ================================================================ -->
440
+ {:else if mode === 'tool-viewer'}
441
+ <header class="nb-imp-head">
442
+ <span class="nb-imp-title">
443
+ {escapeHtml(tool?.name ?? '')}
444
+ {#if tool?.serverName}<span class="nb-imp-recipe-srv">{tool.serverName}</span>{/if}
445
+ </span>
446
+ <button type="button" class="nb-imp-close" onclick={closeModal}>×</button>
447
+ </header>
448
+ <section class="nb-imp-body nb-imp-body-tool">
449
+ {#if tool?.description}<p class="nb-imp-tool-desc">{tool.description}</p>{/if}
450
+ <div class="nb-imp-tool-schema">
451
+ <div class="nb-imp-hint">input schema</div>
452
+ <pre><code>{toolSchema()}</code></pre>
453
+ </div>
454
+ </section>
455
+ <footer class="nb-imp-foot">
456
+ <button type="button" class="nb-imp-btn nb-imp-primary" onclick={handleInjectTool}>↳ inject as cell</button>
457
+ </footer>
458
+ {/if}
459
+
460
+ </div>
461
+ </div>
462
+ {/if}
463
+
464
+ <style>
465
+ .nb-import-overlay {
466
+ position: fixed;
467
+ inset: 0;
468
+ z-index: 2000;
469
+ background: rgba(0, 0, 0, 0.5);
470
+ display: flex;
471
+ align-items: center;
472
+ justify-content: center;
473
+ }
474
+
475
+ .nb-import-modal {
476
+ width: min(680px, 92vw);
477
+ max-height: 84vh;
478
+ background: var(--color-surface, #fff);
479
+ color: var(--color-text1, #111);
480
+ border-radius: 14px;
481
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
482
+ display: flex;
483
+ flex-direction: column;
484
+ overflow: hidden;
485
+ font-family: var(--font-sans, system-ui);
486
+ }
487
+
488
+ .nb-imp-head {
489
+ display: flex;
490
+ align-items: center;
491
+ padding: 14px 18px;
492
+ border-bottom: 1px solid var(--color-border, #eee);
493
+ }
494
+
495
+ .nb-imp-title {
496
+ flex: 1;
497
+ font-weight: 600;
498
+ font-size: 14px;
499
+ }
500
+
501
+ .nb-imp-recipe-srv {
502
+ font-family: monospace;
503
+ font-size: 11px;
504
+ color: var(--color-text2, #666);
505
+ margin-left: 6px;
506
+ }
507
+
508
+ .nb-imp-close {
509
+ background: none;
510
+ border: none;
511
+ cursor: pointer;
512
+ font-size: 20px;
513
+ line-height: 1;
514
+ color: var(--color-text2, #666);
515
+ }
516
+
517
+ .nb-imp-tabs {
518
+ display: flex;
519
+ padding: 0 14px;
520
+ border-bottom: 1px solid var(--color-border, #eee);
521
+ }
522
+
523
+ .nb-imp-tab {
524
+ background: none;
525
+ border: none;
526
+ cursor: pointer;
527
+ padding: 10px 14px;
528
+ font-size: 12px;
529
+ color: var(--color-text2, #666);
530
+ border-bottom: 2px solid transparent;
531
+ }
532
+
533
+ .nb-imp-tab-active {
534
+ color: var(--color-text1, #111);
535
+ border-bottom-color: var(--color-accent, #6a55ff);
536
+ }
537
+
538
+ .nb-imp-body {
539
+ padding: 16px 18px;
540
+ overflow-y: auto;
541
+ flex: 1;
542
+ }
543
+
544
+ .nb-imp-body-recipe,
545
+ .nb-imp-body-tool {
546
+ font-size: 13px;
547
+ }
548
+
549
+ .nb-imp-hint {
550
+ font-size: 12px;
551
+ color: var(--color-text2, #666);
552
+ margin: 0 0 10px 0;
553
+ }
554
+
555
+ .nb-imp-btn {
556
+ background: var(--color-surface2, #f4f4f5);
557
+ border: 1px solid var(--color-border, #e4e4e7);
558
+ border-radius: 6px;
559
+ padding: 8px 14px;
560
+ font-size: 12px;
561
+ cursor: pointer;
562
+ }
563
+
564
+ .nb-imp-btn:hover {
565
+ background: var(--color-surface3, #eeeef0);
566
+ }
567
+
568
+ .nb-imp-primary {
569
+ background: var(--color-accent, #6a55ff);
570
+ color: #fff;
571
+ border: 0;
572
+ }
573
+
574
+ .nb-imp-primary:hover {
575
+ filter: brightness(1.1);
576
+ color: #fff;
577
+ }
578
+
579
+ .nb-imp-search,
580
+ .nb-imp-url {
581
+ width: 100%;
582
+ padding: 8px 10px;
583
+ border: 1px solid var(--color-border, #e4e4e7);
584
+ border-radius: 6px;
585
+ font-size: 12px;
586
+ margin-bottom: 10px;
587
+ background: var(--color-surface, #fff);
588
+ color: var(--color-text1, #111);
589
+ box-sizing: border-box;
590
+ }
591
+
592
+ .nb-imp-file {
593
+ font-size: 12px;
594
+ }
595
+
596
+ .nb-imp-md-textarea {
597
+ width: 100%;
598
+ min-height: 180px;
599
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
600
+ font-size: 13px;
601
+ line-height: 1.5;
602
+ padding: 10px 12px;
603
+ border: 1px solid var(--color-border, #e4e4e7);
604
+ border-radius: 6px;
605
+ background: var(--color-bg, #fff);
606
+ color: var(--color-text1, #111);
607
+ resize: vertical;
608
+ outline: none;
609
+ box-sizing: border-box;
610
+ }
611
+
612
+ .nb-imp-md-textarea:focus {
613
+ border-color: var(--color-accent, #6a55ff);
614
+ }
615
+
616
+ .nb-imp-md-actions {
617
+ display: flex;
618
+ justify-content: flex-end;
619
+ gap: 8px;
620
+ margin-top: 10px;
621
+ }
622
+
623
+ .nb-imp-recipes {
624
+ display: flex;
625
+ flex-direction: column;
626
+ gap: 6px;
627
+ max-height: 46vh;
628
+ overflow-y: auto;
629
+ }
630
+
631
+ .nb-imp-recipe {
632
+ padding: 10px 12px;
633
+ border-radius: 8px;
634
+ cursor: pointer;
635
+ background: var(--color-surface2, #f4f4f5);
636
+ transition: background 0.12s;
637
+ }
638
+
639
+ .nb-imp-recipe:hover {
640
+ background: var(--color-surface3, #eeeef0);
641
+ }
642
+
643
+ .nb-imp-recipe-name {
644
+ font-weight: 500;
645
+ font-size: 13px;
646
+ }
647
+
648
+ .nb-imp-recipe-desc {
649
+ font-size: 11.5px;
650
+ color: var(--color-text2, #666);
651
+ margin-top: 3px;
652
+ }
653
+
654
+ .nb-imp-recipe-meta {
655
+ padding: 0 18px 10px;
656
+ border-bottom: 1px solid var(--color-border, #eee);
657
+ }
658
+
659
+ .nb-imp-empty {
660
+ color: var(--color-text2, #666);
661
+ font-size: 12px;
662
+ padding: 14px;
663
+ text-align: center;
664
+ }
665
+
666
+ .nb-imp-foot {
667
+ padding: 12px 18px;
668
+ border-top: 1px solid var(--color-border, #eee);
669
+ display: flex;
670
+ justify-content: flex-end;
671
+ gap: 8px;
672
+ }
673
+
674
+ .nb-imp-error {
675
+ color: #c2323a;
676
+ font-size: 12px;
677
+ margin-top: 8px;
678
+ }
679
+
680
+ .nb-imp-tool-desc {
681
+ font-size: 13px;
682
+ margin: 0 0 10px;
683
+ }
684
+
685
+ .nb-imp-tool-schema pre {
686
+ background: var(--color-surface2, #f4f4f5);
687
+ padding: 10px;
688
+ border-radius: 6px;
689
+ max-height: 40vh;
690
+ overflow: auto;
691
+ font-size: 11.5px;
692
+ }
693
+
694
+ /* ---- Styles for prose rendered inside recipe-viewer (delegated via {@html} in consumers) ---- */
695
+ :global(.nb-md-render) { font-size: 13px; line-height: 1.5; }
696
+ :global(.nb-md-render h1),
697
+ :global(.nb-md-render h2),
698
+ :global(.nb-md-render h3) { margin: 12px 0 6px; }
699
+ :global(.nb-md-render p) { margin: 6px 0; }
700
+ :global(.nb-md-render ul),
701
+ :global(.nb-md-render ol) { margin: 6px 0 6px 20px; }
702
+ :global(.nb-md-render pre) {
703
+ background: var(--color-surface2, #f4f4f5);
704
+ padding: 10px 12px;
705
+ border-radius: 6px;
706
+ overflow-x: auto;
707
+ font-size: 12px;
708
+ }
709
+ :global(.nb-md-render code) { font-family: var(--font-mono, monospace); font-size: 12px; }
710
+ :global(.nb-md-fence) {
711
+ border: 1px solid var(--color-border, #e4e4e7);
712
+ border-radius: 8px;
713
+ margin: 10px 0;
714
+ overflow: hidden;
715
+ }
716
+ :global(.nb-md-fence-head) {
717
+ display: flex;
718
+ align-items: center;
719
+ gap: 10px;
720
+ padding: 6px 10px;
721
+ background: var(--color-surface2, #f4f4f5);
722
+ font-size: 11px;
723
+ color: var(--color-text2, #666);
724
+ border-bottom: 1px solid var(--color-border, #e4e4e7);
725
+ }
726
+ :global(.nb-md-fence-lang) { font-family: monospace; flex: 1; }
727
+ :global(.nb-md-fence-inject) {
728
+ background: var(--color-accent, #6a55ff);
729
+ color: #fff;
730
+ border: 0;
731
+ border-radius: 4px;
732
+ padding: 3px 9px;
733
+ font-size: 11px;
734
+ cursor: pointer;
735
+ }
736
+ :global(.nb-md-fence-inject:hover) { filter: brightness(1.08); }
737
+ :global(.nb-md-fence pre) { margin: 0; border-radius: 0; background: transparent; }
738
+ </style>