@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.
- package/CHANGELOG.md +12 -0
- package/dist/assets/{ChannelsList-ZBPiF0y2.js → ChannelsList-Byfj2R01.js} +1 -1
- package/dist/assets/{ChatPage-BOgoolWK.js → ChatPage-DM1ewbWf.js} +1 -1
- package/dist/assets/{DocBrowser-BUYNHg0Y.js → DocBrowser-BLv77lJ0.js} +1 -1
- package/dist/assets/{LogoBadge-DXPq99LJ.js → LogoBadge-D7j1al-w.js} +1 -1
- package/dist/assets/{MarketplacePage-Dx7nexYN.js → MarketplacePage-DuskLKYh.js} +1 -1
- package/dist/assets/{McpMarketplacePage-064wdotP.js → McpMarketplacePage-DpMjaD3m.js} +1 -1
- package/dist/assets/{ModelConfig-BDIfLesG.js → ModelConfig-ubaecweS.js} +1 -1
- package/dist/assets/{ProvidersList-DrlIr46m.js → ProvidersList-w8MJH2LI.js} +1 -1
- package/dist/assets/RemoteAccessPage-D79_5Kbn.js +1 -0
- package/dist/assets/{RuntimeConfig-BPxXEGzM.js → RuntimeConfig-BbX4yFKy.js} +1 -1
- package/dist/assets/{SearchConfig-BIqnlpne.js → SearchConfig-BmmmeyJd.js} +1 -1
- package/dist/assets/{SecretsConfig-jKZEVF2q.js → SecretsConfig-CWG8J01H.js} +1 -1
- package/dist/assets/{SessionsConfig-C_FXgVe1.js → SessionsConfig-D-vg_Lgv.js} +1 -1
- package/dist/assets/{chat-message-DmzpZJc_.js → chat-message-CGXiVhyN.js} +1 -1
- package/dist/assets/index-COrhpAdh.css +1 -0
- package/dist/assets/index-CeRbsQ90.js +8 -0
- package/dist/assets/{label-B1MloEtn.js → label-CCSffS1D.js} +1 -1
- package/dist/assets/{page-layout-BGg1EhM5.js → page-layout-ud8wZ8gX.js} +1 -1
- package/dist/assets/{popover-jJMv74Fp.js → popover-Bfoe6YBX.js} +1 -1
- package/dist/assets/{security-config-Boh9NIMz.js → security-config-DJJUCMov.js} +1 -1
- package/dist/assets/{skeleton-CmATs_b3.js → skeleton-IOOTmHzP.js} +1 -1
- package/dist/assets/{status-dot-DNyCdxPZ.js → status-dot-Fz9-eKsl.js} +1 -1
- package/dist/assets/{switch-DE_MYk7x.js → switch-B-_SrMSL.js} +1 -1
- package/dist/assets/{tabs-custom-B-zErYPr.js → tabs-custom-6Tm1ZHfS.js} +1 -1
- package/dist/assets/{useConfirmDialog-BqQ6QfhB.js → useConfirmDialog-BeOW2bOI.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/api/remote.ts +20 -0
- package/src/api/remote.types.ts +24 -0
- package/src/components/remote/RemoteAccessPage.tsx +392 -223
- package/src/hooks/useRemoteAccess.ts +28 -0
- package/src/lib/i18n.remote.ts +29 -2
- package/dist/assets/RemoteAccessPage-ZkUBA-Av.js +0 -1
- package/dist/assets/index-Byfw276e.js +0 -8
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
<
|
|
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
|
}
|