@webmcp-auto-ui/ui 2.5.27 → 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/package.json +15 -3
- package/src/agent/DataServersPanel.svelte +164 -0
- package/src/agent/LLMSelector.svelte +11 -3
- package/src/agent/ModelCacheManager.svelte +359 -0
- package/src/index.ts +42 -30
- package/src/widgets/WidgetRenderer.svelte +114 -104
- 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/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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webmcp-auto-ui/ui",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.28",
|
|
4
4
|
"description": "Svelte 5 UI components — primitives, widgets, window manager",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,12 @@
|
|
|
15
15
|
"./canvas": {
|
|
16
16
|
"svelte": "./src/stores/canvas.svelte.ts",
|
|
17
17
|
"import": "./src/stores/canvas.svelte.ts"
|
|
18
|
-
}
|
|
18
|
+
},
|
|
19
|
+
"./widgets/notebook/*": {
|
|
20
|
+
"svelte": "./src/widgets/notebook/*",
|
|
21
|
+
"import": "./src/widgets/notebook/*"
|
|
22
|
+
},
|
|
23
|
+
"./widgets/notebook/recipes/*": "./src/widgets/notebook/recipes/*"
|
|
19
24
|
},
|
|
20
25
|
"scripts": {
|
|
21
26
|
"build": "svelte-package -i src",
|
|
@@ -25,7 +30,13 @@
|
|
|
25
30
|
"peerDependencies": {
|
|
26
31
|
"d3": "^7.9.0",
|
|
27
32
|
"leaflet": ">=1.9.0",
|
|
28
|
-
"svelte": "^5.0.0"
|
|
33
|
+
"svelte": "^5.0.0",
|
|
34
|
+
"vega-embed": "^6.24.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"vega-embed": {
|
|
38
|
+
"optional": true
|
|
39
|
+
}
|
|
29
40
|
},
|
|
30
41
|
"devDependencies": {
|
|
31
42
|
"@sveltejs/package": "^2.3.0",
|
|
@@ -46,6 +57,7 @@
|
|
|
46
57
|
"auto-ui"
|
|
47
58
|
],
|
|
48
59
|
"dependencies": {
|
|
60
|
+
"@webmcp-auto-ui/core": "*",
|
|
49
61
|
"@webmcp-auto-ui/sdk": "*",
|
|
50
62
|
"@types/d3": "^7.4.3",
|
|
51
63
|
"bits-ui": "^2.17.2",
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DataServersPanel — manages the list of data (MCP) servers in the canvas store.
|
|
4
|
+
*
|
|
5
|
+
* Reads/writes through `canvas.dataServers` (reactive, Svelte 5 runes wrapper).
|
|
6
|
+
* A companion `MultiMcpBridge` (installed in the app root) observes the store
|
|
7
|
+
* and connects/disconnects servers according to their `enabled` flag.
|
|
8
|
+
*
|
|
9
|
+
* Visual state per server:
|
|
10
|
+
* - disabled : grey dot
|
|
11
|
+
* - enabled + !connected : yellow dot (connecting)
|
|
12
|
+
* - enabled + connected : green dot (ok)
|
|
13
|
+
* - error : red dot with tooltip
|
|
14
|
+
*/
|
|
15
|
+
import { canvas } from '@webmcp-auto-ui/sdk/canvas';
|
|
16
|
+
|
|
17
|
+
let connectOpen = $state(false);
|
|
18
|
+
let newName = $state('');
|
|
19
|
+
let newUrl = $state('');
|
|
20
|
+
let formError = $state<string | null>(null);
|
|
21
|
+
let nameInputEl: HTMLInputElement | null = $state(null);
|
|
22
|
+
|
|
23
|
+
function openModal() {
|
|
24
|
+
connectOpen = true;
|
|
25
|
+
formError = null;
|
|
26
|
+
// Focus the name field once the modal mounts.
|
|
27
|
+
queueMicrotask(() => nameInputEl?.focus());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function closeModal() {
|
|
31
|
+
connectOpen = false;
|
|
32
|
+
newName = '';
|
|
33
|
+
newUrl = '';
|
|
34
|
+
formError = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function onKey(e: KeyboardEvent) {
|
|
38
|
+
if (e.key === 'Escape' && connectOpen) {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
closeModal();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function submitConnect(e?: Event) {
|
|
45
|
+
e?.preventDefault();
|
|
46
|
+
const name = newName.trim();
|
|
47
|
+
const url = newUrl.trim();
|
|
48
|
+
if (!name) { formError = 'Name required'; return; }
|
|
49
|
+
if (canvas.getDataServer(name)) { formError = 'A server with this name already exists'; return; }
|
|
50
|
+
try { new URL(url); } catch { formError = 'Invalid URL'; return; }
|
|
51
|
+
canvas.addDataServer({ name, url });
|
|
52
|
+
// addDataServer initialises enabled=true; the bridge will pick it up.
|
|
53
|
+
closeModal();
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<svelte:window onkeydown={onKey} />
|
|
58
|
+
|
|
59
|
+
<section class="flex flex-col gap-2">
|
|
60
|
+
<div class="flex items-center justify-between">
|
|
61
|
+
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Data servers</span>
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
class="font-mono text-[10px] h-6 px-2 rounded border border-accent/40 text-accent hover:bg-accent/10 transition-colors"
|
|
65
|
+
onclick={openModal}
|
|
66
|
+
>
|
|
67
|
+
+ Connect
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{#if canvas.dataServers.length === 0}
|
|
72
|
+
<div class="text-[10px] font-mono text-text2/50 italic">No data servers connected.</div>
|
|
73
|
+
{:else}
|
|
74
|
+
<ul class="flex flex-col gap-1">
|
|
75
|
+
{#each canvas.dataServers as s (s.name)}
|
|
76
|
+
<li class="flex items-center gap-2 px-2 py-1.5 rounded border border-border2/60 bg-surface2/40">
|
|
77
|
+
<label class="flex items-center gap-2 flex-1 min-w-0 cursor-pointer">
|
|
78
|
+
<input
|
|
79
|
+
type="checkbox"
|
|
80
|
+
class="accent-accent w-3.5 h-3.5 flex-shrink-0"
|
|
81
|
+
checked={s.enabled}
|
|
82
|
+
onchange={() => canvas.toggleDataServer(s.name)}
|
|
83
|
+
aria-label={`Toggle ${s.name}`}
|
|
84
|
+
/>
|
|
85
|
+
<span
|
|
86
|
+
class="w-1.5 h-1.5 rounded-full flex-shrink-0
|
|
87
|
+
{s.error ? 'bg-accent2'
|
|
88
|
+
: !s.enabled ? 'bg-text2/40'
|
|
89
|
+
: s.connected ? 'bg-teal'
|
|
90
|
+
: 'bg-amber animate-pulse'}"
|
|
91
|
+
title={s.error
|
|
92
|
+
? `Error: ${s.error}`
|
|
93
|
+
: !s.enabled ? 'Disabled'
|
|
94
|
+
: s.connected ? 'Connected'
|
|
95
|
+
: 'Connecting…'}
|
|
96
|
+
></span>
|
|
97
|
+
<span class="font-mono text-xs text-text1 truncate" title={s.url}>{s.name}</span>
|
|
98
|
+
{#if s.tools && s.tools.length > 0}
|
|
99
|
+
<span class="font-mono text-[9px] text-text2/60 flex-shrink-0">{s.tools.length} tools</span>
|
|
100
|
+
{/if}
|
|
101
|
+
</label>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
class="text-text2/60 hover:text-accent2 transition-colors flex-shrink-0 text-sm leading-none px-1"
|
|
105
|
+
onclick={() => canvas.removeDataServer(s.name)}
|
|
106
|
+
aria-label={`Remove ${s.name}`}
|
|
107
|
+
title="Remove"
|
|
108
|
+
>×</button>
|
|
109
|
+
</li>
|
|
110
|
+
{/each}
|
|
111
|
+
</ul>
|
|
112
|
+
{/if}
|
|
113
|
+
</section>
|
|
114
|
+
|
|
115
|
+
{#if connectOpen}
|
|
116
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
117
|
+
<div
|
|
118
|
+
class="fixed inset-0 bg-black/50 z-[60] flex items-center justify-center p-4"
|
|
119
|
+
onclick={closeModal}
|
|
120
|
+
>
|
|
121
|
+
<form
|
|
122
|
+
class="w-full max-w-sm bg-surface border border-border2 rounded-lg shadow-xl p-5 flex flex-col gap-3"
|
|
123
|
+
onclick={(e) => e.stopPropagation()}
|
|
124
|
+
onsubmit={submitConnect}
|
|
125
|
+
>
|
|
126
|
+
<h4 class="font-mono text-sm font-bold text-text1">Connect data server</h4>
|
|
127
|
+
<label class="flex flex-col gap-1">
|
|
128
|
+
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Name</span>
|
|
129
|
+
<input
|
|
130
|
+
bind:this={nameInputEl}
|
|
131
|
+
bind:value={newName}
|
|
132
|
+
type="text"
|
|
133
|
+
placeholder="tricoteuses"
|
|
134
|
+
required
|
|
135
|
+
class="font-mono text-xs h-7 px-2 rounded border border-border2 bg-surface2 text-text1"
|
|
136
|
+
/>
|
|
137
|
+
</label>
|
|
138
|
+
<label class="flex flex-col gap-1">
|
|
139
|
+
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">URL</span>
|
|
140
|
+
<input
|
|
141
|
+
bind:value={newUrl}
|
|
142
|
+
type="url"
|
|
143
|
+
placeholder="https://mcp.example.com/mcp"
|
|
144
|
+
required
|
|
145
|
+
class="font-mono text-xs h-7 px-2 rounded border border-border2 bg-surface2 text-text1"
|
|
146
|
+
/>
|
|
147
|
+
</label>
|
|
148
|
+
{#if formError}
|
|
149
|
+
<div class="font-mono text-[10px] text-accent2">{formError}</div>
|
|
150
|
+
{/if}
|
|
151
|
+
<div class="flex items-center justify-end gap-2 pt-1">
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
class="font-mono text-xs h-7 px-3 rounded border border-border2 text-text2 hover:text-text1 transition-colors"
|
|
155
|
+
onclick={closeModal}
|
|
156
|
+
>Cancel</button>
|
|
157
|
+
<button
|
|
158
|
+
type="submit"
|
|
159
|
+
class="font-mono text-xs h-7 px-3 rounded border border-accent/40 text-accent hover:bg-accent/10 transition-colors"
|
|
160
|
+
>Connect</button>
|
|
161
|
+
</div>
|
|
162
|
+
</form>
|
|
163
|
+
</div>
|
|
164
|
+
{/if}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { listTransformersModels } from '@webmcp-auto-ui/agent';
|
|
2
|
+
import { listTransformersModels, listHawkModels } from '@webmcp-auto-ui/agent';
|
|
3
3
|
|
|
4
4
|
export interface ModelOption {
|
|
5
5
|
value: string;
|
|
6
6
|
label: string;
|
|
7
|
-
group: 'remote' | 'wasm' | 'transformers' | 'local';
|
|
7
|
+
group: 'remote' | 'wasm' | 'transformers' | 'local' | 'hawk';
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const transformersOptions: ModelOption[] = listTransformersModels().map(({ id, entry }) => ({
|
|
@@ -13,15 +13,23 @@
|
|
|
13
13
|
group: 'transformers' as const,
|
|
14
14
|
}));
|
|
15
15
|
|
|
16
|
+
const hawkOptions: ModelOption[] = listHawkModels().map(m => ({
|
|
17
|
+
value: `hawk-${m.id}`,
|
|
18
|
+
label: m.label,
|
|
19
|
+
group: 'hawk' as const,
|
|
20
|
+
}));
|
|
21
|
+
|
|
16
22
|
const DEFAULT_MODELS: ModelOption[] = [
|
|
17
23
|
{ value: 'haiku', label: 'claude-haiku-4-5', group: 'remote' },
|
|
18
24
|
{ value: 'gemma-e2b', label: 'Gemma E2B (MediaPipe)', group: 'wasm' },
|
|
19
25
|
{ value: 'gemma-e4b', label: 'Gemma E4B (MediaPipe)', group: 'wasm' },
|
|
20
26
|
...transformersOptions,
|
|
27
|
+
...hawkOptions,
|
|
21
28
|
];
|
|
22
29
|
|
|
23
30
|
const GROUP_LABELS: Record<string, string> = {
|
|
24
31
|
remote: 'Remote',
|
|
32
|
+
hawk: 'Remote (Hawk)',
|
|
25
33
|
wasm: 'In-Browser (MediaPipe)',
|
|
26
34
|
transformers: 'In-Browser (Transformers.js)',
|
|
27
35
|
local: 'Local',
|
|
@@ -36,7 +44,7 @@
|
|
|
36
44
|
let { value, onchange, models = DEFAULT_MODELS, class: cls = '' }: Props = $props();
|
|
37
45
|
|
|
38
46
|
const groups = $derived(
|
|
39
|
-
['remote', 'wasm', 'transformers', 'local'].filter(g => models.some(m => m.group === g))
|
|
47
|
+
['remote', 'hawk', 'wasm', 'transformers', 'local'].filter(g => models.some(m => m.group === g))
|
|
40
48
|
);
|
|
41
49
|
</script>
|
|
42
50
|
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
listCachedModels, clearModelCache, clearAllModelCaches, type CachedModelInfo,
|
|
4
|
+
listAllStorage, deleteStorageEntry, clearAllStorage, type StorageEntry,
|
|
5
|
+
} from '@webmcp-auto-ui/agent';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
let { class: cls = '' }: Props = $props();
|
|
11
|
+
|
|
12
|
+
let models = $state<CachedModelInfo[]>([]);
|
|
13
|
+
let storage = $state<StorageEntry[]>([]);
|
|
14
|
+
let usage = $state<number | null>(null);
|
|
15
|
+
let quota = $state<number | null>(null);
|
|
16
|
+
let loading = $state(true);
|
|
17
|
+
let supported = $state(true);
|
|
18
|
+
let busy = $state<string | null>(null);
|
|
19
|
+
let collapsed = $state(true);
|
|
20
|
+
|
|
21
|
+
let opfsCollapsed = $state(false);
|
|
22
|
+
let cacheCollapsed = $state(true);
|
|
23
|
+
let idbCollapsed = $state(true);
|
|
24
|
+
|
|
25
|
+
const totalSize = $derived(models.reduce((s, m) => s + m.size, 0));
|
|
26
|
+
const usagePct = $derived(quota && quota > 0 && usage !== null ? Math.min(100, (usage / quota) * 100) : 0);
|
|
27
|
+
|
|
28
|
+
const opfsExtras = $derived(storage.filter((e) => e.source === 'opfs'));
|
|
29
|
+
const cacheEntries = $derived(storage.filter((e) => e.source === 'cache-storage'));
|
|
30
|
+
const idbEntries = $derived(storage.filter((e) => e.source === 'indexeddb'));
|
|
31
|
+
|
|
32
|
+
const cacheTotalSize = $derived(cacheEntries.reduce((s, e) => s + e.size, 0));
|
|
33
|
+
const opfsExtrasTotalSize = $derived(opfsExtras.reduce((s, e) => s + e.size, 0));
|
|
34
|
+
|
|
35
|
+
function formatBytes(n: number): string {
|
|
36
|
+
if (!Number.isFinite(n) || n <= 0) return '0 B';
|
|
37
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
38
|
+
let v = n;
|
|
39
|
+
let i = 0;
|
|
40
|
+
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
|
|
41
|
+
return `${v.toFixed(v >= 100 || i === 0 ? 0 : v >= 10 ? 1 : 2)} ${units[i]}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatDate(ms: number): string {
|
|
45
|
+
if (!ms) return '—';
|
|
46
|
+
const d = new Date(ms);
|
|
47
|
+
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatSize(e: StorageEntry): string {
|
|
51
|
+
if (!e.sizeKnown) return 'taille inconnue';
|
|
52
|
+
return formatBytes(e.size);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function refresh() {
|
|
56
|
+
loading = true;
|
|
57
|
+
try {
|
|
58
|
+
if (typeof navigator === 'undefined' || !navigator.storage?.getDirectory) {
|
|
59
|
+
supported = false;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
supported = true;
|
|
63
|
+
const [ms, sto] = await Promise.all([listCachedModels(), listAllStorage()]);
|
|
64
|
+
models = ms;
|
|
65
|
+
storage = sto;
|
|
66
|
+
try {
|
|
67
|
+
const est = await navigator.storage.estimate();
|
|
68
|
+
usage = est.usage ?? null;
|
|
69
|
+
quota = est.quota ?? null;
|
|
70
|
+
} catch {
|
|
71
|
+
usage = null;
|
|
72
|
+
quota = null;
|
|
73
|
+
}
|
|
74
|
+
} finally {
|
|
75
|
+
loading = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function deleteOne(repo: string) {
|
|
80
|
+
if (!confirm(`Supprimer le cache "${repo}" ?`)) return;
|
|
81
|
+
busy = `opfs:${repo}`;
|
|
82
|
+
try {
|
|
83
|
+
await clearModelCache(repo);
|
|
84
|
+
await refresh();
|
|
85
|
+
} finally {
|
|
86
|
+
busy = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function deleteAll() {
|
|
91
|
+
if (!confirm(`Supprimer TOUS les modèles en cache (${models.length} repo·s, ${formatBytes(totalSize)}) ?`)) return;
|
|
92
|
+
busy = '__opfs_all__';
|
|
93
|
+
try {
|
|
94
|
+
await clearAllModelCaches();
|
|
95
|
+
await refresh();
|
|
96
|
+
} finally {
|
|
97
|
+
busy = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function deleteEntry(e: StorageEntry) {
|
|
102
|
+
const label = `${e.source} · ${e.key}`;
|
|
103
|
+
if (!confirm(`Supprimer ${label} ?`)) return;
|
|
104
|
+
busy = `${e.source}:${e.key}`;
|
|
105
|
+
try {
|
|
106
|
+
await deleteStorageEntry(e);
|
|
107
|
+
await refresh();
|
|
108
|
+
} finally {
|
|
109
|
+
busy = null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function deleteSource(source: 'opfs' | 'cache-storage' | 'indexeddb', count: number) {
|
|
114
|
+
if (!confirm(`Supprimer TOUT le ${source} (${count} entrée·s) ?`)) return;
|
|
115
|
+
busy = `__${source}_all__`;
|
|
116
|
+
try {
|
|
117
|
+
await clearAllStorage(source);
|
|
118
|
+
await refresh();
|
|
119
|
+
} finally {
|
|
120
|
+
busy = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
$effect(() => { refresh(); });
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<div class="flex flex-col gap-2 {cls}">
|
|
128
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
129
|
+
<div class="flex items-center gap-1 cursor-pointer select-none"
|
|
130
|
+
onclick={() => collapsed = !collapsed}>
|
|
131
|
+
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Model cache (OPFS)</span>
|
|
132
|
+
<span class="text-[9px] text-text2/60 font-mono">
|
|
133
|
+
{#if !supported}(n/a){:else if loading}(…){:else}({models.length} · {formatBytes(totalSize)}){/if}
|
|
134
|
+
</span>
|
|
135
|
+
<span class="text-[10px] text-text2 ml-auto transition-transform {collapsed ? '' : 'rotate-90'}">{@html '▶'}</span>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{#if !collapsed}
|
|
139
|
+
<div class="text-[9px] font-mono text-text2/50 -mt-1">OPFS + Cache Storage + IndexedDB</div>
|
|
140
|
+
|
|
141
|
+
{#if !supported}
|
|
142
|
+
<div class="text-[10px] font-mono text-text2/60 p-2 border border-border2 rounded">
|
|
143
|
+
OPFS indisponible dans ce navigateur.
|
|
144
|
+
</div>
|
|
145
|
+
{:else if loading}
|
|
146
|
+
<div class="text-[10px] font-mono text-text2/60">Lecture du cache…</div>
|
|
147
|
+
{:else}
|
|
148
|
+
{#if quota !== null && usage !== null}
|
|
149
|
+
<div class="flex flex-col gap-1">
|
|
150
|
+
<div class="flex items-center justify-between font-mono text-[10px] text-text2">
|
|
151
|
+
<span>{formatBytes(usage)} / {formatBytes(quota)}</span>
|
|
152
|
+
<span class="text-text2/60">origine entière · {usagePct.toFixed(1)}%</span>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="h-1 bg-surface2 rounded overflow-hidden">
|
|
155
|
+
<div class="h-full bg-teal transition-all" style="width: {usagePct}%"></div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
{/if}
|
|
159
|
+
|
|
160
|
+
<!-- ── LLM models (OPFS webmcp-models) ───────────────────────────── -->
|
|
161
|
+
<div class="flex flex-col gap-1">
|
|
162
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
163
|
+
<div class="flex items-center gap-1 cursor-pointer select-none"
|
|
164
|
+
onclick={() => opfsCollapsed = !opfsCollapsed}>
|
|
165
|
+
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">LLM models (OPFS)</span>
|
|
166
|
+
<span class="text-[9px] text-text2/60 font-mono">({models.length} · {formatBytes(totalSize)})</span>
|
|
167
|
+
<span class="text-[10px] text-text2 ml-auto transition-transform {opfsCollapsed ? '' : 'rotate-90'}">{@html '▶'}</span>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{#if !opfsCollapsed}
|
|
171
|
+
<div class="flex items-center justify-between">
|
|
172
|
+
<span class="font-mono text-[10px] text-text2">
|
|
173
|
+
Modèles : <span class="text-text1">{models.length}</span> · {formatBytes(totalSize)}
|
|
174
|
+
</span>
|
|
175
|
+
{#if models.length > 0}
|
|
176
|
+
<button
|
|
177
|
+
class="font-mono text-[10px] h-6 px-2 rounded border border-accent2/40 text-accent2 hover:bg-accent2/10 transition-colors disabled:opacity-40"
|
|
178
|
+
disabled={busy !== null}
|
|
179
|
+
onclick={deleteAll}
|
|
180
|
+
>
|
|
181
|
+
{busy === '__opfs_all__' ? 'Suppression…' : 'Tout supprimer'}
|
|
182
|
+
</button>
|
|
183
|
+
{/if}
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{#if models.length === 0}
|
|
187
|
+
<div class="text-[10px] font-mono text-text2/60 p-2 border border-dashed border-border2 rounded text-center">
|
|
188
|
+
Aucun modèle en cache.
|
|
189
|
+
</div>
|
|
190
|
+
{:else}
|
|
191
|
+
<ul class="flex flex-col gap-1">
|
|
192
|
+
{#each models as m (m.repo)}
|
|
193
|
+
<li class="flex items-center gap-2 p-2 rounded border border-border2 bg-surface2/40">
|
|
194
|
+
<div class="flex-1 min-w-0 flex flex-col gap-0.5">
|
|
195
|
+
<span class="font-mono text-[11px] text-text1 truncate" title={m.repo}>{m.repo}</span>
|
|
196
|
+
<span class="font-mono text-[9px] text-text2/60">
|
|
197
|
+
{formatBytes(m.size)} · {m.fileCount} fichier{m.fileCount > 1 ? 's' : ''} · {formatDate(m.lastModified)}
|
|
198
|
+
</span>
|
|
199
|
+
</div>
|
|
200
|
+
<button
|
|
201
|
+
class="font-mono text-[10px] h-6 px-2 rounded border border-accent2/30 text-accent2 hover:bg-accent2/10 transition-colors disabled:opacity-40 flex-shrink-0"
|
|
202
|
+
disabled={busy !== null}
|
|
203
|
+
onclick={() => deleteOne(m.repo)}
|
|
204
|
+
title="Supprimer ce cache"
|
|
205
|
+
>
|
|
206
|
+
{busy === `opfs:${m.repo}` ? '…' : 'Supprimer'}
|
|
207
|
+
</button>
|
|
208
|
+
</li>
|
|
209
|
+
{/each}
|
|
210
|
+
</ul>
|
|
211
|
+
{/if}
|
|
212
|
+
|
|
213
|
+
{#if opfsExtras.length > 0}
|
|
214
|
+
<div class="text-[9px] font-mono text-text2/50 mt-1">Autres OPFS : {opfsExtras.length} · {formatBytes(opfsExtrasTotalSize)}</div>
|
|
215
|
+
<ul class="flex flex-col gap-1">
|
|
216
|
+
{#each opfsExtras as e (e.key)}
|
|
217
|
+
<li class="flex items-center gap-2 p-2 rounded border border-border2 bg-surface2/40">
|
|
218
|
+
<div class="flex-1 min-w-0 flex flex-col gap-0.5">
|
|
219
|
+
<span class="font-mono text-[11px] text-text1 truncate flex items-center gap-1" title={e.key}>
|
|
220
|
+
{#if e.modelLike}<span class="text-accent text-[9px]">model</span>{/if}
|
|
221
|
+
{e.key}
|
|
222
|
+
</span>
|
|
223
|
+
<span class="font-mono text-[9px] text-text2/60">
|
|
224
|
+
{formatSize(e)} · {e.itemCount} fichier{e.itemCount > 1 ? 's' : ''} · {formatDate(e.lastModified)}
|
|
225
|
+
</span>
|
|
226
|
+
</div>
|
|
227
|
+
<button
|
|
228
|
+
class="font-mono text-[10px] h-6 px-2 rounded border border-accent2/30 text-accent2 hover:bg-accent2/10 transition-colors disabled:opacity-40 flex-shrink-0"
|
|
229
|
+
disabled={busy !== null}
|
|
230
|
+
onclick={() => deleteEntry(e)}
|
|
231
|
+
title="Supprimer ce répertoire OPFS"
|
|
232
|
+
>
|
|
233
|
+
{busy === `opfs:${e.key}` ? '…' : 'Supprimer'}
|
|
234
|
+
</button>
|
|
235
|
+
</li>
|
|
236
|
+
{/each}
|
|
237
|
+
</ul>
|
|
238
|
+
{/if}
|
|
239
|
+
{/if}
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<!-- ── Cache Storage ─────────────────────────────────────────────── -->
|
|
243
|
+
<div class="flex flex-col gap-1">
|
|
244
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
245
|
+
<div class="flex items-center gap-1 cursor-pointer select-none"
|
|
246
|
+
onclick={() => cacheCollapsed = !cacheCollapsed}>
|
|
247
|
+
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Cache Storage</span>
|
|
248
|
+
<span class="text-[9px] text-text2/60 font-mono">({cacheEntries.length} · {formatBytes(cacheTotalSize)})</span>
|
|
249
|
+
<span class="text-[10px] text-text2 ml-auto transition-transform {cacheCollapsed ? '' : 'rotate-90'}">{@html '▶'}</span>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{#if !cacheCollapsed}
|
|
253
|
+
<div class="flex items-center justify-between">
|
|
254
|
+
<span class="font-mono text-[10px] text-text2">
|
|
255
|
+
Caches : <span class="text-text1">{cacheEntries.length}</span> · {formatBytes(cacheTotalSize)}
|
|
256
|
+
</span>
|
|
257
|
+
{#if cacheEntries.length > 0}
|
|
258
|
+
<button
|
|
259
|
+
class="font-mono text-[10px] h-6 px-2 rounded border border-accent2/40 text-accent2 hover:bg-accent2/10 transition-colors disabled:opacity-40"
|
|
260
|
+
disabled={busy !== null}
|
|
261
|
+
onclick={() => deleteSource('cache-storage', cacheEntries.length)}
|
|
262
|
+
>
|
|
263
|
+
{busy === '__cache-storage_all__' ? 'Suppression…' : 'Tout supprimer'}
|
|
264
|
+
</button>
|
|
265
|
+
{/if}
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{#if cacheEntries.length === 0}
|
|
269
|
+
<div class="text-[10px] font-mono text-text2/60 p-2 border border-dashed border-border2 rounded text-center">
|
|
270
|
+
Aucun cache.
|
|
271
|
+
</div>
|
|
272
|
+
{:else}
|
|
273
|
+
<ul class="flex flex-col gap-1">
|
|
274
|
+
{#each cacheEntries as e (e.key)}
|
|
275
|
+
<li class="flex items-center gap-2 p-2 rounded border border-border2 bg-surface2/40">
|
|
276
|
+
<div class="flex-1 min-w-0 flex flex-col gap-0.5">
|
|
277
|
+
<span class="font-mono text-[11px] text-text1 truncate flex items-center gap-1" title={e.key}>
|
|
278
|
+
{#if e.modelLike}<span class="text-accent text-[9px]">model</span>{/if}
|
|
279
|
+
{e.key}
|
|
280
|
+
</span>
|
|
281
|
+
<span class="font-mono text-[9px] text-text2/60">
|
|
282
|
+
{formatSize(e)} · {e.itemCount} item{e.itemCount > 1 ? 's' : ''} · {formatDate(e.lastModified)}
|
|
283
|
+
</span>
|
|
284
|
+
</div>
|
|
285
|
+
<button
|
|
286
|
+
class="font-mono text-[10px] h-6 px-2 rounded border border-accent2/30 text-accent2 hover:bg-accent2/10 transition-colors disabled:opacity-40 flex-shrink-0"
|
|
287
|
+
disabled={busy !== null}
|
|
288
|
+
onclick={() => deleteEntry(e)}
|
|
289
|
+
title="Supprimer ce cache"
|
|
290
|
+
>
|
|
291
|
+
{busy === `cache-storage:${e.key}` ? '…' : 'Supprimer'}
|
|
292
|
+
</button>
|
|
293
|
+
</li>
|
|
294
|
+
{/each}
|
|
295
|
+
</ul>
|
|
296
|
+
{/if}
|
|
297
|
+
{/if}
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<!-- ── IndexedDB ─────────────────────────────────────────────────── -->
|
|
301
|
+
<div class="flex flex-col gap-1">
|
|
302
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
303
|
+
<div class="flex items-center gap-1 cursor-pointer select-none"
|
|
304
|
+
onclick={() => idbCollapsed = !idbCollapsed}>
|
|
305
|
+
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">IndexedDB</span>
|
|
306
|
+
<span class="text-[9px] text-text2/60 font-mono">({idbEntries.length})</span>
|
|
307
|
+
<span class="text-[10px] text-text2 ml-auto transition-transform {idbCollapsed ? '' : 'rotate-90'}">{@html '▶'}</span>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
{#if !idbCollapsed}
|
|
311
|
+
<div class="flex items-center justify-between">
|
|
312
|
+
<span class="font-mono text-[10px] text-text2">
|
|
313
|
+
Bases : <span class="text-text1">{idbEntries.length}</span> · taille inconnue
|
|
314
|
+
</span>
|
|
315
|
+
{#if idbEntries.length > 0}
|
|
316
|
+
<button
|
|
317
|
+
class="font-mono text-[10px] h-6 px-2 rounded border border-accent2/40 text-accent2 hover:bg-accent2/10 transition-colors disabled:opacity-40"
|
|
318
|
+
disabled={busy !== null}
|
|
319
|
+
onclick={() => deleteSource('indexeddb', idbEntries.length)}
|
|
320
|
+
>
|
|
321
|
+
{busy === '__indexeddb_all__' ? 'Suppression…' : 'Tout supprimer'}
|
|
322
|
+
</button>
|
|
323
|
+
{/if}
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
{#if idbEntries.length === 0}
|
|
327
|
+
<div class="text-[10px] font-mono text-text2/60 p-2 border border-dashed border-border2 rounded text-center">
|
|
328
|
+
Aucune base IndexedDB.
|
|
329
|
+
</div>
|
|
330
|
+
{:else}
|
|
331
|
+
<ul class="flex flex-col gap-1">
|
|
332
|
+
{#each idbEntries as e (e.key)}
|
|
333
|
+
<li class="flex items-center gap-2 p-2 rounded border border-border2 bg-surface2/40">
|
|
334
|
+
<div class="flex-1 min-w-0 flex flex-col gap-0.5">
|
|
335
|
+
<span class="font-mono text-[11px] text-text1 truncate flex items-center gap-1" title={e.key}>
|
|
336
|
+
{#if e.modelLike}<span class="text-accent text-[9px]">model</span>{/if}
|
|
337
|
+
{e.key}
|
|
338
|
+
</span>
|
|
339
|
+
<span class="font-mono text-[9px] text-text2/60">
|
|
340
|
+
taille inconnue · version {e.itemCount}
|
|
341
|
+
</span>
|
|
342
|
+
</div>
|
|
343
|
+
<button
|
|
344
|
+
class="font-mono text-[10px] h-6 px-2 rounded border border-accent2/30 text-accent2 hover:bg-accent2/10 transition-colors disabled:opacity-40 flex-shrink-0"
|
|
345
|
+
disabled={busy !== null}
|
|
346
|
+
onclick={() => deleteEntry(e)}
|
|
347
|
+
title="Supprimer cette base"
|
|
348
|
+
>
|
|
349
|
+
{busy === `indexeddb:${e.key}` ? '…' : 'Supprimer'}
|
|
350
|
+
</button>
|
|
351
|
+
</li>
|
|
352
|
+
{/each}
|
|
353
|
+
</ul>
|
|
354
|
+
{/if}
|
|
355
|
+
{/if}
|
|
356
|
+
</div>
|
|
357
|
+
{/if}
|
|
358
|
+
{/if}
|
|
359
|
+
</div>
|