@svadmin/ui 0.40.6 → 0.41.1

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.
@@ -2,7 +2,11 @@ import type { ChatMessage, ChatAction } from '@svadmin/core';
2
2
  interface Props {
3
3
  /** Render the launcher inside a layout toolbar instead of as a floating action button. */
4
4
  docked?: boolean;
5
- /** localStorage key for chat history persistence. Set to '' to disable. */
5
+ /**
6
+ * Panel suffix for chat history persistence. Both default and explicit
7
+ * suffixes are scoped by backend API + typed tenant identity.
8
+ * Set to '' to disable persistence.
9
+ */
6
10
  persistKey?: string;
7
11
  /** Custom persistence callback — called when messages change */
8
12
  onPersist?: (messages: ChatMessage[]) => void;
@@ -10,6 +14,10 @@ interface Props {
10
14
  onRestore?: () => ChatMessage[];
11
15
  /** Callback when user clicks an action button in an assistant message */
12
16
  onAction?: (action: ChatAction, message: ChatMessage) => void;
17
+ /** Stable target for tree-scoped ask-ai events. */
18
+ scope?: string;
19
+ /** Owning Layout scope used to arbitrate global keyboard shortcuts. */
20
+ ownerScope?: string;
13
21
  }
14
22
  declare const ChatDialog: import("svelte").Component<Props, {}, "">;
15
23
  type ChatDialog = ReturnType<typeof ChatDialog>;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { captureAdminContext, getResources, getAgentProvider, getChatProvider, toggleTheme } from '@svadmin/core';
2
+ import { captureAdminContext, getResources, toggleTheme } from '@svadmin/core';
3
3
  import { useTranslation } from '@svadmin/core/i18n';
4
4
  import { Command } from './ui/command/index.js';
5
5
  import * as Dialog from './ui/dialog/index.js';
@@ -18,7 +18,7 @@
18
18
  let searchValue = $state('');
19
19
 
20
20
  const resources = $derived(getResources());
21
- const hasAI = $derived(!!(getAgentProvider() || getChatProvider()));
21
+ const hasAI = $derived(!!(adminContext.agentProvider || adminContext.chatProvider));
22
22
 
23
23
  function close() {
24
24
  open = false;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { getChatProvider, getChatContext } from '@svadmin/core';
2
+ import { captureAdminContext, useParsed, type ChatProvider } from '@svadmin/core';
3
3
  import { Sparkles, X, Check, RefreshCcw } from '@lucide/svelte';
4
4
  import { slide } from 'svelte/transition';
5
5
  import { Button } from './ui/button/index.js';
@@ -8,60 +8,101 @@
8
8
 
9
9
  let { open = $bindable(false) } = $props<{ open?: boolean }>();
10
10
 
11
- const provider = $derived(getChatProvider());
12
- const ctx = $derived(getChatContext());
11
+ const adminContext = captureAdminContext();
12
+ const provider = $derived(adminContext.chatProvider);
13
+ const parsed = useParsed();
14
+ const ctx = $derived({
15
+ currentResource: parsed.resource,
16
+ selectedRecordId: parsed.id,
17
+ currentView: parsed.action,
18
+ });
13
19
 
14
20
  let isPredicting = $state(false);
15
21
  let insightText = $state('');
16
22
  let abortController: AbortController | null = null;
17
- let hasFetchedForContext = $state('');
23
+ let requestEpoch = 0;
18
24
 
19
- // Automatically request insights when the context changes
20
25
  $effect(() => {
21
- if (!open || !provider) return;
22
-
23
- // Create a context hash to avoid refetching for the same page
24
- const currentHash = `${ctx.currentResource}-${ctx.currentView}-${ctx.selectedRecordId}`;
25
- if (hasFetchedForContext === currentHash) return;
26
-
27
- hasFetchedForContext = currentHash;
28
- getInsights();
26
+ const scopedProvider = provider;
27
+ const scopedContext = {
28
+ currentResource: ctx.currentResource,
29
+ selectedRecordId: ctx.selectedRecordId,
30
+ currentView: ctx.currentView,
31
+ };
32
+ const shouldFetch = open;
33
+ void adminContext.tenantCacheKey?.__svadminTenant;
34
+
35
+ clearCopilotScope();
36
+ if (shouldFetch && scopedProvider) {
37
+ void requestInsights(scopedProvider, scopedContext);
38
+ }
39
+
40
+ return cancelCopilotRequest;
29
41
  });
30
42
 
31
43
  async function getInsights() {
32
- if (!provider) return;
33
-
44
+ const scopedProvider = provider;
45
+ if (!scopedProvider) return;
46
+ await requestInsights(scopedProvider, {
47
+ currentResource: ctx.currentResource,
48
+ selectedRecordId: ctx.selectedRecordId,
49
+ currentView: ctx.currentView,
50
+ });
51
+ }
52
+
53
+ function cancelCopilotRequest() {
54
+ requestEpoch += 1;
55
+ abortController?.abort();
56
+ abortController = null;
57
+ }
58
+
59
+ function clearCopilotScope() {
60
+ cancelCopilotRequest();
61
+ isPredicting = false;
62
+ insightText = '';
63
+ }
64
+
65
+ async function requestInsights(
66
+ scopedProvider: ChatProvider,
67
+ scopedContext: typeof ctx,
68
+ ) {
69
+ cancelCopilotRequest();
70
+ const epoch = requestEpoch;
71
+ const controller = new AbortController();
72
+ abortController = controller;
34
73
  isPredicting = true;
35
74
  insightText = '';
36
-
37
- if (abortController) abortController.abort();
38
- abortController = new AbortController();
39
75
 
40
76
  const prompt = `Analyze the current context and provide 3 quick insights or context-aware suggestions for the admin user.
41
- Resource: ${ctx.currentResource}
42
- View: ${ctx.currentView}
43
- Record ID: ${ctx.selectedRecordId || 'None'}
77
+ Resource: ${scopedContext.currentResource}
78
+ View: ${scopedContext.currentView}
79
+ Record ID: ${scopedContext.selectedRecordId || 'None'}
44
80
  Keep it concise, professional, and use bullet points.`;
45
81
 
46
82
  try {
47
- const result = provider.sendMessage(
83
+ const result = scopedProvider.sendMessage(
48
84
  [{ id: 'insights', role: 'system', content: prompt, timestamp: Date.now() }],
49
- { signal: abortController.signal }
85
+ { signal: controller.signal }
50
86
  );
51
87
 
52
88
  if (Symbol.asyncIterator in result) {
53
89
  for await (const chunk of result as AsyncIterable<string>) {
90
+ if (epoch !== requestEpoch) return;
54
91
  insightText += chunk;
55
92
  }
56
93
  } else {
57
- insightText = await result;
94
+ const response = await result;
95
+ if (epoch === requestEpoch) insightText = response;
58
96
  }
59
97
  } catch (err: unknown) {
60
- if (err instanceof Error && err.name !== 'AbortError') {
98
+ if (epoch === requestEpoch && err instanceof Error && err.name !== 'AbortError') {
61
99
  insightText = 'Failed to generate insights for this context.';
62
100
  }
63
101
  } finally {
64
- isPredicting = false;
102
+ if (epoch === requestEpoch) {
103
+ isPredicting = false;
104
+ if (abortController === controller) abortController = null;
105
+ }
65
106
  }
66
107
  }
67
108
  </script>
@@ -1,7 +1,12 @@
1
1
  <script lang="ts">
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { onMount } from 'svelte';
3
3
  import { fly } from 'svelte/transition';
4
- import { captureAdminContext, getResources, getTheme, getColorTheme } from '@svadmin/core';
4
+ import { useQueryClient } from '@tanstack/svelte-query';
5
+ import {
6
+ captureAdminContext,
7
+ getColorTheme,
8
+ getTheme,
9
+ } from '@svadmin/core';
5
10
  import { useTranslation } from '@svadmin/core/i18n';
6
11
 
7
12
  import TooltipButton from './TooltipButton.svelte';
@@ -12,43 +17,183 @@
12
17
  import { X, Bug, ChevronDown, ChevronUp, Wand2 } from '@lucide/svelte';
13
18
  import InferencerPanel from './InferencerPanel.svelte';
14
19
 
15
- const i18n = useTranslation();
20
+ type CacheDiagnostics = {
21
+ queries: { total: number; fetching: number; stale: number; errors: number };
22
+ mutations: { total: number; pending: number; paused: number; errors: number };
23
+ };
16
24
 
17
- let { docked = false }: { docked?: boolean } = $props();
25
+ type ProviderDiagnostic = {
26
+ name: string;
27
+ configured: boolean;
28
+ capabilities: string;
29
+ };
30
+
31
+ const EMPTY_CACHE_DIAGNOSTICS: CacheDiagnostics = {
32
+ queries: { total: 0, fetching: 0, stale: 0, errors: 0 },
33
+ mutations: { total: 0, pending: 0, paused: 0, errors: 0 },
34
+ };
18
35
 
36
+ const isDev = import.meta.env.DEV;
37
+ const i18n = useTranslation();
38
+ const queryClient = useQueryClient();
19
39
  const adminContext = captureAdminContext();
40
+
41
+ let { docked = false }: { docked?: boolean } = $props();
20
42
  let visible = $state(false);
21
43
  let collapsed = $state(false);
44
+ let cacheDiagnostics = $state.raw<CacheDiagnostics>(EMPTY_CACHE_DIAGNOSTICS);
22
45
 
23
46
  function toggle() {
24
47
  visible = !visible;
25
48
  }
26
49
 
27
- function handleKeydown(e: KeyboardEvent) {
28
- if (e.ctrlKey && e.shiftKey && e.key === 'D') {
29
- e.preventDefault();
50
+ function handleKeydown(event: KeyboardEvent) {
51
+ if (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'd') {
52
+ event.preventDefault();
30
53
  toggle();
31
54
  }
32
55
  }
33
56
 
34
- const resources = $derived.by(() => { try { return getResources(); } catch { return []; } });
57
+ function refreshCacheDiagnostics() {
58
+ const queries = queryClient.getQueryCache().getAll();
59
+ const mutations = queryClient.getMutationCache().getAll();
60
+ cacheDiagnostics = {
61
+ queries: {
62
+ total: queries.length,
63
+ fetching: queries.filter((query) => query.state.fetchStatus === 'fetching').length,
64
+ stale: queries.filter((query) => query.isStale()).length,
65
+ errors: queries.filter((query) => query.state.status === 'error').length,
66
+ },
67
+ mutations: {
68
+ total: mutations.length,
69
+ pending: mutations.filter((mutation) => mutation.state.status === 'pending').length,
70
+ paused: mutations.filter((mutation) => mutation.state.isPaused).length,
71
+ errors: mutations.filter((mutation) => mutation.state.status === 'error').length,
72
+ },
73
+ };
74
+ }
75
+
76
+ onMount(() => {
77
+ if (!isDev) return;
78
+ refreshCacheDiagnostics();
79
+ const unsubscribeQueries = queryClient.getQueryCache().subscribe(refreshCacheDiagnostics);
80
+ const unsubscribeMutations = queryClient.getMutationCache().subscribe(refreshCacheDiagnostics);
81
+ return () => {
82
+ unsubscribeQueries();
83
+ unsubscribeMutations();
84
+ };
85
+ });
86
+
87
+ const resources = $derived(adminContext.resources);
35
88
  const path = $derived(adminContext.currentPath());
36
89
  const theme = $derived(getTheme());
37
90
  const colorTheme = $derived(getColorTheme());
38
91
  const locale = $derived(i18n.locale);
39
92
 
93
+ const dataProviders = $derived.by(() => {
94
+ try {
95
+ return adminContext.getDataProviderNames().map((name) => {
96
+ const provider = adminContext.getDataProvider(name);
97
+ const capabilities = [
98
+ provider.getMany || provider.createMany || provider.updateMany || provider.deleteMany ? 'bulk' : undefined,
99
+ provider.custom ? 'custom' : undefined,
100
+ ].filter((capability): capability is string => Boolean(capability));
101
+ const resourceCount = resources.filter((resource) =>
102
+ (resource.provider?.dataProviderName ?? resource.meta?.dataProviderName ?? 'default') === name,
103
+ ).length;
104
+ return {
105
+ name,
106
+ resourceCount,
107
+ capabilities: capabilities.length > 0 ? capabilities.join(', ') : 'CRUD',
108
+ };
109
+ });
110
+ } catch {
111
+ return [];
112
+ }
113
+ });
114
+
115
+ const frameworkProviders = $derived.by((): ProviderDiagnostic[] => {
116
+ const access = adminContext.accessControlProvider;
117
+ const audit = adminContext.auditLogProvider;
118
+ const notification = adminContext.notificationProvider;
119
+ const chat = adminContext.chatProvider;
120
+ const agent = adminContext.agentProvider;
121
+ const live = adminContext.liveProvider;
122
+ const task = adminContext.taskProvider;
123
+ const auth = adminContext.authProvider;
124
+
125
+ return [
126
+ {
127
+ name: 'Auth',
128
+ configured: Boolean(auth),
129
+ capabilities: auth
130
+ ? [auth.getPermissions ? 'permissions' : undefined, auth.getRoles ? 'roles' : undefined]
131
+ .filter((capability): capability is string => Boolean(capability))
132
+ .join(', ') || 'identity'
133
+ : 'not configured',
134
+ },
135
+ {
136
+ name: 'Access control',
137
+ configured: Boolean(access),
138
+ capabilities: access?.options?.buttons?.enableAccessControl ? 'button checks enabled' : access ? 'policy checks' : 'not configured',
139
+ },
140
+ {
141
+ name: 'Live',
142
+ configured: Boolean(live),
143
+ capabilities: live
144
+ ? ['subscribe', live.publish ? 'publish' : undefined, live.unsubscribe ? 'unsubscribe' : undefined]
145
+ .filter((capability): capability is string => Boolean(capability))
146
+ .join(', ')
147
+ : 'not configured',
148
+ },
149
+ {
150
+ name: 'Audit',
151
+ configured: Boolean(audit),
152
+ capabilities: audit ? 'create, get, update' : 'not configured',
153
+ },
154
+ {
155
+ name: 'Notification',
156
+ configured: Boolean(notification),
157
+ capabilities: notification ? 'open, close' : 'built-in toast fallback',
158
+ },
159
+ {
160
+ name: 'Chat',
161
+ configured: Boolean(chat),
162
+ capabilities: chat ? 'message stream' : 'not configured',
163
+ },
164
+ {
165
+ name: 'Agent',
166
+ configured: Boolean(agent),
167
+ capabilities: agent ? `${agent.tools?.length ?? 0} tools (names hidden)` : 'not configured',
168
+ },
169
+ {
170
+ name: 'Task',
171
+ configured: Boolean(task),
172
+ capabilities: task
173
+ ? [task.list ? 'list' : undefined, task.subscribe ? 'subscribe' : undefined, task.retry ? 'retry' : undefined, task.cancel ? 'cancel' : undefined]
174
+ .filter((capability): capability is string => Boolean(capability))
175
+ .join(', ') || 'submit, get'
176
+ : 'not configured',
177
+ },
178
+ {
179
+ name: 'Tenant',
180
+ configured: Boolean(adminContext.tenant),
181
+ capabilities: adminContext.tenant ? 'tree scoped (identifier hidden)' : 'not configured',
182
+ },
183
+ ];
184
+ });
40
185
  </script>
41
186
 
42
- <svelte:window onkeydown={handleKeydown} />
187
+ <svelte:window onkeydown={isDev ? handleKeydown : undefined} />
43
188
 
44
- {#if (import.meta as any).env?.DEV}
189
+ {#if isDev}
45
190
  {#if visible}
46
191
  <div
47
- class="fixed bottom-0 right-4 z-[9999] w-[420px] max-w-[95vw] rounded-t-xl border border-b-0 bg-card shadow-2xl text-[0.8125rem] overflow-hidden"
48
- class:w-auto={collapsed} class:min-w-[200px]={collapsed}
192
+ class="fixed bottom-0 right-4 z-[9999] w-[560px] max-w-[95vw] rounded-t-xl border border-b-0 bg-card shadow-2xl text-[0.8125rem] overflow-hidden"
193
+ class:w-auto={collapsed}
194
+ class:min-w-[200px]={collapsed}
49
195
  transition:fly={{ y: 400, duration: 300 }}
50
196
  >
51
- <!-- Header -->
52
197
  <div class="flex items-center justify-between px-3 py-2 bg-muted border-b">
53
198
  <div class="flex items-center gap-1.5 font-semibold text-xs uppercase tracking-wider text-foreground">
54
199
  <Bug class="h-4 w-4" />
@@ -70,16 +215,18 @@
70
215
 
71
216
  {#if !collapsed}
72
217
  <Tabs.Root value="state" class="w-full">
73
- <Tabs.List class="grid w-full grid-cols-2 h-8 rounded-none border-b">
218
+ <Tabs.List class="grid w-full grid-cols-4 h-8 rounded-none border-b">
74
219
  <Tabs.Trigger value="state" class="text-xs data-[state=active]:shadow-none rounded-none border-b-2 border-transparent data-[state=active]:border-primary">State</Tabs.Trigger>
220
+ <Tabs.Trigger value="providers" class="text-xs data-[state=active]:shadow-none rounded-none border-b-2 border-transparent data-[state=active]:border-primary">Providers</Tabs.Trigger>
221
+ <Tabs.Trigger value="cache" class="text-xs data-[state=active]:shadow-none rounded-none border-b-2 border-transparent data-[state=active]:border-primary">Cache</Tabs.Trigger>
75
222
  <Tabs.Trigger value="inferencer" class="text-xs data-[state=active]:shadow-none rounded-none border-b-2 border-transparent data-[state=active]:border-primary">
76
223
  <Wand2 class="h-3 w-3 mr-1" /> Inferencer
77
224
  </Tabs.Trigger>
78
225
  </Tabs.List>
226
+
79
227
  <Tabs.Content value="state" class="p-0">
80
- <ScrollArea class="max-h-[400px]">
228
+ <ScrollArea class="max-h-[420px]">
81
229
  <div class="p-2 space-y-1">
82
- <!-- Router -->
83
230
  <div class="py-1">
84
231
  <h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground mb-1 px-1">Router</h4>
85
232
  <div class="flex items-center justify-between px-1 py-0.5 rounded hover:bg-muted/50">
@@ -90,7 +237,6 @@
90
237
 
91
238
  <Separator />
92
239
 
93
- <!-- Theme -->
94
240
  <div class="py-1">
95
241
  <h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground mb-1 px-1">Theme</h4>
96
242
  <div class="flex items-center justify-between px-1 py-0.5 rounded hover:bg-muted/50">
@@ -105,7 +251,6 @@
105
251
 
106
252
  <Separator />
107
253
 
108
- <!-- i18n -->
109
254
  <div class="py-1">
110
255
  <h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground mb-1 px-1">i18n</h4>
111
256
  <div class="flex items-center justify-between px-1 py-0.5 rounded hover:bg-muted/50">
@@ -116,19 +261,94 @@
116
261
 
117
262
  <Separator />
118
263
 
119
- <!-- Resources -->
120
264
  <div class="py-1">
121
265
  <h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground mb-1 px-1">Resources ({resources.length})</h4>
122
- {#each resources as r, _i (_i)}
266
+ {#each resources as resource (resource.identifier ?? resource.name)}
123
267
  <div class="flex items-center justify-between px-1 py-0.5 rounded hover:bg-muted/50">
124
- <span class="text-xs text-foreground">{r.name}</span>
125
- <Badge variant="outline" class="font-mono text-[0.6875rem]">{r.fields.length} fields</Badge>
268
+ <span class="text-xs text-foreground">{resource.name}</span>
269
+ <Badge variant="outline" class="font-mono text-[0.6875rem]">{resource.fields.length} fields</Badge>
126
270
  </div>
127
271
  {/each}
128
272
  </div>
129
273
  </div>
130
274
  </ScrollArea>
131
275
  </Tabs.Content>
276
+
277
+ <Tabs.Content value="providers" class="p-0">
278
+ <ScrollArea class="max-h-[420px]">
279
+ <div class="p-3 space-y-3">
280
+ <div class="rounded-md border bg-muted/20 p-2 text-[0.6875rem] text-muted-foreground">
281
+ Sensitive values are hidden: endpoints, credentials, record data, messages, and live/audit payloads.
282
+ </div>
283
+
284
+ <section class="space-y-1.5">
285
+ <h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground">Data providers ({dataProviders.length})</h4>
286
+ {#each dataProviders as provider (provider.name)}
287
+ <div class="rounded-md border px-2.5 py-2">
288
+ <div class="flex items-center justify-between gap-2">
289
+ <span class="font-medium text-foreground">{provider.name}</span>
290
+ <Badge variant="outline">{provider.resourceCount} resources</Badge>
291
+ </div>
292
+ <div class="mt-1 flex items-center justify-between gap-2 text-[0.6875rem] text-muted-foreground">
293
+ <span>{provider.capabilities}</span>
294
+ <span>Endpoint hidden</span>
295
+ </div>
296
+ </div>
297
+ {/each}
298
+ </section>
299
+
300
+ <Separator />
301
+
302
+ <section class="space-y-1">
303
+ <h4 class="text-[0.6875rem] font-bold uppercase tracking-widest text-muted-foreground">Framework providers</h4>
304
+ {#each frameworkProviders as provider (provider.name)}
305
+ <div class="flex items-center justify-between gap-3 rounded px-1 py-1 hover:bg-muted/50">
306
+ <div class="min-w-0">
307
+ <div class="text-xs text-foreground">{provider.name}</div>
308
+ <div class="truncate text-[0.6875rem] text-muted-foreground">{provider.capabilities}</div>
309
+ </div>
310
+ <Badge variant={provider.configured ? 'secondary' : 'outline'}>{provider.configured ? 'configured' : 'fallback'}</Badge>
311
+ </div>
312
+ {/each}
313
+ </section>
314
+ </div>
315
+ </ScrollArea>
316
+ </Tabs.Content>
317
+
318
+ <Tabs.Content value="cache" class="p-0">
319
+ <ScrollArea class="max-h-[420px]">
320
+ <div class="grid gap-3 p-3 sm:grid-cols-2">
321
+ <section class="rounded-md border p-3">
322
+ <div class="mb-2 flex items-center justify-between">
323
+ <h4 class="text-xs font-semibold text-foreground">Queries</h4>
324
+ <Badge variant="secondary" data-testid="devtools-query-total">{cacheDiagnostics.queries.total}</Badge>
325
+ </div>
326
+ <dl class="space-y-1 text-xs">
327
+ <div class="flex justify-between"><dt class="text-muted-foreground">Fetching</dt><dd>{cacheDiagnostics.queries.fetching}</dd></div>
328
+ <div class="flex justify-between"><dt class="text-muted-foreground">Stale</dt><dd>{cacheDiagnostics.queries.stale}</dd></div>
329
+ <div class="flex justify-between"><dt class="text-muted-foreground">Errors</dt><dd>{cacheDiagnostics.queries.errors}</dd></div>
330
+ </dl>
331
+ </section>
332
+
333
+ <section class="rounded-md border p-3">
334
+ <div class="mb-2 flex items-center justify-between">
335
+ <h4 class="text-xs font-semibold text-foreground">Mutations</h4>
336
+ <Badge variant="secondary" data-testid="devtools-mutation-total">{cacheDiagnostics.mutations.total}</Badge>
337
+ </div>
338
+ <dl class="space-y-1 text-xs">
339
+ <div class="flex justify-between"><dt class="text-muted-foreground">Pending</dt><dd>{cacheDiagnostics.mutations.pending}</dd></div>
340
+ <div class="flex justify-between"><dt class="text-muted-foreground">Paused</dt><dd>{cacheDiagnostics.mutations.paused}</dd></div>
341
+ <div class="flex justify-between"><dt class="text-muted-foreground">Errors</dt><dd>{cacheDiagnostics.mutations.errors}</dd></div>
342
+ </dl>
343
+ </section>
344
+
345
+ <p class="text-[0.6875rem] text-muted-foreground sm:col-span-2">
346
+ Query keys, variables, cached records, mutation payloads, and error bodies are intentionally hidden.
347
+ </p>
348
+ </div>
349
+ </ScrollArea>
350
+ </Tabs.Content>
351
+
132
352
  <Tabs.Content value="inferencer" class="p-0">
133
353
  <InferencerPanel />
134
354
  </Tabs.Content>
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
 
3
- import { getDataProvider, getResources, inferResource } from '@svadmin/core';
3
+ import { captureAdminContext, inferResource } from '@svadmin/core';
4
4
  import type { InferResult } from '@svadmin/core';
5
5
  import { Button } from './ui/button/index.js';
6
6
  import { Input } from './ui/input/index.js';
@@ -13,8 +13,8 @@
13
13
  import { Select } from './ui/select/index.js';
14
14
  import { Wand2, Copy, Check, RefreshCw, Loader2, AlertCircle } from '@lucide/svelte';
15
15
 
16
- const dataProvider = getDataProvider();
17
- const resources = getResources();
16
+ const adminContext = captureAdminContext();
17
+ const resources = $derived(adminContext.resources);
18
18
 
19
19
  let selectedResource = $state('');
20
20
  let inferResult = $state<InferResult | null>(null);
@@ -23,24 +23,42 @@
23
23
  let copied = $state(false);
24
24
  let customEndpoint = $state('');
25
25
  let copyTimer: ReturnType<typeof setTimeout> | undefined;
26
+ let inferenceEpoch = 0;
26
27
 
27
28
  $effect(() => {
28
29
  return () => { if (copyTimer) clearTimeout(copyTimer); };
29
30
  });
30
31
 
32
+ $effect(() => {
33
+ void adminContext.providerBundle;
34
+ void adminContext.tenantCacheKey?.__svadminTenant;
35
+ void resources;
36
+ inferenceEpoch += 1;
37
+ inferResult = null;
38
+ loading = false;
39
+ error = null;
40
+
41
+ return () => {
42
+ inferenceEpoch += 1;
43
+ };
44
+ });
45
+
31
46
  async function runInference() {
32
47
  const resourceName = customEndpoint.trim() || selectedResource;
33
48
  if (!resourceName) return;
34
49
 
50
+ const epoch = ++inferenceEpoch;
35
51
  loading = true;
36
52
  error = null;
37
53
  inferResult = null;
38
54
 
39
55
  try {
56
+ const dataProvider = adminContext.getDataProviderForResource(resourceName);
40
57
  const response = await dataProvider.getList({
41
58
  resource: resourceName,
42
59
  pagination: { current: 1, pageSize: 25 },
43
60
  });
61
+ if (epoch !== inferenceEpoch) return;
44
62
 
45
63
  if (!response.data || response.data.length === 0) {
46
64
  error = `No data returned from "${resourceName}". The API must return at least one record to infer fields.`;
@@ -52,9 +70,11 @@
52
70
  response.data as Record<string, unknown>[],
53
71
  );
54
72
  } catch (e: unknown) {
55
- error = e instanceof Error ? e.message : 'Failed to fetch data for inference.';
73
+ if (epoch === inferenceEpoch) {
74
+ error = e instanceof Error ? e.message : 'Failed to fetch data for inference.';
75
+ }
56
76
  } finally {
57
- loading = false;
77
+ if (epoch === inferenceEpoch) loading = false;
58
78
  }
59
79
  }
60
80