@nextclaw/ui 0.5.8 → 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.
@@ -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">Model Configuration</h2>
73
- <p className="text-sm text-gray-500 mt-1">Configure default AI model and runtime limits</p>
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">Default Model</h3>
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">Workspace</h3>
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">Generation Parameters</h3>
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 ?? 'Max Tokens'}
143
+ {maxTokensHint?.label ?? t('maxTokens')}
143
144
  </Label>
144
- <span className="text-sm font-semibold text-gray-900">{maxTokens.toLocaleString()}</span>
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
- 'Save Changes'
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>Configure API keys and parameters for AI provider</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 ?? 'Enter API Key'
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 ? 'Saving...' : t('save')}
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">Loading...</div>;
25
+ return <div className="p-8">{t('providersLoading')}</div>;
25
26
  }
26
27
 
27
28
  const tabs = [
28
- { id: 'installed', label: 'Configured', count: config.providers ? Object.keys(config.providers).filter(k => config.providers[k].apiKeySet).length : 0 },
29
- { id: 'all', label: 'All Providers' }
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">AI Providers</h2>
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 || 'Configure AI services for your agents';
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 ? 'Ready' : 'Setup'}
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 ? 'Configure' : 'Add Provider'} />
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
- No providers configured
101
+ {t('providersEmptyTitle')}
101
102
  </h3>
102
103
  <p className="text-[13px] text-gray-400 max-w-sm">
103
- Add an AI provider to start using the platform.
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(`agents.list[${index}].id is required`);
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(`Duplicate agent id: ${duplicates[0]}`);
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(`bindings[${index}].agentId is required`);
173
+ throw new Error(t('bindingAgentIdRequired').replace('{index}', String(index)));
173
174
  }
174
175
  if (!knownAgentIds.has(agentId)) {
175
- throw new Error(`bindings[${index}].agentId '${agentId}' not found in agents.list/main`);
176
+ throw new Error(`${t('bindingAgentIdNotFound').replace('{index}', String(index))}: ${agentId}`);
176
177
  }
177
178
  if (!channel) {
178
- throw new Error(`bindings[${index}].match.channel is required`);
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(`bindings[${index}].match.peer.id is required when peer.kind is set`);
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">Loading runtime settings...</div>;
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">Routing & Runtime</h2>
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
- Align multi-agent routing with OpenClaw: bindings, agent pool, and DM scope.
238
+ {t('runtimePageDescription')}
238
239
  </p>
239
240
  </div>
240
241
 
241
242
  <Card>
242
243
  <CardHeader>
243
- <CardTitle>{dmScopeHint?.label ?? 'DM Scope'}</CardTitle>
244
- <CardDescription>{dmScopeHint?.help ?? 'Control how direct-message sessions are isolated.'}</CardDescription>
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 ?? 'Default Context Tokens'}
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 ?? 'Input context budget for agents when no per-agent override is set.'}
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 ?? 'DM Scope'}</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 ?? 'Max Ping-Pong Turns'}
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 ?? 'Set to 0 to block automatic agent-to-agent ping-pong loops.'}
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 ?? 'Agent List'}</CardTitle>
298
- <CardDescription>{agentsHint?.help ?? 'Run multiple fixed-role agents in one gateway process.'}</CardDescription>
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="Agent ID (e.g. engineer)"
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="Workspace override (optional)"
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="Model override (optional)"
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="Max tokens"
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 ?? 'Context tokens'}
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="Max tools"
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>Default agent</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
- Remove
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
- Add Agent
386
+ {t('addAgent')}
386
387
  </Button>
387
388
  </CardContent>
388
389
  </Card>
389
390
 
390
391
  <Card>
391
392
  <CardHeader>
392
- <CardTitle>{bindingsHint?.label ?? 'Bindings'}</CardTitle>
393
+ <CardTitle>{bindingsHint?.label ?? t('bindings')}</CardTitle>
393
394
  <CardDescription>
394
- {bindingsHint?.help ?? 'Route inbound message by channel + account + peer to target agent.'}
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="Target agent ID"
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="Channel (e.g. discord)"
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="Account ID (optional)"
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__">Peer kind (optional)</SelectItem>
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="Peer ID (requires peer kind)"
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
- Remove
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
- Add Binding
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 ? 'Saving...' : 'Save Runtime Settings'}
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
- if (!value) {
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]">{formatDate(session.updatedAt).split(' ')[0]}</span>
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="All Channels" />
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">All Channels</SelectItem>
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
- Metadata
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">No Session Selected</h3>
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
- Select a session from the list on the left to view its chat history and configure its metadata.
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
- const navItems = [
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"