@webmcp-auto-ui/ui 2.5.26 → 2.5.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +15 -3
- package/src/agent/AgentConsole.svelte +1 -21
- package/src/agent/DataServersPanel.svelte +164 -0
- package/src/agent/LLMSelector.svelte +26 -8
- package/src/agent/ModelCacheManager.svelte +359 -0
- package/src/agent/{GemmaLoader.svelte → ModelLoader.svelte} +1 -1
- package/src/agent/SettingsPanel.svelte +16 -2
- package/src/index.ts +45 -31
- package/src/widgets/WidgetRenderer.svelte +118 -115
- package/src/widgets/export-widget.ts +28 -1
- package/src/widgets/helpers/safe-image.ts +78 -0
- package/src/widgets/notebook/.gitkeep +0 -0
- package/src/widgets/notebook/chart-renderer.ts +63 -0
- package/src/widgets/notebook/compact.ts +823 -0
- package/src/widgets/notebook/document.ts +1065 -0
- package/src/widgets/notebook/editorial.ts +936 -0
- package/src/widgets/notebook/executors/.gitkeep +1 -0
- package/src/widgets/notebook/executors/index.ts +4 -0
- package/src/widgets/notebook/executors/js-worker.ts +269 -0
- package/src/widgets/notebook/executors/sql.ts +206 -0
- package/src/widgets/notebook/import-modals.ts +553 -0
- package/src/widgets/notebook/left-pane.ts +249 -0
- package/src/widgets/notebook/prose.ts +280 -0
- package/src/widgets/notebook/recipe-browser.ts +350 -0
- package/src/widgets/notebook/recipes/compact.md +124 -0
- package/src/widgets/notebook/recipes/document.md +139 -0
- package/src/widgets/notebook/recipes/editorial.md +120 -0
- package/src/widgets/notebook/recipes/workspace.md +119 -0
- package/src/widgets/notebook/resource-extractor.ts +162 -0
- package/src/widgets/notebook/share-handlers.ts +222 -0
- package/src/widgets/notebook/shared.ts +1592 -0
- package/src/widgets/notebook/workspace.ts +852 -0
- package/src/widgets/rich/cards.ts +181 -0
- package/src/widgets/rich/carousel.ts +319 -0
- package/src/widgets/rich/chart-rich.ts +386 -0
- package/src/widgets/rich/d3.ts +503 -0
- package/src/widgets/rich/data-table.ts +342 -0
- package/src/widgets/rich/gallery.ts +350 -0
- package/src/widgets/rich/grid-data.ts +173 -0
- package/src/widgets/rich/hemicycle.ts +313 -0
- package/src/widgets/rich/js-sandbox.ts +106 -0
- package/src/widgets/rich/json-viewer.ts +202 -0
- package/src/widgets/rich/log.ts +143 -0
- package/src/widgets/rich/map.ts +218 -0
- package/src/widgets/rich/profile.ts +256 -0
- package/src/widgets/rich/sankey.ts +262 -0
- package/src/widgets/rich/stat-card.ts +125 -0
- package/src/widgets/rich/timeline.ts +179 -0
- package/src/widgets/rich/trombinoscope.ts +246 -0
- package/src/widgets/simple/actions.ts +89 -0
- package/src/widgets/simple/alert.ts +100 -0
- package/src/widgets/simple/chart.ts +189 -0
- package/src/widgets/simple/code.ts +79 -0
- package/src/widgets/simple/kv.ts +68 -0
- package/src/widgets/simple/list.ts +89 -0
- package/src/widgets/simple/stat.ts +58 -0
- package/src/widgets/simple/tags.ts +125 -0
- package/src/widgets/simple/text.ts +198 -0
- package/src/wm/FloatingLayout.svelte +2 -0
- package/src/wm/LinkIndicators.svelte +8 -15
- package/src/widgets/SafeImage.svelte +0 -76
- package/src/widgets/rich/Cards.svelte +0 -39
- package/src/widgets/rich/Carousel.svelte +0 -88
- package/src/widgets/rich/Chart.svelte +0 -142
- package/src/widgets/rich/D3Widget.svelte +0 -373
- package/src/widgets/rich/DataTable.svelte +0 -62
- package/src/widgets/rich/Gallery.svelte +0 -94
- package/src/widgets/rich/GridData.svelte +0 -44
- package/src/widgets/rich/Hemicycle.svelte +0 -78
- package/src/widgets/rich/JsSandbox.svelte +0 -51
- package/src/widgets/rich/JsonViewer.svelte +0 -42
- package/src/widgets/rich/LogViewer.svelte +0 -24
- package/src/widgets/rich/MapView.svelte +0 -140
- package/src/widgets/rich/ProfileCard.svelte +0 -59
- package/src/widgets/rich/Sankey.svelte +0 -38
- package/src/widgets/rich/StatCard.svelte +0 -35
- package/src/widgets/rich/Timeline.svelte +0 -43
- package/src/widgets/rich/Trombinoscope.svelte +0 -48
- package/src/widgets/simple/ActionsBlock.svelte +0 -15
- package/src/widgets/simple/AlertBlock.svelte +0 -11
- package/src/widgets/simple/ChartBlock.svelte +0 -21
- package/src/widgets/simple/CodeBlock.svelte +0 -11
- package/src/widgets/simple/KVBlock.svelte +0 -16
- package/src/widgets/simple/ListBlock.svelte +0 -17
- package/src/widgets/simple/StatBlock.svelte +0 -14
- package/src/widgets/simple/TagsBlock.svelte +0 -15
- package/src/widgets/simple/TextBlock.svelte +0 -122
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
compressPreview?: number;
|
|
12
12
|
contextRAGEnabled?: boolean;
|
|
13
13
|
ragResidueSize?: number;
|
|
14
|
+
visualTrace?: boolean;
|
|
14
15
|
cacheEnabled?: boolean;
|
|
15
16
|
temperature?: number;
|
|
16
17
|
topK?: number;
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
compressPreview = $bindable(500),
|
|
30
31
|
contextRAGEnabled = $bindable(false),
|
|
31
32
|
ragResidueSize = $bindable(200),
|
|
33
|
+
visualTrace = $bindable(false),
|
|
32
34
|
cacheEnabled = $bindable(true),
|
|
33
35
|
temperature = $bindable(0.7),
|
|
34
36
|
topK = $bindable(10),
|
|
@@ -186,10 +188,10 @@
|
|
|
186
188
|
<div>
|
|
187
189
|
<div class="flex justify-between items-baseline mb-1">
|
|
188
190
|
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Max result (chars)</span>
|
|
189
|
-
<span class="font-mono text-xs text-text1">{formatNumber(maxResultLength)}</span>
|
|
191
|
+
<span class="font-mono text-xs text-text1">{maxResultLength >= 50000 ? '∞' : formatNumber(maxResultLength)}</span>
|
|
190
192
|
</div>
|
|
191
193
|
<input type="range" bind:value={maxResultLength}
|
|
192
|
-
min={500} max={
|
|
194
|
+
min={500} max={50000} step={500}
|
|
193
195
|
class="w-full accent-accent" />
|
|
194
196
|
</div>
|
|
195
197
|
|
|
@@ -230,6 +232,18 @@
|
|
|
230
232
|
class="w-full accent-accent" />
|
|
231
233
|
</div>
|
|
232
234
|
{/if}
|
|
235
|
+
|
|
236
|
+
<!-- Visual trace -->
|
|
237
|
+
<label class="flex items-center gap-2.5 cursor-pointer select-none">
|
|
238
|
+
<input type="checkbox" bind:checked={visualTrace} class="accent-accent w-3.5 h-3.5" />
|
|
239
|
+
<span class="text-xs font-mono text-text1">Visual trace</span>
|
|
240
|
+
<span class="text-[8px] font-mono text-text2/40 ml-auto">experimental</span>
|
|
241
|
+
</label>
|
|
242
|
+
{#if visualTrace}
|
|
243
|
+
<div class="text-[9px] font-mono text-text2/60 pl-5 mb-2">
|
|
244
|
+
Live DAG of agent execution
|
|
245
|
+
</div>
|
|
246
|
+
{/if}
|
|
233
247
|
</section>
|
|
234
248
|
|
|
235
249
|
<!-- Cache (disabled for WASM/Gemma — prompt caching is provider-dependent) -->
|
package/src/index.ts
CHANGED
|
@@ -16,38 +16,48 @@ export { default as MarkdownView } from './primitives/MarkdownView.svelte';
|
|
|
16
16
|
export { default as CodeView } from './primitives/CodeView.svelte';
|
|
17
17
|
export { renderMarkdown, highlightCode, createMarkdownRenderer } from './primitives/markdown-renderer.js';
|
|
18
18
|
|
|
19
|
-
// Simple widgets (
|
|
20
|
-
export {
|
|
21
|
-
export {
|
|
22
|
-
export {
|
|
23
|
-
export {
|
|
24
|
-
export {
|
|
25
|
-
export {
|
|
26
|
-
export {
|
|
27
|
-
export {
|
|
28
|
-
export {
|
|
19
|
+
// Simple widgets (vanilla renderers) — contract: render(container, data): () => void
|
|
20
|
+
export { render as renderStat } from './widgets/simple/stat.js';
|
|
21
|
+
export { render as renderKv } from './widgets/simple/kv.js';
|
|
22
|
+
export { render as renderList } from './widgets/simple/list.js';
|
|
23
|
+
export { render as renderChart } from './widgets/simple/chart.js';
|
|
24
|
+
export { render as renderAlert } from './widgets/simple/alert.js';
|
|
25
|
+
export { render as renderCode } from './widgets/simple/code.js';
|
|
26
|
+
export { render as renderText } from './widgets/simple/text.js';
|
|
27
|
+
export { render as renderActions } from './widgets/simple/actions.js';
|
|
28
|
+
export { render as renderTags } from './widgets/simple/tags.js';
|
|
29
29
|
|
|
30
|
-
// Rich widgets (
|
|
31
|
-
export {
|
|
32
|
-
export {
|
|
33
|
-
export {
|
|
34
|
-
export {
|
|
35
|
-
export {
|
|
36
|
-
export {
|
|
37
|
-
export {
|
|
38
|
-
export {
|
|
39
|
-
export {
|
|
40
|
-
export {
|
|
41
|
-
export {
|
|
42
|
-
export {
|
|
43
|
-
export {
|
|
44
|
-
export {
|
|
45
|
-
export {
|
|
46
|
-
export {
|
|
47
|
-
export {
|
|
30
|
+
// Rich widgets (vanilla renderers)
|
|
31
|
+
export { render as renderStatCard } from './widgets/rich/stat-card.js';
|
|
32
|
+
export { render as renderDataTable } from './widgets/rich/data-table.js';
|
|
33
|
+
export { render as renderTimeline } from './widgets/rich/timeline.js';
|
|
34
|
+
export { render as renderProfile } from './widgets/rich/profile.js';
|
|
35
|
+
export { render as renderTrombinoscope } from './widgets/rich/trombinoscope.js';
|
|
36
|
+
export { render as renderJsonViewer } from './widgets/rich/json-viewer.js';
|
|
37
|
+
export { render as renderHemicycle } from './widgets/rich/hemicycle.js';
|
|
38
|
+
export { render as renderChartRich } from './widgets/rich/chart-rich.js';
|
|
39
|
+
export { render as renderCards } from './widgets/rich/cards.js';
|
|
40
|
+
export { render as renderGridData } from './widgets/rich/grid-data.js';
|
|
41
|
+
export { render as renderSankey } from './widgets/rich/sankey.js';
|
|
42
|
+
export { render as renderMap } from './widgets/rich/map.js';
|
|
43
|
+
export { render as renderD3 } from './widgets/rich/d3.js';
|
|
44
|
+
export { render as renderJsSandbox } from './widgets/rich/js-sandbox.js';
|
|
45
|
+
export { render as renderLog } from './widgets/rich/log.js';
|
|
46
|
+
export { render as renderGallery } from './widgets/rich/gallery.js';
|
|
47
|
+
export { render as renderCarousel } from './widgets/rich/carousel.js';
|
|
48
48
|
|
|
49
|
-
//
|
|
50
|
-
export {
|
|
49
|
+
// Notebook widget renderers (vanilla)
|
|
50
|
+
export { render as renderCompact } from './widgets/notebook/compact.js';
|
|
51
|
+
export { render as renderWorkspace } from './widgets/notebook/workspace.js';
|
|
52
|
+
export { render as renderDocument } from './widgets/notebook/document.js';
|
|
53
|
+
export { render as renderEditorial } from './widgets/notebook/editorial.js';
|
|
54
|
+
export { render as renderRecipeBrowserWidget } from './widgets/notebook/recipe-browser.js';
|
|
55
|
+
// Notebook types (optional public API)
|
|
56
|
+
export type { NotebookState, NotebookCell } from './widgets/notebook/shared.js';
|
|
57
|
+
|
|
58
|
+
// Safe image helper (URL validation + error fallback)
|
|
59
|
+
export { createSafeImage } from './widgets/helpers/safe-image.js';
|
|
60
|
+
export type { SafeImageOptions } from './widgets/helpers/safe-image.js';
|
|
51
61
|
|
|
52
62
|
// Widget export utility
|
|
53
63
|
export { exportWidget, getExportFormats, exportWidgetAs } from './widgets/export-widget.js';
|
|
@@ -86,8 +96,11 @@ export type { BusMessage } from './messaging/bus.svelte.js';
|
|
|
86
96
|
|
|
87
97
|
// Agent UI components
|
|
88
98
|
export { default as LLMSelector } from './agent/LLMSelector.svelte';
|
|
89
|
-
export { default as
|
|
99
|
+
export { default as ModelLoader } from './agent/ModelLoader.svelte';
|
|
100
|
+
/** @deprecated Use ModelLoader instead. Alias maintained for backward compatibility. */
|
|
101
|
+
export { default as GemmaLoader } from './agent/ModelLoader.svelte';
|
|
90
102
|
export { default as McpStatus } from './agent/McpStatus.svelte';
|
|
103
|
+
export { default as ModelCacheManager } from './agent/ModelCacheManager.svelte';
|
|
91
104
|
export { default as AgentProgress } from './agent/AgentProgress.svelte';
|
|
92
105
|
export { default as McpConnector } from './agent/McpConnector.svelte';
|
|
93
106
|
export { default as ChatPanel } from './agent/ChatPanel.svelte';
|
|
@@ -96,6 +109,7 @@ export { default as AgentConsole } from './agent/AgentConsole.svelte';
|
|
|
96
109
|
export { default as SettingsPanel } from './agent/SettingsPanel.svelte';
|
|
97
110
|
export { default as RemoteMCPserversDemo } from './agent/RemoteMCPserversDemo.svelte';
|
|
98
111
|
export { default as WebMCPserversList } from './agent/WebMCPserversList.svelte';
|
|
112
|
+
export { default as DataServersPanel } from './agent/DataServersPanel.svelte';
|
|
99
113
|
export { default as EphemeralBubble } from './agent/EphemeralBubble.svelte';
|
|
100
114
|
export { default as TokenBubble } from './agent/TokenBubble.svelte';
|
|
101
115
|
export { default as DiagnosticModal } from './agent/DiagnosticModal.svelte';
|
|
@@ -2,95 +2,94 @@
|
|
|
2
2
|
import { type Component, onMount, onDestroy } from 'svelte';
|
|
3
3
|
import type { WebMcpServer } from '@webmcp-auto-ui/core';
|
|
4
4
|
import { bus } from '../messaging/bus.svelte.js';
|
|
5
|
-
// Simple widgets
|
|
6
|
-
import StatBlock from './simple/StatBlock.svelte';
|
|
7
|
-
import KVBlock from './simple/KVBlock.svelte';
|
|
8
|
-
import ListBlock from './simple/ListBlock.svelte';
|
|
9
|
-
import ChartBlock from './simple/ChartBlock.svelte';
|
|
10
|
-
import AlertBlock from './simple/AlertBlock.svelte';
|
|
11
|
-
import CodeBlock from './simple/CodeBlock.svelte';
|
|
12
|
-
import TextBlock from './simple/TextBlock.svelte';
|
|
13
|
-
import ActionsBlock from './simple/ActionsBlock.svelte';
|
|
14
|
-
import TagsBlock from './simple/TagsBlock.svelte';
|
|
15
|
-
// Rich widgets
|
|
16
|
-
import StatCard from './rich/StatCard.svelte';
|
|
17
|
-
import DataTable from './rich/DataTable.svelte';
|
|
18
|
-
import Timeline from './rich/Timeline.svelte';
|
|
19
|
-
import ProfileCard from './rich/ProfileCard.svelte';
|
|
20
|
-
import Trombinoscope from './rich/Trombinoscope.svelte';
|
|
21
|
-
import JsonViewer from './rich/JsonViewer.svelte';
|
|
22
|
-
import Hemicycle from './rich/Hemicycle.svelte';
|
|
23
|
-
import Chart from './rich/Chart.svelte';
|
|
24
|
-
import Cards from './rich/Cards.svelte';
|
|
25
|
-
import GridData from './rich/GridData.svelte';
|
|
26
|
-
import Sankey from './rich/Sankey.svelte';
|
|
27
|
-
import MapView from './rich/MapView.svelte';
|
|
28
|
-
import D3Widget from './rich/D3Widget.svelte';
|
|
29
|
-
import JsSandbox from './rich/JsSandbox.svelte';
|
|
30
|
-
import LogViewer from './rich/LogViewer.svelte';
|
|
31
|
-
import Gallery from './rich/Gallery.svelte';
|
|
32
|
-
import Carousel from './rich/Carousel.svelte';
|
|
33
5
|
|
|
34
|
-
|
|
6
|
+
// Monotonic counter for busId generation — avoids collisions on sub-ms remounts.
|
|
7
|
+
let busIdCounter = 0;
|
|
8
|
+
function makeBusId(widgetType: string): string {
|
|
9
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
10
|
+
return `block_${widgetType}_${crypto.randomUUID()}`;
|
|
11
|
+
}
|
|
12
|
+
return `block_${widgetType}_${Date.now()}-${++busIdCounter}`;
|
|
13
|
+
}
|
|
35
14
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
15
|
+
// Safe clone for data passed to vanilla renderers — strips Svelte 5 $state
|
|
16
|
+
// proxies without crashing on BigInt / circular refs / undefined values.
|
|
17
|
+
function safeClone<T>(value: T): T {
|
|
18
|
+
if (value === null || typeof value !== 'object') return value;
|
|
19
|
+
try {
|
|
20
|
+
return structuredClone(value);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.warn('[WidgetRenderer] structuredClone failed, falling back to shallow copy', err);
|
|
23
|
+
return { ...(value as Record<string, unknown>) } as T;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
41
26
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
27
|
+
// ── Native vanilla renderers ─────────────────────────────────────────────
|
|
28
|
+
// Simple widgets
|
|
29
|
+
import { render as renderStat } from './simple/stat.js';
|
|
30
|
+
import { render as renderKv } from './simple/kv.js';
|
|
31
|
+
import { render as renderList } from './simple/list.js';
|
|
32
|
+
import { render as renderChart } from './simple/chart.js';
|
|
33
|
+
import { render as renderAlert } from './simple/alert.js';
|
|
34
|
+
import { render as renderCode } from './simple/code.js';
|
|
35
|
+
import { render as renderText } from './simple/text.js';
|
|
36
|
+
import { render as renderActions } from './simple/actions.js';
|
|
37
|
+
import { render as renderTags } from './simple/tags.js';
|
|
38
|
+
// Rich widgets
|
|
39
|
+
import { render as renderStatCard } from './rich/stat-card.js';
|
|
40
|
+
import { render as renderProfile } from './rich/profile.js';
|
|
41
|
+
import { render as renderJsonViewer } from './rich/json-viewer.js';
|
|
42
|
+
import { render as renderChartRich } from './rich/chart-rich.js';
|
|
43
|
+
import { render as renderSankey } from './rich/sankey.js';
|
|
44
|
+
import { render as renderHemicycle } from './rich/hemicycle.js';
|
|
45
|
+
import { render as renderDataTable } from './rich/data-table.js';
|
|
46
|
+
import { render as renderTimeline } from './rich/timeline.js';
|
|
47
|
+
import { render as renderTrombinoscope } from './rich/trombinoscope.js';
|
|
48
|
+
import { render as renderCards } from './rich/cards.js';
|
|
49
|
+
import { render as renderGridData } from './rich/grid-data.js';
|
|
50
|
+
import { render as renderMap } from './rich/map.js';
|
|
51
|
+
import { render as renderD3 } from './rich/d3.js';
|
|
52
|
+
import { render as renderJsSandbox } from './rich/js-sandbox.js';
|
|
53
|
+
import { render as renderLog } from './rich/log.js';
|
|
54
|
+
import { render as renderGallery } from './rich/gallery.js';
|
|
55
|
+
import { render as renderCarousel } from './rich/carousel.js';
|
|
47
56
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
/** A vanilla renderer: returns cleanup or Promise thereof. */
|
|
58
|
+
type VanillaRenderer = (
|
|
59
|
+
container: HTMLElement,
|
|
60
|
+
data: Record<string, unknown>,
|
|
61
|
+
) => void | (() => void) | Promise<void | (() => void)>;
|
|
53
62
|
|
|
54
|
-
/**
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'log': s(LogViewer),
|
|
85
|
-
// Rich widgets (spec prop + event callbacks)
|
|
86
|
-
'data-table': se(DataTable, (emit) => ({ onrowclick: (row: unknown) => emit('rowclick', row) })),
|
|
87
|
-
'timeline': se(Timeline, (emit) => ({ oneventclick: (e: unknown) => emit('eventclick', e) })),
|
|
88
|
-
'trombinoscope': se(Trombinoscope, (emit) => ({ onpersonclick: (p: unknown) => emit('personclick', p) })),
|
|
89
|
-
'hemicycle': se(Hemicycle, (emit) => ({ ongroupclick: (g: unknown) => emit('groupclick', g) })),
|
|
90
|
-
'cards': se(Cards, (emit) => ({ oncardclick: (c: unknown) => emit('cardclick', c) })),
|
|
91
|
-
'grid-data': se(GridData, (emit) => ({ oncellclick: (r: unknown, c: unknown, v: unknown) => emit('cellclick', { row: r, col: c, value: v }) })),
|
|
92
|
-
'gallery': se(Gallery, (emit) => ({ onimageclick: (img: unknown, i: unknown) => emit('imageclick', { image: img, index: i }) })),
|
|
93
|
-
'carousel': se(Carousel, (emit) => ({ onslidechange: (slide: unknown, i: unknown) => emit('slidechange', { slide, index: i }) })),
|
|
63
|
+
/** Static map of all native widget types → vanilla renderer */
|
|
64
|
+
const NATIVE_VANILLA_MAP: Record<string, VanillaRenderer> = {
|
|
65
|
+
// Simple
|
|
66
|
+
'stat': renderStat,
|
|
67
|
+
'kv': renderKv,
|
|
68
|
+
'list': renderList,
|
|
69
|
+
'chart': renderChart,
|
|
70
|
+
'alert': renderAlert,
|
|
71
|
+
'code': renderCode,
|
|
72
|
+
'text': renderText,
|
|
73
|
+
'actions': renderActions,
|
|
74
|
+
'tags': renderTags,
|
|
75
|
+
// Rich
|
|
76
|
+
'stat-card': renderStatCard,
|
|
77
|
+
'profile': renderProfile,
|
|
78
|
+
'json-viewer': renderJsonViewer,
|
|
79
|
+
'chart-rich': renderChartRich,
|
|
80
|
+
'sankey': renderSankey,
|
|
81
|
+
'hemicycle': renderHemicycle,
|
|
82
|
+
'data-table': renderDataTable,
|
|
83
|
+
'timeline': renderTimeline,
|
|
84
|
+
'trombinoscope': renderTrombinoscope,
|
|
85
|
+
'cards': renderCards,
|
|
86
|
+
'grid-data': renderGridData,
|
|
87
|
+
'map': renderMap,
|
|
88
|
+
'd3': renderD3,
|
|
89
|
+
'js-sandbox': renderJsSandbox,
|
|
90
|
+
'log': renderLog,
|
|
91
|
+
'gallery': renderGallery,
|
|
92
|
+
'carousel': renderCarousel,
|
|
94
93
|
};
|
|
95
94
|
|
|
96
95
|
interface Props {
|
|
@@ -103,7 +102,7 @@
|
|
|
103
102
|
let { id, type, data, servers, oninteract }: Props = $props();
|
|
104
103
|
|
|
105
104
|
// Auto-register on the FONC message bus
|
|
106
|
-
const busId = id ??
|
|
105
|
+
const busId = id ?? makeBusId(type);
|
|
107
106
|
const unregisterBus = bus.register(busId, type, ['data-update', 'interact', '*'], (msg) => {
|
|
108
107
|
if (msg.channel === 'data-update' && msg.payload && typeof msg.payload === 'object') {
|
|
109
108
|
oninteract?.(type, 'bus-update', msg.payload);
|
|
@@ -116,7 +115,7 @@
|
|
|
116
115
|
bus.broadcast(busId, 'interact', { type, action, payload });
|
|
117
116
|
}
|
|
118
117
|
|
|
119
|
-
// ── Renderer resolution: servers > native > fallback ────────────
|
|
118
|
+
// ── Renderer resolution: servers > native vanilla > fallback ────────────
|
|
120
119
|
|
|
121
120
|
// Look up a custom widget entry from connected WebMCP servers
|
|
122
121
|
const customWidgetEntry = $derived.by(() => {
|
|
@@ -129,36 +128,48 @@
|
|
|
129
128
|
});
|
|
130
129
|
|
|
131
130
|
const customRenderer = $derived(customWidgetEntry?.renderer ?? null);
|
|
131
|
+
const isCustomVanilla = $derived(customWidgetEntry?.vanilla === true);
|
|
132
|
+
|
|
133
|
+
const nativeVanillaRenderer = $derived<VanillaRenderer | undefined>(
|
|
134
|
+
customRenderer ? undefined : NATIVE_VANILLA_MAP[type],
|
|
135
|
+
);
|
|
132
136
|
|
|
133
|
-
/** True when
|
|
134
|
-
const
|
|
137
|
+
/** True when a vanilla renderer (custom or native) should be used */
|
|
138
|
+
const useVanilla = $derived(isCustomVanilla || !!nativeVanillaRenderer);
|
|
135
139
|
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
/** The effective vanilla renderer to invoke */
|
|
141
|
+
const vanillaRenderer = $derived<VanillaRenderer | null>(
|
|
142
|
+
isCustomVanilla
|
|
143
|
+
? (customRenderer as VanillaRenderer)
|
|
144
|
+
: (nativeVanillaRenderer ?? null),
|
|
138
145
|
);
|
|
139
146
|
|
|
140
|
-
// Deep-clone data to strip Svelte 5 $state proxies —
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
// Native Svelte components handle proxied data fine and benefit from
|
|
144
|
-
// fine-grained reactivity, so they receive raw `data` directly.
|
|
145
|
-
const plainData: Record<string, unknown> = $derived(JSON.parse(JSON.stringify(data)));
|
|
147
|
+
// Deep-clone data to strip Svelte 5 $state proxies — vanilla renderers + third-party
|
|
148
|
+
// libs (D3, Leaflet, etc.) rely on Object.defineProperty which conflicts with proxies.
|
|
149
|
+
const plainData: Record<string, unknown> = $derived(safeClone(data) as Record<string, unknown>);
|
|
146
150
|
|
|
147
151
|
// ── Vanilla renderer container + lifecycle ────────────
|
|
148
152
|
let vanillaContainer: HTMLElement | undefined = $state(undefined);
|
|
149
153
|
|
|
150
154
|
$effect(() => {
|
|
151
|
-
if (!
|
|
155
|
+
if (!useVanilla || !vanillaRenderer || !vanillaContainer) return;
|
|
156
|
+
const container = vanillaContainer;
|
|
152
157
|
// Clear previous content
|
|
153
|
-
|
|
158
|
+
container.innerHTML = '';
|
|
159
|
+
|
|
160
|
+
// Listen for the standard vanilla event contract: CustomEvent('widget:interact')
|
|
161
|
+
const onInteract = (ev: Event) => {
|
|
162
|
+
const ce = ev as CustomEvent<{ action?: string; payload?: unknown }>;
|
|
163
|
+
const action = ce.detail?.action ?? 'interact';
|
|
164
|
+
emit(action, ce.detail?.payload);
|
|
165
|
+
};
|
|
166
|
+
container.addEventListener('widget:interact', onInteract);
|
|
154
167
|
|
|
155
168
|
let cleanup: (() => void) | void;
|
|
156
169
|
let cancelled = false;
|
|
157
170
|
|
|
158
171
|
try {
|
|
159
|
-
const result = (
|
|
160
|
-
vanillaContainer, plainData,
|
|
161
|
-
);
|
|
172
|
+
const result = vanillaRenderer(container, plainData);
|
|
162
173
|
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
|
163
174
|
(result as Promise<void | (() => void)>).then(
|
|
164
175
|
(c) => { if (!cancelled) cleanup = c ?? undefined; },
|
|
@@ -172,6 +183,7 @@
|
|
|
172
183
|
|
|
173
184
|
return () => {
|
|
174
185
|
cancelled = true;
|
|
186
|
+
container.removeEventListener('widget:interact', onInteract);
|
|
175
187
|
if (typeof cleanup === 'function') {
|
|
176
188
|
try { cleanup(); } catch (err) { console.error('[WidgetRenderer] cleanup failed:', err); }
|
|
177
189
|
}
|
|
@@ -238,33 +250,24 @@
|
|
|
238
250
|
});
|
|
239
251
|
</script>
|
|
240
252
|
|
|
241
|
-
{#if
|
|
253
|
+
{#if useVanilla}
|
|
242
254
|
<div bind:this={vanillaContainer} class="vanilla-container w-full h-full overflow-auto p-2"></div>
|
|
243
255
|
{:else if customRenderer}
|
|
244
256
|
<svelte:component this={customRenderer as Component<any>} {data} {id} />
|
|
245
|
-
{:else if nativeEntry}
|
|
246
|
-
<svelte:component this={nativeEntry.component} {...nativeEntry.props(data, emit)} />
|
|
247
257
|
{:else}
|
|
248
258
|
<div class="p-3 font-mono text-xs text-text2">[{type}]</div>
|
|
249
259
|
{/if}
|
|
250
260
|
|
|
251
261
|
<style>
|
|
252
|
-
.vanilla-container :global(svg)
|
|
262
|
+
.vanilla-container > :global(svg),
|
|
263
|
+
.vanilla-container > :global(canvas),
|
|
264
|
+
.vanilla-container > :global(img) {
|
|
253
265
|
width: 100%;
|
|
254
266
|
height: auto;
|
|
255
267
|
max-height: 100%;
|
|
256
268
|
display: block;
|
|
257
269
|
}
|
|
258
|
-
.vanilla-container :global(
|
|
259
|
-
width: 100%;
|
|
260
|
-
height: auto;
|
|
261
|
-
max-height: 100%;
|
|
262
|
-
display: block;
|
|
263
|
-
}
|
|
264
|
-
.vanilla-container :global(img) {
|
|
265
|
-
width: 100%;
|
|
266
|
-
height: auto;
|
|
267
|
-
max-height: 100%;
|
|
270
|
+
.vanilla-container > :global(img) {
|
|
268
271
|
object-fit: contain;
|
|
269
272
|
}
|
|
270
273
|
</style>
|
|
@@ -5,7 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
import { toPng } from 'html-to-image';
|
|
7
7
|
|
|
8
|
-
const TARGET_PNG_WIDTH =
|
|
8
|
+
const TARGET_PNG_WIDTH = 4096;
|
|
9
|
+
|
|
10
|
+
export type WidgetExportHook = (scale: number) => Promise<Blob>;
|
|
11
|
+
|
|
12
|
+
declare global {
|
|
13
|
+
interface HTMLElement {
|
|
14
|
+
__exportPng?: WidgetExportHook;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
9
17
|
|
|
10
18
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
11
19
|
|
|
@@ -95,6 +103,25 @@ async function exportPng(type: string, containerEl: HTMLElement): Promise<void>
|
|
|
95
103
|
const scrollH = Math.max(containerEl.scrollHeight, containerEl.clientHeight, 1);
|
|
96
104
|
const pixelRatio = TARGET_PNG_WIDTH / scrollW;
|
|
97
105
|
|
|
106
|
+
// Canvas-based widgets (cytoscape, etc.) expose a native exporter via __exportPng.
|
|
107
|
+
// html-to-image would only recapture the already-rasterised on-screen pixels,
|
|
108
|
+
// so we route to the widget's own resolution-independent exporter when available.
|
|
109
|
+
if (typeof containerEl.__exportPng === 'function') {
|
|
110
|
+
const scale = Math.max(2, Math.ceil(TARGET_PNG_WIDTH / scrollW));
|
|
111
|
+
try {
|
|
112
|
+
const blob = await containerEl.__exportPng(scale);
|
|
113
|
+
const url = URL.createObjectURL(blob);
|
|
114
|
+
const a = document.createElement('a');
|
|
115
|
+
a.href = url;
|
|
116
|
+
a.download = name;
|
|
117
|
+
a.click();
|
|
118
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
119
|
+
return;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error('[exportWidget] native __exportPng failed, falling back to html-to-image', err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
98
125
|
const bg = (typeof document !== 'undefined'
|
|
99
126
|
? getComputedStyle(document.documentElement).getPropertyValue('--color-surface').trim()
|
|
100
127
|
: '') || '#ffffff';
|
|
@@ -0,0 +1,78 @@
|
|
|
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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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
|
+
}
|
|
File without changes
|