@nextclaw/ui 0.5.9 → 0.5.10
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 +6 -0
- package/dist/assets/index-BYV0BsFw.js +342 -0
- package/dist/assets/index-DrC_vHy_.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/components/common/MaskedInput.tsx +1 -1
- package/src/components/common/StatusBadge.tsx +3 -5
- package/src/components/common/TagInput.tsx +3 -2
- package/src/components/config/ChannelForm.tsx +106 -104
- package/src/components/config/ChannelsList.tsx +19 -19
- package/src/components/config/CronConfig.tsx +3 -14
- package/src/components/config/ModelConfig.tsx +9 -8
- package/src/components/config/ProviderForm.tsx +3 -3
- package/src/components/config/ProvidersList.tsx +10 -9
- package/src/components/config/RuntimeConfig.tsx +38 -37
- package/src/components/config/SessionsConfig.tsx +8 -15
- package/src/components/layout/Sidebar.tsx +70 -40
- package/src/components/marketplace/MarketplacePage.tsx +32 -31
- package/src/components/providers/I18nProvider.tsx +64 -0
- package/src/components/ui/confirm-dialog.tsx +3 -2
- package/src/components/ui/tabs-custom.tsx +2 -1
- package/src/hooks/useConfirmDialog.tsx +5 -4
- package/src/hooks/useMarketplace.ts +4 -3
- package/src/lib/i18n.ts +267 -5
- package/src/main.tsx +6 -3
- package/dist/assets/index-BtwwwWcv.css +0 -1
- package/dist/assets/index-STUSj6p9.js +0 -337
|
@@ -5,6 +5,7 @@ import { Label } from '@/components/ui/label';
|
|
|
5
5
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
6
6
|
import { useConfig, useConfigSchema, useUpdateModel } from '@/hooks/useConfig';
|
|
7
7
|
import { hintForPath } from '@/lib/config-hints';
|
|
8
|
+
import { formatNumber, t } from '@/lib/i18n';
|
|
8
9
|
import { Folder, Loader2, Sliders, Sparkles } from 'lucide-react';
|
|
9
10
|
import { useEffect, useState } from 'react';
|
|
10
11
|
|
|
@@ -69,8 +70,8 @@ export function ModelConfig() {
|
|
|
69
70
|
return (
|
|
70
71
|
<div className="max-w-4xl animate-fade-in pb-20">
|
|
71
72
|
<div className="mb-10">
|
|
72
|
-
<h2 className="text-xl font-semibold text-gray-900">
|
|
73
|
-
<p className="text-sm text-gray-500 mt-1">
|
|
73
|
+
<h2 className="text-xl font-semibold text-gray-900">{t('modelPageTitle')}</h2>
|
|
74
|
+
<p className="text-sm text-gray-500 mt-1">{t('modelPageDescription')}</p>
|
|
74
75
|
</div>
|
|
75
76
|
|
|
76
77
|
<form onSubmit={handleSubmit} className="space-y-8">
|
|
@@ -81,7 +82,7 @@ export function ModelConfig() {
|
|
|
81
82
|
<div className="h-10 w-10 rounded-xl bg-primary flex items-center justify-center text-white">
|
|
82
83
|
<Sparkles className="h-5 w-5" />
|
|
83
84
|
</div>
|
|
84
|
-
<h3 className="text-lg font-bold text-gray-900">
|
|
85
|
+
<h3 className="text-lg font-bold text-gray-900">{t('defaultModel')}</h3>
|
|
85
86
|
</div>
|
|
86
87
|
|
|
87
88
|
<div className="space-y-2">
|
|
@@ -108,7 +109,7 @@ export function ModelConfig() {
|
|
|
108
109
|
<div className="h-10 w-10 rounded-xl bg-primary flex items-center justify-center text-white">
|
|
109
110
|
<Folder className="h-5 w-5" />
|
|
110
111
|
</div>
|
|
111
|
-
<h3 className="text-lg font-bold text-gray-900">
|
|
112
|
+
<h3 className="text-lg font-bold text-gray-900">{t('workspace')}</h3>
|
|
112
113
|
</div>
|
|
113
114
|
|
|
114
115
|
<div className="space-y-2">
|
|
@@ -132,16 +133,16 @@ export function ModelConfig() {
|
|
|
132
133
|
<div className="h-10 w-10 rounded-xl bg-primary flex items-center justify-center text-white">
|
|
133
134
|
<Sliders className="h-5 w-5" />
|
|
134
135
|
</div>
|
|
135
|
-
<h3 className="text-lg font-bold text-gray-900">
|
|
136
|
+
<h3 className="text-lg font-bold text-gray-900">{t('generationParameters')}</h3>
|
|
136
137
|
</div>
|
|
137
138
|
|
|
138
139
|
<div className="grid grid-cols-1 gap-12">
|
|
139
140
|
<div className="space-y-4">
|
|
140
141
|
<div className="flex justify-between items-center mb-2">
|
|
141
142
|
<Label className="text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
|
142
|
-
{maxTokensHint?.label ?? '
|
|
143
|
+
{maxTokensHint?.label ?? t('maxTokens')}
|
|
143
144
|
</Label>
|
|
144
|
-
<span className="text-sm font-semibold text-gray-900">{maxTokens
|
|
145
|
+
<span className="text-sm font-semibold text-gray-900">{formatNumber(maxTokens)}</span>
|
|
145
146
|
</div>
|
|
146
147
|
<input
|
|
147
148
|
type="range"
|
|
@@ -165,7 +166,7 @@ export function ModelConfig() {
|
|
|
165
166
|
{updateModel.isPending ? (
|
|
166
167
|
<Loader2 className="h-5 w-5 animate-spin" />
|
|
167
168
|
) : (
|
|
168
|
-
'
|
|
169
|
+
t('saveChanges')
|
|
169
170
|
)}
|
|
170
171
|
</Button>
|
|
171
172
|
</div>
|
|
@@ -96,7 +96,7 @@ export function ProviderForm() {
|
|
|
96
96
|
</div>
|
|
97
97
|
<div>
|
|
98
98
|
<DialogTitle>{providerSpec?.displayName || providerName}</DialogTitle>
|
|
99
|
-
<DialogDescription>
|
|
99
|
+
<DialogDescription>{t('providerFormDescription')}</DialogDescription>
|
|
100
100
|
</div>
|
|
101
101
|
</div>
|
|
102
102
|
</DialogHeader>
|
|
@@ -116,7 +116,7 @@ export function ProviderForm() {
|
|
|
116
116
|
placeholder={
|
|
117
117
|
providerConfig?.apiKeySet
|
|
118
118
|
? t('apiKeySet')
|
|
119
|
-
: apiKeyHint?.placeholder ?? '
|
|
119
|
+
: apiKeyHint?.placeholder ?? t('enterApiKey')
|
|
120
120
|
}
|
|
121
121
|
className="rounded-xl"
|
|
122
122
|
/>
|
|
@@ -193,7 +193,7 @@ export function ProviderForm() {
|
|
|
193
193
|
type="submit"
|
|
194
194
|
disabled={updateProvider.isPending}
|
|
195
195
|
>
|
|
196
|
-
{updateProvider.isPending ? '
|
|
196
|
+
{updateProvider.isPending ? t('saving') : t('save')}
|
|
197
197
|
</Button>
|
|
198
198
|
</DialogFooter>
|
|
199
199
|
</form>
|
|
@@ -11,6 +11,7 @@ import { hintForPath } from '@/lib/config-hints';
|
|
|
11
11
|
import { ConfigCard, ConfigCardHeader, ConfigCardBody, ConfigCardFooter } from '@/components/ui/config-card';
|
|
12
12
|
import { StatusDot } from '@/components/ui/status-dot';
|
|
13
13
|
import { ActionLink } from '@/components/ui/action-link';
|
|
14
|
+
import { t } from '@/lib/i18n';
|
|
14
15
|
|
|
15
16
|
export function ProvidersList() {
|
|
16
17
|
const { data: config } = useConfig();
|
|
@@ -21,12 +22,12 @@ export function ProvidersList() {
|
|
|
21
22
|
const uiHints = schema?.uiHints;
|
|
22
23
|
|
|
23
24
|
if (!config || !meta) {
|
|
24
|
-
return <div className="p-8">
|
|
25
|
+
return <div className="p-8">{t('providersLoading')}</div>;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const tabs = [
|
|
28
|
-
{ id: 'installed', label: '
|
|
29
|
-
{ id: 'all', label: '
|
|
29
|
+
{ id: 'installed', label: t('providersTabConfigured'), count: config.providers ? Object.keys(config.providers).filter(k => config.providers[k].apiKeySet).length : 0 },
|
|
30
|
+
{ id: 'all', label: t('providersTabAll'), count: meta.providers.length }
|
|
30
31
|
];
|
|
31
32
|
|
|
32
33
|
const filteredProviders = activeTab === 'installed'
|
|
@@ -36,7 +37,7 @@ export function ProvidersList() {
|
|
|
36
37
|
return (
|
|
37
38
|
<div className="animate-fade-in pb-20">
|
|
38
39
|
<div className="flex items-center justify-between mb-6">
|
|
39
|
-
<h2 className="text-xl font-semibold text-gray-900">
|
|
40
|
+
<h2 className="text-xl font-semibold text-gray-900">{t('providersPageTitle')}</h2>
|
|
40
41
|
</div>
|
|
41
42
|
|
|
42
43
|
<Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} />
|
|
@@ -47,7 +48,7 @@ export function ProvidersList() {
|
|
|
47
48
|
const providerConfig = config.providers[provider.name];
|
|
48
49
|
const hasConfig = providerConfig?.apiKeySet;
|
|
49
50
|
const providerHint = hintForPath(`providers.${provider.name}`, uiHints);
|
|
50
|
-
const description = providerHint?.help || '
|
|
51
|
+
const description = providerHint?.help || t('providersDefaultDescription');
|
|
51
52
|
|
|
52
53
|
return (
|
|
53
54
|
<ConfigCard key={provider.name} onClick={() => openProviderModal(provider.name)}>
|
|
@@ -73,7 +74,7 @@ export function ProvidersList() {
|
|
|
73
74
|
/>
|
|
74
75
|
<StatusDot
|
|
75
76
|
status={hasConfig ? 'ready' : 'setup'}
|
|
76
|
-
label={hasConfig ? '
|
|
77
|
+
label={hasConfig ? t('statusReady') : t('statusSetup')}
|
|
77
78
|
/>
|
|
78
79
|
</ConfigCardHeader>
|
|
79
80
|
|
|
@@ -83,7 +84,7 @@ export function ProvidersList() {
|
|
|
83
84
|
/>
|
|
84
85
|
|
|
85
86
|
<ConfigCardFooter>
|
|
86
|
-
<ActionLink label={hasConfig ? '
|
|
87
|
+
<ActionLink label={hasConfig ? t('actionConfigure') : t('actionAddProvider')} />
|
|
87
88
|
</ConfigCardFooter>
|
|
88
89
|
</ConfigCard>
|
|
89
90
|
);
|
|
@@ -97,10 +98,10 @@ export function ProvidersList() {
|
|
|
97
98
|
<KeyRound className="h-6 w-6 text-gray-300" />
|
|
98
99
|
</div>
|
|
99
100
|
<h3 className="text-[14px] font-semibold text-gray-900 mb-1.5">
|
|
100
|
-
|
|
101
|
+
{t('providersEmptyTitle')}
|
|
101
102
|
</h3>
|
|
102
103
|
<p className="text-[13px] text-gray-400 max-w-sm">
|
|
103
|
-
|
|
104
|
+
{t('providersEmptyDescription')}
|
|
104
105
|
</p>
|
|
105
106
|
</div>
|
|
106
107
|
)}
|
|
@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input';
|
|
|
7
7
|
import { Switch } from '@/components/ui/switch';
|
|
8
8
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
9
9
|
import { hintForPath } from '@/lib/config-hints';
|
|
10
|
+
import { t } from '@/lib/i18n';
|
|
10
11
|
import { Plus, Save, Trash2 } from 'lucide-react';
|
|
11
12
|
import { toast } from 'sonner';
|
|
12
13
|
|
|
@@ -129,7 +130,7 @@ export function RuntimeConfig() {
|
|
|
129
130
|
const normalizedAgents = agents.map((agent, index) => {
|
|
130
131
|
const id = agent.id.trim();
|
|
131
132
|
if (!id) {
|
|
132
|
-
throw new Error(
|
|
133
|
+
throw new Error(t('agentIdRequiredError').replace('{index}', String(index)));
|
|
133
134
|
}
|
|
134
135
|
const normalized: AgentProfileView = { id };
|
|
135
136
|
if (agent.default) {
|
|
@@ -157,7 +158,7 @@ export function RuntimeConfig() {
|
|
|
157
158
|
.map((agent) => agent.id)
|
|
158
159
|
.filter((id, index, all) => all.indexOf(id) !== index);
|
|
159
160
|
if (duplicates.length > 0) {
|
|
160
|
-
toast.error(
|
|
161
|
+
toast.error(`${t('duplicateAgentId')}: ${duplicates[0]}`);
|
|
161
162
|
return;
|
|
162
163
|
}
|
|
163
164
|
|
|
@@ -169,13 +170,13 @@ export function RuntimeConfig() {
|
|
|
169
170
|
const peerId = binding.match.peer?.id?.trim() ?? '';
|
|
170
171
|
|
|
171
172
|
if (!agentId) {
|
|
172
|
-
throw new Error(
|
|
173
|
+
throw new Error(t('bindingAgentIdRequired').replace('{index}', String(index)));
|
|
173
174
|
}
|
|
174
175
|
if (!knownAgentIds.has(agentId)) {
|
|
175
|
-
throw new Error(
|
|
176
|
+
throw new Error(`${t('bindingAgentIdNotFound').replace('{index}', String(index))}: ${agentId}`);
|
|
176
177
|
}
|
|
177
178
|
if (!channel) {
|
|
178
|
-
throw new Error(
|
|
179
|
+
throw new Error(t('bindingChannelRequired').replace('{index}', String(index)));
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
const normalized: AgentBindingView = {
|
|
@@ -191,7 +192,7 @@ export function RuntimeConfig() {
|
|
|
191
192
|
|
|
192
193
|
if (peerKind) {
|
|
193
194
|
if (!peerId) {
|
|
194
|
-
throw new Error(
|
|
195
|
+
throw new Error(t('bindingPeerIdRequired').replace('{index}', String(index)));
|
|
195
196
|
}
|
|
196
197
|
normalized.match.peer = {
|
|
197
198
|
kind: peerKind,
|
|
@@ -226,27 +227,27 @@ export function RuntimeConfig() {
|
|
|
226
227
|
};
|
|
227
228
|
|
|
228
229
|
if (isLoading || !config) {
|
|
229
|
-
return <div className="p-8 text-gray-400">
|
|
230
|
+
return <div className="p-8 text-gray-400">{t('runtimeLoading')}</div>;
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
return (
|
|
233
234
|
<div className="space-y-6 pb-20 animate-fade-in">
|
|
234
235
|
<div>
|
|
235
|
-
<h2 className="text-xl font-semibold text-gray-900">
|
|
236
|
+
<h2 className="text-xl font-semibold text-gray-900">{t('runtimePageTitle')}</h2>
|
|
236
237
|
<p className="text-sm text-gray-500 mt-1">
|
|
237
|
-
|
|
238
|
+
{t('runtimePageDescription')}
|
|
238
239
|
</p>
|
|
239
240
|
</div>
|
|
240
241
|
|
|
241
242
|
<Card>
|
|
242
243
|
<CardHeader>
|
|
243
|
-
<CardTitle>{dmScopeHint?.label ?? '
|
|
244
|
-
<CardDescription>{dmScopeHint?.help ?? '
|
|
244
|
+
<CardTitle>{dmScopeHint?.label ?? t('dmScope')}</CardTitle>
|
|
245
|
+
<CardDescription>{dmScopeHint?.help ?? t('dmScopeHelp')}</CardDescription>
|
|
245
246
|
</CardHeader>
|
|
246
247
|
<CardContent className="space-y-4">
|
|
247
248
|
<div className="space-y-2">
|
|
248
249
|
<label className="text-sm font-medium text-gray-800">
|
|
249
|
-
{defaultContextTokensHint?.label ?? '
|
|
250
|
+
{defaultContextTokensHint?.label ?? t('defaultContextTokens')}
|
|
250
251
|
</label>
|
|
251
252
|
<Input
|
|
252
253
|
type="number"
|
|
@@ -256,11 +257,11 @@ export function RuntimeConfig() {
|
|
|
256
257
|
onChange={(event) => setDefaultContextTokens(Math.max(1000, Number.parseInt(event.target.value, 10) || 1000))}
|
|
257
258
|
/>
|
|
258
259
|
<p className="text-xs text-gray-500">
|
|
259
|
-
{defaultContextTokensHint?.help ?? '
|
|
260
|
+
{defaultContextTokensHint?.help ?? t('defaultContextTokensHelp')}
|
|
260
261
|
</p>
|
|
261
262
|
</div>
|
|
262
263
|
<div className="space-y-2">
|
|
263
|
-
<label className="text-sm font-medium text-gray-800">{dmScopeHint?.label ?? '
|
|
264
|
+
<label className="text-sm font-medium text-gray-800">{dmScopeHint?.label ?? t('dmScope')}</label>
|
|
264
265
|
<Select value={dmScope} onValueChange={(v) => setDmScope(v as DmScope)}>
|
|
265
266
|
<SelectTrigger>
|
|
266
267
|
<SelectValue />
|
|
@@ -276,7 +277,7 @@ export function RuntimeConfig() {
|
|
|
276
277
|
</div>
|
|
277
278
|
<div className="space-y-2">
|
|
278
279
|
<label className="text-sm font-medium text-gray-800">
|
|
279
|
-
{maxPingHint?.label ?? '
|
|
280
|
+
{maxPingHint?.label ?? t('maxPingPongTurns')}
|
|
280
281
|
</label>
|
|
281
282
|
<Input
|
|
282
283
|
type="number"
|
|
@@ -286,7 +287,7 @@ export function RuntimeConfig() {
|
|
|
286
287
|
onChange={(event) => setMaxPingPongTurns(Math.max(0, Number.parseInt(event.target.value, 10) || 0))}
|
|
287
288
|
/>
|
|
288
289
|
<p className="text-xs text-gray-500">
|
|
289
|
-
{maxPingHint?.help ?? '
|
|
290
|
+
{maxPingHint?.help ?? t('maxPingPongTurnsHelp')}
|
|
290
291
|
</p>
|
|
291
292
|
</div>
|
|
292
293
|
</CardContent>
|
|
@@ -294,8 +295,8 @@ export function RuntimeConfig() {
|
|
|
294
295
|
|
|
295
296
|
<Card>
|
|
296
297
|
<CardHeader>
|
|
297
|
-
<CardTitle>{agentsHint?.label ?? '
|
|
298
|
-
<CardDescription>{agentsHint?.help ?? '
|
|
298
|
+
<CardTitle>{agentsHint?.label ?? t('agentList')}</CardTitle>
|
|
299
|
+
<CardDescription>{agentsHint?.help ?? t('agentListHelp')}</CardDescription>
|
|
299
300
|
</CardHeader>
|
|
300
301
|
<CardContent className="space-y-3">
|
|
301
302
|
{agents.map((agent, index) => (
|
|
@@ -304,17 +305,17 @@ export function RuntimeConfig() {
|
|
|
304
305
|
<Input
|
|
305
306
|
value={agent.id}
|
|
306
307
|
onChange={(event) => updateAgent(index, { id: event.target.value })}
|
|
307
|
-
placeholder=
|
|
308
|
+
placeholder={t('agentIdPlaceholder')}
|
|
308
309
|
/>
|
|
309
310
|
<Input
|
|
310
311
|
value={agent.workspace ?? ''}
|
|
311
312
|
onChange={(event) => updateAgent(index, { workspace: event.target.value })}
|
|
312
|
-
placeholder=
|
|
313
|
+
placeholder={t('workspaceOverridePlaceholder')}
|
|
313
314
|
/>
|
|
314
315
|
<Input
|
|
315
316
|
value={agent.model ?? ''}
|
|
316
317
|
onChange={(event) => updateAgent(index, { model: event.target.value })}
|
|
317
|
-
placeholder=
|
|
318
|
+
placeholder={t('modelOverridePlaceholder')}
|
|
318
319
|
/>
|
|
319
320
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
|
|
320
321
|
<Input
|
|
@@ -326,7 +327,7 @@ export function RuntimeConfig() {
|
|
|
326
327
|
maxTokens: parseOptionalInt(event.target.value)
|
|
327
328
|
})
|
|
328
329
|
}
|
|
329
|
-
placeholder=
|
|
330
|
+
placeholder={t('maxTokensPlaceholder')}
|
|
330
331
|
/>
|
|
331
332
|
<Input
|
|
332
333
|
type="number"
|
|
@@ -338,7 +339,7 @@ export function RuntimeConfig() {
|
|
|
338
339
|
contextTokens: parseOptionalInt(event.target.value)
|
|
339
340
|
})
|
|
340
341
|
}
|
|
341
|
-
placeholder={agentContextTokensHint?.label ?? '
|
|
342
|
+
placeholder={agentContextTokensHint?.label ?? t('contextTokensPlaceholder')}
|
|
342
343
|
/>
|
|
343
344
|
<Input
|
|
344
345
|
type="number"
|
|
@@ -349,7 +350,7 @@ export function RuntimeConfig() {
|
|
|
349
350
|
maxToolIterations: parseOptionalInt(event.target.value)
|
|
350
351
|
})
|
|
351
352
|
}
|
|
352
|
-
placeholder=
|
|
353
|
+
placeholder={t('maxToolsPlaceholder')}
|
|
353
354
|
/>
|
|
354
355
|
</div>
|
|
355
356
|
</div>
|
|
@@ -370,11 +371,11 @@ export function RuntimeConfig() {
|
|
|
370
371
|
);
|
|
371
372
|
}}
|
|
372
373
|
/>
|
|
373
|
-
<span>
|
|
374
|
+
<span>{t('defaultAgent')}</span>
|
|
374
375
|
</div>
|
|
375
376
|
<Button type="button" variant="outline" size="sm" onClick={() => setAgents((prev) => prev.filter((_, cursor) => cursor !== index))}>
|
|
376
377
|
<Trash2 className="h-4 w-4 mr-1" />
|
|
377
|
-
|
|
378
|
+
{t('remove')}
|
|
378
379
|
</Button>
|
|
379
380
|
</div>
|
|
380
381
|
</div>
|
|
@@ -382,16 +383,16 @@ export function RuntimeConfig() {
|
|
|
382
383
|
|
|
383
384
|
<Button type="button" variant="outline" onClick={() => setAgents((prev) => [...prev, createEmptyAgent()])}>
|
|
384
385
|
<Plus className="h-4 w-4 mr-2" />
|
|
385
|
-
|
|
386
|
+
{t('addAgent')}
|
|
386
387
|
</Button>
|
|
387
388
|
</CardContent>
|
|
388
389
|
</Card>
|
|
389
390
|
|
|
390
391
|
<Card>
|
|
391
392
|
<CardHeader>
|
|
392
|
-
<CardTitle>{bindingsHint?.label ?? '
|
|
393
|
+
<CardTitle>{bindingsHint?.label ?? t('bindings')}</CardTitle>
|
|
393
394
|
<CardDescription>
|
|
394
|
-
{bindingsHint?.help ?? '
|
|
395
|
+
{bindingsHint?.help ?? t('bindingsHelp')}
|
|
395
396
|
</CardDescription>
|
|
396
397
|
</CardHeader>
|
|
397
398
|
<CardContent className="space-y-3">
|
|
@@ -403,7 +404,7 @@ export function RuntimeConfig() {
|
|
|
403
404
|
<Input
|
|
404
405
|
value={binding.agentId}
|
|
405
406
|
onChange={(event) => updateBinding(index, { ...binding, agentId: event.target.value })}
|
|
406
|
-
placeholder=
|
|
407
|
+
placeholder={t('targetAgentIdPlaceholder')}
|
|
407
408
|
/>
|
|
408
409
|
<Input
|
|
409
410
|
value={binding.match.channel}
|
|
@@ -416,7 +417,7 @@ export function RuntimeConfig() {
|
|
|
416
417
|
}
|
|
417
418
|
})
|
|
418
419
|
}
|
|
419
|
-
placeholder=
|
|
420
|
+
placeholder={t('channelPlaceholder')}
|
|
420
421
|
/>
|
|
421
422
|
<Input
|
|
422
423
|
value={binding.match.accountId ?? ''}
|
|
@@ -429,7 +430,7 @@ export function RuntimeConfig() {
|
|
|
429
430
|
}
|
|
430
431
|
})
|
|
431
432
|
}
|
|
432
|
-
placeholder=
|
|
433
|
+
placeholder={t('accountIdOptionalPlaceholder')}
|
|
433
434
|
/>
|
|
434
435
|
<Select
|
|
435
436
|
value={peerKind || '__none__'}
|
|
@@ -461,7 +462,7 @@ export function RuntimeConfig() {
|
|
|
461
462
|
<SelectValue />
|
|
462
463
|
</SelectTrigger>
|
|
463
464
|
<SelectContent>
|
|
464
|
-
<SelectItem value="__none__">
|
|
465
|
+
<SelectItem value="__none__">{t('peerKindOptional')}</SelectItem>
|
|
465
466
|
<SelectItem value="direct">direct</SelectItem>
|
|
466
467
|
<SelectItem value="group">group</SelectItem>
|
|
467
468
|
<SelectItem value="channel">channel</SelectItem>
|
|
@@ -483,7 +484,7 @@ export function RuntimeConfig() {
|
|
|
483
484
|
}
|
|
484
485
|
})
|
|
485
486
|
}
|
|
486
|
-
placeholder=
|
|
487
|
+
placeholder={t('peerIdPlaceholder')}
|
|
487
488
|
/>
|
|
488
489
|
</div>
|
|
489
490
|
<div className="flex justify-end">
|
|
@@ -494,7 +495,7 @@ export function RuntimeConfig() {
|
|
|
494
495
|
onClick={() => setBindings((prev) => prev.filter((_, cursor) => cursor !== index))}
|
|
495
496
|
>
|
|
496
497
|
<Trash2 className="h-4 w-4 mr-1" />
|
|
497
|
-
|
|
498
|
+
{t('remove')}
|
|
498
499
|
</Button>
|
|
499
500
|
</div>
|
|
500
501
|
</div>
|
|
@@ -503,7 +504,7 @@ export function RuntimeConfig() {
|
|
|
503
504
|
|
|
504
505
|
<Button type="button" variant="outline" onClick={() => setBindings((prev) => [...prev, createEmptyBinding()])}>
|
|
505
506
|
<Plus className="h-4 w-4 mr-2" />
|
|
506
|
-
|
|
507
|
+
{t('addBinding')}
|
|
507
508
|
</Button>
|
|
508
509
|
</CardContent>
|
|
509
510
|
</Card>
|
|
@@ -511,7 +512,7 @@ export function RuntimeConfig() {
|
|
|
511
512
|
<div className="flex justify-end">
|
|
512
513
|
<Button type="button" onClick={handleSave} disabled={updateRuntime.isPending}>
|
|
513
514
|
<Save className="h-4 w-4 mr-2" />
|
|
514
|
-
{updateRuntime.isPending ? '
|
|
515
|
+
{updateRuntime.isPending ? t('saving') : t('saveRuntimeSettings')}
|
|
515
516
|
</Button>
|
|
516
517
|
</div>
|
|
517
518
|
</div>
|
|
@@ -6,20 +6,13 @@ import { Button } from '@/components/ui/button';
|
|
|
6
6
|
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
|
-
import { t } from '@/lib/i18n';
|
|
9
|
+
import { formatDateShort, formatDateTime, t } from '@/lib/i18n';
|
|
10
10
|
import { RefreshCw, Search, Clock, Inbox, Hash, Bot, User, MessageCircle, Settings as SettingsIcon } from 'lucide-react';
|
|
11
11
|
|
|
12
12
|
const UNKNOWN_CHANNEL_KEY = '__unknown_channel__';
|
|
13
13
|
|
|
14
14
|
function formatDate(value?: string): string {
|
|
15
|
-
|
|
16
|
-
return '-';
|
|
17
|
-
}
|
|
18
|
-
const date = new Date(value);
|
|
19
|
-
if (Number.isNaN(date.getTime())) {
|
|
20
|
-
return value;
|
|
21
|
-
}
|
|
22
|
-
return date.toLocaleString();
|
|
15
|
+
return formatDateTime(value);
|
|
23
16
|
}
|
|
24
17
|
|
|
25
18
|
function resolveChannelFromSessionKey(key: string): string {
|
|
@@ -75,7 +68,7 @@ function SessionListItem({ session, channel, isSelected, onSelect }: SessionList
|
|
|
75
68
|
<div className={cn("flex items-center text-xs justify-between mt-2 font-medium", isSelected ? "text-brand-600/80" : "text-gray-400")}>
|
|
76
69
|
<div className="flex items-center gap-1.5">
|
|
77
70
|
<Clock className="w-3.5 h-3.5 opacity-70" />
|
|
78
|
-
<span className="truncate max-w-[100px]">{
|
|
71
|
+
<span className="truncate max-w-[100px]">{formatDateShort(session.updatedAt)}</span>
|
|
79
72
|
</div>
|
|
80
73
|
<div className="flex items-center gap-1">
|
|
81
74
|
<MessageCircle className="w-3.5 h-3.5 opacity-70" />
|
|
@@ -248,10 +241,10 @@ export function SessionsConfig() {
|
|
|
248
241
|
|
|
249
242
|
<Select value={selectedChannel} onValueChange={setSelectedChannel}>
|
|
250
243
|
<SelectTrigger className="w-full h-8.5 rounded-lg bg-gray-50/50 hover:bg-gray-100 border-gray-200 focus:ring-0 shadow-none text-xs font-medium text-gray-700">
|
|
251
|
-
<SelectValue placeholder=
|
|
244
|
+
<SelectValue placeholder={t('sessionsAllChannels')} />
|
|
252
245
|
</SelectTrigger>
|
|
253
246
|
<SelectContent className="rounded-xl shadow-lg border-gray-100 max-w-[280px]">
|
|
254
|
-
<SelectItem value="all" className="rounded-lg text-xs">
|
|
247
|
+
<SelectItem value="all" className="rounded-lg text-xs">{t('sessionsAllChannels')}</SelectItem>
|
|
255
248
|
{channels.map(c => (
|
|
256
249
|
<SelectItem key={c} value={c} className="rounded-lg text-xs truncate pr-6">{displayChannelName(c)}</SelectItem>
|
|
257
250
|
))}
|
|
@@ -326,7 +319,7 @@ export function SessionsConfig() {
|
|
|
326
319
|
<div className="flex items-center gap-2 shrink-0">
|
|
327
320
|
<Button variant="outline" size="sm" onClick={() => setIsEditingMeta(!isEditingMeta)} className={cn("h-8.5 rounded-lg shadow-none border-gray-200 transition-all text-xs font-semibold", isEditingMeta ? "bg-gray-100 text-gray-900" : "hover:bg-gray-50 hover:text-gray-900")}>
|
|
328
321
|
<SettingsIcon className="w-3.5 h-3.5 mr-1.5" />
|
|
329
|
-
|
|
322
|
+
{t('sessionsMetadata')}
|
|
330
323
|
</Button>
|
|
331
324
|
<Button variant="outline" size="sm" onClick={handleClearHistory} className="h-8.5 rounded-lg shadow-none hover:bg-gray-50 hover:text-gray-900 border-gray-200 text-xs font-semibold text-gray-500">
|
|
332
325
|
{t('sessionsClearHistory')}
|
|
@@ -397,9 +390,9 @@ export function SessionsConfig() {
|
|
|
397
390
|
<div className="w-20 h-20 bg-gray-50 rounded-3xl flex items-center justify-center mb-6 border border-gray-100 shadow-[0_2px_8px_-2px_rgba(0,0,0,0.02)] rotate-3">
|
|
398
391
|
<Inbox className="h-8 w-8 text-gray-300 -rotate-3" />
|
|
399
392
|
</div>
|
|
400
|
-
<h3 className="text-lg font-bold text-gray-900 mb-2">
|
|
393
|
+
<h3 className="text-lg font-bold text-gray-900 mb-2">{t('sessionsNoSelectionTitle')}</h3>
|
|
401
394
|
<p className="text-sm text-center max-w-sm leading-relaxed">
|
|
402
|
-
|
|
395
|
+
{t('sessionsNoSelectionDescription')}
|
|
403
396
|
</p>
|
|
404
397
|
</div>
|
|
405
398
|
)}
|
|
@@ -1,49 +1,61 @@
|
|
|
1
1
|
import { cn } from '@/lib/utils';
|
|
2
|
-
import { t } from '@/lib/i18n';
|
|
3
|
-
import { Cpu, GitBranch, History, MessageSquare, Sparkles, BookOpen, Store, AlarmClock } from 'lucide-react';
|
|
2
|
+
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
|
|
3
|
+
import { Cpu, GitBranch, History, MessageSquare, Sparkles, BookOpen, Store, AlarmClock, Languages } from 'lucide-react';
|
|
4
4
|
import { NavLink } from 'react-router-dom';
|
|
5
5
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
{
|
|
9
|
-
target: '/model',
|
|
10
|
-
label: 'Models',
|
|
11
|
-
icon: Cpu,
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
target: '/providers',
|
|
15
|
-
label: 'Providers',
|
|
16
|
-
icon: Sparkles,
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
target: '/channels',
|
|
20
|
-
label: 'Channels',
|
|
21
|
-
icon: MessageSquare,
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
target: '/runtime',
|
|
25
|
-
label: 'Routing & Runtime',
|
|
26
|
-
icon: GitBranch,
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
target: '/sessions',
|
|
30
|
-
label: t('sessions'),
|
|
31
|
-
icon: History,
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
target: '/cron',
|
|
35
|
-
label: t('cron'),
|
|
36
|
-
icon: AlarmClock,
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
target: '/marketplace',
|
|
40
|
-
label: 'Marketplace',
|
|
41
|
-
icon: Store,
|
|
42
|
-
}
|
|
43
|
-
];
|
|
6
|
+
import { useI18n } from '@/components/providers/I18nProvider';
|
|
7
|
+
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
|
|
44
8
|
|
|
45
9
|
export function Sidebar() {
|
|
46
10
|
const docBrowser = useDocBrowser();
|
|
11
|
+
const { language, setLanguage } = useI18n();
|
|
12
|
+
const currentLanguageLabel = LANGUAGE_OPTIONS.find((option) => option.value === language)?.label ?? language;
|
|
13
|
+
|
|
14
|
+
const handleLanguageSwitch = (nextLanguage: I18nLanguage) => {
|
|
15
|
+
if (language === nextLanguage) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
setLanguage(nextLanguage);
|
|
19
|
+
window.location.reload();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const navItems = [
|
|
23
|
+
{
|
|
24
|
+
target: '/model',
|
|
25
|
+
label: t('model'),
|
|
26
|
+
icon: Cpu,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
target: '/providers',
|
|
30
|
+
label: t('providers'),
|
|
31
|
+
icon: Sparkles,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
target: '/channels',
|
|
35
|
+
label: t('channels'),
|
|
36
|
+
icon: MessageSquare,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
target: '/runtime',
|
|
40
|
+
label: t('runtime'),
|
|
41
|
+
icon: GitBranch,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
target: '/sessions',
|
|
45
|
+
label: t('sessions'),
|
|
46
|
+
icon: History,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
target: '/cron',
|
|
50
|
+
label: t('cron'),
|
|
51
|
+
icon: AlarmClock,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
target: '/marketplace',
|
|
55
|
+
label: t('marketplace'),
|
|
56
|
+
icon: Store,
|
|
57
|
+
}
|
|
58
|
+
];
|
|
47
59
|
|
|
48
60
|
return (
|
|
49
61
|
<aside className="w-[240px] bg-[#f0f2f7] flex flex-col h-full py-6 px-4">
|
|
@@ -92,6 +104,24 @@ export function Sidebar() {
|
|
|
92
104
|
|
|
93
105
|
{/* Help Button */}
|
|
94
106
|
<div className="pt-3 border-t border-[#dde0ea] mt-3">
|
|
107
|
+
<div className="mb-2">
|
|
108
|
+
<Select value={language} onValueChange={(value) => handleLanguageSwitch(value as I18nLanguage)}>
|
|
109
|
+
<SelectTrigger className="w-full h-auto rounded-xl border-0 bg-transparent shadow-none px-3 py-2.5 text-[14px] font-medium text-gray-600 hover:bg-[#e4e7ef] focus:ring-0">
|
|
110
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
111
|
+
<Languages className="h-[17px] w-[17px] text-gray-400" />
|
|
112
|
+
<span className="text-left">{t('language')}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<span className="ml-auto text-xs text-gray-500">{currentLanguageLabel}</span>
|
|
115
|
+
</SelectTrigger>
|
|
116
|
+
<SelectContent>
|
|
117
|
+
{LANGUAGE_OPTIONS.map((option) => (
|
|
118
|
+
<SelectItem key={option.value} value={option.value} className="text-xs">
|
|
119
|
+
{option.label}
|
|
120
|
+
</SelectItem>
|
|
121
|
+
))}
|
|
122
|
+
</SelectContent>
|
|
123
|
+
</Select>
|
|
124
|
+
</div>
|
|
95
125
|
<button
|
|
96
126
|
onClick={() => docBrowser.open()}
|
|
97
127
|
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-[14px] font-medium transition-all duration-base text-gray-600 hover:bg-[#e4e7ef] hover:text-gray-800"
|