@svadmin/ui 0.19.0 → 0.20.0
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"marked": "^15.0.0",
|
|
31
31
|
"marked-highlight": "^2.2.0",
|
|
32
32
|
"highlight.js": "^11.0.0",
|
|
33
|
-
"isomorphic-dompurify": "^2.0.0"
|
|
33
|
+
"isomorphic-dompurify": "^2.0.0",
|
|
34
|
+
"@svadmin/editor": "^0.1.0"
|
|
34
35
|
},
|
|
35
36
|
"peerDependenciesMeta": {
|
|
36
37
|
"marked": {
|
|
@@ -44,6 +45,9 @@
|
|
|
44
45
|
},
|
|
45
46
|
"isomorphic-dompurify": {
|
|
46
47
|
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"@svadmin/editor": {
|
|
50
|
+
"optional": true
|
|
47
51
|
}
|
|
48
52
|
},
|
|
49
53
|
"dependencies": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import ConfirmDialog from './ConfirmDialog.svelte';
|
|
37
37
|
import TooltipButton from './TooltipButton.svelte';
|
|
38
38
|
import InlineEdit from './InlineEdit.svelte';
|
|
39
|
+
import DraggableHeader from './DraggableHeader.svelte';
|
|
39
40
|
import type { Snippet } from 'svelte';
|
|
40
41
|
|
|
41
42
|
// ─── Props with Snippet composability ─────────────────────────
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { getChatProvider, getChatContext, setChatContext } from '@svadmin/core';
|
|
3
|
-
import type { ChatMessage, ChatAction } from '@svadmin/core';
|
|
2
|
+
import { getChatProvider, getChatContext, setChatContext, getAgentProvider, registerApproval, resolveApproval } from '@svadmin/core';
|
|
3
|
+
import type { ChatMessage, ChatAction, AgentEvent } from '@svadmin/core';
|
|
4
4
|
import { t } from '@svadmin/core/i18n';
|
|
5
5
|
import { useParsed } from '@svadmin/core';
|
|
6
6
|
import { fly, fade, scale } from 'svelte/transition';
|
|
7
7
|
import { Button } from './ui/button/index.js';
|
|
8
8
|
import TooltipButton from './TooltipButton.svelte';
|
|
9
|
-
import { MessageCircle, X, Minus, Send, Loader2, Bot, Trash2 } from 'lucide-svelte';
|
|
9
|
+
import { MessageCircle, X, Minus, Send, Loader2, Bot, Trash2, ShieldCheck, ShieldX, Wrench, LayoutDashboard } from 'lucide-svelte';
|
|
10
10
|
|
|
11
11
|
interface Props {
|
|
12
12
|
/** localStorage key for chat history persistence. Set to '' to disable. */
|
|
@@ -37,8 +37,12 @@
|
|
|
37
37
|
let initialized = $state(false);
|
|
38
38
|
|
|
39
39
|
const provider = $derived(getChatProvider());
|
|
40
|
+
const agent = $derived(getAgentProvider());
|
|
40
41
|
const parsed = useParsed();
|
|
41
42
|
|
|
43
|
+
/** Track pending approval requests for UI rendering */
|
|
44
|
+
let pendingApprovalIds = $state<string[]>([]);
|
|
45
|
+
|
|
42
46
|
// ─── Auto-update ChatContext from current route ─────────────
|
|
43
47
|
$effect(() => {
|
|
44
48
|
setChatContext({
|
|
@@ -125,7 +129,7 @@
|
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
async function sendMessage() {
|
|
128
|
-
if (!inputValue.trim() || isStreaming || !provider) return;
|
|
132
|
+
if (!inputValue.trim() || isStreaming || (!provider && !agent)) return;
|
|
129
133
|
|
|
130
134
|
const userMsg: ChatMessage = {
|
|
131
135
|
id: genId(),
|
|
@@ -157,24 +161,93 @@
|
|
|
157
161
|
const allMessages = messages.filter((m) => m.content);
|
|
158
162
|
const messagesToSend = contextMsg ? [contextMsg, ...allMessages] : allMessages;
|
|
159
163
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
if (agent) {
|
|
165
|
+
// ─── AgentProvider path: typed event stream ─────────
|
|
166
|
+
const stream = agent.chat(messagesToSend, {
|
|
167
|
+
signal: abortController.signal,
|
|
168
|
+
context: getChatContext(),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const collectedActions: ChatAction[] = [];
|
|
172
|
+
|
|
173
|
+
for await (const event of stream) {
|
|
174
|
+
switch (event.type) {
|
|
175
|
+
case 'text':
|
|
176
|
+
assistantMsg.content += event.content;
|
|
177
|
+
messages = [...messages.slice(0, -1), { ...assistantMsg }];
|
|
178
|
+
scrollToBottom();
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case 'tool_call':
|
|
182
|
+
assistantMsg.content += `\n🔧 *${event.tool}*(${JSON.stringify(event.args)})`;
|
|
183
|
+
messages = [...messages.slice(0, -1), { ...assistantMsg }];
|
|
184
|
+
scrollToBottom();
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
case 'tool_result':
|
|
188
|
+
if (event.result.success) {
|
|
189
|
+
assistantMsg.content += `\n✅ ${event.tool} completed`;
|
|
190
|
+
} else {
|
|
191
|
+
assistantMsg.content += `\n❌ ${event.tool} failed: ${event.result.error ?? 'Unknown error'}`;
|
|
192
|
+
}
|
|
193
|
+
messages = [...messages.slice(0, -1), { ...assistantMsg }];
|
|
194
|
+
scrollToBottom();
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
case 'approval_request': {
|
|
198
|
+
// Register in the core approval system
|
|
199
|
+
const reqId = event.id;
|
|
200
|
+
pendingApprovalIds = [...pendingApprovalIds, reqId];
|
|
201
|
+
|
|
202
|
+
collectedActions.push(
|
|
203
|
+
{
|
|
204
|
+
label: `✅ ${t('common.confirm') || 'Approve'}: ${event.description}`,
|
|
205
|
+
variant: 'default',
|
|
206
|
+
payload: { approvalId: reqId, approved: true },
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
label: `❌ ${t('common.cancel') || 'Reject'}`,
|
|
210
|
+
variant: 'destructive',
|
|
211
|
+
payload: { approvalId: reqId, approved: false },
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
assistantMsg.content += `\n⚠️ **${t('chat.approvalRequired') || 'Approval required'}**: ${event.description}`;
|
|
216
|
+
assistantMsg.actions = [...collectedActions];
|
|
217
|
+
messages = [...messages.slice(0, -1), { ...assistantMsg }];
|
|
218
|
+
scrollToBottom();
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
case 'component':
|
|
223
|
+
assistantMsg.content += `\n📊 [${event.name}]`;
|
|
224
|
+
messages = [...messages.slice(0, -1), { ...assistantMsg }];
|
|
225
|
+
scrollToBottom();
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
case 'done':
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} else if (provider) {
|
|
233
|
+
// ─── ChatProvider path: raw text stream ─────────────
|
|
234
|
+
const result = provider.sendMessage(
|
|
235
|
+
messagesToSend,
|
|
236
|
+
{ signal: abortController.signal },
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (result && typeof result === 'object' && Symbol.asyncIterator in result) {
|
|
240
|
+
for await (const chunk of result as AsyncGenerator<string>) {
|
|
241
|
+
assistantMsg.content += chunk;
|
|
242
|
+
messages = [...messages.slice(0, -1), { ...assistantMsg }];
|
|
243
|
+
scrollToBottom();
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
const text = await (result as Promise<string>);
|
|
247
|
+
assistantMsg.content = text;
|
|
169
248
|
messages = [...messages.slice(0, -1), { ...assistantMsg }];
|
|
170
249
|
scrollToBottom();
|
|
171
250
|
}
|
|
172
|
-
} else {
|
|
173
|
-
// Non-streaming response
|
|
174
|
-
const text = await (result as Promise<string>);
|
|
175
|
-
assistantMsg.content = text;
|
|
176
|
-
messages = [...messages.slice(0, -1), { ...assistantMsg }];
|
|
177
|
-
scrollToBottom();
|
|
178
251
|
}
|
|
179
252
|
} catch (err: unknown) {
|
|
180
253
|
if (err instanceof Error && err.name === 'AbortError') return;
|
|
@@ -228,6 +301,17 @@
|
|
|
228
301
|
}
|
|
229
302
|
|
|
230
303
|
function handleAction(action: ChatAction, msg: ChatMessage) {
|
|
304
|
+
// Handle approval actions from AgentProvider
|
|
305
|
+
if (action.payload?.approvalId) {
|
|
306
|
+
const id = action.payload.approvalId as string;
|
|
307
|
+
const approved = action.payload.approved as boolean;
|
|
308
|
+
resolveApproval(id, approved);
|
|
309
|
+
pendingApprovalIds = pendingApprovalIds.filter(pid => pid !== id);
|
|
310
|
+
// Remove action buttons after resolution
|
|
311
|
+
msg.actions = undefined;
|
|
312
|
+
messages = [...messages];
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
231
315
|
if (onAction) {
|
|
232
316
|
onAction(action, msg);
|
|
233
317
|
}
|
|
@@ -252,7 +336,7 @@
|
|
|
252
336
|
|
|
253
337
|
<svelte:window onkeydown={handleGlobalKeydown} />
|
|
254
338
|
|
|
255
|
-
{#if provider}
|
|
339
|
+
{#if provider || agent}
|
|
256
340
|
<!-- FAB Button -->
|
|
257
341
|
{#if !open}
|
|
258
342
|
<div transition:scale={{ duration: 200 }}>
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { getResources } from '@svadmin/core';
|
|
2
|
+
import { getResources, getAgentProvider, getChatProvider } from '@svadmin/core';
|
|
3
3
|
import { navigate } from '@svadmin/core/router';
|
|
4
4
|
import { t } from '@svadmin/core/i18n';
|
|
5
5
|
import { toggleTheme } from '@svadmin/core';
|
|
6
6
|
import { Command } from 'cmdk-sv';
|
|
7
7
|
import * as Dialog from './ui/dialog/index.js';
|
|
8
|
-
import { Search, LayoutDashboard, Plus, Sun, FileText } from 'lucide-svelte';
|
|
8
|
+
import { Search, LayoutDashboard, Plus, Sun, FileText, Bot, Sparkles } from 'lucide-svelte';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
interface Props {
|
|
11
|
+
open?: boolean;
|
|
12
|
+
/** Callback fired when user selects "Ask AI" with a query string */
|
|
13
|
+
onAskAI?: (query: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { open = $bindable(false), onAskAI }: Props = $props();
|
|
11
17
|
let searchValue = $state('');
|
|
12
18
|
|
|
13
19
|
const resources = getResources();
|
|
20
|
+
const hasAI = $derived(!!(getAgentProvider() || getChatProvider()));
|
|
14
21
|
|
|
15
22
|
function close() {
|
|
16
23
|
open = false;
|
|
@@ -22,6 +29,14 @@
|
|
|
22
29
|
close();
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
function askAI() {
|
|
33
|
+
const query = searchValue.trim();
|
|
34
|
+
if (onAskAI && query) {
|
|
35
|
+
onAskAI(query);
|
|
36
|
+
}
|
|
37
|
+
close();
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
const itemClass = 'relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50';
|
|
26
41
|
</script>
|
|
27
42
|
|
|
@@ -32,16 +47,42 @@
|
|
|
32
47
|
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
33
48
|
<Command.Input
|
|
34
49
|
bind:value={searchValue}
|
|
35
|
-
placeholder={t('common.search')}
|
|
50
|
+
placeholder={hasAI ? (t('commandPalette.searchOrAsk') || 'Search or ask AI...') : t('common.search')}
|
|
36
51
|
class="flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
37
52
|
/>
|
|
38
53
|
<kbd class="ml-2 text-[10px] font-mono font-medium text-muted-foreground bg-muted px-1.5 py-0.5 rounded border border-border">ESC</kbd>
|
|
39
54
|
</div>
|
|
40
55
|
<Command.List class="max-h-[320px] overflow-y-auto overflow-x-hidden p-1">
|
|
41
56
|
<Command.Empty class="py-6 text-center text-sm text-muted-foreground">
|
|
42
|
-
{
|
|
57
|
+
{#if hasAI && searchValue.trim()}
|
|
58
|
+
<button
|
|
59
|
+
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-primary/10 text-primary hover:bg-primary/20 transition-colors cursor-pointer text-sm font-medium"
|
|
60
|
+
onclick={askAI}
|
|
61
|
+
>
|
|
62
|
+
<Sparkles class="h-3.5 w-3.5" />
|
|
63
|
+
{t('commandPalette.askAI') || 'Ask AI'}: "{searchValue.trim()}"
|
|
64
|
+
</button>
|
|
65
|
+
{:else}
|
|
66
|
+
{t('common.noData')}
|
|
67
|
+
{/if}
|
|
43
68
|
</Command.Empty>
|
|
44
69
|
|
|
70
|
+
<!-- AI -->
|
|
71
|
+
{#if hasAI && searchValue.trim()}
|
|
72
|
+
<Command.Group heading="AI">
|
|
73
|
+
<Command.Item
|
|
74
|
+
value="ask-ai-{searchValue}"
|
|
75
|
+
onSelect={askAI}
|
|
76
|
+
class={itemClass}
|
|
77
|
+
>
|
|
78
|
+
<Sparkles class="h-4 w-4 text-primary" />
|
|
79
|
+
<span class="text-primary font-medium">{t('commandPalette.askAI') || 'Ask AI'}</span>
|
|
80
|
+
<span class="text-muted-foreground truncate ml-1">"{searchValue.trim()}"</span>
|
|
81
|
+
</Command.Item>
|
|
82
|
+
</Command.Group>
|
|
83
|
+
<Command.Separator class="mx-1 my-1 h-px bg-border" />
|
|
84
|
+
{/if}
|
|
85
|
+
|
|
45
86
|
<!-- Navigation -->
|
|
46
87
|
<Command.Group heading={t('common.home')}>
|
|
47
88
|
<Command.Item
|
|
@@ -91,3 +132,4 @@
|
|
|
91
132
|
</Command.Root>
|
|
92
133
|
</Dialog.DialogContent>
|
|
93
134
|
</Dialog.Dialog>
|
|
135
|
+
|
|
@@ -163,13 +163,43 @@
|
|
|
163
163
|
required={field.required}
|
|
164
164
|
/>
|
|
165
165
|
|
|
166
|
-
{:else if field.type === '
|
|
166
|
+
{:else if field.type === 'richtext'}
|
|
167
|
+
{#await import('@svadmin/editor')}
|
|
168
|
+
<Textarea
|
|
169
|
+
id={field.key}
|
|
170
|
+
value={strVal}
|
|
171
|
+
oninput={(e) => onchange((e.target as HTMLTextAreaElement).value)}
|
|
172
|
+
required={field.required}
|
|
173
|
+
rows={10}
|
|
174
|
+
placeholder={t('field.enterValue', { label: field.label })}
|
|
175
|
+
class="resize-y"
|
|
176
|
+
/>
|
|
177
|
+
{:then editorMod}
|
|
178
|
+
<editorMod.Editor
|
|
179
|
+
value={strVal}
|
|
180
|
+
placeholder={t('field.enterValue', { label: field.label })}
|
|
181
|
+
preset="full"
|
|
182
|
+
onchange={(html) => onchange(html)}
|
|
183
|
+
/>
|
|
184
|
+
{:catch}
|
|
185
|
+
<Textarea
|
|
186
|
+
id={field.key}
|
|
187
|
+
value={strVal}
|
|
188
|
+
oninput={(e) => onchange((e.target as HTMLTextAreaElement).value)}
|
|
189
|
+
required={field.required}
|
|
190
|
+
rows={10}
|
|
191
|
+
placeholder={t('field.enterValue', { label: field.label })}
|
|
192
|
+
class="resize-y"
|
|
193
|
+
/>
|
|
194
|
+
{/await}
|
|
195
|
+
|
|
196
|
+
{:else if field.type === 'textarea'}
|
|
167
197
|
<Textarea
|
|
168
198
|
id={field.key}
|
|
169
199
|
value={strVal}
|
|
170
200
|
oninput={(e) => onchange((e.target as HTMLTextAreaElement).value)}
|
|
171
201
|
required={field.required}
|
|
172
|
-
rows={
|
|
202
|
+
rows={4}
|
|
173
203
|
placeholder={t('field.enterValue', { label: field.label })}
|
|
174
204
|
class="resize-y"
|
|
175
205
|
/>
|
package/src/index.ts
CHANGED
|
@@ -93,6 +93,9 @@ export { default as RelationField } from './components/fields/RelationField.svel
|
|
|
93
93
|
export { default as JsonField } from './components/fields/JsonField.svelte';
|
|
94
94
|
export { default as RichTextField } from './components/fields/RichTextField.svelte';
|
|
95
95
|
|
|
96
|
+
// Rich text editor (requires @svadmin/editor)
|
|
97
|
+
export { Editor as RichTextEditor } from '@svadmin/editor';
|
|
98
|
+
|
|
96
99
|
// CRUD Buttons
|
|
97
100
|
export {
|
|
98
101
|
CreateButton, EditButton, DeleteButton, ShowButton, ListButton,
|