@nextclaw/ui 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/{ChannelsList-ZBPiF0y2.js → ChannelsList-Byfj2R01.js} +1 -1
  3. package/dist/assets/{ChatPage-BOgoolWK.js → ChatPage-DM1ewbWf.js} +1 -1
  4. package/dist/assets/{DocBrowser-BUYNHg0Y.js → DocBrowser-BLv77lJ0.js} +1 -1
  5. package/dist/assets/{LogoBadge-DXPq99LJ.js → LogoBadge-D7j1al-w.js} +1 -1
  6. package/dist/assets/{MarketplacePage-Dx7nexYN.js → MarketplacePage-DuskLKYh.js} +1 -1
  7. package/dist/assets/{McpMarketplacePage-064wdotP.js → McpMarketplacePage-DpMjaD3m.js} +1 -1
  8. package/dist/assets/{ModelConfig-BDIfLesG.js → ModelConfig-ubaecweS.js} +1 -1
  9. package/dist/assets/{ProvidersList-DrlIr46m.js → ProvidersList-w8MJH2LI.js} +1 -1
  10. package/dist/assets/RemoteAccessPage-D79_5Kbn.js +1 -0
  11. package/dist/assets/{RuntimeConfig-BPxXEGzM.js → RuntimeConfig-BbX4yFKy.js} +1 -1
  12. package/dist/assets/{SearchConfig-BIqnlpne.js → SearchConfig-BmmmeyJd.js} +1 -1
  13. package/dist/assets/{SecretsConfig-jKZEVF2q.js → SecretsConfig-CWG8J01H.js} +1 -1
  14. package/dist/assets/{SessionsConfig-C_FXgVe1.js → SessionsConfig-D-vg_Lgv.js} +1 -1
  15. package/dist/assets/{chat-message-DmzpZJc_.js → chat-message-CGXiVhyN.js} +1 -1
  16. package/dist/assets/index-COrhpAdh.css +1 -0
  17. package/dist/assets/index-CeRbsQ90.js +8 -0
  18. package/dist/assets/{label-B1MloEtn.js → label-CCSffS1D.js} +1 -1
  19. package/dist/assets/{page-layout-BGg1EhM5.js → page-layout-ud8wZ8gX.js} +1 -1
  20. package/dist/assets/{popover-jJMv74Fp.js → popover-Bfoe6YBX.js} +1 -1
  21. package/dist/assets/{security-config-Boh9NIMz.js → security-config-DJJUCMov.js} +1 -1
  22. package/dist/assets/{skeleton-CmATs_b3.js → skeleton-IOOTmHzP.js} +1 -1
  23. package/dist/assets/{status-dot-DNyCdxPZ.js → status-dot-Fz9-eKsl.js} +1 -1
  24. package/dist/assets/{switch-DE_MYk7x.js → switch-B-_SrMSL.js} +1 -1
  25. package/dist/assets/{tabs-custom-B-zErYPr.js → tabs-custom-6Tm1ZHfS.js} +1 -1
  26. package/dist/assets/{useConfirmDialog-BqQ6QfhB.js → useConfirmDialog-BeOW2bOI.js} +1 -1
  27. package/dist/index.html +2 -2
  28. package/package.json +4 -4
  29. package/src/api/remote.ts +20 -0
  30. package/src/api/remote.types.ts +24 -0
  31. package/src/components/remote/RemoteAccessPage.tsx +392 -223
  32. package/src/hooks/useRemoteAccess.ts +28 -0
  33. package/src/lib/i18n.remote.ts +29 -2
  34. package/dist/assets/RemoteAccessPage-ZkUBA-Av.js +0 -1
  35. package/dist/assets/index-Byfw276e.js +0 -8
  36. package/dist/assets/index-bhNuQis7.css +0 -1
@@ -2,7 +2,8 @@ import { useEffect, useMemo, useState } from 'react';
2
2
  import type { RemoteRuntimeView, RemoteServiceView } from '@/api/types';
