@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/assets/ChannelsList-dmUfRDVE.js +1 -0
  3. package/dist/assets/ChatPage-CmRkhiCX.js +32 -0
  4. package/dist/assets/CronConfig-B15BA--M.js +1 -0
  5. package/dist/assets/{DocBrowser-C35MebbI.js → DocBrowser-CBwMA2wK.js} +1 -1
  6. package/dist/assets/{MarketplacePage-HjEQ8sFt.js → MarketplacePage-BZRz4tCA.js} +1 -1
  7. package/dist/assets/{ModelConfig-BpBoi1sz.js → ModelConfig-CByjZNvf.js} +1 -1
  8. package/dist/assets/ProvidersList-BbsQEnoZ.js +1 -0
  9. package/dist/assets/RuntimeConfig-CrQsFzP7.js +1 -0
  10. package/dist/assets/SecretsConfig-DPZqWmry.js +3 -0
  11. package/dist/assets/SessionsConfig-Bk7RCsWw.js +2 -0
  12. package/dist/assets/{action-link-CSScZ_id.js → action-link-CKSHFT5k.js} +1 -1
  13. package/dist/assets/{card-Cj58-DCd.js → card-6hv7Kf0F.js} +1 -1
  14. package/dist/assets/chat-message-B7oqvJ2d.js +3 -0
  15. package/dist/assets/{dialog-Ce8jNftN.js → dialog-Bmy_bApp.js} +2 -2
  16. package/dist/assets/index-BsDasSXm.css +1 -0
  17. package/dist/assets/index-y6creQ7S.js +2 -0
  18. package/dist/assets/{label-CQdP2NhF.js → label-Btp6gGxV.js} +1 -1
  19. package/dist/assets/{page-layout-Byyxptub.js → page-layout-DWVKbt_g.js} +1 -1
  20. package/dist/assets/{switch-ChJzdp0x.js → switch-AeXayTRS.js} +1 -1
  21. package/dist/assets/{tabs-custom-DWlAbbCy.js → tabs-custom-DEZwNpXo.js} +1 -1
  22. package/dist/assets/useConfig-BiQH98MD.js +1 -0
  23. package/dist/assets/{useConfirmDialog-B7iWHb5k.js → useConfirmDialog-BY0hni-H.js} +1 -1
  24. package/dist/assets/{vendor-Dz2q6Qmc.js → vendor-H2M3a_4Z.js} +87 -62
  25. package/dist/index.html +3 -3
  26. package/package.json +4 -1
  27. package/src/App.tsx +2 -0
  28. package/src/api/config.ts +16 -0
  29. package/src/api/types.ts +55 -1
  30. package/src/components/chat/ChatPage.tsx +4 -39
  31. package/src/components/chat/ChatThread.tsx +210 -0
  32. package/src/components/config/ChannelForm.tsx +9 -0
  33. package/src/components/config/SecretsConfig.tsx +469 -0
  34. package/src/components/config/SessionsConfig.tsx +3 -1
  35. package/src/components/layout/Sidebar.tsx +6 -1
  36. package/src/hooks/useConfig.ts +17 -0
  37. package/src/index.css +69 -0
  38. package/src/lib/chat-message.ts +215 -0
  39. package/src/lib/i18n.ts +52 -0
  40. package/dist/assets/ChannelsList-DqgRRdUH.js +0 -1
  41. package/dist/assets/ChatPage-BQyomkth.js +0 -1
  42. package/dist/assets/CronConfig-Bmg449JI.js +0 -1
  43. package/dist/assets/ProvidersList-0tYTV40v.js +0 -1
  44. package/dist/assets/RuntimeConfig-B_WI-DHf.js +0 -1
  45. package/dist/assets/SessionsConfig-BEt-f6WS.js +0 -2
  46. package/dist/assets/index-CPFSdkyQ.css +0 -1
  47. package/dist/assets/index-C_z1Na9N.js +0 -2
  48. 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
- {message.content}
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'),
@@ -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
+ }