@webmcp-auto-ui/ui 2.5.27 → 2.5.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +18 -5
- package/src/agent/LLMSelector.svelte +11 -3
- package/src/agent/ModelCacheManager.svelte +359 -0
- package/src/index.ts +42 -30
- package/src/theme/scale.ts +128 -0
- package/src/widgets/WidgetRenderer.svelte +144 -107
- package/src/widgets/export-widget.ts +28 -1
- package/src/widgets/helpers/safe-image.ts +78 -0
- package/src/widgets/notebook/.gitkeep +0 -0
- package/src/widgets/notebook/chart-renderer.ts +63 -0
- package/src/widgets/notebook/executors/.gitkeep +1 -0
- package/src/widgets/notebook/executors/index.ts +4 -0
- package/src/widgets/notebook/executors/js-worker.ts +269 -0
- package/src/widgets/notebook/executors/sql.ts +206 -0
- package/src/widgets/notebook/import-modals.ts +560 -0
- package/src/widgets/notebook/left-pane.ts +256 -0
- package/src/widgets/notebook/notebook.ts +930 -0
- package/src/widgets/notebook/prose.ts +615 -0
- package/src/widgets/notebook/recipe-browser.ts +350 -0
- package/src/widgets/notebook/recipes/notebook.md +124 -0
- package/src/widgets/notebook/resource-extractor.ts +162 -0
- package/src/widgets/notebook/share-handlers.ts +222 -0
- package/src/widgets/notebook/shared.ts +1633 -0
- package/src/widgets/rich/cards.ts +181 -0
- package/src/widgets/rich/carousel.ts +319 -0
- package/src/widgets/rich/chart-rich.ts +386 -0
- package/src/widgets/rich/d3.ts +503 -0
- package/src/widgets/rich/data-table.ts +342 -0
- package/src/widgets/rich/gallery.ts +350 -0
- package/src/widgets/rich/grid-data.ts +173 -0
- package/src/widgets/rich/hemicycle.ts +313 -0
- package/src/widgets/rich/js-sandbox.ts +122 -0
- package/src/widgets/rich/json-viewer.ts +202 -0
- package/src/widgets/rich/log.ts +143 -0
- package/src/widgets/rich/map.ts +218 -0
- package/src/widgets/rich/profile.ts +256 -0
- package/src/widgets/rich/sankey.ts +257 -0
- package/src/widgets/rich/stat-card.ts +125 -0
- package/src/widgets/rich/timeline.ts +179 -0
- package/src/widgets/rich/trombinoscope.ts +246 -0
- package/src/widgets/simple/actions.ts +89 -0
- package/src/widgets/simple/alert.ts +100 -0
- package/src/widgets/simple/chart.ts +189 -0
- package/src/widgets/simple/code.ts +79 -0
- package/src/widgets/simple/kv.ts +68 -0
- package/src/widgets/simple/list.ts +89 -0
- package/src/widgets/simple/stat.ts +58 -0
- package/src/widgets/simple/tags.ts +125 -0
- package/src/widgets/simple/text.ts +198 -0
- package/src/widgets/SafeImage.svelte +0 -76
- package/src/widgets/rich/Cards.svelte +0 -39
- package/src/widgets/rich/Carousel.svelte +0 -88
- package/src/widgets/rich/Chart.svelte +0 -142
- package/src/widgets/rich/D3Widget.svelte +0 -378
- package/src/widgets/rich/DataTable.svelte +0 -62
- package/src/widgets/rich/Gallery.svelte +0 -94
- package/src/widgets/rich/GridData.svelte +0 -44
- package/src/widgets/rich/Hemicycle.svelte +0 -78
- package/src/widgets/rich/JsSandbox.svelte +0 -51
- package/src/widgets/rich/JsonViewer.svelte +0 -42
- package/src/widgets/rich/LogViewer.svelte +0 -24
- package/src/widgets/rich/MapView.svelte +0 -140
- package/src/widgets/rich/ProfileCard.svelte +0 -59
- package/src/widgets/rich/Sankey.svelte +0 -56
- package/src/widgets/rich/StatCard.svelte +0 -35
- package/src/widgets/rich/Timeline.svelte +0 -43
- package/src/widgets/rich/Trombinoscope.svelte +0 -48
- package/src/widgets/simple/ActionsBlock.svelte +0 -15
- package/src/widgets/simple/AlertBlock.svelte +0 -11
- package/src/widgets/simple/ChartBlock.svelte +0 -21
- package/src/widgets/simple/CodeBlock.svelte +0 -11
- package/src/widgets/simple/KVBlock.svelte +0 -16
- package/src/widgets/simple/ListBlock.svelte +0 -17
- package/src/widgets/simple/StatBlock.svelte +0 -14
- package/src/widgets/simple/TagsBlock.svelte +0 -15
- package/src/widgets/simple/TextBlock.svelte +0 -122
|
@@ -0,0 +1,560 @@
|
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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
|
+
}
|