3
3
  import {
4
4
  useRemoteDoctor,
5
- useRemoteLogin,
5
+ useRemoteBrowserAuthPoll,
6
+ useRemoteBrowserAuthStart,
6
7
  useRemoteLogout,
7
8
  useRemoteServiceControl,
8
9
  useRemoteSettings,
@@ -17,6 +18,7 @@ import { StatusDot } from '@/components/ui/status-dot';
17
18
  import { Switch } from '@/components/ui/switch';
18
19
  import { formatDateTime, t } from '@/lib/i18n';
19
20
  import { Activity, KeyRound, Laptop, RefreshCcw, ServerCog, ShieldCheck, SquareTerminal } from 'lucide-react';
21
+ import type { RemoteAccessView, RemoteDoctorView } from '@/api/remote.types';
20
22
 
21
23
  function getRuntimeStatus(runtime: RemoteRuntimeView | null): { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string } {
22
24
  if (!runtime) {
@@ -56,9 +58,365 @@ function KeyValueRow(props: { label: string; value?: string | number | null; mut
56
58
  );
57
59
  }
58
60
 
61
+ function resolvePlatformApiBase(platformApiBase: string, status: RemoteAccessView | undefined) {
62
+ return platformApiBase.trim() || status?.settings.platformApiBase || status?.account.apiBase || undefined;
63
+ }
64
+
65
+ function useRemoteBrowserAuthFlow(props: {
66
+ status: RemoteAccessView | undefined;
67
+ platformApiBase: string;
68
+ startBrowserAuth: ReturnType<typeof useRemoteBrowserAuthStart>;
69
+ pollBrowserAuth: ReturnType<typeof useRemoteBrowserAuthPoll>;
70
+ }) {
71
+ const { status, platformApiBase, startBrowserAuth, pollBrowserAuth } = props;
72
+ const [authSessionId, setAuthSessionId] = useState<string | null>(null);
73
+ const [authVerificationUri, setAuthVerificationUri] = useState<string | null>(null);
74
+ const [authStatusMessage, setAuthStatusMessage] = useState('');
75
+ const [authExpiresAt, setAuthExpiresAt] = useState<string | null>(null);
76
+ const [authPollIntervalMs, setAuthPollIntervalMs] = useState(1500);
77
+
78
+ useEffect(() => {
79
+ if (!status?.account.loggedIn) {
80
+ return;
81
+ }
82
+ setAuthSessionId(null);
83
+ setAuthVerificationUri(null);
84
+ setAuthExpiresAt(null);
85
+ setAuthStatusMessage('');
86
+ setAuthPollIntervalMs(1500);
87
+ }, [status?.account.loggedIn]);
88
+
89
+ useEffect(() => {
90
+ if (!authSessionId || status?.account.loggedIn) {
91
+ return;
92
+ }
93
+
94
+ let cancelled = false;
95
+ const timerId = window.setTimeout(async () => {
96
+ try {
97
+ const result = await pollBrowserAuth.mutateAsync({
98
+ sessionId: authSessionId,
99
+ apiBase: resolvePlatformApiBase(platformApiBase, status)
100
+ });
101
+ if (cancelled) {
102
+ return;
103
+ }
104
+ if (result.status === 'pending') {
105
+ setAuthStatusMessage(t('remoteBrowserAuthWaiting'));
106
+ setAuthPollIntervalMs(result.nextPollMs ?? 1500);
107
+ return;
108
+ }
109
+ if (result.status === 'authorized') {
110
+ setAuthStatusMessage(t('remoteBrowserAuthCompleted'));
111
+ setAuthSessionId(null);
112
+ setAuthVerificationUri(null);
113
+ return;
114
+ }
115
+ setAuthStatusMessage(result.message || t('remoteBrowserAuthExpired'));
116
+ setAuthSessionId(null);
117
+ setAuthVerificationUri(null);
118
+ } catch {
119
+ if (cancelled) {
120
+ return;
121
+ }
122
+ setAuthSessionId(null);
123
+ setAuthVerificationUri(null);
124
+ }
125
+ }, authPollIntervalMs);
126
+
127
+ return () => {
128
+ cancelled = true;
129
+ window.clearTimeout(timerId);
130
+ };
131
+ }, [authPollIntervalMs, authSessionId, platformApiBase, pollBrowserAuth, status]);
132
+
133
+ const start = async () => {
134
+ const result = await startBrowserAuth.mutateAsync({
135
+ apiBase: resolvePlatformApiBase(platformApiBase, status)
136
+ });
137
+ setAuthSessionId(result.sessionId);
138
+ setAuthVerificationUri(result.verificationUri);
139
+ setAuthExpiresAt(result.expiresAt);
140
+ setAuthPollIntervalMs(result.intervalMs);
141
+ setAuthStatusMessage(t('remoteBrowserAuthWaiting'));
142
+ const opened = window.open(result.verificationUri, '_blank', 'noopener,noreferrer');
143
+ if (!opened) {
144
+ setAuthStatusMessage(t('remoteBrowserAuthPopupBlocked'));
145
+ }
146
+ };
147
+
148
+ const resume = () => {
149
+ if (!authVerificationUri) {
150
+ return;
151
+ }
152
+ window.open(authVerificationUri, '_blank', 'noopener,noreferrer');
153
+ };
154
+
155
+ return {
156
+ authExpiresAt,
157
+ authSessionId,
158
+ authStatusMessage,
159
+ authVerificationUri,
160
+ resume,
161
+ start
162
+ };
163
+ }
164
+
165
+ function RemoteOverviewCard(props: {
166
+ status: RemoteAccessView | undefined;
167
+ runtimeStatus: { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string };
168
+ serviceStatus: { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string };
169
+ }) {
170
+ return (
171
+ <Card>
172
+ <CardHeader>
173
+ <CardTitle className="flex items-center gap-2">
174
+ <Activity className="h-4 w-4 text-primary" />
175
+ {t('remoteOverviewTitle')}
176
+ </CardTitle>
177
+ <CardDescription>{t('remoteOverviewDescription')}</CardDescription>
178
+ </CardHeader>
179
+ <CardContent className="space-y-5">
180
+ <div className="flex flex-wrap gap-2">
181
+ <StatusDot status={props.status?.account.loggedIn ? 'ready' : 'inactive'} label={props.status?.account.loggedIn ? t('remoteAccountConnected') : t('remoteAccountNotConnected')} />
182
+ <StatusDot status={props.serviceStatus.status} label={props.serviceStatus.label} />
183
+ <StatusDot status={props.runtimeStatus.status} label={props.runtimeStatus.label} />
184
+ </div>
185
+
186
+ <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
187
+ <KeyValueRow label={t('remoteLocalOrigin')} value={props.status?.localOrigin} />
188
+ <KeyValueRow label={t('remotePublicPlatform')} value={props.status?.platformBase ?? props.status?.account.platformBase} />
189
+ <KeyValueRow label={t('remoteDeviceId')} value={props.status?.runtime?.deviceId} muted />
190
+ <KeyValueRow label={t('remoteLastConnectedAt')} value={props.status?.runtime?.lastConnectedAt ? formatDateTime(props.status.runtime.lastConnectedAt) : '-'} muted />
191
+ <KeyValueRow label={t('remoteLastError')} value={props.status?.runtime?.lastError} muted />
192
+ </div>
193
+ </CardContent>
194
+ </Card>
195
+ );
196
+ }
197
+
198
+ function RemoteDeviceCard(props: {
199
+ enabled: boolean;
200
+ setEnabled: (value: boolean) => void;
201
+ deviceName: string;
202
+ setDeviceName: (value: string) => void;
203
+ platformApiBase: string;
204
+ setPlatformApiBase: (value: string) => void;
205
+ settingsMutation: ReturnType<typeof useRemoteSettings>;
206
+ serviceMutation: ReturnType<typeof useRemoteServiceControl>;
207
+ }) {
208
+ return (
209
+ <Card>
210
+ <CardHeader>
211
+ <CardTitle className="flex items-center gap-2">
212
+ <Laptop className="h-4 w-4 text-primary" />
213
+ {t('remoteDeviceTitle')}
214
+ </CardTitle>
215
+ <CardDescription>{t('remoteDeviceDescription')}</CardDescription>
216
+ </CardHeader>
217
+ <CardContent className="space-y-4">
218
+ <div className="space-y-2">
219
+ <div className="flex items-center justify-between rounded-2xl border border-gray-200/70 px-4 py-3">
220
+ <div>
221
+ <p className="text-sm font-medium text-gray-900">{t('remoteEnabled')}</p>
222
+ <p className="mt-1 text-xs text-gray-500">{t('remoteEnabledHelp')}</p>
223
+ </div>
224
+ <Switch checked={props.enabled} onCheckedChange={props.setEnabled} />
225
+ </div>
226
+ </div>
227
+
228
+ <div className="space-y-2">
229
+ <Label htmlFor="remote-device-name">{t('remoteDeviceName')}</Label>
230
+ <Input id="remote-device-name" value={props.deviceName} onChange={(event) => props.setDeviceName(event.target.value)} placeholder={t('remoteDeviceNamePlaceholder')} />
231
+ </div>
232
+
233
+ <div className="space-y-2">
234
+ <Label htmlFor="remote-platform-api-base">{t('remotePlatformApiBase')}</Label>
235
+ <Input
236
+ id="remote-platform-api-base"
237
+ value={props.platformApiBase}
238
+ onChange={(event) => props.setPlatformApiBase(event.target.value)}
239
+ placeholder="https://ai-gateway-api.nextclaw.io/v1"
240
+ />
241
+ <p className="text-xs text-gray-500">{t('remotePlatformApiBaseHelp')}</p>
242
+ </div>
243
+
244
+ <div className="flex flex-wrap gap-3">
245
+ <Button
246
+ onClick={() =>
247
+ props.settingsMutation.mutate({
248
+ enabled: props.enabled,
249
+ deviceName: props.deviceName,
250
+ platformApiBase: props.platformApiBase
251
+ })
252
+ }
253
+ disabled={props.settingsMutation.isPending}
254
+ >
255
+ {props.settingsMutation.isPending ? t('saving') : t('remoteSaveSettings')}
256
+ </Button>
257
+ <Button variant="outline" onClick={() => props.serviceMutation.mutate('restart')} disabled={props.serviceMutation.isPending}>
258
+ <RefreshCcw className="mr-2 h-4 w-4" />
259
+ {t('remoteRestartService')}
260
+ </Button>
261
+ </div>
262
+ <p className="text-xs text-gray-500">{t('remoteSaveHint')}</p>
263
+ </CardContent>
264
+ </Card>
265
+ );
266
+ }
267
+
268
+ function RemoteAccountCard(props: {
269
+ status: RemoteAccessView | undefined;
270
+ platformApiBase: string;
271
+ browserAuthStartMutation: ReturnType<typeof useRemoteBrowserAuthStart>;
272
+ logoutMutation: ReturnType<typeof useRemoteLogout>;
273
+ authSessionId: string | null;
274
+ authExpiresAt: string | null;
275
+ authStatusMessage: string;
276
+ authVerificationUri: string | null;
277
+ onStartBrowserAuth: () => Promise<void>;
278
+ onResumeBrowserAuth: () => void;
279
+ }) {
280
+ return (
281
+ <Card>
282
+ <CardHeader>
283
+ <CardTitle className="flex items-center gap-2">
284
+ <KeyRound className="h-4 w-4 text-primary" />
285
+ {t('remoteAccountTitle')}
286
+ </CardTitle>
287
+ <CardDescription>{t('remoteAccountDescription')}</CardDescription>
288
+ </CardHeader>
289
+ <CardContent className="space-y-4">
290
+ {props.status?.account.loggedIn ? (
291
+ <>
292
+ <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
293
+ <KeyValueRow label={t('remoteAccountEmail')} value={props.status.account.email} />
294
+ <KeyValueRow label={t('remoteAccountRole')} value={props.status.account.role} />
295
+ <KeyValueRow label={t('remoteApiBase')} value={props.status.account.apiBase} />
296
+ </div>
297
+ <Button variant="outline" onClick={() => props.logoutMutation.mutate()} disabled={props.logoutMutation.isPending}>
298
+ {props.logoutMutation.isPending ? t('remoteLoggingOut') : t('remoteLogout')}
299
+ </Button>
300
+ </>
301
+ ) : (
302
+ <>
303
+ <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
304
+ <p className="text-sm font-medium text-gray-900">{t('remoteBrowserAuthTitle')}</p>
305
+ <p className="mt-1 text-sm text-gray-600">{t('remoteBrowserAuthDescription')}</p>
306
+ <div className="mt-3 border-t border-white/80 pt-3">
307
+ <KeyValueRow label={t('remoteApiBase')} value={props.platformApiBase || props.status?.settings.platformApiBase || props.status?.account.apiBase} muted />
308
+ <KeyValueRow label={t('remoteBrowserAuthSession')} value={props.authSessionId} muted />
309
+ <KeyValueRow label={t('remoteBrowserAuthExpiresAt')} value={props.authExpiresAt ? formatDateTime(props.authExpiresAt) : '-'} muted />
310
+ </div>
311
+ </div>
312
+ {props.authStatusMessage ? <p className="text-sm text-gray-600">{props.authStatusMessage}</p> : null}
313
+ <div className="flex flex-wrap gap-3">
314
+ <Button onClick={props.onStartBrowserAuth} disabled={props.browserAuthStartMutation.isPending || !!props.authSessionId}>
315
+ {props.browserAuthStartMutation.isPending
316
+ ? t('remoteBrowserAuthStarting')
317
+ : props.authSessionId
318
+ ? t('remoteBrowserAuthAuthorizing')
319
+ : t('remoteBrowserAuthAction')}
320
+ </Button>
321
+ {props.authVerificationUri ? (
322
+ <Button variant="outline" onClick={props.onResumeBrowserAuth}>
323
+ {t('remoteBrowserAuthResume')}
324
+ </Button>
325
+ ) : null}
326
+ </div>
327
+ <p className="text-xs text-gray-500">{t('remoteBrowserAuthHint')}</p>
328
+ </>
329
+ )}
330
+ </CardContent>
331
+ </Card>
332
+ );
333
+ }
334
+
335
+ function RemoteServiceCard(props: {
336
+ status: RemoteAccessView | undefined;
337
+ serviceMutation: ReturnType<typeof useRemoteServiceControl>;
338
+ }) {
339
+ return (
340
+ <Card>
341
+ <CardHeader>
342
+ <CardTitle className="flex items-center gap-2">
343
+ <ServerCog className="h-4 w-4 text-primary" />
344
+ {t('remoteServiceTitle')}
345
+ </CardTitle>
346
+ <CardDescription>{t('remoteServiceDescription')}</CardDescription>
347
+ </CardHeader>
348
+ <CardContent className="space-y-4">
349
+ <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
350
+ <KeyValueRow label={t('remoteServicePid')} value={props.status?.service.pid} />
351
+ <KeyValueRow label={t('remoteServiceUiUrl')} value={props.status?.service.uiUrl} />
352
+ <KeyValueRow label={t('remoteServiceCurrentProcess')} value={props.status?.service.currentProcess ? t('yes') : t('no')} />
353
+ </div>
354
+ <div className="flex flex-wrap gap-3">
355
+ <Button variant="primary" onClick={() => props.serviceMutation.mutate('start')} disabled={props.serviceMutation.isPending}>
356
+ {t('remoteStartService')}
357
+ </Button>
358
+ <Button variant="outline" onClick={() => props.serviceMutation.mutate('restart')} disabled={props.serviceMutation.isPending}>
359
+ {t('remoteRestartService')}
360
+ </Button>
361
+ <Button variant="outline" onClick={() => props.serviceMutation.mutate('stop')} disabled={props.serviceMutation.isPending}>
362
+ {t('remoteStopService')}
363
+ </Button>
364
+ </div>
365
+ <p className="text-xs text-gray-500">{t('remoteServiceHint')}</p>
366
+ </CardContent>
367
+ </Card>
368
+ );
369
+ }
370
+
371
+ function RemoteDoctorCard(props: {
372
+ doctorMutation: ReturnType<typeof useRemoteDoctor>;
373
+ }) {
374
+ return (
375
+ <Card>
376
+ <CardHeader>
377
+ <CardTitle className="flex items-center gap-2">
378
+ <ShieldCheck className="h-4 w-4 text-primary" />
379
+ {t('remoteDoctorTitle')}
380
+ </CardTitle>
381
+ <CardDescription>{t('remoteDoctorDescription')}</CardDescription>
382
+ </CardHeader>
383
+ <CardContent className="space-y-4">
384
+ <div className="flex flex-wrap gap-3">
385
+ <Button variant="outline" onClick={() => props.doctorMutation.mutate()} disabled={props.doctorMutation.isPending}>
386
+ <SquareTerminal className="mr-2 h-4 w-4" />
387
+ {props.doctorMutation.isPending ? t('remoteDoctorRunning') : t('remoteRunDoctor')}
388
+ </Button>
389
+ </div>
390
+
391
+ {props.doctorMutation.data ? <DoctorResultPanel doctor={props.doctorMutation.data} /> : <p className="text-sm text-gray-500">{t('remoteDoctorEmpty')}</p>}
392
+ </CardContent>
393
+ </Card>
394
+ );
395
+ }
396
+
397
+ function DoctorResultPanel(props: { doctor: RemoteDoctorView }) {
398
+ return (
399
+ <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
400
+ <KeyValueRow label={t('remoteDoctorGeneratedAt')} value={formatDateTime(props.doctor.generatedAt)} muted />
401
+ <div className="mt-3 space-y-2">
402
+ {props.doctor.checks.map((check) => (
403
+ <div key={check.name} className="rounded-xl border border-white bg-white px-3 py-3">
404
+ <div className="flex items-center justify-between gap-3">
405
+ <span className="text-sm font-medium text-gray-900">{check.name}</span>
406
+ <StatusDot status={check.ok ? 'ready' : 'warning'} label={check.ok ? t('remoteCheckPassed') : t('remoteCheckFailed')} />
407
+ </div>
408
+ <p className="mt-2 text-sm text-gray-600">{check.detail}</p>
409
+ </div>
410
+ ))}
411
+ </div>
412
+ </div>
413
+ );
414
+ }
415
+
59
416
  export function RemoteAccessPage() {
60
417
  const remoteStatus = useRemoteStatus();
61
- const loginMutation = useRemoteLogin();
418
+ const browserAuthStartMutation = useRemoteBrowserAuthStart();
419
+ const browserAuthPollMutation = useRemoteBrowserAuthPoll();
62
420
  const logoutMutation = useRemoteLogout();
63
421
  const settingsMutation = useRemoteSettings();
64
422
  const doctorMutation = useRemoteDoctor();
@@ -68,13 +426,15 @@ export function RemoteAccessPage() {
68
426
  const runtimeStatus = useMemo(() => getRuntimeStatus(status?.runtime ?? null), [status?.runtime]);
69
427
  const serviceStatus = useMemo(() => getServiceStatus(status?.service ?? { running: false, currentProcess: false }), [status?.service]);
70
428
 
71
- const [email, setEmail] = useState('');
72
- const [password, setPassword] = useState('');
73
- const [loginApiBase, setLoginApiBase] = useState('');
74
- const [register, setRegister] = useState(false);
75
429
  const [enabled, setEnabled] = useState(false);
76
430
  const [deviceName, setDeviceName] = useState('');
77
431
  const [platformApiBase, setPlatformApiBase] = useState('');
432
+ const browserAuth = useRemoteBrowserAuthFlow({
433
+ status,
434
+ platformApiBase,
435
+ startBrowserAuth: browserAuthStartMutation,
436
+ pollBrowserAuth: browserAuthPollMutation
437
+ });
78
438
 
79
439
  useEffect(() => {
80
440
  if (!status) {
@@ -83,10 +443,7 @@ export function RemoteAccessPage() {
83
443
  setEnabled(status.settings.enabled);
84
444
  setDeviceName(status.settings.deviceName);
85
445
  setPlatformApiBase(status.settings.platformApiBase);
86
- if (!loginApiBase) {
87
- setLoginApiBase(status.account.apiBase ?? status.settings.platformApiBase ?? '');
88
- }
89
- }, [loginApiBase, status]);
446
+ }, [status]);
90
447
 
91
448
  if (remoteStatus.isLoading && !status) {
92
449
  return <div className="p-8 text-gray-400">{t('remoteLoading')}</div>;
@@ -97,224 +454,36 @@ export function RemoteAccessPage() {
97
454
  <PageHeader title={t('remotePageTitle')} description={t('remotePageDescription')} />
98
455
 
99
456
  <div className="grid gap-6 xl:grid-cols-[1.35fr_0.95fr]">
100
- <Card>
101
- <CardHeader>
102
- <CardTitle className="flex items-center gap-2">
103
- <Activity className="h-4 w-4 text-primary" />
104
- {t('remoteOverviewTitle')}
105
- </CardTitle>
106
- <CardDescription>{t('remoteOverviewDescription')}</CardDescription>
107
- </CardHeader>
108
- <CardContent className="space-y-5">
109
- <div className="flex flex-wrap gap-2">
110
- <StatusDot status={status?.account.loggedIn ? 'ready' : 'inactive'} label={status?.account.loggedIn ? t('remoteAccountConnected') : t('remoteAccountNotConnected')} />
111
- <StatusDot status={serviceStatus.status} label={serviceStatus.label} />
112
- <StatusDot status={runtimeStatus.status} label={runtimeStatus.label} />
113
- </div>
114
-
115
- <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
116
- <KeyValueRow label={t('remoteLocalOrigin')} value={status?.localOrigin} />
117
- <KeyValueRow label={t('remotePublicPlatform')} value={status?.platformBase ?? status?.account.platformBase} />
118
- <KeyValueRow label={t('remoteDeviceId')} value={status?.runtime?.deviceId} muted />
119
- <KeyValueRow label={t('remoteLastConnectedAt')} value={status?.runtime?.lastConnectedAt ? formatDateTime(status.runtime.lastConnectedAt) : '-'} muted />
120
- <KeyValueRow label={t('remoteLastError')} value={status?.runtime?.lastError} muted />
121
- </div>
122
- </CardContent>
123
- </Card>
124
-
125
- <Card>
126
- <CardHeader>
127
- <CardTitle className="flex items-center gap-2">
128
- <Laptop className="h-4 w-4 text-primary" />
129
- {t('remoteDeviceTitle')}
130
- </CardTitle>
131
- <CardDescription>{t('remoteDeviceDescription')}</CardDescription>
132
- </CardHeader>
133
- <CardContent className="space-y-4">
134
- <div className="space-y-2">
135
- <div className="flex items-center justify-between rounded-2xl border border-gray-200/70 px-4 py-3">
136
- <div>
137
- <p className="text-sm font-medium text-gray-900">{t('remoteEnabled')}</p>
138
- <p className="mt-1 text-xs text-gray-500">{t('remoteEnabledHelp')}</p>
139
- </div>
140
- <Switch checked={enabled} onCheckedChange={setEnabled} />
141
- </div>
142
- </div>
143
-
144
- <div className="space-y-2">
145
- <Label htmlFor="remote-device-name">{t('remoteDeviceName')}</Label>
146
- <Input id="remote-device-name" value={deviceName} onChange={(event) => setDeviceName(event.target.value)} placeholder={t('remoteDeviceNamePlaceholder')} />
147
- </div>
148
-
149
- <div className="space-y-2">
150
- <Label htmlFor="remote-platform-api-base">{t('remotePlatformApiBase')}</Label>
151
- <Input
152
- id="remote-platform-api-base"
153
- value={platformApiBase}
154
- onChange={(event) => setPlatformApiBase(event.target.value)}
155
- placeholder="https://ai-gateway-api.nextclaw.io/v1"
156
- />
157
- <p className="text-xs text-gray-500">{t('remotePlatformApiBaseHelp')}</p>
158
- </div>
159
-
160
- <div className="flex flex-wrap gap-3">
161
- <Button
162
- onClick={() =>
163
- settingsMutation.mutate({
164
- enabled,
165
- deviceName,
166
- platformApiBase
167
- })
168
- }
169
- disabled={settingsMutation.isPending}
170
- >
171
- {settingsMutation.isPending ? t('saving') : t('remoteSaveSettings')}
172
- </Button>
173
- <Button
174
- variant="outline"
175
- onClick={() => serviceMutation.mutate('restart')}
176
- disabled={serviceMutation.isPending}
177
- >
178
- <RefreshCcw className="mr-2 h-4 w-4" />
179
- {t('remoteRestartService')}
180
- </Button>
181
- </div>
182
- <p className="text-xs text-gray-500">{t('remoteSaveHint')}</p>
183
- </CardContent>
184
- </Card>
457
+ <RemoteOverviewCard status={status} runtimeStatus={runtimeStatus} serviceStatus={serviceStatus} />
458
+ <RemoteDeviceCard
459
+ enabled={enabled}
460
+ setEnabled={setEnabled}
461
+ deviceName={deviceName}
462
+ setDeviceName={setDeviceName}
463
+ platformApiBase={platformApiBase}
464
+ setPlatformApiBase={setPlatformApiBase}
465
+ settingsMutation={settingsMutation}
466
+ serviceMutation={serviceMutation}
467
+ />
185
468
  </div>
186
469
 
187
470
  <div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
188
- <Card>
189
- <CardHeader>
190
- <CardTitle className="flex items-center gap-2">
191
- <KeyRound className="h-4 w-4 text-primary" />
192
- {t('remoteAccountTitle')}
193
- </CardTitle>
194
- <CardDescription>{t('remoteAccountDescription')}</CardDescription>
195
- </CardHeader>
196
- <CardContent className="space-y-4">
197
- {status?.account.loggedIn ? (
198
- <>
199
- <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
200
- <KeyValueRow label={t('remoteAccountEmail')} value={status.account.email} />
201
- <KeyValueRow label={t('remoteAccountRole')} value={status.account.role} />
202
- <KeyValueRow label={t('remoteApiBase')} value={status.account.apiBase} />
203
- </div>
204
- <Button variant="outline" onClick={() => logoutMutation.mutate()} disabled={logoutMutation.isPending}>
205
- {logoutMutation.isPending ? t('remoteLoggingOut') : t('remoteLogout')}
206
- </Button>
207
- </>
208
- ) : (
209
- <>
210
- <div className="space-y-2">
211
- <Label htmlFor="remote-email">{t('remoteEmail')}</Label>
212
- <Input id="remote-email" value={email} onChange={(event) => setEmail(event.target.value)} placeholder="name@example.com" />
213
- </div>
214
- <div className="space-y-2">
215
- <Label htmlFor="remote-password">{t('remotePassword')}</Label>
216
- <Input id="remote-password" type="password" value={password} onChange={(event) => setPassword(event.target.value)} placeholder={t('remotePasswordPlaceholder')} />
217
- </div>
218
- <div className="space-y-2">
219
- <Label htmlFor="remote-login-api-base">{t('remoteApiBase')}</Label>
220
- <Input
221
- id="remote-login-api-base"
222
- value={loginApiBase}
223
- onChange={(event) => setLoginApiBase(event.target.value)}
224
- placeholder="https://ai-gateway-api.nextclaw.io/v1"
225
- />
226
- </div>
227
- <div className="flex items-center justify-between rounded-2xl border border-gray-200/70 px-4 py-3">
228
- <div>
229
- <p className="text-sm font-medium text-gray-900">{t('remoteRegisterIfNeeded')}</p>
230
- <p className="mt-1 text-xs text-gray-500">{t('remoteRegisterIfNeededHelp')}</p>
231
- </div>
232
- <Switch checked={register} onCheckedChange={setRegister} />
233
- </div>
234
- <Button
235
- onClick={() =>
236
- loginMutation.mutate({
237
- email,
238
- password,
239
- apiBase: loginApiBase,
240
- register
241
- })
242
- }
243
- disabled={loginMutation.isPending || !email.trim() || !password}
244
- >
245
- {loginMutation.isPending ? t('remoteLoggingIn') : register ? t('remoteCreateAccount') : t('remoteLogin')}
246
- </Button>
247
- </>
248
- )}
249
- </CardContent>
250
- </Card>
251
-
252
- <Card>
253
- <CardHeader>
254
- <CardTitle className="flex items-center gap-2">
255
- <ServerCog className="h-4 w-4 text-primary" />
256
- {t('remoteServiceTitle')}
257
- </CardTitle>
258
- <CardDescription>{t('remoteServiceDescription')}</CardDescription>
259
- </CardHeader>
260
- <CardContent className="space-y-4">
261
- <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
262
- <KeyValueRow label={t('remoteServicePid')} value={status?.service.pid} />
263
- <KeyValueRow label={t('remoteServiceUiUrl')} value={status?.service.uiUrl} />
264
- <KeyValueRow label={t('remoteServiceCurrentProcess')} value={status?.service.currentProcess ? t('yes') : t('no')} />
265
- </div>
266
- <div className="flex flex-wrap gap-3">
267
- <Button variant="primary" onClick={() => serviceMutation.mutate('start')} disabled={serviceMutation.isPending}>
268
- {t('remoteStartService')}
269
- </Button>
270
- <Button variant="outline" onClick={() => serviceMutation.mutate('restart')} disabled={serviceMutation.isPending}>
271
- {t('remoteRestartService')}
272
- </Button>
273
- <Button variant="outline" onClick={() => serviceMutation.mutate('stop')} disabled={serviceMutation.isPending}>
274
- {t('remoteStopService')}
275
- </Button>
276
- </div>
277
- <p className="text-xs text-gray-500">{t('remoteServiceHint')}</p>
278
- </CardContent>
279
- </Card>
471
+ <RemoteAccountCard
472
+ status={status}
473
+ platformApiBase={platformApiBase}
474
+ browserAuthStartMutation={browserAuthStartMutation}
475
+ logoutMutation={logoutMutation}
476
+ authSessionId={browserAuth.authSessionId}
477
+ authExpiresAt={browserAuth.authExpiresAt}
478
+ authStatusMessage={browserAuth.authStatusMessage}
479
+ authVerificationUri={browserAuth.authVerificationUri}
480
+ onStartBrowserAuth={browserAuth.start}
481
+ onResumeBrowserAuth={browserAuth.resume}
482
+ />
483
+ <RemoteServiceCard status={status} serviceMutation={serviceMutation} />
280
484
  </div>
281
485
 
282
- <Card>
283
- <CardHeader>
284
- <CardTitle className="flex items-center gap-2">
285
- <ShieldCheck className="h-4 w-4 text-primary" />
286
- {t('remoteDoctorTitle')}
287
- </CardTitle>
288
- <CardDescription>{t('remoteDoctorDescription')}</CardDescription>
289
- </CardHeader>
290
- <CardContent className="space-y-4">
291
- <div className="flex flex-wrap gap-3">
292
- <Button variant="outline" onClick={() => doctorMutation.mutate()} disabled={doctorMutation.isPending}>
293
- <SquareTerminal className="mr-2 h-4 w-4" />
294
- {doctorMutation.isPending ? t('remoteDoctorRunning') : t('remoteRunDoctor')}
295
- </Button>
296
- </div>
297
-
298
- {doctorMutation.data ? (
299
- <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
300
- <KeyValueRow label={t('remoteDoctorGeneratedAt')} value={formatDateTime(doctorMutation.data.generatedAt)} muted />
301
- <div className="mt-3 space-y-2">
302
- {doctorMutation.data.checks.map((check) => (
303
- <div key={check.name} className="rounded-xl border border-white bg-white px-3 py-3">
304
- <div className="flex items-center justify-between gap-3">
305
- <span className="text-sm font-medium text-gray-900">{check.name}</span>
306
- <StatusDot status={check.ok ? 'ready' : 'warning'} label={check.ok ? t('remoteCheckPassed') : t('remoteCheckFailed')} />
307
- </div>
308
- <p className="mt-2 text-sm text-gray-600">{check.detail}</p>
309
- </div>
310
- ))}
311
- </div>
312
- </div>
313
- ) : (
314
- <p className="text-sm text-gray-500">{t('remoteDoctorEmpty')}</p>
315
- )}
316
- </CardContent>
317
- </Card>
486
+ <RemoteDoctorCard doctorMutation={doctorMutation} />
318
487
  </PageLayout>
319
488
  );
320
489
  }