@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.
- package/package.json +15 -2
- package/src/agent/DiagnosticModal.svelte +126 -50
- package/src/agent/EphemeralBubble.svelte +13 -3
- package/src/agent/MCPserversList.svelte +147 -0
- package/src/agent/McpConnector.svelte +10 -1
- package/src/agent/RecipeBrowser.svelte +384 -0
- package/src/agent/RemoteMCPserversDemo.svelte +5 -121
- package/src/agent/ToolBrowser.svelte +133 -0
- package/src/agent/WebMCPserversList.svelte +2 -0
- package/src/agent/useAgentLoop.svelte.ts +396 -0
- package/src/base/chat-inline.svelte +64 -0
- package/src/base/dialog-content.svelte +3 -1
- package/src/components/HeaderControls.svelte +78 -0
- package/src/index.ts +13 -35
- package/src/stores/canvas.svelte.ts +0 -6
- package/src/widgets/SafeImage.svelte +67 -0
- package/src/widgets/WidgetRenderer.svelte +153 -78
- package/src/widgets/notebook/executors/index.ts +0 -1
- package/src/widgets/notebook/executors/sql.ts +32 -182
- package/src/widgets/notebook/import-modal-api.ts +237 -0
- package/src/widgets/notebook/import-modal.svelte +738 -0
- package/src/widgets/notebook/left-pane.ts +1 -1
- package/src/widgets/notebook/notebook.svelte +75 -0
- package/src/widgets/notebook/notebook.ts +38 -73
- package/src/widgets/notebook/prose.ts +6 -3
- package/src/widgets/notebook/shared.ts +68 -49
- package/src/widgets/rich/cards.svelte +74 -0
- package/src/widgets/rich/carousel.svelte +126 -0
- package/src/widgets/rich/chart-rich.svelte +221 -0
- package/src/widgets/rich/chat-input.svelte +52 -0
- package/src/widgets/rich/data-table.svelte +132 -0
- package/src/widgets/rich/gallery.svelte +115 -0
- package/src/widgets/rich/grid-data.svelte +85 -0
- package/src/widgets/rich/hemicycle.svelte +95 -0
- package/src/widgets/rich/js-sandbox.svelte +67 -0
- package/src/widgets/rich/json-viewer.svelte +82 -0
- package/src/widgets/rich/log.svelte +62 -0
- package/src/widgets/rich/profile.svelte +91 -0
- package/src/widgets/rich/sankey.svelte +73 -0
- package/src/widgets/rich/stat-card.svelte +60 -0
- package/src/widgets/rich/timeline.svelte +95 -0
- package/src/widgets/rich/trombinoscope.svelte +87 -0
- package/src/widgets/simple/actions.svelte +36 -0
- package/src/widgets/simple/alert.svelte +52 -0
- package/src/widgets/simple/chart.svelte +38 -0
- package/src/widgets/simple/code.svelte +30 -0
- package/src/widgets/simple/kv.svelte +31 -0
- package/src/widgets/simple/list.svelte +35 -0
- package/src/widgets/simple/stat.svelte +36 -0
- package/src/widgets/simple/tags.svelte +34 -0
- package/src/widgets/simple/text.svelte +130 -0
- package/src/widgets/helpers/safe-image.ts +0 -78
- package/src/widgets/notebook/import-modals.ts +0 -560
- package/src/widgets/notebook/recipe-browser.ts +0 -350
- package/src/widgets/rich/cards.ts +0 -181
- package/src/widgets/rich/carousel.ts +0 -319
- package/src/widgets/rich/chart-rich.ts +0 -386
- package/src/widgets/rich/d3.ts +0 -503
- package/src/widgets/rich/data-table.ts +0 -342
- package/src/widgets/rich/gallery.ts +0 -350
- package/src/widgets/rich/grid-data.ts +0 -173
- package/src/widgets/rich/hemicycle.ts +0 -313
- package/src/widgets/rich/js-sandbox.ts +0 -122
- package/src/widgets/rich/json-viewer.ts +0 -202
- package/src/widgets/rich/log.ts +0 -143
- package/src/widgets/rich/map.ts +0 -218
- package/src/widgets/rich/profile.ts +0 -256
- package/src/widgets/rich/sankey.ts +0 -257
- package/src/widgets/rich/stat-card.ts +0 -125
- package/src/widgets/rich/timeline.ts +0 -179
- package/src/widgets/rich/trombinoscope.ts +0 -246
- package/src/widgets/simple/actions.ts +0 -89
- package/src/widgets/simple/alert.ts +0 -100
- package/src/widgets/simple/chart.ts +0 -189
- package/src/widgets/simple/code.ts +0 -79
- package/src/widgets/simple/kv.ts +0 -68
- package/src/widgets/simple/list.ts +0 -89
- package/src/widgets/simple/stat.ts +0 -58
- package/src/widgets/simple/tags.ts +0 -125
- package/src/widgets/simple/text.ts +0 -198
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAgentLoop — shared composable for agent loop setup.
|
|
3
|
+
*
|
|
4
|
+
* Centralises the duplicated provider selection, Gemma WASM lifecycle,
|
|
5
|
+
* HawkProvider routing, smart LLM defaults, and TokenTracker wiring that
|
|
6
|
+
* was copy-pasted across template, recipes, boilerplate, and showcase.
|
|
7
|
+
*
|
|
8
|
+
* Usage (Svelte 5, .svelte.ts files or <script lang="ts"> blocks):
|
|
9
|
+
*
|
|
10
|
+
* const agent = createAgentLoop({
|
|
11
|
+
* chatApiBase: `${base}/api/chat`,
|
|
12
|
+
* hawkApiBase: `${base}/api/hawk`, // optional
|
|
13
|
+
* enabledProviders: ['remote', 'wasm'], // optional, defaults to all
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // Read reactive state
|
|
17
|
+
* agent.gemmaStatus // 'idle' | 'loading' | 'ready' | 'error'
|
|
18
|
+
* agent.gemmaProgress // 0–100
|
|
19
|
+
* agent.tokenMetrics // { inputTokens, outputTokens, … }
|
|
20
|
+
*
|
|
21
|
+
* // Use inside runAgentLoop
|
|
22
|
+
* const result = await runAgentLoop(msg, {
|
|
23
|
+
* provider: agent.getProvider(),
|
|
24
|
+
* ...agent.runOptions(),
|
|
25
|
+
* callbacks: { ... },
|
|
26
|
+
* });
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { canvas } from '@webmcp-auto-ui/sdk/canvas';
|
|
30
|
+
import {
|
|
31
|
+
RemoteLLMProvider,
|
|
32
|
+
WasmProvider,
|
|
33
|
+
HawkProvider,
|
|
34
|
+
LocalLLMProvider,
|
|
35
|
+
TokenTracker,
|
|
36
|
+
} from '@webmcp-auto-ui/agent';
|
|
37
|
+
import type { LLMProvider, AgentCallbacks } from '@webmcp-auto-ui/agent';
|
|
38
|
+
|
|
39
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export type GemmaStatus = 'idle' | 'loading' | 'ready' | 'error';
|
|
42
|
+
|
|
43
|
+
export type EnabledProvider = 'remote' | 'wasm' | 'hawk' | 'local';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Options for a custom LLM provider registered via `customProviders`.
|
|
47
|
+
* The factory receives the same progress/status callbacks that useAgentLoop
|
|
48
|
+
* wires up for Gemma — so Gemma-style loading UI works out of the box.
|
|
49
|
+
*/
|
|
50
|
+
export interface CustomProviderOpts {
|
|
51
|
+
model: string;
|
|
52
|
+
onProgress: (progress: number, status: string, loadedBytes?: number, totalBytes?: number) => void;
|
|
53
|
+
onStatusChange: (status: GemmaStatus) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface UseAgentLoopOptions {
|
|
57
|
+
/** Base URL for the remote (Claude) proxy, e.g. `${base}/api/chat` */
|
|
58
|
+
chatApiBase: string;
|
|
59
|
+
/** Base URL for the Hawk proxy, e.g. `${base}/api/hawk` (optional) */
|
|
60
|
+
hawkApiBase?: string;
|
|
61
|
+
/** Restrict which providers can be selected. Defaults to all. */
|
|
62
|
+
enabledProviders?: EnabledProvider[];
|
|
63
|
+
/** Ollama base URL (only used when 'local' provider is enabled) */
|
|
64
|
+
localUrl?: string;
|
|
65
|
+
/** Ollama model name override */
|
|
66
|
+
localModel?: string;
|
|
67
|
+
/** Gemma contextSize for WasmProvider (default 32768) */
|
|
68
|
+
gemmaContextSize?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Custom provider registrations. Each entry maps a model-name prefix to a
|
|
71
|
+
* factory. The factory receives the standard progress/status callbacks so
|
|
72
|
+
* the GemmaLoader UI works transparently (e.g. TransformersProvider).
|
|
73
|
+
*
|
|
74
|
+
* Example (flex):
|
|
75
|
+
* customProviders: [{ prefix: 'transformers-', factory: (opts) => new TransformersProvider(opts) }]
|
|
76
|
+
*/
|
|
77
|
+
customProviders?: Array<{ prefix: string; factory: (opts: CustomProviderOpts) => LLMProvider }>;
|
|
78
|
+
/**
|
|
79
|
+
* Optional observer whose callbacks are merged (via `mergeCallbacks`) into
|
|
80
|
+
* every `runAgentLoop` call. Designed to accept a `TraceObserver` directly.
|
|
81
|
+
*
|
|
82
|
+
* Example (flex):
|
|
83
|
+
* observer: traceObserver
|
|
84
|
+
*/
|
|
85
|
+
observer?: { callbacks: Partial<AgentCallbacks> };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface AgentRunOptions {
|
|
89
|
+
/** Schema sanitize option (smart default from LLM) */
|
|
90
|
+
schemaSanitize: boolean;
|
|
91
|
+
/** Schema flatten option (smart default from LLM) */
|
|
92
|
+
schemaFlatten: boolean;
|
|
93
|
+
/** Max tool result length */
|
|
94
|
+
maxResultLength: number;
|
|
95
|
+
/** Whether to truncate tool results */
|
|
96
|
+
truncateResults: boolean;
|
|
97
|
+
/** Compress history length (false = disabled) */
|
|
98
|
+
compressHistory: false | number;
|
|
99
|
+
/** Whether prompt caching is enabled */
|
|
100
|
+
cacheEnabled: boolean;
|
|
101
|
+
/** LLM temperature */
|
|
102
|
+
temperature: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Factory ────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
export function createAgentLoop(options: UseAgentLoopOptions) {
|
|
108
|
+
const {
|
|
109
|
+
chatApiBase,
|
|
110
|
+
hawkApiBase,
|
|
111
|
+
enabledProviders,
|
|
112
|
+
localUrl = 'http://localhost:11434',
|
|
113
|
+
localModel = 'llama3.2',
|
|
114
|
+
gemmaContextSize = 32_768,
|
|
115
|
+
customProviders = [],
|
|
116
|
+
observer,
|
|
117
|
+
} = options;
|
|
118
|
+
|
|
119
|
+
const canUse = (p: EnabledProvider) =>
|
|
120
|
+
!enabledProviders || enabledProviders.includes(p);
|
|
121
|
+
|
|
122
|
+
// ── Provider singletons ──────────────────────────────────────────────────
|
|
123
|
+
const remoteProvider = new RemoteLLMProvider({ proxyUrl: chatApiBase });
|
|
124
|
+
let hawkProvider: HawkProvider | null = null;
|
|
125
|
+
|
|
126
|
+
// ── Gemma state ──────────────────────────────────────────────────────────
|
|
127
|
+
let gemmaProvider = $state<WasmProvider | null>(null);
|
|
128
|
+
let gemmaStatus = $state<GemmaStatus>('idle');
|
|
129
|
+
let gemmaProgress = $state(0);
|
|
130
|
+
let gemmaElapsed = $state(0);
|
|
131
|
+
let gemmaLoadedMB = $state(0);
|
|
132
|
+
let gemmaTotalMB = $state(0);
|
|
133
|
+
let gemmaLoadStart = 0;
|
|
134
|
+
let gemmaTimerInterval: ReturnType<typeof setInterval> | null = null;
|
|
135
|
+
|
|
136
|
+
// ── Smart defaults state ─────────────────────────────────────────────────
|
|
137
|
+
let schemaSanitize = $state(true);
|
|
138
|
+
let schemaFlatten = $state(false);
|
|
139
|
+
let maxResultLength = $state(10_000);
|
|
140
|
+
let truncateResults = $state(false);
|
|
141
|
+
let compressHistoryEnabled = $state(false);
|
|
142
|
+
let compressPreview = $state(500);
|
|
143
|
+
let cacheEnabled = $state(true);
|
|
144
|
+
let temperature = $state(1.0);
|
|
145
|
+
|
|
146
|
+
// ── TokenTracker ─────────────────────────────────────────────────────────
|
|
147
|
+
const tokenTracker = new TokenTracker();
|
|
148
|
+
let tokenMetrics = $state(tokenTracker.metrics);
|
|
149
|
+
tokenTracker.subscribe((m) => { tokenMetrics = m; });
|
|
150
|
+
|
|
151
|
+
// ── Internal helpers ─────────────────────────────────────────────────────
|
|
152
|
+
function _startGemmaTimer() {
|
|
153
|
+
gemmaLoadStart = Date.now();
|
|
154
|
+
gemmaElapsed = 0;
|
|
155
|
+
if (gemmaTimerInterval) clearInterval(gemmaTimerInterval);
|
|
156
|
+
gemmaTimerInterval = setInterval(() => {
|
|
157
|
+
gemmaElapsed = Math.floor((Date.now() - gemmaLoadStart) / 1000);
|
|
158
|
+
}, 1000);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _stopGemmaTimer() {
|
|
162
|
+
if (gemmaTimerInterval) { clearInterval(gemmaTimerInterval); gemmaTimerInterval = null; }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function _makeWasmProvider(model: string): WasmProvider {
|
|
166
|
+
return new WasmProvider({
|
|
167
|
+
// WasmProviderOptions.model is typed as WasmModelId; cast via unknown for safety
|
|
168
|
+
model: model as any,
|
|
169
|
+
contextSize: gemmaContextSize,
|
|
170
|
+
onProgress: (p, _s, loaded, total) => {
|
|
171
|
+
gemmaProgress = p * 100;
|
|
172
|
+
if (loaded) gemmaLoadedMB = Math.round(loaded / 1048576 * 100) / 100;
|
|
173
|
+
if (total) gemmaTotalMB = Math.round(total / 1048576 * 100) / 100;
|
|
174
|
+
},
|
|
175
|
+
onStatusChange: (s: GemmaStatus) => {
|
|
176
|
+
gemmaStatus = s;
|
|
177
|
+
if (s === 'loading') _startGemmaTimer();
|
|
178
|
+
if (s === 'ready' || s === 'error') _stopGemmaTimer();
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Public API ───────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
// Gemma reactive state
|
|
187
|
+
get gemmaProvider() { return gemmaProvider; },
|
|
188
|
+
get gemmaStatus() { return gemmaStatus; },
|
|
189
|
+
get gemmaProgress() { return gemmaProgress; },
|
|
190
|
+
get gemmaElapsed() { return gemmaElapsed; },
|
|
191
|
+
get gemmaLoadedMB() { return gemmaLoadedMB; },
|
|
192
|
+
get gemmaTotalMB() { return gemmaTotalMB; },
|
|
193
|
+
|
|
194
|
+
// TokenTracker
|
|
195
|
+
get tokenTracker() { return tokenTracker; },
|
|
196
|
+
get tokenMetrics() { return tokenMetrics; },
|
|
197
|
+
|
|
198
|
+
// Smart defaults (read/write)
|
|
199
|
+
get schemaSanitize() { return schemaSanitize; },
|
|
200
|
+
set schemaSanitize(v: boolean) { schemaSanitize = v; },
|
|
201
|
+
get schemaFlatten() { return schemaFlatten; },
|
|
202
|
+
set schemaFlatten(v: boolean) { schemaFlatten = v; },
|
|
203
|
+
get maxResultLength() { return maxResultLength; },
|
|
204
|
+
set maxResultLength(v: number) { maxResultLength = v; },
|
|
205
|
+
get truncateResults() { return truncateResults; },
|
|
206
|
+
set truncateResults(v: boolean) { truncateResults = v; },
|
|
207
|
+
get compressHistoryEnabled() { return compressHistoryEnabled; },
|
|
208
|
+
set compressHistoryEnabled(v: boolean) { compressHistoryEnabled = v; },
|
|
209
|
+
get compressPreview() { return compressPreview; },
|
|
210
|
+
set compressPreview(v: number) { compressPreview = v; },
|
|
211
|
+
get cacheEnabled() { return cacheEnabled; },
|
|
212
|
+
set cacheEnabled(v: boolean) { cacheEnabled = v; },
|
|
213
|
+
get temperature() { return temperature; },
|
|
214
|
+
set temperature(v: number) { temperature = v; },
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Apply smart defaults for the current `canvas.llm` value.
|
|
218
|
+
* Call this imperatively when the LLM changes (e.g. in a `$effect`).
|
|
219
|
+
*/
|
|
220
|
+
applySmartDefaults() {
|
|
221
|
+
const llm = canvas.llm;
|
|
222
|
+
const isGemma = llm.startsWith('gemma');
|
|
223
|
+
const isLocal = llm === 'local';
|
|
224
|
+
schemaSanitize = isLocal ? true : !isGemma;
|
|
225
|
+
schemaFlatten = isGemma || isLocal;
|
|
226
|
+
truncateResults = isGemma || isLocal;
|
|
227
|
+
compressHistoryEnabled = isLocal;
|
|
228
|
+
if (isGemma) {
|
|
229
|
+
maxResultLength = 2_000;
|
|
230
|
+
temperature = 0.7;
|
|
231
|
+
cacheEnabled = false;
|
|
232
|
+
} else if (isLocal) {
|
|
233
|
+
maxResultLength = 3_000;
|
|
234
|
+
} else {
|
|
235
|
+
maxResultLength = 10_000;
|
|
236
|
+
temperature = 1.0;
|
|
237
|
+
cacheEnabled = true;
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Resolve the LLM provider for the current `canvas.llm`.
|
|
243
|
+
* Manages WasmProvider singleton lifecycle (create / swap model).
|
|
244
|
+
*/
|
|
245
|
+
getProvider(): LLMProvider {
|
|
246
|
+
const llm = canvas.llm;
|
|
247
|
+
|
|
248
|
+
// Hawk (server-side proxy)
|
|
249
|
+
if (canUse('hawk') && llm.startsWith('hawk-')) {
|
|
250
|
+
const model = llm.slice(5);
|
|
251
|
+
if (!hawkProvider || hawkProvider.model !== model) {
|
|
252
|
+
hawkProvider = new HawkProvider({ proxyUrl: hawkApiBase ?? `${chatApiBase}/hawk`, model });
|
|
253
|
+
}
|
|
254
|
+
return hawkProvider;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Local Ollama
|
|
258
|
+
if (canUse('local') && llm === 'local') {
|
|
259
|
+
return new LocalLLMProvider({ baseUrl: localUrl, model: localModel });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Gemma WASM
|
|
263
|
+
if (canUse('wasm') && (llm === 'gemma-e2b' || llm === 'gemma-e4b')) {
|
|
264
|
+
if (gemmaProvider && gemmaProvider.model !== llm) {
|
|
265
|
+
this.unloadGemma();
|
|
266
|
+
}
|
|
267
|
+
if (!gemmaProvider) {
|
|
268
|
+
gemmaProvider = _makeWasmProvider(llm);
|
|
269
|
+
}
|
|
270
|
+
return gemmaProvider;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Custom providers (e.g. TransformersProvider registered by the host app)
|
|
274
|
+
for (const { prefix, factory } of customProviders) {
|
|
275
|
+
if (llm.startsWith(prefix)) {
|
|
276
|
+
// Custom providers share the gemma progress/status slots so GemmaLoader
|
|
277
|
+
// works transparently without extra state in the host component.
|
|
278
|
+
if (gemmaProvider && (gemmaProvider as unknown as { model?: string }).model !== llm) {
|
|
279
|
+
this.unloadGemma();
|
|
280
|
+
}
|
|
281
|
+
if (!gemmaProvider) {
|
|
282
|
+
const provider = factory({
|
|
283
|
+
model: llm,
|
|
284
|
+
onProgress: (p, _s, loaded, total) => {
|
|
285
|
+
gemmaProgress = p * 100;
|
|
286
|
+
if (loaded) gemmaLoadedMB = Math.round(loaded / 1048576 * 100) / 100;
|
|
287
|
+
if (total) gemmaTotalMB = Math.round(total / 1048576 * 100) / 100;
|
|
288
|
+
},
|
|
289
|
+
onStatusChange: (s: GemmaStatus) => {
|
|
290
|
+
gemmaStatus = s;
|
|
291
|
+
if (s === 'loading') _startGemmaTimer();
|
|
292
|
+
if (s === 'ready' || s === 'error') _stopGemmaTimer();
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
// Store under gemmaProvider slot so lifecycle (unload, status) works
|
|
296
|
+
gemmaProvider = provider as unknown as WasmProvider;
|
|
297
|
+
}
|
|
298
|
+
return gemmaProvider as unknown as LLMProvider;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Remote Claude (default)
|
|
303
|
+
remoteProvider.setModel(llm as Parameters<typeof remoteProvider.setModel>[0]);
|
|
304
|
+
return remoteProvider;
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Initialize Gemma (or a custom WASM-style provider) if `canvas.llm` matches
|
|
309
|
+
* and status is idle. Idempotent — safe to call on every LLM change.
|
|
310
|
+
*/
|
|
311
|
+
initGemmaIfNeeded() {
|
|
312
|
+
const llm = canvas.llm;
|
|
313
|
+
if (gemmaStatus !== 'idle') return;
|
|
314
|
+
const isGemma = llm === 'gemma-e2b' || llm === 'gemma-e4b';
|
|
315
|
+
const isCustom = customProviders.some(({ prefix }) => llm.startsWith(prefix));
|
|
316
|
+
if (isGemma || isCustom) {
|
|
317
|
+
const p = this.getProvider();
|
|
318
|
+
// Both WasmProvider and custom providers expose initialize()
|
|
319
|
+
(p as unknown as { initialize?: () => void }).initialize?.();
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
/** Destroy and reset the current WasmProvider instance (or custom provider stored in the same slot). */
|
|
324
|
+
unloadGemma() {
|
|
325
|
+
(gemmaProvider as unknown as { destroy?: () => void })?.destroy?.();
|
|
326
|
+
gemmaProvider = null;
|
|
327
|
+
gemmaStatus = 'idle';
|
|
328
|
+
gemmaProgress = 0;
|
|
329
|
+
_stopGemmaTimer();
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Merge the observer's callbacks (if any) with the host component's own callbacks.
|
|
334
|
+
* Call this to produce the final `callbacks` object passed to `runAgentLoop`:
|
|
335
|
+
*
|
|
336
|
+
* callbacks: agent.mergeCallbacks({ onWidget, onLLMResponse, ... })
|
|
337
|
+
*
|
|
338
|
+
* Each observer callback is invoked AFTER the host callback so host logic
|
|
339
|
+
* (e.g. agentLogs push) runs first.
|
|
340
|
+
*/
|
|
341
|
+
mergeCallbacks(userCallbacks: Partial<AgentCallbacks>): Partial<AgentCallbacks> {
|
|
342
|
+
if (!observer) return userCallbacks;
|
|
343
|
+
const obs = observer.callbacks;
|
|
344
|
+
const merged: Partial<AgentCallbacks> = { ...userCallbacks };
|
|
345
|
+
// For each key in observer.callbacks, wrap with the user callback if present.
|
|
346
|
+
(Object.keys(obs) as Array<keyof AgentCallbacks>).forEach((key) => {
|
|
347
|
+
const obsFn = obs[key] as ((...args: unknown[]) => unknown) | undefined;
|
|
348
|
+
if (!obsFn) return;
|
|
349
|
+
const userFn = userCallbacks[key] as ((...args: unknown[]) => unknown) | undefined;
|
|
350
|
+
if (userFn) {
|
|
351
|
+
(merged as Record<string, unknown>)[key] = (...args: unknown[]) => {
|
|
352
|
+
const r = (userFn as (...a: unknown[]) => unknown)(...args);
|
|
353
|
+
(obsFn as (...a: unknown[]) => unknown)(...args);
|
|
354
|
+
return r;
|
|
355
|
+
};
|
|
356
|
+
} else {
|
|
357
|
+
(merged as Record<string, unknown>)[key] = obsFn;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
return merged;
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Return the agent loop options derived from current smart defaults.
|
|
365
|
+
* Spread into `runAgentLoop` options alongside app-specific overrides.
|
|
366
|
+
*/
|
|
367
|
+
runOptions(): AgentRunOptions {
|
|
368
|
+
return {
|
|
369
|
+
schemaSanitize,
|
|
370
|
+
schemaFlatten,
|
|
371
|
+
maxResultLength,
|
|
372
|
+
truncateResults,
|
|
373
|
+
compressHistory: compressHistoryEnabled ? compressPreview : false,
|
|
374
|
+
cacheEnabled,
|
|
375
|
+
temperature,
|
|
376
|
+
};
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
/** Record an LLM response into the TokenTracker (use in onLLMResponse callback). */
|
|
380
|
+
recordTokens(response: { usage?: Record<string, number>; stats?: { totalTokens: number } }, latencyMs: number) {
|
|
381
|
+
const isWasm = canvas.llm?.startsWith('gemma') || canvas.llm?.startsWith('transformers-') || false;
|
|
382
|
+
if (response.usage) {
|
|
383
|
+
tokenTracker.record(response.usage as any, latencyMs, isWasm);
|
|
384
|
+
} else if (response.stats) {
|
|
385
|
+
tokenTracker.recordEstimate(0, response.stats.totalTokens * 4, latencyMs);
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
/** Clean up intervals on component destroy. */
|
|
390
|
+
destroy() {
|
|
391
|
+
_stopGemmaTimer();
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export type AgentLoop = ReturnType<typeof createAgentLoop>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
placeholder?: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
value?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let { placeholder = 'Type a message...', disabled = false, value = $bindable('') }: Props = $props();
|
|
9
|
+
|
|
10
|
+
let inputEl = $state<HTMLInputElement | null>(null);
|
|
11
|
+
|
|
12
|
+
function handleSubmit() {
|
|
13
|
+
const text = value?.trim();
|
|
14
|
+
if (!text || disabled) return;
|
|
15
|
+
inputEl?.dispatchEvent(new CustomEvent('submit', { detail: text, bubbles: true }));
|
|
16
|
+
value = '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
20
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
handleSubmit();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function handleStop() {
|
|
27
|
+
inputEl?.dispatchEvent(new CustomEvent('stop', { bubbles: true }));
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<div class="flex items-center gap-2 w-full">
|
|
32
|
+
<input
|
|
33
|
+
bind:this={inputEl}
|
|
34
|
+
bind:value
|
|
35
|
+
type="text"
|
|
36
|
+
{placeholder}
|
|
37
|
+
{disabled}
|
|
38
|
+
onkeydown={handleKeydown}
|
|
39
|
+
class="flex-1 h-9 px-3 rounded-lg border border-border2 bg-surface2 text-sm text-text1
|
|
40
|
+
placeholder:text-text2/40 focus:outline-none focus:border-accent/50
|
|
41
|
+
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
42
|
+
/>
|
|
43
|
+
{#if disabled}
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
onclick={handleStop}
|
|
47
|
+
class="h-9 px-3 rounded-lg border border-accent2/40 bg-accent2/10 text-accent2
|
|
48
|
+
text-xs font-mono hover:bg-accent2/20 transition-colors flex-shrink-0"
|
|
49
|
+
>
|
|
50
|
+
stop
|
|
51
|
+
</button>
|
|
52
|
+
{:else}
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onclick={handleSubmit}
|
|
56
|
+
disabled={!value?.trim()}
|
|
57
|
+
class="h-9 px-3 rounded-lg border border-accent/40 bg-accent/10 text-accent
|
|
58
|
+
text-xs font-mono hover:bg-accent/20 disabled:opacity-40 disabled:cursor-not-allowed
|
|
59
|
+
transition-colors flex-shrink-0"
|
|
60
|
+
>
|
|
61
|
+
send
|
|
62
|
+
</button>
|
|
63
|
+
{/if}
|
|
64
|
+
</div>
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
type Props = {
|
|
6
6
|
class?: string;
|
|
7
|
+
style?: string;
|
|
7
8
|
children?: import('svelte').Snippet;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
|
-
let { class: className, children }: Props = $props();
|
|
11
|
+
let { class: className, style, children }: Props = $props();
|
|
11
12
|
</script>
|
|
12
13
|
|
|
13
14
|
<Dialog.Portal>
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2 bg-surface border border-border2 rounded-xl shadow-2xl max-w-lg w-full mx-4 p-6',
|
|
18
19
|
className
|
|
19
20
|
)}
|
|
21
|
+
{style}
|
|
20
22
|
>
|
|
21
23
|
{@render children?.()}
|
|
22
24
|
</Dialog.Content>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { getTheme } from '../theme/ThemeProvider.svelte';
|
|
4
|
+
import { toggleUIScale, getUIScale, initUIScale, type UIScale } from '../theme/scale.js';
|
|
5
|
+
import { THEME_MAP } from '../theme/tokens.js';
|
|
6
|
+
|
|
7
|
+
let { compact = false }: { compact?: boolean } = $props();
|
|
8
|
+
|
|
9
|
+
// Theme: prefer ThemeProvider context if available; fallback to dataset-driven
|
|
10
|
+
// toggle so apps without a provider (notebook-viewer) still work.
|
|
11
|
+
const themeCtx = getTheme();
|
|
12
|
+
|
|
13
|
+
let mode = $state<'light' | 'dark'>('light');
|
|
14
|
+
let scale = $state<UIScale>(1);
|
|
15
|
+
|
|
16
|
+
function fallbackToggleTheme() {
|
|
17
|
+
const root = document.documentElement;
|
|
18
|
+
const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
19
|
+
root.dataset.theme = next;
|
|
20
|
+
try { localStorage.setItem('webmcp-theme', next); } catch {}
|
|
21
|
+
const tokens = THEME_MAP[next];
|
|
22
|
+
if (tokens) for (const [k, v] of Object.entries(tokens)) root.style.setProperty(`--${k}`, v);
|
|
23
|
+
mode = next;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function onToggleTheme() {
|
|
27
|
+
if (themeCtx) {
|
|
28
|
+
themeCtx.toggle();
|
|
29
|
+
mode = themeCtx.mode;
|
|
30
|
+
} else {
|
|
31
|
+
fallbackToggleTheme();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function onToggleScale() {
|
|
36
|
+
scale = toggleUIScale();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onMount(() => {
|
|
40
|
+
initUIScale();
|
|
41
|
+
scale = getUIScale();
|
|
42
|
+
if (themeCtx) {
|
|
43
|
+
mode = themeCtx.mode;
|
|
44
|
+
} else {
|
|
45
|
+
mode = (document.documentElement.dataset.theme === 'dark') ? 'dark' : 'light';
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Keep `mode` in sync when the provider updates (e.g. from another component).
|
|
50
|
+
$effect(() => {
|
|
51
|
+
if (themeCtx) mode = themeCtx.mode;
|
|
52
|
+
});
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<div class="header-controls" class:compact>
|
|
56
|
+
<button type="button" class="hc-btn" onclick={onToggleTheme} aria-label="Toggle theme"
|
|
57
|
+
title={mode === 'dark' ? 'Switch to light' : 'Switch to dark'}>
|
|
58
|
+
{mode === 'dark' ? '☀' : '☾'}
|
|
59
|
+
</button>
|
|
60
|
+
<button type="button" class="hc-btn" onclick={onToggleScale} aria-label="Toggle UI scale"
|
|
61
|
+
title={scale === 1 ? 'Scale UI up (2×)' : 'Reset UI scale'}>
|
|
62
|
+
{scale === 1 ? '2×' : '1×'}
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<style>
|
|
67
|
+
.header-controls { display: inline-flex; gap: 4px; align-items: center; }
|
|
68
|
+
.hc-btn {
|
|
69
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
70
|
+
height: 28px; min-width: 32px; padding: 0 8px;
|
|
71
|
+
border: 1px solid var(--border2, var(--border, rgba(127,127,127,0.25)));
|
|
72
|
+
background: transparent; color: var(--text2, currentColor);
|
|
73
|
+
border-radius: 6px; cursor: pointer; font-size: 12px; line-height: 1;
|
|
74
|
+
font-family: inherit; transition: background 0.15s ease, color 0.15s ease;
|
|
75
|
+
}
|
|
76
|
+
.hc-btn:hover { background: rgba(127,127,127,0.1); color: var(--text1, currentColor); }
|
|
77
|
+
.compact .hc-btn { height: 24px; min-width: 28px; padding: 0 6px; font-size: 11px; }
|
|
78
|
+
</style>
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { DARK_TOKENS, LIGHT_TOKENS, THEME_MAP } from './theme/tokens.js';
|
|
|
7
7
|
export type { ThemeMode, ThemeOverrides, ThemeTokens } from './theme/tokens.js';
|
|
8
8
|
export { getUIScale, setUIScale, toggleUIScale, initUIScale, isUIScaled } from './theme/scale.js';
|
|
9
9
|
export type { UIScale, UIScaleKey } from './theme/scale.js';
|
|
10
|
+
export { default as HeaderControls } from './components/HeaderControls.svelte';
|
|
10
11
|
|
|
11
12
|
// Primitives
|
|
12
13
|
export { default as Card } from './primitives/Card.svelte';
|
|
@@ -18,47 +19,20 @@ export { default as MarkdownView } from './primitives/MarkdownView.svelte';
|
|
|
18
19
|
export { default as CodeView } from './primitives/CodeView.svelte';
|
|
19
20
|
export { renderMarkdown, highlightCode, createMarkdownRenderer } from './primitives/markdown-renderer.js';
|
|
20
21
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export { render as renderList } from './widgets/simple/list.js';
|
|
25
|
-
export { render as renderChart } from './widgets/simple/chart.js';
|
|
26
|
-
export { render as renderAlert } from './widgets/simple/alert.js';
|
|
27
|
-
export { render as renderCode } from './widgets/simple/code.js';
|
|
28
|
-
export { render as renderText } from './widgets/simple/text.js';
|
|
29
|
-
export { render as renderActions } from './widgets/simple/actions.js';
|
|
30
|
-
export { render as renderTags } from './widgets/simple/tags.js';
|
|
22
|
+
// Widgets are shipped as Svelte 5 custom elements — import the widget file
|
|
23
|
+
// side-effect to register its tag (e.g. `import '@webmcp-auto-ui/ui/widgets/simple/stat.svelte';`
|
|
24
|
+
// then use `<auto-stat data={spec}></auto-stat>`). `WidgetRenderer` does this for you.
|
|
31
25
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
export { render as
|
|
35
|
-
export { render as renderTimeline } from './widgets/rich/timeline.js';
|
|
36
|
-
export { render as renderProfile } from './widgets/rich/profile.js';
|
|
37
|
-
export { render as renderTrombinoscope } from './widgets/rich/trombinoscope.js';
|
|
38
|
-
export { render as renderJsonViewer } from './widgets/rich/json-viewer.js';
|
|
39
|
-
export { render as renderHemicycle } from './widgets/rich/hemicycle.js';
|
|
40
|
-
export { render as renderChartRich } from './widgets/rich/chart-rich.js';
|
|
41
|
-
export { render as renderCards } from './widgets/rich/cards.js';
|
|
42
|
-
export { render as renderGridData } from './widgets/rich/grid-data.js';
|
|
43
|
-
export { render as renderSankey } from './widgets/rich/sankey.js';
|
|
44
|
-
export { render as renderMap } from './widgets/rich/map.js';
|
|
45
|
-
export { render as renderD3 } from './widgets/rich/d3.js';
|
|
46
|
-
export { render as renderJsSandbox } from './widgets/rich/js-sandbox.js';
|
|
47
|
-
export { render as renderLog } from './widgets/rich/log.js';
|
|
48
|
-
export { render as renderGallery } from './widgets/rich/gallery.js';
|
|
49
|
-
export { render as renderCarousel } from './widgets/rich/carousel.js';
|
|
50
|
-
|
|
51
|
-
// Notebook widget renderer (vanilla)
|
|
52
|
-
export { render as renderNotebook } from './widgets/notebook/notebook.js';
|
|
53
|
-
export { render as renderRecipeBrowserWidget } from './widgets/notebook/recipe-browser.js';
|
|
26
|
+
// Notebook vanilla renderer (kept — notebook is wrapped by a custom element that
|
|
27
|
+
// delegates to the legacy vanilla code; full Svelte rewrite is Phase 3).
|
|
28
|
+
export { render as renderNotebook } from './widgets/notebook/notebook.js';
|
|
54
29
|
// Notebook types (optional public API)
|
|
55
30
|
export type { NotebookState, NotebookCell } from './widgets/notebook/shared.js';
|
|
56
31
|
// Notebook cell extractors (for hosts that build notebooks from recipes/tools)
|
|
57
32
|
export { extractCellsFromRecipe, extractCellsFromTool, extractCellFromMarkdown, extractCellFromFence } from './widgets/notebook/resource-extractor.js';
|
|
58
33
|
|
|
59
|
-
// Safe image
|
|
60
|
-
export {
|
|
61
|
-
export type { SafeImageOptions } from './widgets/helpers/safe-image.js';
|
|
34
|
+
// Safe image Svelte component — prefer this in .svelte code over the legacy helper.
|
|
35
|
+
export { default as SafeImage } from './widgets/SafeImage.svelte';
|
|
62
36
|
|
|
63
37
|
// Widget export utility
|
|
64
38
|
export { exportWidget, getExportFormats, exportWidgetAs } from './widgets/export-widget.js';
|
|
@@ -108,8 +82,12 @@ export { default as ChatPanel } from './agent/ChatPanel.svelte';
|
|
|
108
82
|
export type { ChatFeedItem, ChatBubble, ChatBlock } from './agent/ChatPanel.svelte';
|
|
109
83
|
export { default as AgentConsole } from './agent/AgentConsole.svelte';
|
|
110
84
|
export { default as SettingsPanel } from './agent/SettingsPanel.svelte';
|
|
85
|
+
export { default as MCPserversList } from './agent/MCPserversList.svelte';
|
|
86
|
+
/** @deprecated Use MCPserversList instead. Alias kept for backward compatibility. */
|
|
111
87
|
export { default as RemoteMCPserversDemo } from './agent/RemoteMCPserversDemo.svelte';
|
|
112
88
|
export { default as WebMCPserversList } from './agent/WebMCPserversList.svelte';
|
|
89
|
+
export { default as RecipeBrowser } from './agent/RecipeBrowser.svelte';
|
|
90
|
+
export { default as ToolBrowser } from './agent/ToolBrowser.svelte';
|
|
113
91
|
export { default as EphemeralBubble } from './agent/EphemeralBubble.svelte';
|
|
114
92
|
export { default as TokenBubble } from './agent/TokenBubble.svelte';
|
|
115
93
|
export { default as DiagnosticModal } from './agent/DiagnosticModal.svelte';
|
|
@@ -75,7 +75,6 @@ function createSvelteCanvas() {
|
|
|
75
75
|
// Setters
|
|
76
76
|
setMode: canvasVanilla.setMode.bind(canvasVanilla),
|
|
77
77
|
setLlm: canvasVanilla.setLlm.bind(canvasVanilla),
|
|
78
|
-
setMcpUrl: canvasVanilla.setMcpUrl.bind(canvasVanilla),
|
|
79
78
|
setGenerating: canvasVanilla.setGenerating.bind(canvasVanilla),
|
|
80
79
|
|
|
81
80
|
// Block actions
|
|
@@ -91,11 +90,6 @@ function createSvelteCanvas() {
|
|
|
91
90
|
updateMsg: canvasVanilla.updateMsg.bind(canvasVanilla),
|
|
92
91
|
clearMessages: canvasVanilla.clearMessages.bind(canvasVanilla),
|
|
93
92
|
|
|
94
|
-
// MCP
|
|
95
|
-
setMcpConnecting: canvasVanilla.setMcpConnecting.bind(canvasVanilla),
|
|
96
|
-
setMcpConnected: canvasVanilla.setMcpConnected.bind(canvasVanilla),
|
|
97
|
-
setMcpError: canvasVanilla.setMcpError.bind(canvasVanilla),
|
|
98
|
-
|
|
99
93
|
// Theme
|
|
100
94
|
get themeOverrides() { return themeOverrides; },
|
|
101
95
|
setThemeOverrides: canvasVanilla.setThemeOverrides.bind(canvasVanilla),
|