@nextclaw/ui 0.5.20 → 0.5.22
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-dmUfRDVE.js +1 -0
- package/dist/assets/ChatPage-CmRkhiCX.js +32 -0
- package/dist/assets/CronConfig-B15BA--M.js +1 -0
- package/dist/assets/{DocBrowser-C35MebbI.js → DocBrowser-CBwMA2wK.js} +1 -1
- package/dist/assets/{MarketplacePage-HjEQ8sFt.js → MarketplacePage-BZRz4tCA.js} +1 -1
- package/dist/assets/{ModelConfig-BpBoi1sz.js → ModelConfig-CByjZNvf.js} +1 -1
- package/dist/assets/ProvidersList-BbsQEnoZ.js +1 -0
- package/dist/assets/RuntimeConfig-CrQsFzP7.js +1 -0
- package/dist/assets/SecretsConfig-DPZqWmry.js +3 -0
- package/dist/assets/SessionsConfig-Bk7RCsWw.js +2 -0
- package/dist/assets/{action-link-CSScZ_id.js → action-link-CKSHFT5k.js} +1 -1
- package/dist/assets/{card-Cj58-DCd.js → card-6hv7Kf0F.js} +1 -1
- package/dist/assets/chat-message-B7oqvJ2d.js +3 -0
- package/dist/assets/{dialog-Ce8jNftN.js → dialog-Bmy_bApp.js} +2 -2
- package/dist/assets/index-BsDasSXm.css +1 -0
- package/dist/assets/index-y6creQ7S.js +2 -0
- package/dist/assets/{label-CQdP2NhF.js → label-Btp6gGxV.js} +1 -1
- package/dist/assets/{page-layout-Byyxptub.js → page-layout-DWVKbt_g.js} +1 -1
- package/dist/assets/{switch-ChJzdp0x.js → switch-AeXayTRS.js} +1 -1
- package/dist/assets/{tabs-custom-DWlAbbCy.js → tabs-custom-DEZwNpXo.js} +1 -1
- package/dist/assets/useConfig-BiQH98MD.js +1 -0
- package/dist/assets/{useConfirmDialog-B7iWHb5k.js → useConfirmDialog-BY0hni-H.js} +1 -1
- package/dist/assets/{vendor-Dz2q6Qmc.js → vendor-H2M3a_4Z.js} +87 -62
- package/dist/index.html +3 -3
- package/package.json +4 -1
- package/src/App.tsx +2 -0
- package/src/api/config.ts +16 -0
- package/src/api/types.ts +55 -1
- package/src/components/chat/ChatPage.tsx +4 -39
- package/src/components/chat/ChatThread.tsx +210 -0
- package/src/components/config/ChannelForm.tsx +9 -0
- package/src/components/config/SecretsConfig.tsx +469 -0
- package/src/components/config/SessionsConfig.tsx +3 -1
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/hooks/useConfig.ts +17 -0
- package/src/index.css +69 -0
- package/src/lib/chat-message.ts +215 -0
- package/src/lib/i18n.ts +52 -0
- package/dist/assets/ChannelsList-DqgRRdUH.js +0 -1
- package/dist/assets/ChatPage-BQyomkth.js +0 -1
- package/dist/assets/CronConfig-Bmg449JI.js +0 -1
- package/dist/assets/ProvidersList-0tYTV40v.js +0 -1
- package/dist/assets/RuntimeConfig-B_WI-DHf.js +0 -1
- package/dist/assets/SessionsConfig-BEt-f6WS.js +0 -2
- package/dist/assets/index-CPFSdkyQ.css +0 -1
- package/dist/assets/index-C_z1Na9N.js +0 -2
- package/dist/assets/useConfig-8lC_4LwH.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
|
+
}
|
|
@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input';
|
|
|
7
7
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
8
8
|
import { cn } from '@/lib/utils';
|
|
9
9
|
import { formatDateShort, formatDateTime, t } from '@/lib/i18n';
|
|
10
|
+
import { extractMessageText } from '@/lib/chat-message';
|
|
10
11
|
import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
11
12
|
import { RefreshCw, Search, Clock, Inbox, Hash, Bot, User, MessageCircle, Settings as SettingsIcon } from 'lucide-react';
|
|
12
13
|
|
|
@@ -86,6 +87,7 @@ function SessionListItem({ session, channel, isSelected, onSelect }: SessionList
|
|
|
86
87
|
|
|
87
88
|
function SessionMessageBubble({ message }: { message: SessionMessageView }) {
|
|
88
89
|
const isUser = message.role.toLowerCase() === 'user';
|
|
90
|
+
const content = extractMessageText(message.content).trim();
|
|
89
91
|
|
|
90
92
|
return (
|
|
91
93
|
<div className={cn("flex w-full mb-6", isUser ? "justify-end" : "justify-start")}>
|
|
@@ -108,7 +110,7 @@ function SessionMessageBubble({ message }: { message: SessionMessageView }) {
|
|
|
108
110
|
</span>
|
|
109
111
|
</div>
|
|
110
112
|
<div className="whitespace-pre-wrap break-words leading-relaxed text-[15px]">
|
|
111
|
-
{
|
|
113
|
+
{content || '-'}
|
|
112
114
|
</div>
|
|
113
115
|
</div>
|
|
114
116
|
</div>
|
|
@@ -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/index.css
CHANGED
|
@@ -182,3 +182,72 @@
|
|
|
182
182
|
.animate-pulse-soft {
|
|
183
183
|
animation: pulse-soft 3s ease-in-out infinite;
|
|
184
184
|
}
|
|
185
|
+
|
|
186
|
+
.chat-markdown {
|
|
187
|
+
font-size: 0.9rem;
|
|
188
|
+
line-height: 1.6;
|
|
189
|
+
word-break: break-word;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.chat-markdown > * + * {
|
|
193
|
+
margin-top: 0.5rem;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.chat-markdown h1,
|
|
197
|
+
.chat-markdown h2,
|
|
198
|
+
.chat-markdown h3 {
|
|
199
|
+
font-size: 1rem;
|
|
200
|
+
font-weight: 700;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.chat-markdown ul,
|
|
204
|
+
.chat-markdown ol {
|
|
205
|
+
padding-left: 1.1rem;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.chat-markdown code {
|
|
209
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
210
|
+
font-size: 0.8rem;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.chat-markdown :not(pre) > code {
|
|
214
|
+
padding: 0.1rem 0.3rem;
|
|
215
|
+
border-radius: 0.35rem;
|
|
216
|
+
background: rgba(148, 163, 184, 0.18);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.chat-markdown pre {
|
|
220
|
+
overflow-x: auto;
|
|
221
|
+
border-radius: 0.65rem;
|
|
222
|
+
padding: 0.65rem 0.75rem;
|
|
223
|
+
background: rgba(15, 23, 42, 0.9);
|
|
224
|
+
color: #e2e8f0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.chat-markdown table {
|
|
228
|
+
width: 100%;
|
|
229
|
+
border-collapse: collapse;
|
|
230
|
+
font-size: 0.8rem;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.chat-markdown th,
|
|
234
|
+
.chat-markdown td {
|
|
235
|
+
border: 1px solid rgba(148, 163, 184, 0.3);
|
|
236
|
+
padding: 0.35rem 0.45rem;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.chat-markdown blockquote {
|
|
240
|
+
border-left: 3px solid rgba(148, 163, 184, 0.55);
|
|
241
|
+
padding-left: 0.65rem;
|
|
242
|
+
color: rgba(30, 41, 59, 0.85);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.chat-markdown-user a {
|
|
246
|
+
color: #dbeafe;
|
|
247
|
+
text-decoration: underline;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.chat-markdown-assistant a {
|
|
251
|
+
color: hsl(var(--primary));
|
|
252
|
+
text-decoration: underline;
|
|
253
|
+
}
|