@nextclaw/ui 0.5.9 → 0.5.11

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,8 @@ 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';
9
+ import { PageLayout, PageHeader } from '@/components/layout/page-layout';
8
10
  import { Folder, Loader2, Sliders, Sparkles } from 'lucide-react';
9
11
  import { useEffect, useState } from 'react';
10
12
 
@@ -67,11 +69,8 @@ export function ModelConfig() {
67
69
  }
68
70
 
69
71
  return (
70
- <div className="max-w-4xl animate-fade-in pb-20">
71
- <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>
74
- </div>
72
+ <PageLayout>
73
+ <PageHeader title={t('modelPageTitle')} description={t('modelPageDescription')} />
75
74
 
76
75
  <form onSubmit={handleSubmit} className="space-y-8">
77
76
  <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
@@ -81,7 +80,7 @@ export function ModelConfig() {
81
80
  <div className="h-10 w-10 rounded-xl bg-primary flex items-center justify-center text-white">
82
81
  <Sparkles className="h-5 w-5" />
83
82
  </div>
84
- <h3 className="text-lg font-bold text-gray-900">Default Model</h3>
83
+ <h3 className="text-lg font-bold text-gray-900">{t('defaultModel')}</h3>
85
84
  </div>
86
85
 
87
86
  <div className="space-y-2">
@@ -108,7 +107,7 @@ export function ModelConfig() {
108
107
  <div className="h-10 w-10 rounded-xl bg-primary flex items-center justify-center text-white">
109
108
  <Folder className="h-5 w-5" />
110
109
  </div>
111
- <h3 className="text-lg font-bold text-gray-900">Workspace</h3>
110
+ <h3 className="text-lg font-bold text-gray-900">{t('workspace')}</h3>
112
111
  </div>
113
112
 
114
113
  <div className="space-y-2">
@@ -132,16 +131,16 @@ export function ModelConfig() {
132
131
  <div className="h-10 w-10 rounded-xl bg-primary flex items-center justify-center text-white">
133
132
  <Sliders className="h-5 w-5" />
134
133
  </div>
135
- <h3 className="text-lg font-bold text-gray-900">Generation Parameters</h3>
134
+ <h3 className="text-lg font-bold text-gray-900">{t('generationParameters')}</h3>
136
135
  </div>
137
136
 
138
137
  <div className="grid grid-cols-1 gap-12">
139
138
  <div className="space-y-4">
140
139
  <div className="flex justify-between items-center mb-2">
141
140
  <Label className="text-xs font-semibold text-gray-500 uppercase tracking-wider">
142
- {maxTokensHint?.label ?? 'Max Tokens'}
141
+ {maxTokensHint?.label ?? t('maxTokens')}
143
142
  </Label>
144
- <span className="text-sm font-semibold text-gray-900">{maxTokens.toLocaleString()}</span>
143
+ <span className="text-sm font-semibold text-gray-900">{formatNumber(maxTokens)}</span>
145
144
  </div>
146
145
  <input
147
146
  type="range"
@@ -165,11 +164,11 @@ export function ModelConfig() {
165
164
  {updateModel.isPending ? (
166
165
  <Loader2 className="h-5 w-5 animate-spin" />
167
166
  ) : (
168
- 'Save Changes'
167
+ t('saveChanges')
169
168
  )}
170
169
  </Button>
171
170
  </div>
172
171
  </form>
173
- </div>
172
+ </PageLayout>
174
173
  );
175
174
  }
@@ -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,8 @@ 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';
15
+ import { PageLayout, PageHeader } from '@/components/layout/page-layout';
14
16
 
15
17
  export function ProvidersList() {
16
18
  const { data: config } = useConfig();
@@ -21,12 +23,12 @@ export function ProvidersList() {
21
23
  const uiHints = schema?.uiHints;
22
24
 
23
25
  if (!config || !meta) {
24
- return <div className="p-8">Loading...</div>;
26
+ return <div className="p-8">{t('providersLoading')}</div>;
25
27
  }
26
28
 
27
29
  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' }
30
+ { id: 'installed', label: t('providersTabConfigured'), count: config.providers ? Object.keys(config.providers).filter(k => config.providers[k].apiKeySet).length : 0 },
31
+ { id: 'all', label: t('providersTabAll'), count: meta.providers.length }
30
32
  ];
31
33
 
32
34
  const filteredProviders = activeTab === 'installed'
@@ -34,10 +36,8 @@ export function ProvidersList() {
34
36
  : meta.providers;
35
37
 
36
38
  return (
37
- <div className="animate-fade-in pb-20">
38
- <div className="flex items-center justify-between mb-6">
39
- <h2 className="text-xl font-semibold text-gray-900">AI Providers</h2>
40
- </div>
39
+ <PageLayout>
40
+ <PageHeader title={t('providersPageTitle')} />
41
41
 
42
42
  <Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} />
43
43
 
@@ -47,7 +47,7 @@ export function ProvidersList() {
47
47
  const providerConfig = config.providers[provider.name];
48
48
  const hasConfig = providerConfig?.apiKeySet;
49
49
  const providerHint = hintForPath(`providers.${provider.name}`, uiHints);
50
- const description = providerHint?.help || 'Configure AI services for your agents';
50
+ const description = providerHint?.help || t('providersDefaultDescription');
51
51
 
52
52
  return (
53
53
  <ConfigCard key={provider.name} onClick={() => openProviderModal(provider.name)}>
@@ -73,7 +73,7 @@ export function ProvidersList() {
73
73
  />
74
74
  <StatusDot
75
75
  status={hasConfig ? 'ready' : 'setup'}
76
- label={hasConfig ? 'Ready' : 'Setup'}
76
+ label={hasConfig ? t('statusReady') : t('statusSetup')}
77
77
  />
78
78
  </ConfigCardHeader>
79
79
 
@@ -83,7 +83,7 @@ export function ProvidersList() {
83
83
  />
84
84
 
85
85
  <ConfigCardFooter>
86
- <ActionLink label={hasConfig ? 'Configure' : 'Add Provider'} />
86
+ <ActionLink label={hasConfig ? t('actionConfigure') : t('actionAddProvider')} />
87
87
  </ConfigCardFooter>
88
88
  </ConfigCard>
89
89
  );
@@ -97,15 +97,15 @@ export function ProvidersList() {
97
97
  <KeyRound className="h-6 w-6 text-gray-300" />
98
98
  </div>
99
99
  <h3 className="text-[14px] font-semibold text-gray-900 mb-1.5">
100
- No providers configured
100
+ {t('providersEmptyTitle')}
101
101
  </h3>
102
102
  <p className="text-[13px] text-gray-400 max-w-sm">
103
- Add an AI provider to start using the platform.
103
+ {t('providersEmptyDescription')}
104
104
  </p>
105
105
  </div>
106
106
  )}
107
107
 
108
108
  <ProviderForm />
109
- </div>
109
+ </PageLayout>
110
110
  );
111
111
  }
@@ -7,6 +7,8 @@ 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';
11
+ import { PageLayout, PageHeader } from '@/components/layout/page-layout';
10
12
  import { Plus, Save, Trash2 } from 'lucide-react';
11
13
  import { toast } from 'sonner';
12
14
 
@@ -129,7 +131,7 @@ export function RuntimeConfig() {
129
131
  const normalizedAgents = agents.map((agent, index) => {
130
132
  const id = agent.id.trim();
131
133
  if (!id) {
132
- throw new Error(`agents.list[${index}].id is required`);
134
+ throw new Error(t('agentIdRequiredError').replace('{index}', String(index)));
133
135
  }
134
136
  const normalized: AgentProfileView = { id };
135
137
  if (agent.default) {
@@ -157,7 +159,7 @@ export function RuntimeConfig() {
157
159
  .map((agent) => agent.id)
158
160
  .filter((id, index, all) => all.indexOf(id) !== index);
159
161
  if (duplicates.length > 0) {
160
- toast.error(`Duplicate agent id: ${duplicates[0]}`);
162
+ toast.error(`${t('duplicateAgentId')}: ${duplicates[0]}`);
161
163
  return;
162
164
  }
163
165
 
@@ -169,13 +171,13 @@ export function RuntimeConfig() {
169
171
  const peerId = binding.match.peer?.id?.trim() ?? '';
170
172
 
171
173
  if (!agentId) {
172
- throw new Error(`bindings[${index}].agentId is required`);
174
+ throw new Error(t('bindingAgentIdRequired').replace('{index}', String(index)));
173
175
  }
174
176
  if (!knownAgentIds.has(agentId)) {
175
- throw new Error(`bindings[${index}].agentId '${agentId}' not found in agents.list/main`);
177
+ throw new Error(`${t('bindingAgentIdNotFound').replace('{index}', String(index))}: ${agentId}`);
176
178
  }
177
179
  if (!channel) {
178
- throw new Error(`bindings[${index}].match.channel is required`);
180
+ throw new Error(t('bindingChannelRequired').replace('{index}', String(index)));
179
181
  }
180
182
 
181
183
  const normalized: AgentBindingView = {
@@ -191,7 +193,7 @@ export function RuntimeConfig() {
191
193
 
192
194
  if (peerKind) {
193
195
  if (!peerId) {
194
- throw new Error(`bindings[${index}].match.peer.id is required when peer.kind is set`);
196
+ throw new Error(t('bindingPeerIdRequired').replace('{index}', String(index)));
195
197
  }
196
198
  normalized.match.peer = {
197
199
  kind: peerKind,
@@ -226,27 +228,22 @@ export function RuntimeConfig() {
226
228
  };
227
229
 
228
230
  if (isLoading || !config) {
229
- return <div className="p-8 text-gray-400">Loading runtime settings...</div>;
231
+ return <div className="p-8 text-gray-400">{t('runtimeLoading')}</div>;
230
232
  }
231
233
 
232
234
  return (
233
- <div className="space-y-6 pb-20 animate-fade-in">
234
- <div>
235
- <h2 className="text-xl font-semibold text-gray-900">Routing & Runtime</h2>
236
- <p className="text-sm text-gray-500 mt-1">
237
- Align multi-agent routing with OpenClaw: bindings, agent pool, and DM scope.
238
- </p>
239
- </div>
235
+ <PageLayout className="space-y-6">
236
+ <PageHeader title={t('runtimePageTitle')} description={t('runtimePageDescription')} />
240
237
 
241
238
  <Card>
242
239
  <CardHeader>
243
- <CardTitle>{dmScopeHint?.label ?? 'DM Scope'}</CardTitle>
244
- <CardDescription>{dmScopeHint?.help ?? 'Control how direct-message sessions are isolated.'}</CardDescription>
240
+ <CardTitle>{dmScopeHint?.label ?? t('dmScope')}</CardTitle>
241
+ <CardDescription>{dmScopeHint?.help ?? t('dmScopeHelp')}</CardDescription>
245
242
  </CardHeader>
246
243
  <CardContent className="space-y-4">
247
244
  <div className="space-y-2">
248
245
  <label className="text-sm font-medium text-gray-800">
249
- {defaultContextTokensHint?.label ?? 'Default Context Tokens'}
246
+ {defaultContextTokensHint?.label ?? t('defaultContextTokens')}
250
247
  </label>
251
248
  <Input
252
249
  type="number"
@@ -256,11 +253,11 @@ export function RuntimeConfig() {
256
253
  onChange={(event) => setDefaultContextTokens(Math.max(1000, Number.parseInt(event.target.value, 10) || 1000))}
257
254
  />
258
255
  <p className="text-xs text-gray-500">
259
- {defaultContextTokensHint?.help ?? 'Input context budget for agents when no per-agent override is set.'}
256
+ {defaultContextTokensHint?.help ?? t('defaultContextTokensHelp')}
260
257
  </p>
261
258
  </div>
262
259
  <div className="space-y-2">
263
- <label className="text-sm font-medium text-gray-800">{dmScopeHint?.label ?? 'DM Scope'}</label>
260
+ <label className="text-sm font-medium text-gray-800">{dmScopeHint?.label ?? t('dmScope')}</label>
264
261
  <Select value={dmScope} onValueChange={(v) => setDmScope(v as DmScope)}>
265
262
  <SelectTrigger>
266
263
  <SelectValue />
@@ -276,7 +273,7 @@ export function RuntimeConfig() {
276
273
  </div>
277
274
  <div className="space-y-2">
278
275
  <label className="text-sm font-medium text-gray-800">
279
- {maxPingHint?.label ?? 'Max Ping-Pong Turns'}
276
+ {maxPingHint?.label ?? t('maxPingPongTurns')}
280
277
  </label>
281
278
  <Input
282
279
  type="number"
@@ -286,7 +283,7 @@ export function RuntimeConfig() {
286
283
  onChange={(event) => setMaxPingPongTurns(Math.max(0, Number.parseInt(event.target.value, 10) || 0))}
287
284
  />
288
285
  <p className="text-xs text-gray-500">
289
- {maxPingHint?.help ?? 'Set to 0 to block automatic agent-to-agent ping-pong loops.'}
286
+ {maxPingHint?.help ?? t('maxPingPongTurnsHelp')}
290
287
  </p>
291
288
  </div>
292
289
  </CardContent>
@@ -294,8 +291,8 @@ export function RuntimeConfig() {
294
291
 
295
292
  <Card>
296
293
  <CardHeader>
297
- <CardTitle>{agentsHint?.label ?? 'Agent List'}</CardTitle>
298
- <CardDescription>{agentsHint?.help ?? 'Run multiple fixed-role agents in one gateway process.'}</CardDescription>
294
+ <CardTitle>{agentsHint?.label ?? t('agentList')}</CardTitle>
295
+ <CardDescription>{agentsHint?.help ?? t('agentListHelp')}</CardDescription>
299
296
  </CardHeader>
300
297
  <CardContent className="space-y-3">
301
298
  {agents.map((agent, index) => (
@@ -304,17 +301,17 @@ export function RuntimeConfig() {
304
301
  <Input
305
302
  value={agent.id}
306
303
  onChange={(event) => updateAgent(index, { id: event.target.value })}
307
- placeholder="Agent ID (e.g. engineer)"
304
+ placeholder={t('agentIdPlaceholder')}
308
305
  />
309
306
  <Input
310
307
  value={agent.workspace ?? ''}
311
308
  onChange={(event) => updateAgent(index, { workspace: event.target.value })}
312
- placeholder="Workspace override (optional)"
309
+ placeholder={t('workspaceOverridePlaceholder')}
313
310
  />
314
311
  <Input
315
312
  value={agent.model ?? ''}
316
313
  onChange={(event) => updateAgent(index, { model: event.target.value })}
317
- placeholder="Model override (optional)"
314
+ placeholder={t('modelOverridePlaceholder')}
318
315
  />
319
316
  <div className="grid grid-cols-1 md:grid-cols-3 gap-2">
320
317
  <Input
@@ -326,7 +323,7 @@ export function RuntimeConfig() {
326
323
  maxTokens: parseOptionalInt(event.target.value)
327
324
  })
328
325
  }
329
- placeholder="Max tokens"
326
+ placeholder={t('maxTokensPlaceholder')}
330
327
  />
331
328
  <Input
332
329
  type="number"
@@ -338,7 +335,7 @@ export function RuntimeConfig() {
338
335
  contextTokens: parseOptionalInt(event.target.value)
339
336
  })
340
337
  }
341
- placeholder={agentContextTokensHint?.label ?? 'Context tokens'}
338
+ placeholder={agentContextTokensHint?.label ?? t('contextTokensPlaceholder')}
342
339
  />
343
340
  <Input
344
341
  type="number"
@@ -349,7 +346,7 @@ export function RuntimeConfig() {
349
346
  maxToolIterations: parseOptionalInt(event.target.value)
350
347
  })
351
348
  }
352
- placeholder="Max tools"
349
+ placeholder={t('maxToolsPlaceholder')}
353
350
  />
354
351
  </div>
355
352
  </div>
@@ -370,11 +367,11 @@ export function RuntimeConfig() {
370
367
  );
371
368
  }}
372
369
  />
373
- <span>Default agent</span>
370
+ <span>{t('defaultAgent')}</span>
374
371
  </div>
375
372
  <Button type="button" variant="outline" size="sm" onClick={() => setAgents((prev) => prev.filter((_, cursor) => cursor !== index))}>
376
373
  <Trash2 className="h-4 w-4 mr-1" />
377
- Remove
374
+ {t('remove')}
378
375
  </Button>
379
376
  </div>
380
377
  </div>
@@ -382,16 +379,16 @@ export function RuntimeConfig() {
382
379
 
383
380
  <Button type="button" variant="outline" onClick={() => setAgents((prev) => [...prev, createEmptyAgent()])}>
384
381
  <Plus className="h-4 w-4 mr-2" />
385
- Add Agent
382
+ {t('addAgent')}
386
383
  </Button>
387
384
  </CardContent>
388
385
  </Card>
389
386
 
390
387
  <Card>
391
388
  <CardHeader>
392
- <CardTitle>{bindingsHint?.label ?? 'Bindings'}</CardTitle>
389
+ <CardTitle>{bindingsHint?.label ?? t('bindings')}</CardTitle>
393
390
  <CardDescription>
394
- {bindingsHint?.help ?? 'Route inbound message by channel + account + peer to target agent.'}
391
+ {bindingsHint?.help ?? t('bindingsHelp')}
395
392
  </CardDescription>
396
393
  </CardHeader>
397
394
  <CardContent className="space-y-3">
@@ -403,7 +400,7 @@ export function RuntimeConfig() {
403
400
  <Input
404
401
  value={binding.agentId}
405
402
  onChange={(event) => updateBinding(index, { ...binding, agentId: event.target.value })}
406
- placeholder="Target agent ID"
403
+ placeholder={t('targetAgentIdPlaceholder')}
407
404
  />
408
405
  <Input
409
406
  value={binding.match.channel}
@@ -416,7 +413,7 @@ export function RuntimeConfig() {
416
413
  }
417
414
  })
418
415
  }
419
- placeholder="Channel (e.g. discord)"
416
+ placeholder={t('channelPlaceholder')}
420
417
  />
421
418
  <Input
422
419
  value={binding.match.accountId ?? ''}
@@ -429,7 +426,7 @@ export function RuntimeConfig() {
429
426
  }
430
427
  })
431
428
  }
432
- placeholder="Account ID (optional)"
429
+ placeholder={t('accountIdOptionalPlaceholder')}
433
430
  />
434
431
  <Select
435
432
  value={peerKind || '__none__'}
@@ -461,7 +458,7 @@ export function RuntimeConfig() {
461
458
  <SelectValue />
462
459
  </SelectTrigger>
463
460
  <SelectContent>
464
- <SelectItem value="__none__">Peer kind (optional)</SelectItem>
461
+ <SelectItem value="__none__">{t('peerKindOptional')}</SelectItem>
465
462
  <SelectItem value="direct">direct</SelectItem>
466
463
  <SelectItem value="group">group</SelectItem>
467
464
  <SelectItem value="channel">channel</SelectItem>
@@ -483,7 +480,7 @@ export function RuntimeConfig() {
483
480
  }
484
481
  })
485
482
  }
486
- placeholder="Peer ID (requires peer kind)"
483
+ placeholder={t('peerIdPlaceholder')}
487
484
  />
488
485
  </div>
489
486
  <div className="flex justify-end">
@@ -494,7 +491,7 @@ export function RuntimeConfig() {
494
491
  onClick={() => setBindings((prev) => prev.filter((_, cursor) => cursor !== index))}
495
492
  >
496
493
  <Trash2 className="h-4 w-4 mr-1" />
497
- Remove
494
+ {t('remove')}
498
495
  </Button>
499
496
  </div>
500
497
  </div>
@@ -503,7 +500,7 @@ export function RuntimeConfig() {
503
500
 
504
501
  <Button type="button" variant="outline" onClick={() => setBindings((prev) => [...prev, createEmptyBinding()])}>
505
502
  <Plus className="h-4 w-4 mr-2" />
506
- Add Binding
503
+ {t('addBinding')}
507
504
  </Button>
508
505
  </CardContent>
509
506
  </Card>
@@ -511,9 +508,9 @@ export function RuntimeConfig() {
511
508
  <div className="flex justify-end">
512
509
  <Button type="button" onClick={handleSave} disabled={updateRuntime.isPending}>
513
510
  <Save className="h-4 w-4 mr-2" />
514
- {updateRuntime.isPending ? 'Saving...' : 'Save Runtime Settings'}
511
+ {updateRuntime.isPending ? t('saving') : t('saveRuntimeSettings')}
515
512
  </Button>
516
513
  </div>
517
- </div>
514
+ </PageLayout>
518
515
  );
519
516
  }
@@ -6,20 +6,14 @@ 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
+ import { PageLayout, PageHeader, PageBody } from '@/components/layout/page-layout';
10
11
  import { RefreshCw, Search, Clock, Inbox, Hash, Bot, User, MessageCircle, Settings as SettingsIcon } from 'lucide-react';
11
12
 
12
13
  const UNKNOWN_CHANNEL_KEY = '__unknown_channel__';
13
14
 
14
15
  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();
16
+ return formatDateTime(value);
23
17
  }
24
18
 
25
19
  function resolveChannelFromSessionKey(key: string): string {
@@ -75,7 +69,7 @@ function SessionListItem({ session, channel, isSelected, onSelect }: SessionList
75
69
  <div className={cn("flex items-center text-xs justify-between mt-2 font-medium", isSelected ? "text-brand-600/80" : "text-gray-400")}>
76
70
  <div className="flex items-center gap-1.5">
77
71
  <Clock className="w-3.5 h-3.5 opacity-70" />
78
- <span className="truncate max-w-[100px]">{formatDate(session.updatedAt).split(' ')[0]}</span>
72
+ <span className="truncate max-w-[100px]">{formatDateShort(session.updatedAt)}</span>
79
73
  </div>
80
74
  <div className="flex items-center gap-1">
81
75
  <MessageCircle className="w-3.5 h-3.5 opacity-70" />
@@ -220,14 +214,8 @@ export function SessionsConfig() {
220
214
  };
221
215
 
222
216
  return (
223
- <div className="h-[calc(100vh-80px)] w-full max-w-[1400px] mx-auto animate-fade-in flex flex-col pt-6 pb-2">
224
-
225
- <div className="flex items-center justify-between mb-6 shrink-0">
226
- <div>
227
- <h2 className="text-xl font-semibold text-gray-900 tracking-tight">{t('sessionsPageTitle')}</h2>
228
- <p className="text-sm text-gray-500 mt-1">{t('sessionsPageDescription')}</p>
229
- </div>
230
- </div>
217
+ <PageLayout fullHeight>
218
+ <PageHeader title={t('sessionsPageTitle')} description={t('sessionsPageDescription')} />
231
219
 
232
220
  {/* Main Mailbox Layout */}
233
221
  <div className="flex-1 flex gap-6 min-h-0 relative">
@@ -248,10 +236,10 @@ export function SessionsConfig() {
248
236
 
249
237
  <Select value={selectedChannel} onValueChange={setSelectedChannel}>
250
238
  <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" />
239
+ <SelectValue placeholder={t('sessionsAllChannels')} />
252
240
  </SelectTrigger>
253
241
  <SelectContent className="rounded-xl shadow-lg border-gray-100 max-w-[280px]">
254
- <SelectItem value="all" className="rounded-lg text-xs">All Channels</SelectItem>
242
+ <SelectItem value="all" className="rounded-lg text-xs">{t('sessionsAllChannels')}</SelectItem>
255
243
  {channels.map(c => (
256
244
  <SelectItem key={c} value={c} className="rounded-lg text-xs truncate pr-6">{displayChannelName(c)}</SelectItem>
257
245
  ))}
@@ -326,7 +314,7 @@ export function SessionsConfig() {
326
314
  <div className="flex items-center gap-2 shrink-0">
327
315
  <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
316
  <SettingsIcon className="w-3.5 h-3.5 mr-1.5" />
329
- Metadata
317
+ {t('sessionsMetadata')}
330
318
  </Button>
331
319
  <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
320
  {t('sessionsClearHistory')}
@@ -397,15 +385,15 @@ export function SessionsConfig() {
397
385
  <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
386
  <Inbox className="h-8 w-8 text-gray-300 -rotate-3" />
399
387
  </div>
400
- <h3 className="text-lg font-bold text-gray-900 mb-2">No Session Selected</h3>
388
+ <h3 className="text-lg font-bold text-gray-900 mb-2">{t('sessionsNoSelectionTitle')}</h3>
401
389
  <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.
390
+ {t('sessionsNoSelectionDescription')}
403
391
  </p>
404
392
  </div>
405
393
  )}
406
394
  </div>
407
395
  </div>
408
396
  <ConfirmDialog />
409
- </div>
397
+ </PageLayout>
410
398
  );
411
399
  }