@nextclaw/ui 0.9.2 → 0.9.4
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-DDfZIiJa.js +1 -0
- package/dist/assets/ChatPage-FpRraTxm.js +38 -0
- package/dist/assets/{DocBrowser-CVwUDJMO.js → DocBrowser-Kndx8OJj.js} +1 -1
- package/dist/assets/LogoBadge-hKHoLH9n.js +1 -0
- package/dist/assets/MarketplacePage-CZIJyfjK.js +49 -0
- package/dist/assets/McpMarketplacePage-BGrAMA37.js +40 -0
- package/dist/assets/{ModelConfig-CsX-_fyy.js → ModelConfig-BpKQeGfb.js} +1 -1
- package/dist/assets/ProvidersList-qfUL6mrW.js +1 -0
- package/dist/assets/RemoteAccessPage-BQuMsngI.js +1 -0
- package/dist/assets/{RuntimeConfig-CX2TGEG1.js → RuntimeConfig-CVlqNWKO.js} +1 -1
- package/dist/assets/{SearchConfig-C-WBTcWi.js → SearchConfig-DXFV6Mvx.js} +1 -1
- package/dist/assets/{SecretsConfig-9kbR0ZCB.js → SecretsConfig-BGW9aUqv.js} +2 -2
- package/dist/assets/{SessionsConfig-Bohn3P1q.js → SessionsConfig-BByfa1ke.js} +2 -2
- package/dist/assets/{chat-message-AWIcksDK.js → chat-message-ZwnDwDuQ.js} +1 -1
- package/dist/assets/index-BWvap_iq.js +8 -0
- package/dist/assets/index-COrhpAdh.css +1 -0
- package/dist/assets/{index-CPDASUXh.js → index-Ct7FQpxN.js} +1 -1
- package/dist/assets/{label-DD61y-4v.js → label-Bklr3fXc.js} +1 -1
- package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
- package/dist/assets/{page-layout-CfnoVycc.js → page-layout-sNhcbwtm.js} +1 -1
- package/dist/assets/{popover-DsugZ6rp.js → popover-C3rJrJJG.js} +1 -1
- package/dist/assets/{security-config-DIrf2Z0O.js → security-config-BueosYw1.js} +1 -1
- package/dist/assets/skeleton-CiG6msbm.js +1 -0
- package/dist/assets/status-dot-CsIV5YrS.js +1 -0
- package/dist/assets/{switch-NX5OmUXQ.js → switch-DSdHSIsC.js} +1 -1
- package/dist/assets/{tabs-custom-9ihB5Jem.js → tabs-custom-BB-VjdL2.js} +1 -1
- package/dist/assets/{useConfirmDialog-BuQnVTeR.js → useConfirmDialog-BL5s8KDC.js} +2 -2
- package/dist/assets/{vendor-DKBNiC31.js → vendor-CwsIoNvJ.js} +128 -93
- package/dist/index.html +3 -3
- package/package.json +3 -3
- package/src/App.tsx +4 -0
- package/src/api/auth.types.ts +24 -0
- package/src/api/chat-session-type.types.ts +21 -0
- package/src/api/marketplace.ts +8 -2
- package/src/api/mcp-marketplace.ts +138 -0
- package/src/api/remote.ts +77 -0
- package/src/api/remote.types.ts +104 -0
- package/src/api/types.ts +28 -34
- package/src/components/chat/ChatSidebar.test.tsx +31 -2
- package/src/components/chat/ChatSidebar.tsx +26 -2
- package/src/components/chat/chat-page-data.ts +36 -38
- package/src/components/chat/chat-page-runtime.test.ts +96 -2
- package/src/components/chat/chat-page-runtime.ts +1 -135
- package/src/components/chat/chat-session-preference-governance.ts +303 -0
- package/src/components/chat/legacy/LegacyChatPage.tsx +4 -19
- package/src/components/chat/ncp/NcpChatPage.tsx +4 -19
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +36 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +62 -21
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +2 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +2 -0
- package/src/components/chat/stores/chat-input.store.ts +14 -1
- package/src/components/chat/useChatSessionTypeState.test.tsx +29 -0
- package/src/components/chat/useChatSessionTypeState.ts +55 -12
- package/src/components/layout/Sidebar.tsx +11 -1
- package/src/components/marketplace/MarketplacePage.test.tsx +152 -0
- package/src/components/marketplace/MarketplacePage.tsx +52 -199
- package/src/components/marketplace/marketplace-installed-cache.test.ts +110 -0
- package/src/components/marketplace/marketplace-installed-cache.ts +149 -0
- package/src/components/marketplace/marketplace-localization.ts +77 -0
- package/src/components/marketplace/marketplace-page-parts.tsx +102 -0
- package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +208 -0
- package/src/components/marketplace/mcp/McpMarketplacePage.tsx +578 -0
- package/src/components/remote/RemoteAccessPage.tsx +396 -0
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/label.tsx +1 -1
- package/src/hooks/useMarketplace.ts +36 -7
- package/src/hooks/useMcpMarketplace.ts +99 -0
- package/src/hooks/useRemoteAccess.ts +120 -0
- package/src/hooks/useWebSocket.ts +25 -16
- package/src/lib/i18n.marketplace.ts +91 -0
- package/src/lib/i18n.remote.ts +142 -0
- package/src/lib/i18n.ts +10 -68
- package/dist/assets/ChannelsList-DKD6Llid.js +0 -1
- package/dist/assets/ChatPage-BK9X4Tin.js +0 -38
- package/dist/assets/LogoBadge-CYQ_b7jk.js +0 -1
- package/dist/assets/MarketplacePage-B_2z3ii_.js +0 -49
- package/dist/assets/ProvidersList-CZstsyv7.js +0 -1
- package/dist/assets/index-BEgClaDH.js +0 -8
- package/dist/assets/index-C8GsgIUn.css +0 -1
- package/dist/assets/skeleton-DJ-Wen2o.js +0 -1
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
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';
|
|
12
|
+
import { PageHeader, PageLayout } from '@/components/layout/page-layout';
|
|
13
|
+
import { Button } from '@/components/ui/button';
|
|
14
|
+
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
|
+
import { StatusDot } from '@/components/ui/status-dot';
|
|
18
|
+
import { Switch } from '@/components/ui/switch';
|
|
19
|
+
import { formatDateTime, t } from '@/lib/i18n';
|
|
20
|
+
import { Activity, KeyRound, Laptop, RefreshCcw, ServerCog, ShieldCheck, SquareTerminal } from 'lucide-react';
|
|
21
|
+
|
|
22
|
+
function getRuntimeStatus(runtime: RemoteRuntimeView | null): { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string } {
|
|
23
|
+
if (!runtime) {
|
|
24
|
+
return { status: 'inactive', label: t('remoteRuntimeMissing') };
|
|
25
|
+
}
|
|
26
|
+
if (runtime.state === 'connected') {
|
|
27
|
+
return { status: 'ready', label: t('remoteStateConnected') };
|
|
28
|
+
}
|
|
29
|
+
if (runtime.state === 'connecting') {
|
|
30
|
+
return { status: 'warning', label: t('remoteStateConnecting') };
|
|
31
|
+
}
|
|
32
|
+
if (runtime.state === 'error') {
|
|
33
|
+
return { status: 'warning', label: t('remoteStateError') };
|
|
34
|
+
}
|
|
35
|
+
if (runtime.state === 'disconnected') {
|
|
36
|
+
return { status: 'warning', label: t('remoteStateDisconnected') };
|
|
37
|
+
}
|
|
38
|
+
return { status: 'inactive', label: t('remoteStateDisabled') };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getServiceStatus(service: RemoteServiceView): { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string } {
|
|
42
|
+
if (!service.running) {
|
|
43
|
+
return { status: 'inactive', label: t('remoteServiceStopped') };
|
|
44
|
+
}
|
|
45
|
+
return service.currentProcess
|
|
46
|
+
? { status: 'ready', label: t('remoteServiceManagedRunning') }
|
|
47
|
+
: { status: 'active', label: t('remoteServiceRunning') };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function KeyValueRow(props: { label: string; value?: string | number | null; muted?: boolean }) {
|
|
51
|
+
const value = props.value === undefined || props.value === null || props.value === '' ? '-' : String(props.value);
|
|
52
|
+
return (
|
|
53
|
+
<div className="flex items-start justify-between gap-4 py-2 text-sm">
|
|
54
|
+
<span className="text-gray-500">{props.label}</span>
|
|
55
|
+
<span className={props.muted ? 'text-right text-gray-500' : 'text-right text-gray-800'}>{value}</span>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function RemoteAccessPage() {
|
|
61
|
+
const remoteStatus = useRemoteStatus();
|
|
62
|
+
const browserAuthStartMutation = useRemoteBrowserAuthStart();
|
|
63
|
+
const browserAuthPollMutation = useRemoteBrowserAuthPoll();
|
|
64
|
+
const logoutMutation = useRemoteLogout();
|
|
65
|
+
const settingsMutation = useRemoteSettings();
|
|
66
|
+
const doctorMutation = useRemoteDoctor();
|
|
67
|
+
const serviceMutation = useRemoteServiceControl();
|
|
68
|
+
|
|
69
|
+
const status = remoteStatus.data;
|
|
70
|
+
const runtimeStatus = useMemo(() => getRuntimeStatus(status?.runtime ?? null), [status?.runtime]);
|
|
71
|
+
const serviceStatus = useMemo(() => getServiceStatus(status?.service ?? { running: false, currentProcess: false }), [status?.service]);
|
|
72
|
+
|
|
73
|
+
const [enabled, setEnabled] = useState(false);
|
|
74
|
+
const [deviceName, setDeviceName] = useState('');
|
|
75
|
+
const [platformApiBase, setPlatformApiBase] = useState('');
|
|
76
|
+
const [authSessionId, setAuthSessionId] = useState<string | null>(null);
|
|
77
|
+
const [authVerificationUri, setAuthVerificationUri] = useState<string | null>(null);
|
|
78
|
+
const [authStatusMessage, setAuthStatusMessage] = useState('');
|
|
79
|
+
const [authExpiresAt, setAuthExpiresAt] = useState<string | null>(null);
|
|
80
|
+
const [authPollIntervalMs, setAuthPollIntervalMs] = useState(1500);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!status) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
setEnabled(status.settings.enabled);
|
|
87
|
+
setDeviceName(status.settings.deviceName);
|
|
88
|
+
setPlatformApiBase(status.settings.platformApiBase);
|
|
89
|
+
}, [status]);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!status?.account.loggedIn) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
setAuthSessionId(null);
|
|
96
|
+
setAuthVerificationUri(null);
|
|
97
|
+
setAuthExpiresAt(null);
|
|
98
|
+
setAuthStatusMessage('');
|
|
99
|
+
setAuthPollIntervalMs(1500);
|
|
100
|
+
}, [status?.account.loggedIn]);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!authSessionId || status?.account.loggedIn) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let cancelled = false;
|
|
108
|
+
const timerId = window.setTimeout(async () => {
|
|
109
|
+
try {
|
|
110
|
+
const result = await browserAuthPollMutation.mutateAsync({
|
|
111
|
+
sessionId: authSessionId,
|
|
112
|
+
apiBase: platformApiBase.trim() || status?.settings.platformApiBase || status?.account.apiBase || undefined
|
|
113
|
+
});
|
|
114
|
+
if (cancelled) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (result.status === 'pending') {
|
|
118
|
+
setAuthStatusMessage(t('remoteBrowserAuthWaiting'));
|
|
119
|
+
setAuthPollIntervalMs(result.nextPollMs ?? 1500);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (result.status === 'authorized') {
|
|
123
|
+
setAuthStatusMessage(t('remoteBrowserAuthCompleted'));
|
|
124
|
+
setAuthSessionId(null);
|
|
125
|
+
setAuthVerificationUri(null);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
setAuthStatusMessage(result.message || t('remoteBrowserAuthExpired'));
|
|
129
|
+
setAuthSessionId(null);
|
|
130
|
+
setAuthVerificationUri(null);
|
|
131
|
+
} catch {
|
|
132
|
+
if (cancelled) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
setAuthSessionId(null);
|
|
136
|
+
setAuthVerificationUri(null);
|
|
137
|
+
}
|
|
138
|
+
}, authPollIntervalMs);
|
|
139
|
+
|
|
140
|
+
return () => {
|
|
141
|
+
cancelled = true;
|
|
142
|
+
window.clearTimeout(timerId);
|
|
143
|
+
};
|
|
144
|
+
}, [
|
|
145
|
+
authPollIntervalMs,
|
|
146
|
+
authSessionId,
|
|
147
|
+
browserAuthPollMutation,
|
|
148
|
+
platformApiBase,
|
|
149
|
+
status?.account.apiBase,
|
|
150
|
+
status?.account.loggedIn,
|
|
151
|
+
status?.settings.platformApiBase
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
const handleStartBrowserAuth = async () => {
|
|
155
|
+
const apiBase = platformApiBase.trim() || status?.settings.platformApiBase || status?.account.apiBase || undefined;
|
|
156
|
+
const result = await browserAuthStartMutation.mutateAsync({ apiBase });
|
|
157
|
+
setAuthSessionId(result.sessionId);
|
|
158
|
+
setAuthVerificationUri(result.verificationUri);
|
|
159
|
+
setAuthExpiresAt(result.expiresAt);
|
|
160
|
+
setAuthPollIntervalMs(result.intervalMs);
|
|
161
|
+
setAuthStatusMessage(t('remoteBrowserAuthWaiting'));
|
|
162
|
+
const opened = window.open(result.verificationUri, '_blank', 'noopener,noreferrer');
|
|
163
|
+
if (!opened) {
|
|
164
|
+
setAuthStatusMessage(t('remoteBrowserAuthPopupBlocked'));
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleResumeBrowserAuth = () => {
|
|
169
|
+
if (!authVerificationUri) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
window.open(authVerificationUri, '_blank', 'noopener,noreferrer');
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
if (remoteStatus.isLoading && !status) {
|
|
176
|
+
return <div className="p-8 text-gray-400">{t('remoteLoading')}</div>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<PageLayout className="space-y-6">
|
|
181
|
+
<PageHeader title={t('remotePageTitle')} description={t('remotePageDescription')} />
|
|
182
|
+
|
|
183
|
+
<div className="grid gap-6 xl:grid-cols-[1.35fr_0.95fr]">
|
|
184
|
+
<Card>
|
|
185
|
+
<CardHeader>
|
|
186
|
+
<CardTitle className="flex items-center gap-2">
|
|
187
|
+
<Activity className="h-4 w-4 text-primary" />
|
|
188
|
+
{t('remoteOverviewTitle')}
|
|
189
|
+
</CardTitle>
|
|
190
|
+
<CardDescription>{t('remoteOverviewDescription')}</CardDescription>
|
|
191
|
+
</CardHeader>
|
|
192
|
+
<CardContent className="space-y-5">
|
|
193
|
+
<div className="flex flex-wrap gap-2">
|
|
194
|
+
<StatusDot status={status?.account.loggedIn ? 'ready' : 'inactive'} label={status?.account.loggedIn ? t('remoteAccountConnected') : t('remoteAccountNotConnected')} />
|
|
195
|
+
<StatusDot status={serviceStatus.status} label={serviceStatus.label} />
|
|
196
|
+
<StatusDot status={runtimeStatus.status} label={runtimeStatus.label} />
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
200
|
+
<KeyValueRow label={t('remoteLocalOrigin')} value={status?.localOrigin} />
|
|
201
|
+
<KeyValueRow label={t('remotePublicPlatform')} value={status?.platformBase ?? status?.account.platformBase} />
|
|
202
|
+
<KeyValueRow label={t('remoteDeviceId')} value={status?.runtime?.deviceId} muted />
|
|
203
|
+
<KeyValueRow label={t('remoteLastConnectedAt')} value={status?.runtime?.lastConnectedAt ? formatDateTime(status.runtime.lastConnectedAt) : '-'} muted />
|
|
204
|
+
<KeyValueRow label={t('remoteLastError')} value={status?.runtime?.lastError} muted />
|
|
205
|
+
</div>
|
|
206
|
+
</CardContent>
|
|
207
|
+
</Card>
|
|
208
|
+
|
|
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={enabled} onCheckedChange={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={deviceName} onChange={(event) => 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={platformApiBase}
|
|
238
|
+
onChange={(event) => 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
|
+
settingsMutation.mutate({
|
|
248
|
+
enabled,
|
|
249
|
+
deviceName,
|
|
250
|
+
platformApiBase
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
disabled={settingsMutation.isPending}
|
|
254
|
+
>
|
|
255
|
+
{settingsMutation.isPending ? t('saving') : t('remoteSaveSettings')}
|
|
256
|
+
</Button>
|
|
257
|
+
<Button
|
|
258
|
+
variant="outline"
|
|
259
|
+
onClick={() => serviceMutation.mutate('restart')}
|
|
260
|
+
disabled={serviceMutation.isPending}
|
|
261
|
+
>
|
|
262
|
+
<RefreshCcw className="mr-2 h-4 w-4" />
|
|
263
|
+
{t('remoteRestartService')}
|
|
264
|
+
</Button>
|
|
265
|
+
</div>
|
|
266
|
+
<p className="text-xs text-gray-500">{t('remoteSaveHint')}</p>
|
|
267
|
+
</CardContent>
|
|
268
|
+
</Card>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
|
|
272
|
+
<Card>
|
|
273
|
+
<CardHeader>
|
|
274
|
+
<CardTitle className="flex items-center gap-2">
|
|
275
|
+
<KeyRound className="h-4 w-4 text-primary" />
|
|
276
|
+
{t('remoteAccountTitle')}
|
|
277
|
+
</CardTitle>
|
|
278
|
+
<CardDescription>{t('remoteAccountDescription')}</CardDescription>
|
|
279
|
+
</CardHeader>
|
|
280
|
+
<CardContent className="space-y-4">
|
|
281
|
+
{status?.account.loggedIn ? (
|
|
282
|
+
<>
|
|
283
|
+
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
284
|
+
<KeyValueRow label={t('remoteAccountEmail')} value={status.account.email} />
|
|
285
|
+
<KeyValueRow label={t('remoteAccountRole')} value={status.account.role} />
|
|
286
|
+
<KeyValueRow label={t('remoteApiBase')} value={status.account.apiBase} />
|
|
287
|
+
</div>
|
|
288
|
+
<Button variant="outline" onClick={() => logoutMutation.mutate()} disabled={logoutMutation.isPending}>
|
|
289
|
+
{logoutMutation.isPending ? t('remoteLoggingOut') : t('remoteLogout')}
|
|
290
|
+
</Button>
|
|
291
|
+
</>
|
|
292
|
+
) : (
|
|
293
|
+
<>
|
|
294
|
+
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
295
|
+
<p className="text-sm font-medium text-gray-900">{t('remoteBrowserAuthTitle')}</p>
|
|
296
|
+
<p className="mt-1 text-sm text-gray-600">{t('remoteBrowserAuthDescription')}</p>
|
|
297
|
+
<div className="mt-3 border-t border-white/80 pt-3">
|
|
298
|
+
<KeyValueRow label={t('remoteApiBase')} value={platformApiBase || status?.settings.platformApiBase || status?.account.apiBase} muted />
|
|
299
|
+
<KeyValueRow label={t('remoteBrowserAuthSession')} value={authSessionId} muted />
|
|
300
|
+
<KeyValueRow
|
|
301
|
+
label={t('remoteBrowserAuthExpiresAt')}
|
|
302
|
+
value={authExpiresAt ? formatDateTime(authExpiresAt) : '-'}
|
|
303
|
+
muted
|
|
304
|
+
/>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
{authStatusMessage ? <p className="text-sm text-gray-600">{authStatusMessage}</p> : null}
|
|
308
|
+
<div className="flex flex-wrap gap-3">
|
|
309
|
+
<Button onClick={handleStartBrowserAuth} disabled={browserAuthStartMutation.isPending || !!authSessionId}>
|
|
310
|
+
{browserAuthStartMutation.isPending
|
|
311
|
+
? t('remoteBrowserAuthStarting')
|
|
312
|
+
: authSessionId
|
|
313
|
+
? t('remoteBrowserAuthAuthorizing')
|
|
314
|
+
: t('remoteBrowserAuthAction')}
|
|
315
|
+
</Button>
|
|
316
|
+
{authVerificationUri ? (
|
|
317
|
+
<Button variant="outline" onClick={handleResumeBrowserAuth}>
|
|
318
|
+
{t('remoteBrowserAuthResume')}
|
|
319
|
+
</Button>
|
|
320
|
+
) : null}
|
|
321
|
+
</div>
|
|
322
|
+
<p className="text-xs text-gray-500">{t('remoteBrowserAuthHint')}</p>
|
|
323
|
+
</>
|
|
324
|
+
)}
|
|
325
|
+
</CardContent>
|
|
326
|
+
</Card>
|
|
327
|
+
|
|
328
|
+
<Card>
|
|
329
|
+
<CardHeader>
|
|
330
|
+
<CardTitle className="flex items-center gap-2">
|
|
331
|
+
<ServerCog className="h-4 w-4 text-primary" />
|
|
332
|
+
{t('remoteServiceTitle')}
|
|
333
|
+
</CardTitle>
|
|
334
|
+
<CardDescription>{t('remoteServiceDescription')}</CardDescription>
|
|
335
|
+
</CardHeader>
|
|
336
|
+
<CardContent className="space-y-4">
|
|
337
|
+
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
338
|
+
<KeyValueRow label={t('remoteServicePid')} value={status?.service.pid} />
|
|
339
|
+
<KeyValueRow label={t('remoteServiceUiUrl')} value={status?.service.uiUrl} />
|
|
340
|
+
<KeyValueRow label={t('remoteServiceCurrentProcess')} value={status?.service.currentProcess ? t('yes') : t('no')} />
|
|
341
|
+
</div>
|
|
342
|
+
<div className="flex flex-wrap gap-3">
|
|
343
|
+
<Button variant="primary" onClick={() => serviceMutation.mutate('start')} disabled={serviceMutation.isPending}>
|
|
344
|
+
{t('remoteStartService')}
|
|
345
|
+
</Button>
|
|
346
|
+
<Button variant="outline" onClick={() => serviceMutation.mutate('restart')} disabled={serviceMutation.isPending}>
|
|
347
|
+
{t('remoteRestartService')}
|
|
348
|
+
</Button>
|
|
349
|
+
<Button variant="outline" onClick={() => serviceMutation.mutate('stop')} disabled={serviceMutation.isPending}>
|
|
350
|
+
{t('remoteStopService')}
|
|
351
|
+
</Button>
|
|
352
|
+
</div>
|
|
353
|
+
<p className="text-xs text-gray-500">{t('remoteServiceHint')}</p>
|
|
354
|
+
</CardContent>
|
|
355
|
+
</Card>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<Card>
|
|
359
|
+
<CardHeader>
|
|
360
|
+
<CardTitle className="flex items-center gap-2">
|
|
361
|
+
<ShieldCheck className="h-4 w-4 text-primary" />
|
|
362
|
+
{t('remoteDoctorTitle')}
|
|
363
|
+
</CardTitle>
|
|
364
|
+
<CardDescription>{t('remoteDoctorDescription')}</CardDescription>
|
|
365
|
+
</CardHeader>
|
|
366
|
+
<CardContent className="space-y-4">
|
|
367
|
+
<div className="flex flex-wrap gap-3">
|
|
368
|
+
<Button variant="outline" onClick={() => doctorMutation.mutate()} disabled={doctorMutation.isPending}>
|
|
369
|
+
<SquareTerminal className="mr-2 h-4 w-4" />
|
|
370
|
+
{doctorMutation.isPending ? t('remoteDoctorRunning') : t('remoteRunDoctor')}
|
|
371
|
+
</Button>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
{doctorMutation.data ? (
|
|
375
|
+
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
376
|
+
<KeyValueRow label={t('remoteDoctorGeneratedAt')} value={formatDateTime(doctorMutation.data.generatedAt)} muted />
|
|
377
|
+
<div className="mt-3 space-y-2">
|
|
378
|
+
{doctorMutation.data.checks.map((check) => (
|
|
379
|
+
<div key={check.name} className="rounded-xl border border-white bg-white px-3 py-3">
|
|
380
|
+
<div className="flex items-center justify-between gap-3">
|
|
381
|
+
<span className="text-sm font-medium text-gray-900">{check.name}</span>
|
|
382
|
+
<StatusDot status={check.ok ? 'ready' : 'warning'} label={check.ok ? t('remoteCheckPassed') : t('remoteCheckFailed')} />
|
|
383
|
+
</div>
|
|
384
|
+
<p className="mt-2 text-sm text-gray-600">{check.detail}</p>
|
|
385
|
+
</div>
|
|
386
|
+
))}
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
) : (
|
|
390
|
+
<p className="text-sm text-gray-500">{t('remoteDoctorEmpty')}</p>
|
|
391
|
+
)}
|
|
392
|
+
</CardContent>
|
|
393
|
+
</Card>
|
|
394
|
+
</PageLayout>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { cn } from '@/lib/utils';
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
|
5
5
|
|
|
6
6
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
7
7
|
({ className, type, ...props }, ref) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { cn } from '@/lib/utils';
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export type LabelProps = React.LabelHTMLAttributes<HTMLLabelElement>;
|
|
5
5
|
|
|
6
6
|
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
|
7
7
|
({ className, ...props }, ref) => (
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
2
|
import { toast } from 'sonner';
|
|
3
3
|
import { t } from '@/lib/i18n';
|
|
4
|
+
import {
|
|
5
|
+
applyInstallResultToInstalledView,
|
|
6
|
+
applyManageResultToInstalledView
|
|
7
|
+
} from '@/components/marketplace/marketplace-installed-cache';
|
|
4
8
|
import {
|
|
5
9
|
fetchMarketplaceItem,
|
|
6
10
|
fetchMarketplaceInstalled,
|
|
@@ -10,7 +14,12 @@ import {
|
|
|
10
14
|
manageMarketplaceItem,
|
|
11
15
|
type MarketplaceListParams
|
|
12
16
|
} from '@/api/marketplace';
|
|
13
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
MarketplaceInstallRequest,
|
|
19
|
+
MarketplaceInstalledView,
|
|
20
|
+
MarketplaceItemType,
|
|
21
|
+
MarketplaceManageRequest
|
|
22
|
+
} from '@/api/types';
|
|
14
23
|
|
|
15
24
|
export function useMarketplaceItems(params: MarketplaceListParams) {
|
|
16
25
|
return useQuery({
|
|
@@ -59,9 +68,19 @@ export function useInstallMarketplaceItem() {
|
|
|
59
68
|
|
|
60
69
|
return useMutation({
|
|
61
70
|
mutationFn: (request: MarketplaceInstallRequest) => installMarketplaceItem(request),
|
|
62
|
-
onSuccess: (result) => {
|
|
63
|
-
queryClient.
|
|
64
|
-
|
|
71
|
+
onSuccess: (result, variables) => {
|
|
72
|
+
queryClient.setQueryData<MarketplaceInstalledView | undefined>(
|
|
73
|
+
['marketplace-installed', result.type],
|
|
74
|
+
(view) => applyInstallResultToInstalledView({
|
|
75
|
+
view,
|
|
76
|
+
request: variables,
|
|
77
|
+
result
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
queryClient.invalidateQueries({
|
|
81
|
+
queryKey: ['marketplace-installed', result.type],
|
|
82
|
+
refetchType: 'inactive'
|
|
83
|
+
});
|
|
65
84
|
if (result.type === 'plugin') {
|
|
66
85
|
queryClient.invalidateQueries({ queryKey: ['ncp-session-types'] });
|
|
67
86
|
}
|
|
@@ -81,9 +100,19 @@ export function useManageMarketplaceItem() {
|
|
|
81
100
|
|
|
82
101
|
return useMutation({
|
|
83
102
|
mutationFn: (request: MarketplaceManageRequest) => manageMarketplaceItem(request),
|
|
84
|
-
onSuccess: (result) => {
|
|
85
|
-
queryClient.
|
|
86
|
-
|
|
103
|
+
onSuccess: (result, variables) => {
|
|
104
|
+
queryClient.setQueryData<MarketplaceInstalledView | undefined>(
|
|
105
|
+
['marketplace-installed', result.type],
|
|
106
|
+
(view) => applyManageResultToInstalledView({
|
|
107
|
+
view,
|
|
108
|
+
request: variables,
|
|
109
|
+
result
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
queryClient.invalidateQueries({
|
|
113
|
+
queryKey: ['marketplace-installed', result.type],
|
|
114
|
+
refetchType: 'inactive'
|
|
115
|
+
});
|
|
87
116
|
if (result.type === 'plugin') {
|
|
88
117
|
queryClient.invalidateQueries({ queryKey: ['ncp-session-types'] });
|
|
89
118
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { toast } from 'sonner';
|
|
3
|
+
import {
|
|
4
|
+
doctorMcpMarketplaceItem,
|
|
5
|
+
fetchMcpMarketplaceContent,
|
|
6
|
+
fetchMcpMarketplaceInstalled,
|
|
7
|
+
fetchMcpMarketplaceItem,
|
|
8
|
+
fetchMcpMarketplaceItems,
|
|
9
|
+
fetchMcpMarketplaceRecommendations,
|
|
10
|
+
installMcpMarketplaceItem,
|
|
11
|
+
manageMcpMarketplaceItem,
|
|
12
|
+
type McpMarketplaceListParams
|
|
13
|
+
} from '@/api/mcp-marketplace';
|
|
14
|
+
import { t } from '@/lib/i18n';
|
|
15
|
+
|
|
16
|
+
export function useMcpMarketplaceItems(params: McpMarketplaceListParams) {
|
|
17
|
+
return useQuery({
|
|
18
|
+
queryKey: ['marketplace-mcp-items', params],
|
|
19
|
+
queryFn: () => fetchMcpMarketplaceItems(params),
|
|
20
|
+
staleTime: 15_000
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useMcpMarketplaceInstalled() {
|
|
25
|
+
return useQuery({
|
|
26
|
+
queryKey: ['marketplace-mcp-installed'],
|
|
27
|
+
queryFn: () => fetchMcpMarketplaceInstalled(),
|
|
28
|
+
staleTime: 10_000
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useMcpMarketplaceItem(slug: string | null) {
|
|
33
|
+
return useQuery({
|
|
34
|
+
queryKey: ['marketplace-mcp-item', slug],
|
|
35
|
+
queryFn: () => fetchMcpMarketplaceItem(slug as string),
|
|
36
|
+
enabled: Boolean(slug),
|
|
37
|
+
staleTime: 30_000
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function useMcpMarketplaceContent(slug: string | null) {
|
|
42
|
+
return useQuery({
|
|
43
|
+
queryKey: ['marketplace-mcp-content', slug],
|
|
44
|
+
queryFn: () => fetchMcpMarketplaceContent(slug as string),
|
|
45
|
+
enabled: Boolean(slug),
|
|
46
|
+
staleTime: 30_000
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function useMcpMarketplaceRecommendations(params: { scene?: string; limit?: number }) {
|
|
51
|
+
return useQuery({
|
|
52
|
+
queryKey: ['marketplace-mcp-recommendations', params],
|
|
53
|
+
queryFn: () => fetchMcpMarketplaceRecommendations(params),
|
|
54
|
+
staleTime: 30_000
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function useInstallMcpMarketplaceItem() {
|
|
59
|
+
const queryClient = useQueryClient();
|
|
60
|
+
return useMutation({
|
|
61
|
+
mutationFn: installMcpMarketplaceItem,
|
|
62
|
+
onSuccess: (result) => {
|
|
63
|
+
queryClient.invalidateQueries({ queryKey: ['marketplace-mcp-installed'] });
|
|
64
|
+
queryClient.invalidateQueries({ queryKey: ['marketplace-mcp-items'] });
|
|
65
|
+
queryClient.invalidateQueries({ queryKey: ['marketplace-mcp-doctor'] });
|
|
66
|
+
toast.success(result.message || t('marketplaceInstallSuccessMcp'));
|
|
67
|
+
},
|
|
68
|
+
onError: (error: Error) => {
|
|
69
|
+
toast.error(error.message || t('marketplaceInstallFailed'));
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function useManageMcpMarketplaceItem() {
|
|
75
|
+
const queryClient = useQueryClient();
|
|
76
|
+
return useMutation({
|
|
77
|
+
mutationFn: manageMcpMarketplaceItem,
|
|
78
|
+
onSuccess: (result: { message?: string }) => {
|
|
79
|
+
queryClient.invalidateQueries({ queryKey: ['marketplace-mcp-installed'] });
|
|
80
|
+
queryClient.invalidateQueries({ queryKey: ['marketplace-mcp-items'] });
|
|
81
|
+
queryClient.invalidateQueries({ queryKey: ['marketplace-mcp-doctor'] });
|
|
82
|
+
toast.success(result.message || t('marketplaceMcpManageSuccess'));
|
|
83
|
+
},
|
|
84
|
+
onError: (error: Error) => {
|
|
85
|
+
toast.error(error.message || t('marketplaceOperationFailed'));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function useDoctorMcpMarketplaceItem(name: string | null) {
|
|
91
|
+
return useQuery({
|
|
92
|
+
queryKey: ['marketplace-mcp-doctor', name],
|
|
93
|
+
queryFn: () => doctorMcpMarketplaceItem(name as string),
|
|
94
|
+
enabled: Boolean(name),
|
|
95
|
+
staleTime: 15_000
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { fetchMcpMarketplaceContent };
|