@nextclaw/ui 0.9.5 → 0.9.7
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 +13 -1
- package/dist/assets/ChannelsList-BEbqjBdM.js +1 -0
- package/dist/assets/{ChatPage-DM1ewbWf.js → ChatPage-BaUQCtqU.js} +2 -2
- package/dist/assets/{DocBrowser-BLv77lJ0.js → DocBrowser-CtDiU1-0.js} +1 -1
- package/dist/assets/{LogoBadge-D7j1al-w.js → LogoBadge-Cy-EHxZ6.js} +1 -1
- package/dist/assets/MarketplacePage-D_r1Xy_C.js +49 -0
- package/dist/assets/{McpMarketplacePage-DpMjaD3m.js → McpMarketplacePage-Bftp9zkB.js} +2 -2
- package/dist/assets/ModelConfig-CdFIVFue.js +1 -0
- package/dist/assets/ProvidersList-Bx3w67f2.js +1 -0
- package/dist/assets/RemoteAccessPage-3JbDaO3y.js +1 -0
- package/dist/assets/{RuntimeConfig-BbX4yFKy.js → RuntimeConfig-V2C7bIJt.js} +1 -1
- package/dist/assets/{SearchConfig-BmmmeyJd.js → SearchConfig-Wq9VNK2x.js} +1 -1
- package/dist/assets/{SecretsConfig-CWG8J01H.js → SecretsConfig-DuYOs4z3.js} +2 -2
- package/dist/assets/SessionsConfig-fuPlbwC7.js +2 -0
- package/dist/assets/{chat-message-CGXiVhyN.js → chat-message-D729qtQ5.js} +1 -1
- package/dist/assets/index-D3eWL7gT.js +8 -0
- package/dist/assets/index-DfEAJJsA.css +1 -0
- package/dist/assets/{label-CCSffS1D.js → label-DCtPX8p3.js} +1 -1
- package/dist/assets/{page-layout-ud8wZ8gX.js → page-layout-Dr5ymRVA.js} +1 -1
- package/dist/assets/popover-YWmgsAlv.js +1 -0
- package/dist/assets/{security-config-DJJUCMov.js → security-config-bwP5X0a-.js} +1 -1
- package/dist/assets/skeleton-C4iXwmBW.js +1 -0
- package/dist/assets/{status-dot-Fz9-eKsl.js → status-dot-SDG0WNvL.js} +1 -1
- package/dist/assets/{switch-B-_SrMSL.js → switch-lC_seKUL.js} +1 -1
- package/dist/assets/{tabs-custom-6Tm1ZHfS.js → tabs-custom-LAWD0u69.js} +1 -1
- package/dist/assets/useConfirmDialog-D0akncY4.js +1 -0
- package/dist/assets/vendor-CmQZsDAE.js +436 -0
- package/dist/index.html +3 -3
- package/package.json +4 -4
- package/src/App.tsx +36 -39
- package/src/account/components/account-panel.tsx +93 -0
- package/src/account/managers/account.manager.ts +179 -0
- package/src/account/stores/account.store.ts +68 -0
- package/src/app-query-client.ts +10 -0
- package/src/components/layout/Sidebar.tsx +26 -0
- package/src/components/remote/RemoteAccessPage.tsx +162 -442
- package/src/hooks/useRemoteAccess.ts +7 -6
- package/src/lib/i18n.remote.ts +108 -4
- package/src/presenter/app-presenter-context.tsx +20 -0
- package/src/presenter/app.presenter.ts +12 -0
- package/src/remote/managers/remote-access.manager.ts +196 -0
- package/src/remote/remote-access.query.ts +78 -0
- package/src/remote/stores/remote-access.store.ts +44 -0
- package/dist/assets/ChannelsList-Byfj2R01.js +0 -1
- package/dist/assets/MarketplacePage-DuskLKYh.js +0 -49
- package/dist/assets/ModelConfig-ubaecweS.js +0 -1
- package/dist/assets/ProvidersList-w8MJH2LI.js +0 -1
- package/dist/assets/RemoteAccessPage-D79_5Kbn.js +0 -1
- package/dist/assets/SessionsConfig-D-vg_Lgv.js +0 -2
- package/dist/assets/index-COrhpAdh.css +0 -1
- package/dist/assets/index-CeRbsQ90.js +0 -8
- package/dist/assets/index-Ct7FQpxN.js +0 -1
- package/dist/assets/popover-Bfoe6YBX.js +0 -1
- package/dist/assets/skeleton-IOOTmHzP.js +0 -1
- package/dist/assets/useConfirmDialog-BeOW2bOI.js +0 -5
- package/dist/assets/vendor-CwsIoNvJ.js +0 -442
|
@@ -1,449 +1,112 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { RemoteRuntimeView, RemoteServiceView } from '@/api/types';
|
|
3
|
-
import {
|
|
4
|
-
useRemoteDoctor,
|
|
5
|
-
useRemoteBrowserAuthPoll,
|
|
6
|
-
useRemoteBrowserAuthStart,
|
|
7
|
-
useRemoteLogout,
|
|
8
|
-
useRemoteServiceControl,
|
|
9
|
-
useRemoteSettings,
|
|
10
|
-
useRemoteStatus
|
|
11
|
-
} from '@/hooks/useRemoteAccess';
|
|
1
|
+
import type { RemoteAccessView } from '@/api/remote.types';
|
|
12
2
|
import { PageHeader, PageLayout } from '@/components/layout/page-layout';
|
|
13
3
|
import { Button } from '@/components/ui/button';
|
|
14
4
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
15
|
-
import { Input } from '@/components/ui/input';
|
|
16
|
-
import { Label } from '@/components/ui/label';
|
|
17
5
|
import { StatusDot } from '@/components/ui/status-dot';
|
|
18
|
-
import {
|
|
6
|
+
import { useRemoteStatus } from '@/hooks/useRemoteAccess';
|
|
19
7
|
import { formatDateTime, t } from '@/lib/i18n';
|
|
20
|
-
import {
|
|
21
|
-
import
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
if (runtime.state === 'error') {
|
|
34
|
-
return { status: 'warning', label: t('remoteStateError') };
|
|
35
|
-
}
|
|
36
|
-
if (runtime.state === 'disconnected') {
|
|
37
|
-
return { status: 'warning', label: t('remoteStateDisconnected') };
|
|
38
|
-
}
|
|
39
|
-
return { status: 'inactive', label: t('remoteStateDisabled') };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function getServiceStatus(service: RemoteServiceView): { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string } {
|
|
43
|
-
if (!service.running) {
|
|
44
|
-
return { status: 'inactive', label: t('remoteServiceStopped') };
|
|
45
|
-
}
|
|
46
|
-
return service.currentProcess
|
|
47
|
-
? { status: 'ready', label: t('remoteServiceManagedRunning') }
|
|
48
|
-
: { status: 'active', label: t('remoteServiceRunning') };
|
|
49
|
-
}
|
|
8
|
+
import { useAppPresenter } from '@/presenter/app-presenter-context';
|
|
9
|
+
import { resolveRemoteWebBase } from '@/remote/remote-access.query';
|
|
10
|
+
import { useRemoteAccessStore } from '@/remote/stores/remote-access.store';
|
|
11
|
+
import { Laptop, RefreshCcw, SquareArrowOutUpRight } from 'lucide-react';
|
|
12
|
+
import { useEffect, useMemo } from 'react';
|
|
13
|
+
|
|
14
|
+
type RemoteHeroView = {
|
|
15
|
+
badgeStatus: 'active' | 'inactive' | 'ready' | 'setup' | 'warning';
|
|
16
|
+
badgeLabel: string;
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
};
|
|
50
20
|
|
|
51
21
|
function KeyValueRow(props: { label: string; value?: string | number | null; muted?: boolean }) {
|
|
52
22
|
const value = props.value === undefined || props.value === null || props.value === '' ? '-' : String(props.value);
|
|
53
23
|
return (
|
|
54
24
|
<div className="flex items-start justify-between gap-4 py-2 text-sm">
|
|
55
25
|
<span className="text-gray-500">{props.label}</span>
|
|
56
|
-
<span className={props.muted ? 'text-right text-gray-500' : 'text-right text-gray-
|
|
26
|
+
<span className={props.muted ? 'text-right text-gray-500' : 'text-right text-gray-900'}>{value}</span>
|
|
57
27
|
</div>
|
|
58
28
|
);
|
|
59
29
|
}
|
|
60
30
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
}
|
|
31
|
+
function buildHeroView(status: RemoteAccessView | undefined): RemoteHeroView {
|
|
32
|
+
if (!status?.account.loggedIn) {
|
|
33
|
+
return {
|
|
34
|
+
badgeStatus: 'setup',
|
|
35
|
+
badgeLabel: t('statusSetup'),
|
|
36
|
+
title: t('remoteStatusNeedsSignIn'),
|
|
37
|
+
description: t('remoteStatusNeedsSignInDescription')
|
|
38
|
+
};
|
|
39
|
+
}
|
|
93
40
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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);
|
|
41
|
+
if (!status.settings.enabled) {
|
|
42
|
+
return {
|
|
43
|
+
badgeStatus: 'inactive',
|
|
44
|
+
badgeLabel: t('statusInactive'),
|
|
45
|
+
title: t('remoteStatusNeedsEnable'),
|
|
46
|
+
description: t('remoteStatusNeedsEnableDescription')
|
|
47
|
+
};
|
|
48
|
+
}
|
|
126
49
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
50
|
+
if (!status.service.running) {
|
|
51
|
+
return {
|
|
52
|
+
badgeStatus: 'warning',
|
|
53
|
+
badgeLabel: t('remoteServiceStopped'),
|
|
54
|
+
title: t('remoteStatusNeedsServiceTitle'),
|
|
55
|
+
description: t('remoteStatusNeedsServiceDescription')
|
|
130
56
|
};
|
|
131
|
-
}
|
|
57
|
+
}
|
|
132
58
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
setAuthStatusMessage(t('remoteBrowserAuthWaiting'));
|
|
142
|
-
const opened = window.open(result.verificationUri, '_blank', 'noopener,noreferrer');
|
|
143
|
-
if (!opened) {
|
|
144
|
-
setAuthStatusMessage(t('remoteBrowserAuthPopupBlocked'));
|
|
145
|
-
}
|
|
146
|
-
};
|
|
59
|
+
if (status.runtime?.state === 'connected') {
|
|
60
|
+
return {
|
|
61
|
+
badgeStatus: 'ready',
|
|
62
|
+
badgeLabel: t('statusReady'),
|
|
63
|
+
title: t('remoteStatusReadyTitle'),
|
|
64
|
+
description: t('remoteStatusReadyDescription')
|
|
65
|
+
};
|
|
66
|
+
}
|
|
147
67
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
68
|
+
if (status.runtime?.state === 'connecting') {
|
|
69
|
+
return {
|
|
70
|
+
badgeStatus: 'active',
|
|
71
|
+
badgeLabel: t('connecting'),
|
|
72
|
+
title: t('remoteStatusConnectingTitle'),
|
|
73
|
+
description: t('remoteStatusConnectingDescription')
|
|
74
|
+
};
|
|
75
|
+
}
|
|
154
76
|
|
|
155
77
|
return {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
resume,
|
|
161
|
-
start
|
|
78
|
+
badgeStatus: 'warning',
|
|
79
|
+
badgeLabel: t('remoteStateDisconnected'),
|
|
80
|
+
title: t('remoteStatusIssueTitle'),
|
|
81
|
+
description: t('remoteStatusIssueDescription')
|
|
162
82
|
};
|
|
163
83
|
}
|
|
164
84
|
|
|
165
|
-
function
|
|
166
|
-
status
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
);
|
|
85
|
+
function buildIssueHint(status: RemoteAccessView | undefined) {
|
|
86
|
+
if (!status?.settings.enabled) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (!status.service.running) {
|
|
90
|
+
return t('remoteStatusIssueDetailServiceStopped');
|
|
91
|
+
}
|
|
92
|
+
return status.runtime?.lastError?.trim() || t('remoteStatusIssueDetailGeneric');
|
|
414
93
|
}
|
|
415
94
|
|
|
416
95
|
export function RemoteAccessPage() {
|
|
96
|
+
const presenter = useAppPresenter();
|
|
417
97
|
const remoteStatus = useRemoteStatus();
|
|
418
|
-
const browserAuthStartMutation = useRemoteBrowserAuthStart();
|
|
419
|
-
const browserAuthPollMutation = useRemoteBrowserAuthPoll();
|
|
420
|
-
const logoutMutation = useRemoteLogout();
|
|
421
|
-
const settingsMutation = useRemoteSettings();
|
|
422
|
-
const doctorMutation = useRemoteDoctor();
|
|
423
|
-
const serviceMutation = useRemoteServiceControl();
|
|
424
|
-
|
|
425
98
|
const status = remoteStatus.data;
|
|
426
|
-
const
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
status,
|
|
434
|
-
platformApiBase,
|
|
435
|
-
startBrowserAuth: browserAuthStartMutation,
|
|
436
|
-
pollBrowserAuth: browserAuthPollMutation
|
|
437
|
-
});
|
|
99
|
+
const actionLabel = useRemoteAccessStore((state) => state.actionLabel);
|
|
100
|
+
const heroView = useMemo(() => buildHeroView(status), [status]);
|
|
101
|
+
const issueHint = useMemo(() => buildIssueHint(status), [status]);
|
|
102
|
+
const busy = Boolean(actionLabel);
|
|
103
|
+
const deviceName = status?.runtime?.deviceName?.trim() || status?.settings.deviceName?.trim() || t('remoteDeviceNameAuto');
|
|
104
|
+
const canOpenDeviceList = Boolean(status?.account.loggedIn && resolveRemoteWebBase(status));
|
|
105
|
+
const shouldShowIssueHint = Boolean(status?.settings.enabled && status?.account.loggedIn && heroView.badgeStatus === 'warning');
|
|
438
106
|
|
|
439
107
|
useEffect(() => {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
setEnabled(status.settings.enabled);
|
|
444
|
-
setDeviceName(status.settings.deviceName);
|
|
445
|
-
setPlatformApiBase(status.settings.platformApiBase);
|
|
446
|
-
}, [status]);
|
|
108
|
+
presenter.remoteAccessManager.syncStatus(status);
|
|
109
|
+
}, [presenter, status]);
|
|
447
110
|
|
|
448
111
|
if (remoteStatus.isLoading && !status) {
|
|
449
112
|
return <div className="p-8 text-gray-400">{t('remoteLoading')}</div>;
|
|
@@ -453,37 +116,94 @@ export function RemoteAccessPage() {
|
|
|
453
116
|
<PageLayout className="space-y-6">
|
|
454
117
|
<PageHeader title={t('remotePageTitle')} description={t('remotePageDescription')} />
|
|
455
118
|
|
|
456
|
-
<div className="grid gap-6 xl:grid-cols-[1.
|
|
457
|
-
<
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
119
|
+
<div className="grid gap-6 xl:grid-cols-[1.2fr_0.8fr]">
|
|
120
|
+
<Card>
|
|
121
|
+
<CardHeader className="space-y-4">
|
|
122
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
123
|
+
<CardTitle>{heroView.title}</CardTitle>
|
|
124
|
+
<StatusDot status={heroView.badgeStatus} label={heroView.badgeLabel} />
|
|
125
|
+
</div>
|
|
126
|
+
<CardDescription>{heroView.description}</CardDescription>
|
|
127
|
+
</CardHeader>
|
|
128
|
+
<CardContent className="space-y-5">
|
|
129
|
+
<div className="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3">
|
|
130
|
+
<KeyValueRow label={t('remoteSignedInAccount')} value={status?.account.email} />
|
|
131
|
+
<KeyValueRow label={t('remoteDeviceName')} value={deviceName} />
|
|
132
|
+
<KeyValueRow label={t('remoteConnectionStatus')} value={heroView.badgeLabel} />
|
|
133
|
+
<KeyValueRow label={t('remoteLastConnectedAt')} value={status?.runtime?.lastConnectedAt ? formatDateTime(status.runtime.lastConnectedAt) : '-'} muted />
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div className="flex flex-wrap gap-3">
|
|
137
|
+
{!status?.account.loggedIn ? (
|
|
138
|
+
<Button onClick={() => void presenter.remoteAccessManager.enableRemoteAccess(status)} disabled={busy}>
|
|
139
|
+
{actionLabel || t('remoteSignInAndEnable')}
|
|
140
|
+
</Button>
|
|
141
|
+
) : !status.settings.enabled ? (
|
|
142
|
+
<Button onClick={() => void presenter.remoteAccessManager.enableRemoteAccess(status)} disabled={busy}>
|
|
143
|
+
{actionLabel || t('remoteEnableNow')}
|
|
144
|
+
</Button>
|
|
145
|
+
) : (
|
|
146
|
+
<Button onClick={() => void presenter.remoteAccessManager.repairRemoteAccess(status)} disabled={busy}>
|
|
147
|
+
<RefreshCcw className="mr-2 h-4 w-4" />
|
|
148
|
+
{actionLabel || t('remoteReconnectNow')}
|
|
149
|
+
</Button>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
<Button
|
|
153
|
+
variant="outline"
|
|
154
|
+
onClick={() => void presenter.accountManager.openNextClawWeb()}
|
|
155
|
+
disabled={busy || !canOpenDeviceList}
|
|
156
|
+
>
|
|
157
|
+
<SquareArrowOutUpRight className="mr-2 h-4 w-4" />
|
|
158
|
+
{t('remoteOpenDeviceList')}
|
|
159
|
+
</Button>
|
|
160
|
+
|
|
161
|
+
{status?.settings.enabled ? (
|
|
162
|
+
<Button variant="outline" onClick={() => void presenter.remoteAccessManager.disableRemoteAccess(status)} disabled={busy}>
|
|
163
|
+
{t('remoteDisable')}
|
|
164
|
+
</Button>
|
|
165
|
+
) : null}
|
|
166
|
+
</div>
|
|
469
167
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
168
|
+
{shouldShowIssueHint ? (
|
|
169
|
+
<div className="rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3">
|
|
170
|
+
<p className="text-sm font-medium text-amber-900">{t('remoteStatusIssueDetailTitle')}</p>
|
|
171
|
+
<p className="mt-1 text-sm leading-6 text-amber-800">{issueHint}</p>
|
|
172
|
+
</div>
|
|
173
|
+
) : null}
|
|
174
|
+
|
|
175
|
+
<p className="text-xs text-gray-500">{t('remoteOpenWebHint')}</p>
|
|
176
|
+
</CardContent>
|
|
177
|
+
</Card>
|
|
178
|
+
|
|
179
|
+
<Card>
|
|
180
|
+
<CardHeader>
|
|
181
|
+
<CardTitle className="flex items-center gap-2">
|
|
182
|
+
<Laptop className="h-4 w-4 text-primary" />
|
|
183
|
+
{t('remoteDeviceSectionTitle')}
|
|
184
|
+
</CardTitle>
|
|
185
|
+
<CardDescription>{t('remoteDeviceSectionDescription')}</CardDescription>
|
|
186
|
+
</CardHeader>
|
|
187
|
+
<CardContent className="space-y-5">
|
|
188
|
+
<div className="flex flex-wrap gap-2">
|
|
189
|
+
<StatusDot status={status?.account.loggedIn ? 'ready' : 'inactive'} label={status?.account.loggedIn ? t('remoteAccountConnected') : t('remoteAccountNotConnected')} />
|
|
190
|
+
<StatusDot status={status?.settings.enabled ? 'active' : 'inactive'} label={status?.settings.enabled ? t('remoteEnabled') : t('remoteStateDisabled')} />
|
|
191
|
+
<StatusDot status={status?.service.running ? 'active' : 'inactive'} label={status?.service.running ? t('remoteServiceRunning') : t('remoteServiceStopped')} />
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div className="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3">
|
|
195
|
+
<KeyValueRow label={t('remoteDeviceName')} value={deviceName} />
|
|
196
|
+
<KeyValueRow label={t('remoteConnectionStatus')} value={heroView.badgeLabel} />
|
|
197
|
+
<KeyValueRow label={t('remoteLastConnectedAt')} value={status?.runtime?.lastConnectedAt ? formatDateTime(status.runtime.lastConnectedAt) : '-'} muted />
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div className="rounded-2xl border border-dashed border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-600">
|
|
201
|
+
{status?.account.loggedIn ? t('remoteOpenWebHint') : t('remoteStatusNeedsSignInDescription')}
|
|
202
|
+
</div>
|
|
203
|
+
</CardContent>
|
|
204
|
+
</Card>
|
|
484
205
|
</div>
|
|
485
206
|
|
|
486
|
-
<RemoteDoctorCard doctorMutation={doctorMutation} />
|
|
487
207
|
</PageLayout>
|
|
488
208
|
);
|
|
489
209
|
}
|