@nextclaw/ui 0.5.21 → 0.5.23
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/CHANGELOG.md +16 -0
- package/dist/assets/ChannelsList-AKnD2r1L.js +1 -0
- package/dist/assets/ChatPage-DidO_pAN.js +32 -0
- package/dist/assets/{CronConfig-9dYfTRJl.js → CronConfig-C1pm-oKA.js} +1 -1
- package/dist/assets/{DocBrowser-BIV0vpA0.js → DocBrowser-BY90Lf6L.js} +1 -1
- package/dist/assets/{MarketplacePage-2Zi0JSVi.js → MarketplacePage-BRtmhP3G.js} +1 -1
- package/dist/assets/{ModelConfig-h21P5rV0.js → ModelConfig-Dga1Ko7_.js} +1 -1
- package/dist/assets/{ProvidersList-DEaK1a3y.js → ProvidersList-DvCoBTrT.js} +1 -1
- package/dist/assets/{RuntimeConfig-DXMzf-gF.js → RuntimeConfig-2aqBJ6Xn.js} +1 -1
- package/dist/assets/SecretsConfig-wlnh__z0.js +3 -0
- package/dist/assets/{SessionsConfig-SdXvn_9E.js → SessionsConfig-CN2WymbH.js} +2 -2
- package/dist/assets/{action-link-C9xMkxl2.js → action-link-DLZDwUfD.js} +1 -1
- package/dist/assets/{card-Cnqfntk5.js → card-D3dD-I5t.js} +1 -1
- package/dist/assets/chat-message-DZV2Z5oc.js +5 -0
- package/dist/assets/{dialog-DJs630RE.js → dialog-DZ0VC-RD.js} +1 -1
- package/dist/assets/index-BsDasSXm.css +1 -0
- package/dist/assets/index-D_vv0E-O.js +2 -0
- package/dist/assets/{label-CXGuE6Oa.js → label-CJIvvG6o.js} +1 -1
- package/dist/assets/{page-layout-BVZlyPFt.js → page-layout-CLgr0qym.js} +1 -1
- package/dist/assets/{switch-BLF45eI3.js → switch-C-0Q8OH2.js} +1 -1
- package/dist/assets/{tabs-custom-DQ0GpEV5.js → tabs-custom-D4Gs3BGM.js} +1 -1
- package/dist/assets/useConfig-R5uGhZtD.js +1 -0
- package/dist/assets/{useConfirmDialog-CK7KAyDf.js → useConfirmDialog-AMeSTA83.js} +1 -1
- package/dist/assets/{vendor-RXIbhDBC.js → vendor-H2M3a_4Z.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/App.tsx +2 -0
- package/src/api/config.ts +16 -0
- package/src/api/types.ts +52 -0
- package/src/components/chat/ChatPage.tsx +67 -9
- package/src/components/chat/ChatThread.tsx +12 -3
- package/src/components/config/ChannelForm.tsx +9 -0
- package/src/components/config/SecretsConfig.tsx +469 -0
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/hooks/useConfig.ts +17 -0
- package/src/lib/chat-message.ts +80 -2
- package/src/lib/i18n.ts +42 -0
- package/dist/assets/ChannelsList-TFFw4Cem.js +0 -1
- package/dist/assets/ChatPage-BUm3UPap.js +0 -32
- package/dist/assets/chat-message-B7oqvJ2d.js +0 -3
- package/dist/assets/index-CrUDzcei.js +0 -2
- package/dist/assets/index-Zy7fAOe1.css +0 -1
- package/dist/assets/useConfig-vFQvF4kn.js +0 -1
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useConfig, useUpdateSecrets } from '@/hooks/useConfig';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { Input } from '@/components/ui/input';
|
|
6
|
+
import { Label } from '@/components/ui/label';
|
|
7
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
8
|
+
import { Switch } from '@/components/ui/switch';
|
|
9
|
+
import { PageHeader, PageLayout } from '@/components/layout/page-layout';
|
|
10
|
+
import type { SecretProviderView, SecretRefView, SecretSourceView } from '@/api/types';
|
|
11
|
+
import { t } from '@/lib/i18n';
|
|
12
|
+
import { Plus, Save, Trash2 } from 'lucide-react';
|
|
13
|
+
import { toast } from 'sonner';
|
|
14
|
+
|
|
15
|
+
type ProviderRow = {
|
|
16
|
+
alias: string;
|
|
17
|
+
source: SecretSourceView;
|
|
18
|
+
prefix: string;
|
|
19
|
+
path: string;
|
|
20
|
+
command: string;
|
|
21
|
+
argsText: string;
|
|
22
|
+
cwd: string;
|
|
23
|
+
timeoutMs: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type RefRow = {
|
|
27
|
+
path: string;
|
|
28
|
+
source: SecretSourceView;
|
|
29
|
+
provider: string;
|
|
30
|
+
id: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const SOURCE_OPTIONS: SecretSourceView[] = ['env', 'file', 'exec'];
|
|
34
|
+
|
|
35
|
+
function createProviderRow(alias = ''): ProviderRow {
|
|
36
|
+
return {
|
|
37
|
+
alias,
|
|
38
|
+
source: 'env',
|
|
39
|
+
prefix: '',
|
|
40
|
+
path: '',
|
|
41
|
+
command: '',
|
|
42
|
+
argsText: '',
|
|
43
|
+
cwd: '',
|
|
44
|
+
timeoutMs: 5000
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createRefRow(): RefRow {
|
|
49
|
+
return {
|
|
50
|
+
path: '',
|
|
51
|
+
source: 'env',
|
|
52
|
+
provider: '',
|
|
53
|
+
id: ''
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function providerToRow(alias: string, provider: SecretProviderView): ProviderRow {
|
|
58
|
+
if (provider.source === 'env') {
|
|
59
|
+
return {
|
|
60
|
+
...createProviderRow(alias),
|
|
61
|
+
source: 'env',
|
|
62
|
+
prefix: provider.prefix ?? ''
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (provider.source === 'file') {
|
|
67
|
+
return {
|
|
68
|
+
...createProviderRow(alias),
|
|
69
|
+
source: 'file',
|
|
70
|
+
path: provider.path
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...createProviderRow(alias),
|
|
76
|
+
source: 'exec',
|
|
77
|
+
command: provider.command,
|
|
78
|
+
argsText: (provider.args ?? []).join('\n'),
|
|
79
|
+
cwd: provider.cwd ?? '',
|
|
80
|
+
timeoutMs: provider.timeoutMs ?? 5000
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function rowToProvider(row: ProviderRow): SecretProviderView {
|
|
85
|
+
if (row.source === 'env') {
|
|
86
|
+
return {
|
|
87
|
+
source: 'env',
|
|
88
|
+
...(row.prefix.trim() ? { prefix: row.prefix.trim() } : {})
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (row.source === 'file') {
|
|
93
|
+
return {
|
|
94
|
+
source: 'file',
|
|
95
|
+
path: row.path.trim(),
|
|
96
|
+
format: 'json'
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
source: 'exec',
|
|
102
|
+
command: row.command.trim(),
|
|
103
|
+
args: row.argsText
|
|
104
|
+
.split('\n')
|
|
105
|
+
.map((item) => item.trim())
|
|
106
|
+
.filter(Boolean),
|
|
107
|
+
...(row.cwd.trim() ? { cwd: row.cwd.trim() } : {}),
|
|
108
|
+
timeoutMs: Math.max(1, Math.trunc(row.timeoutMs || 5000))
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function SecretsConfig() {
|
|
113
|
+
const { data: config, isLoading } = useConfig();
|
|
114
|
+
const updateSecrets = useUpdateSecrets();
|
|
115
|
+
|
|
116
|
+
const [enabled, setEnabled] = useState(true);
|
|
117
|
+
const [defaultEnv, setDefaultEnv] = useState('');
|
|
118
|
+
const [defaultFile, setDefaultFile] = useState('');
|
|
119
|
+
const [defaultExec, setDefaultExec] = useState('');
|
|
120
|
+
const [providers, setProviders] = useState<ProviderRow[]>([]);
|
|
121
|
+
const [refs, setRefs] = useState<RefRow[]>([]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const secrets = config?.secrets;
|
|
125
|
+
if (!secrets) {
|
|
126
|
+
setEnabled(true);
|
|
127
|
+
setDefaultEnv('');
|
|
128
|
+
setDefaultFile('');
|
|
129
|
+
setDefaultExec('');
|
|
130
|
+
setProviders([]);
|
|
131
|
+
setRefs([]);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setEnabled(Boolean(secrets.enabled));
|
|
136
|
+
setDefaultEnv(secrets.defaults.env ?? '');
|
|
137
|
+
setDefaultFile(secrets.defaults.file ?? '');
|
|
138
|
+
setDefaultExec(secrets.defaults.exec ?? '');
|
|
139
|
+
|
|
140
|
+
const nextProviders = Object.entries(secrets.providers).map(([alias, provider]) =>
|
|
141
|
+
providerToRow(alias, provider)
|
|
142
|
+
);
|
|
143
|
+
const nextRefs = Object.entries(secrets.refs).map(([path, ref]) => ({
|
|
144
|
+
path,
|
|
145
|
+
source: ref.source,
|
|
146
|
+
provider: ref.provider ?? '',
|
|
147
|
+
id: ref.id
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
setProviders(nextProviders);
|
|
151
|
+
setRefs(nextRefs);
|
|
152
|
+
}, [config?.secrets]);
|
|
153
|
+
|
|
154
|
+
const providerAliases = useMemo(() => {
|
|
155
|
+
const aliases = providers
|
|
156
|
+
.map((item) => item.alias.trim())
|
|
157
|
+
.filter(Boolean);
|
|
158
|
+
return Array.from(new Set(aliases));
|
|
159
|
+
}, [providers]);
|
|
160
|
+
|
|
161
|
+
const updateProvider = (index: number, patch: Partial<ProviderRow>) => {
|
|
162
|
+
setProviders((prev) => prev.map((entry, cursor) => (cursor === index ? { ...entry, ...patch } : entry)));
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const updateRef = (index: number, patch: Partial<RefRow>) => {
|
|
166
|
+
setRefs((prev) => prev.map((entry, cursor) => (cursor === index ? { ...entry, ...patch } : entry)));
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleSave = () => {
|
|
170
|
+
try {
|
|
171
|
+
const providerMap: Record<string, SecretProviderView> = {};
|
|
172
|
+
for (const [index, row] of providers.entries()) {
|
|
173
|
+
const alias = row.alias.trim();
|
|
174
|
+
if (!alias) {
|
|
175
|
+
throw new Error(`${t('providerAlias')} #${index + 1} ${t('isRequired')}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (providerMap[alias]) {
|
|
179
|
+
throw new Error(`${t('providerAlias')}: ${alias} (${t('duplicate')})`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (row.source === 'file' && !row.path.trim()) {
|
|
183
|
+
throw new Error(`${t('secretFilePath')} #${index + 1} ${t('isRequired')}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (row.source === 'exec' && !row.command.trim()) {
|
|
187
|
+
throw new Error(`${t('secretExecCommand')} #${index + 1} ${t('isRequired')}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
providerMap[alias] = rowToProvider(row);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const refMap: Record<string, SecretRefView> = {};
|
|
194
|
+
for (const [index, row] of refs.entries()) {
|
|
195
|
+
const path = row.path.trim();
|
|
196
|
+
const id = row.id.trim();
|
|
197
|
+
if (!path) {
|
|
198
|
+
throw new Error(`${t('secretConfigPath')} #${index + 1} ${t('isRequired')}`);
|
|
199
|
+
}
|
|
200
|
+
if (!id) {
|
|
201
|
+
throw new Error(`${t('secretId')} #${index + 1} ${t('isRequired')}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const provider = row.provider.trim();
|
|
205
|
+
if (provider && !providerMap[provider]) {
|
|
206
|
+
throw new Error(`${t('secretProviderAlias')}: ${provider} ${t('notFound')}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
refMap[path] = {
|
|
210
|
+
source: row.source,
|
|
211
|
+
...(provider ? { provider } : {}),
|
|
212
|
+
id
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
updateSecrets.mutate({
|
|
217
|
+
data: {
|
|
218
|
+
enabled,
|
|
219
|
+
defaults: {
|
|
220
|
+
env: defaultEnv.trim() || null,
|
|
221
|
+
file: defaultFile.trim() || null,
|
|
222
|
+
exec: defaultExec.trim() || null
|
|
223
|
+
},
|
|
224
|
+
providers: providerMap,
|
|
225
|
+
refs: refMap
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
230
|
+
toast.error(message);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (isLoading) {
|
|
235
|
+
return <div className="p-8 text-gray-400">{t('loading')}</div>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<PageLayout className="space-y-6">
|
|
240
|
+
<PageHeader title={t('secretsPageTitle')} description={t('secretsPageDescription')} />
|
|
241
|
+
|
|
242
|
+
<Card>
|
|
243
|
+
<CardHeader>
|
|
244
|
+
<CardTitle>{t('secrets')}</CardTitle>
|
|
245
|
+
<CardDescription>{t('secretsEnabledHelp')}</CardDescription>
|
|
246
|
+
</CardHeader>
|
|
247
|
+
<CardContent className="space-y-4">
|
|
248
|
+
<div className="flex items-center justify-between rounded-xl border border-gray-200 p-3">
|
|
249
|
+
<div>
|
|
250
|
+
<p className="text-sm font-medium text-gray-800">{t('enabled')}</p>
|
|
251
|
+
<p className="text-xs text-gray-500">{t('secretsEnabledHelp')}</p>
|
|
252
|
+
</div>
|
|
253
|
+
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
257
|
+
<div className="space-y-2">
|
|
258
|
+
<Label>{t('defaultEnvProvider')}</Label>
|
|
259
|
+
<Select value={defaultEnv || '__none__'} onValueChange={(value) => setDefaultEnv(value === '__none__' ? '' : value)}>
|
|
260
|
+
<SelectTrigger>
|
|
261
|
+
<SelectValue placeholder={t('noneOption')} />
|
|
262
|
+
</SelectTrigger>
|
|
263
|
+
<SelectContent>
|
|
264
|
+
<SelectItem value="__none__">{t('noneOption')}</SelectItem>
|
|
265
|
+
{providerAliases.map((alias) => (
|
|
266
|
+
<SelectItem key={alias} value={alias}>
|
|
267
|
+
{alias}
|
|
268
|
+
</SelectItem>
|
|
269
|
+
))}
|
|
270
|
+
</SelectContent>
|
|
271
|
+
</Select>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div className="space-y-2">
|
|
275
|
+
<Label>{t('defaultFileProvider')}</Label>
|
|
276
|
+
<Select value={defaultFile || '__none__'} onValueChange={(value) => setDefaultFile(value === '__none__' ? '' : value)}>
|
|
277
|
+
<SelectTrigger>
|
|
278
|
+
<SelectValue placeholder={t('noneOption')} />
|
|
279
|
+
</SelectTrigger>
|
|
280
|
+
<SelectContent>
|
|
281
|
+
<SelectItem value="__none__">{t('noneOption')}</SelectItem>
|
|
282
|
+
{providerAliases.map((alias) => (
|
|
283
|
+
<SelectItem key={alias} value={alias}>
|
|
284
|
+
{alias}
|
|
285
|
+
</SelectItem>
|
|
286
|
+
))}
|
|
287
|
+
</SelectContent>
|
|
288
|
+
</Select>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div className="space-y-2">
|
|
292
|
+
<Label>{t('defaultExecProvider')}</Label>
|
|
293
|
+
<Select value={defaultExec || '__none__'} onValueChange={(value) => setDefaultExec(value === '__none__' ? '' : value)}>
|
|
294
|
+
<SelectTrigger>
|
|
295
|
+
<SelectValue placeholder={t('noneOption')} />
|
|
296
|
+
</SelectTrigger>
|
|
297
|
+
<SelectContent>
|
|
298
|
+
<SelectItem value="__none__">{t('noneOption')}</SelectItem>
|
|
299
|
+
{providerAliases.map((alias) => (
|
|
300
|
+
<SelectItem key={alias} value={alias}>
|
|
301
|
+
{alias}
|
|
302
|
+
</SelectItem>
|
|
303
|
+
))}
|
|
304
|
+
</SelectContent>
|
|
305
|
+
</Select>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</CardContent>
|
|
309
|
+
</Card>
|
|
310
|
+
|
|
311
|
+
<Card>
|
|
312
|
+
<CardHeader>
|
|
313
|
+
<CardTitle>{t('secretProvidersTitle')}</CardTitle>
|
|
314
|
+
<CardDescription>{t('secretProvidersDescription')}</CardDescription>
|
|
315
|
+
</CardHeader>
|
|
316
|
+
<CardContent className="space-y-3">
|
|
317
|
+
{providers.map((provider, index) => (
|
|
318
|
+
<div key={`provider-${index}`} className="rounded-xl border border-gray-200 p-3 space-y-3">
|
|
319
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
320
|
+
<Input
|
|
321
|
+
value={provider.alias}
|
|
322
|
+
onChange={(event) => updateProvider(index, { alias: event.target.value })}
|
|
323
|
+
placeholder={t('providerAlias')}
|
|
324
|
+
/>
|
|
325
|
+
<Select value={provider.source} onValueChange={(value) => updateProvider(index, { source: value as SecretSourceView })}>
|
|
326
|
+
<SelectTrigger>
|
|
327
|
+
<SelectValue />
|
|
328
|
+
</SelectTrigger>
|
|
329
|
+
<SelectContent>
|
|
330
|
+
{SOURCE_OPTIONS.map((source) => (
|
|
331
|
+
<SelectItem key={source} value={source}>
|
|
332
|
+
{source}
|
|
333
|
+
</SelectItem>
|
|
334
|
+
))}
|
|
335
|
+
</SelectContent>
|
|
336
|
+
</Select>
|
|
337
|
+
<Button type="button" variant="outline" onClick={() => setProviders((prev) => prev.filter((_, i) => i !== index))}>
|
|
338
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
|
339
|
+
{t('removeProvider')}
|
|
340
|
+
</Button>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
{provider.source === 'env' && (
|
|
344
|
+
<Input
|
|
345
|
+
value={provider.prefix}
|
|
346
|
+
onChange={(event) => updateProvider(index, { prefix: event.target.value })}
|
|
347
|
+
placeholder={t('envPrefix')}
|
|
348
|
+
/>
|
|
349
|
+
)}
|
|
350
|
+
|
|
351
|
+
{provider.source === 'file' && (
|
|
352
|
+
<Input
|
|
353
|
+
value={provider.path}
|
|
354
|
+
onChange={(event) => updateProvider(index, { path: event.target.value })}
|
|
355
|
+
placeholder={t('secretFilePath')}
|
|
356
|
+
/>
|
|
357
|
+
)}
|
|
358
|
+
|
|
359
|
+
{provider.source === 'exec' && (
|
|
360
|
+
<div className="space-y-2">
|
|
361
|
+
<Input
|
|
362
|
+
value={provider.command}
|
|
363
|
+
onChange={(event) => updateProvider(index, { command: event.target.value })}
|
|
364
|
+
placeholder={t('secretExecCommand')}
|
|
365
|
+
/>
|
|
366
|
+
<textarea
|
|
367
|
+
className="min-h-[84px] w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"
|
|
368
|
+
value={provider.argsText}
|
|
369
|
+
onChange={(event) => updateProvider(index, { argsText: event.target.value })}
|
|
370
|
+
placeholder={t('secretExecArgs')}
|
|
371
|
+
/>
|
|
372
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
373
|
+
<Input
|
|
374
|
+
value={provider.cwd}
|
|
375
|
+
onChange={(event) => updateProvider(index, { cwd: event.target.value })}
|
|
376
|
+
placeholder={t('secretExecCwd')}
|
|
377
|
+
/>
|
|
378
|
+
<Input
|
|
379
|
+
type="number"
|
|
380
|
+
min={1}
|
|
381
|
+
value={provider.timeoutMs}
|
|
382
|
+
onChange={(event) => updateProvider(index, { timeoutMs: Number.parseInt(event.target.value, 10) || 5000 })}
|
|
383
|
+
placeholder={t('secretExecTimeoutMs')}
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
389
|
+
))}
|
|
390
|
+
|
|
391
|
+
<Button type="button" variant="outline" onClick={() => setProviders((prev) => [...prev, createProviderRow()])}>
|
|
392
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
393
|
+
{t('addSecretProvider')}
|
|
394
|
+
</Button>
|
|
395
|
+
</CardContent>
|
|
396
|
+
</Card>
|
|
397
|
+
|
|
398
|
+
<Card>
|
|
399
|
+
<CardHeader>
|
|
400
|
+
<CardTitle>{t('secretRefsTitle')}</CardTitle>
|
|
401
|
+
<CardDescription>{t('secretRefsDescription')}</CardDescription>
|
|
402
|
+
</CardHeader>
|
|
403
|
+
<CardContent className="space-y-3">
|
|
404
|
+
{refs.map((ref, index) => (
|
|
405
|
+
<div key={`ref-${index}`} className="rounded-xl border border-gray-200 p-3 space-y-3">
|
|
406
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
407
|
+
<Input
|
|
408
|
+
value={ref.path}
|
|
409
|
+
onChange={(event) => updateRef(index, { path: event.target.value })}
|
|
410
|
+
placeholder={t('secretConfigPath')}
|
|
411
|
+
/>
|
|
412
|
+
<Input
|
|
413
|
+
value={ref.id}
|
|
414
|
+
onChange={(event) => updateRef(index, { id: event.target.value })}
|
|
415
|
+
placeholder={t('secretId')}
|
|
416
|
+
/>
|
|
417
|
+
<Select value={ref.source} onValueChange={(value) => updateRef(index, { source: value as SecretSourceView })}>
|
|
418
|
+
<SelectTrigger>
|
|
419
|
+
<SelectValue />
|
|
420
|
+
</SelectTrigger>
|
|
421
|
+
<SelectContent>
|
|
422
|
+
{SOURCE_OPTIONS.map((source) => (
|
|
423
|
+
<SelectItem key={source} value={source}>
|
|
424
|
+
{source}
|
|
425
|
+
</SelectItem>
|
|
426
|
+
))}
|
|
427
|
+
</SelectContent>
|
|
428
|
+
</Select>
|
|
429
|
+
<div className="grid grid-cols-[1fr_auto] gap-2">
|
|
430
|
+
<Select
|
|
431
|
+
value={ref.provider || '__none__'}
|
|
432
|
+
onValueChange={(value) => updateRef(index, { provider: value === '__none__' ? '' : value })}
|
|
433
|
+
>
|
|
434
|
+
<SelectTrigger>
|
|
435
|
+
<SelectValue placeholder={t('secretProviderAlias')} />
|
|
436
|
+
</SelectTrigger>
|
|
437
|
+
<SelectContent>
|
|
438
|
+
<SelectItem value="__none__">{t('noneOption')}</SelectItem>
|
|
439
|
+
{providerAliases.map((alias) => (
|
|
440
|
+
<SelectItem key={alias} value={alias}>
|
|
441
|
+
{alias}
|
|
442
|
+
</SelectItem>
|
|
443
|
+
))}
|
|
444
|
+
</SelectContent>
|
|
445
|
+
</Select>
|
|
446
|
+
<Button type="button" variant="outline" onClick={() => setRefs((prev) => prev.filter((_, i) => i !== index))}>
|
|
447
|
+
<Trash2 className="h-4 w-4" />
|
|
448
|
+
</Button>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
))}
|
|
453
|
+
|
|
454
|
+
<Button type="button" variant="outline" onClick={() => setRefs((prev) => [...prev, createRefRow()])}>
|
|
455
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
456
|
+
{t('addSecretRef')}
|
|
457
|
+
</Button>
|
|
458
|
+
</CardContent>
|
|
459
|
+
</Card>
|
|
460
|
+
|
|
461
|
+
<div className="flex justify-end">
|
|
462
|
+
<Button type="button" onClick={handleSave} disabled={updateSecrets.isPending}>
|
|
463
|
+
<Save className="h-4 w-4 mr-2" />
|
|
464
|
+
{updateSecrets.isPending ? t('saving') : t('save')}
|
|
465
|
+
</Button>
|
|
466
|
+
</div>
|
|
467
|
+
</PageLayout>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn } from '@/lib/utils';
|
|
2
2
|
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
|
|
3
3
|
import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
|
|
4
|
-
import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette } from 'lucide-react';
|
|
4
|
+
import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound } from 'lucide-react';
|
|
5
5
|
import { NavLink } from 'react-router-dom';
|
|
6
6
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
7
7
|
import { useI18n } from '@/components/providers/I18nProvider';
|
|
@@ -66,6 +66,11 @@ export function Sidebar() {
|
|
|
66
66
|
label: t('cron'),
|
|
67
67
|
icon: AlarmClock,
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
target: '/secrets',
|
|
71
|
+
label: t('secrets'),
|
|
72
|
+
icon: KeyRound,
|
|
73
|
+
},
|
|
69
74
|
{
|
|
70
75
|
target: '/marketplace/plugins',
|
|
71
76
|
label: t('marketplaceFilterPlugins'),
|
package/src/hooks/useConfig.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
updateProvider,
|
|
8
8
|
updateChannel,
|
|
9
9
|
updateRuntime,
|
|
10
|
+
updateSecrets,
|
|
10
11
|
executeConfigAction,
|
|
11
12
|
fetchSessions,
|
|
12
13
|
fetchSessionHistory,
|
|
@@ -109,6 +110,22 @@ export function useUpdateRuntime() {
|
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
export function useUpdateSecrets() {
|
|
114
|
+
const queryClient = useQueryClient();
|
|
115
|
+
|
|
116
|
+
return useMutation({
|
|
117
|
+
mutationFn: ({ data }: { data: unknown }) =>
|
|
118
|
+
updateSecrets(data as Parameters<typeof updateSecrets>[0]),
|
|
119
|
+
onSuccess: () => {
|
|
120
|
+
queryClient.invalidateQueries({ queryKey: ['config'] });
|
|
121
|
+
toast.success(t('configSavedApplied'));
|
|
122
|
+
},
|
|
123
|
+
onError: (error: Error) => {
|
|
124
|
+
toast.error(t('configSaveFailed') + ': ' + error.message);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
112
129
|
export function useExecuteConfigAction() {
|
|
113
130
|
return useMutation({
|
|
114
131
|
mutationFn: ({ actionId, data }: { actionId: string; data: unknown }) =>
|
package/src/lib/chat-message.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type ToolCard = {
|
|
|
8
8
|
detail?: string;
|
|
9
9
|
text?: string;
|
|
10
10
|
callId?: string;
|
|
11
|
+
hasResult?: boolean;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export type GroupedChatMessage = {
|
|
@@ -158,11 +159,15 @@ export function extractToolCards(message: SessionMessageView): ToolCard[] {
|
|
|
158
159
|
const fn = isRecord(call.function) ? call.function : null;
|
|
159
160
|
const name = toToolName(fn?.name ?? call.name);
|
|
160
161
|
const args = fn?.arguments ?? call.arguments;
|
|
162
|
+
const resultText = typeof call.result_text === 'string' ? call.result_text.trim() : '';
|
|
163
|
+
const hasResult = call.has_result === true || typeof call.result_text === 'string';
|
|
161
164
|
cards.push({
|
|
162
165
|
kind: 'call',
|
|
163
166
|
name,
|
|
164
167
|
detail: summarizeToolArgs(args),
|
|
165
|
-
callId: typeof call.id === 'string' ? call.id : undefined
|
|
168
|
+
callId: typeof call.id === 'string' ? call.id : undefined,
|
|
169
|
+
text: resultText,
|
|
170
|
+
hasResult
|
|
166
171
|
});
|
|
167
172
|
}
|
|
168
173
|
|
|
@@ -173,13 +178,86 @@ export function extractToolCards(message: SessionMessageView): ToolCard[] {
|
|
|
173
178
|
kind: 'result',
|
|
174
179
|
name: toToolName(message.name ?? cards[0]?.name),
|
|
175
180
|
text,
|
|
176
|
-
callId: typeof message.tool_call_id === 'string' ? message.tool_call_id : undefined
|
|
181
|
+
callId: typeof message.tool_call_id === 'string' ? message.tool_call_id : undefined,
|
|
182
|
+
hasResult: true
|
|
177
183
|
});
|
|
178
184
|
}
|
|
179
185
|
|
|
180
186
|
return cards;
|
|
181
187
|
}
|
|
182
188
|
|
|
189
|
+
type ToolResultBucket = {
|
|
190
|
+
name?: string;
|
|
191
|
+
texts: string[];
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
function cloneMessageForMerge(message: SessionMessageView): SessionMessageView {
|
|
195
|
+
return {
|
|
196
|
+
...message,
|
|
197
|
+
tool_calls: Array.isArray(message.tool_calls)
|
|
198
|
+
? message.tool_calls.map((call) => (isRecord(call) ? { ...call } : call))
|
|
199
|
+
: message.tool_calls
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function combineToolCallAndResults(messages: SessionMessageView[]): SessionMessageView[] {
|
|
204
|
+
const cloned = messages.map(cloneMessageForMerge);
|
|
205
|
+
const resultByCallId = new Map<string, ToolResultBucket>();
|
|
206
|
+
|
|
207
|
+
for (const message of cloned) {
|
|
208
|
+
if (normalizeChatRole(message) !== 'tool') {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (typeof message.tool_call_id !== 'string' || !message.tool_call_id.trim()) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const callId = message.tool_call_id.trim();
|
|
216
|
+
const text = extractMessageText(message.content).trim();
|
|
217
|
+
const existing = resultByCallId.get(callId) ?? { texts: [] };
|
|
218
|
+
if (typeof message.name === 'string' && message.name.trim()) {
|
|
219
|
+
existing.name = message.name.trim();
|
|
220
|
+
}
|
|
221
|
+
existing.texts.push(text);
|
|
222
|
+
resultByCallId.set(callId, existing);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const consumedCallIds = new Set<string>();
|
|
226
|
+
|
|
227
|
+
for (const message of cloned) {
|
|
228
|
+
if (normalizeChatRole(message) !== 'assistant' || !Array.isArray(message.tool_calls)) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
message.tool_calls = message.tool_calls.map((call) => {
|
|
233
|
+
if (!isRecord(call) || typeof call.id !== 'string') {
|
|
234
|
+
return call;
|
|
235
|
+
}
|
|
236
|
+
const result = resultByCallId.get(call.id);
|
|
237
|
+
if (!result) {
|
|
238
|
+
return call;
|
|
239
|
+
}
|
|
240
|
+
consumedCallIds.add(call.id);
|
|
241
|
+
return {
|
|
242
|
+
...call,
|
|
243
|
+
result_text: result.texts.filter(Boolean).join('\n\n'),
|
|
244
|
+
has_result: true,
|
|
245
|
+
result_name: result.name
|
|
246
|
+
};
|
|
247
|
+
}) as Array<Record<string, unknown>>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return cloned.filter((message) => {
|
|
251
|
+
if (normalizeChatRole(message) !== 'tool') {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
if (typeof message.tool_call_id !== 'string' || !message.tool_call_id.trim()) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
return !consumedCallIds.has(message.tool_call_id.trim());
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
183
261
|
export function groupChatMessages(messages: SessionMessageView[]): GroupedChatMessage[] {
|
|
184
262
|
const groups: GroupedChatMessage[] = [];
|
|
185
263
|
let lastTs = 0;
|