@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.
- package/CHANGELOG.md +12 -0
- package/dist/assets/index-BGzsyzDd.css +1 -0
- package/dist/assets/index-BHB8zYn7.js +342 -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 +22 -23
- package/src/components/config/CronConfig.tsx +20 -30
- package/src/components/config/ModelConfig.tsx +11 -12
- package/src/components/config/ProviderForm.tsx +3 -3
- package/src/components/config/ProvidersList.tsx +13 -13
- package/src/components/config/RuntimeConfig.tsx +40 -43
- package/src/components/config/SessionsConfig.tsx +12 -24
- package/src/components/layout/Sidebar.tsx +70 -40
- package/src/components/layout/page-layout.tsx +71 -0
- package/src/components/marketplace/MarketplacePage.tsx +34 -35
- 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,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
|
-
<
|
|
71
|
-
<
|
|
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">
|
|
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">
|
|
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">
|
|
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 ?? '
|
|
141
|
+
{maxTokensHint?.label ?? t('maxTokens')}
|
|
143
142
|
</Label>
|
|
144
|
-
<span className="text-sm font-semibold text-gray-900">{maxTokens
|
|
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
|
-
'
|
|
167
|
+
t('saveChanges')
|
|
169
168
|
)}
|
|
170
169
|
</Button>
|
|
171
170
|
</div>
|
|
172
171
|
</form>
|
|
173
|
-
</
|
|
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>
|
|
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,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">
|
|
26
|
+
return <div className="p-8">{t('providersLoading')}</div>;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
const tabs = [
|
|
28
|
-
{ id: 'installed', label: '
|
|
29
|
-
{ id: 'all', label: '
|
|
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
|
-
<
|
|
38
|
-
<
|
|
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 || '
|
|
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 ? '
|
|
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 ? '
|
|
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
|
-
|
|
100
|
+
{t('providersEmptyTitle')}
|
|
101
101
|
</h3>
|
|
102
102
|
<p className="text-[13px] text-gray-400 max-w-sm">
|
|
103
|
-
|
|
103
|
+
{t('providersEmptyDescription')}
|
|
104
104
|
</p>
|
|
105
105
|
</div>
|
|
106
106
|
)}
|
|
107
107
|
|
|
108
108
|
<ProviderForm />
|
|
109
|
-
</
|
|
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(
|
|
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(
|
|
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(
|
|
174
|
+
throw new Error(t('bindingAgentIdRequired').replace('{index}', String(index)));
|
|
173
175
|
}
|
|
174
176
|
if (!knownAgentIds.has(agentId)) {
|
|
175
|
-
throw new Error(
|
|
177
|
+
throw new Error(`${t('bindingAgentIdNotFound').replace('{index}', String(index))}: ${agentId}`);
|
|
176
178
|
}
|
|
177
179
|
if (!channel) {
|
|
178
|
-
throw new Error(
|
|
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(
|
|
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">
|
|
231
|
+
return <div className="p-8 text-gray-400">{t('runtimeLoading')}</div>;
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
return (
|
|
233
|
-
<
|
|
234
|
-
<
|
|
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 ?? '
|
|
244
|
-
<CardDescription>{dmScopeHint?.help ?? '
|
|
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 ?? '
|
|
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 ?? '
|
|
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 ?? '
|
|
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 ?? '
|
|
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 ?? '
|
|
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 ?? '
|
|
298
|
-
<CardDescription>{agentsHint?.help ?? '
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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 ?? '
|
|
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=
|
|
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>
|
|
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
|
-
|
|
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
|
-
|
|
382
|
+
{t('addAgent')}
|
|
386
383
|
</Button>
|
|
387
384
|
</CardContent>
|
|
388
385
|
</Card>
|
|
389
386
|
|
|
390
387
|
<Card>
|
|
391
388
|
<CardHeader>
|
|
392
|
-
<CardTitle>{bindingsHint?.label ?? '
|
|
389
|
+
<CardTitle>{bindingsHint?.label ?? t('bindings')}</CardTitle>
|
|
393
390
|
<CardDescription>
|
|
394
|
-
{bindingsHint?.help ?? '
|
|
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=
|
|
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=
|
|
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=
|
|
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__">
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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 ? '
|
|
511
|
+
{updateRuntime.isPending ? t('saving') : t('saveRuntimeSettings')}
|
|
515
512
|
</Button>
|
|
516
513
|
</div>
|
|
517
|
-
</
|
|
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
|
-
|
|
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]">{
|
|
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
|
-
<
|
|
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=
|
|
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">
|
|
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
|
-
|
|
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">
|
|
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
|
-
|
|
390
|
+
{t('sessionsNoSelectionDescription')}
|
|
403
391
|
</p>
|
|
404
392
|
</div>
|
|
405
393
|
)}
|
|
406
394
|
</div>
|
|
407
395
|
</div>
|
|
408
396
|
<ConfirmDialog />
|
|
409
|
-
</
|
|
397
|
+
</PageLayout>
|
|
410
398
|
);
|
|
411
399
|
}
|