@nextclaw/ui 0.9.2 → 0.9.3
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 +6 -0
- package/dist/assets/ChannelsList-ZBPiF0y2.js +1 -0
- package/dist/assets/ChatPage-BOgoolWK.js +38 -0
- package/dist/assets/{DocBrowser-CVwUDJMO.js → DocBrowser-BUYNHg0Y.js} +1 -1
- package/dist/assets/LogoBadge-DXPq99LJ.js +1 -0
- package/dist/assets/MarketplacePage-Dx7nexYN.js +49 -0
- package/dist/assets/McpMarketplacePage-064wdotP.js +40 -0
- package/dist/assets/{ModelConfig-CsX-_fyy.js → ModelConfig-BDIfLesG.js} +1 -1
- package/dist/assets/ProvidersList-DrlIr46m.js +1 -0
- package/dist/assets/RemoteAccessPage-ZkUBA-Av.js +1 -0
- package/dist/assets/{RuntimeConfig-CX2TGEG1.js → RuntimeConfig-BPxXEGzM.js} +1 -1
- package/dist/assets/{SearchConfig-C-WBTcWi.js → SearchConfig-BIqnlpne.js} +1 -1
- package/dist/assets/{SecretsConfig-9kbR0ZCB.js → SecretsConfig-jKZEVF2q.js} +2 -2
- package/dist/assets/{SessionsConfig-Bohn3P1q.js → SessionsConfig-C_FXgVe1.js} +2 -2
- package/dist/assets/{chat-message-AWIcksDK.js → chat-message-DmzpZJc_.js} +1 -1
- package/dist/assets/index-Byfw276e.js +8 -0
- package/dist/assets/{index-CPDASUXh.js → index-Ct7FQpxN.js} +1 -1
- package/dist/assets/index-bhNuQis7.css +1 -0
- package/dist/assets/{label-DD61y-4v.js → label-B1MloEtn.js} +1 -1
- package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
- package/dist/assets/{page-layout-CfnoVycc.js → page-layout-BGg1EhM5.js} +1 -1
- package/dist/assets/{popover-DsugZ6rp.js → popover-jJMv74Fp.js} +1 -1
- package/dist/assets/{security-config-DIrf2Z0O.js → security-config-Boh9NIMz.js} +1 -1
- package/dist/assets/skeleton-CmATs_b3.js +1 -0
- package/dist/assets/status-dot-DNyCdxPZ.js +1 -0
- package/dist/assets/{switch-NX5OmUXQ.js → switch-DE_MYk7x.js} +1 -1
- package/dist/assets/{tabs-custom-9ihB5Jem.js → tabs-custom-B-zErYPr.js} +1 -1
- package/dist/assets/{useConfirmDialog-BuQnVTeR.js → useConfirmDialog-BqQ6QfhB.js} +2 -2
- package/dist/assets/{vendor-DKBNiC31.js → vendor-CwsIoNvJ.js} +128 -93
- package/dist/index.html +3 -3
- package/package.json +4 -4
- 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 +57 -0
- package/src/api/remote.types.ts +80 -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 +320 -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 +92 -0
- package/src/hooks/useWebSocket.ts +25 -16
- package/src/lib/i18n.marketplace.ts +91 -0
- package/src/lib/i18n.remote.ts +115 -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,320 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import type { RemoteRuntimeView, RemoteServiceView } from '@/api/types';
|
|
3
|
+
import {
|
|
4
|
+
useRemoteDoctor,
|
|
5
|
+
useRemoteLogin,
|
|
6
|
+
useRemoteLogout,
|
|
7
|
+
useRemoteServiceControl,
|
|
8
|
+
useRemoteSettings,
|
|
9
|
+
useRemoteStatus
|
|
10
|
+
} from '@/hooks/useRemoteAccess';
|
|
11
|
+
import { PageHeader, PageLayout } from '@/components/layout/page-layout';
|
|
12
|
+
import { Button } from '@/components/ui/button';
|
|
13
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
14
|
+
import { Input } from '@/components/ui/input';
|
|
15
|
+
import { Label } from '@/components/ui/label';
|
|
16
|
+
import { StatusDot } from '@/components/ui/status-dot';
|
|
17
|
+
import { Switch } from '@/components/ui/switch';
|
|
18
|
+
import { formatDateTime, t } from '@/lib/i18n';
|
|
19
|
+
import { Activity, KeyRound, Laptop, RefreshCcw, ServerCog, ShieldCheck, SquareTerminal } from 'lucide-react';
|
|
20
|
+
|
|
21
|
+
function getRuntimeStatus(runtime: RemoteRuntimeView | null): { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string } {
|
|
22
|
+
if (!runtime) {
|
|
23
|
+
return { status: 'inactive', label: t('remoteRuntimeMissing') };
|
|
24
|
+
}
|
|
25
|
+
if (runtime.state === 'connected') {
|
|
26
|
+
return { status: 'ready', label: t('remoteStateConnected') };
|
|
27
|
+
}
|
|
28
|
+
if (runtime.state === 'connecting') {
|
|
29
|
+
return { status: 'warning', label: t('remoteStateConnecting') };
|
|
30
|
+
}
|
|
31
|
+
if (runtime.state === 'error') {
|
|
32
|
+
return { status: 'warning', label: t('remoteStateError') };
|
|
33
|
+
}
|
|
34
|
+
if (runtime.state === 'disconnected') {
|
|
35
|
+
return { status: 'warning', label: t('remoteStateDisconnected') };
|
|
36
|
+
}
|
|
37
|
+
return { status: 'inactive', label: t('remoteStateDisabled') };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getServiceStatus(service: RemoteServiceView): { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string } {
|
|
41
|
+
if (!service.running) {
|
|
42
|
+
return { status: 'inactive', label: t('remoteServiceStopped') };
|
|
43
|
+
}
|
|
44
|
+
return service.currentProcess
|
|
45
|
+
? { status: 'ready', label: t('remoteServiceManagedRunning') }
|
|
46
|
+
: { status: 'active', label: t('remoteServiceRunning') };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function KeyValueRow(props: { label: string; value?: string | number | null; muted?: boolean }) {
|
|
50
|
+
const value = props.value === undefined || props.value === null || props.value === '' ? '-' : String(props.value);
|
|
51
|
+
return (
|
|
52
|
+
<div className="flex items-start justify-between gap-4 py-2 text-sm">
|
|
53
|
+
<span className="text-gray-500">{props.label}</span>
|
|
54
|
+
<span className={props.muted ? 'text-right text-gray-500' : 'text-right text-gray-800'}>{value}</span>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function RemoteAccessPage() {
|
|
60
|
+
const remoteStatus = useRemoteStatus();
|
|
61
|
+
const loginMutation = useRemoteLogin();
|
|
62
|
+
const logoutMutation = useRemoteLogout();
|
|
63
|
+
const settingsMutation = useRemoteSettings();
|
|
64
|
+
const doctorMutation = useRemoteDoctor();
|
|
65
|
+
const serviceMutation = useRemoteServiceControl();
|
|
66
|
+
|
|
67
|
+
const status = remoteStatus.data;
|
|
68
|
+
const runtimeStatus = useMemo(() => getRuntimeStatus(status?.runtime ?? null), [status?.runtime]);
|
|
69
|
+
const serviceStatus = useMemo(() => getServiceStatus(status?.service ?? { running: false, currentProcess: false }), [status?.service]);
|
|
70
|
+
|
|
71
|
+
const [email, setEmail] = useState('');
|
|
72
|
+
const [password, setPassword] = useState('');
|
|
73
|
+
const [loginApiBase, setLoginApiBase] = useState('');
|
|
74
|
+
const [register, setRegister] = useState(false);
|
|
75
|
+
const [enabled, setEnabled] = useState(false);
|
|
76
|
+
const [deviceName, setDeviceName] = useState('');
|
|
77
|
+
const [platformApiBase, setPlatformApiBase] = useState('');
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!status) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
setEnabled(status.settings.enabled);
|
|
84
|
+
setDeviceName(status.settings.deviceName);
|
|
85
|
+
setPlatformApiBase(status.settings.platformApiBase);
|
|
86
|
+
if (!loginApiBase) {
|
|
87
|
+
setLoginApiBase(status.account.apiBase ?? status.settings.platformApiBase ?? '');
|
|
88
|
+
}
|
|
89
|
+
}, [loginApiBase, status]);
|
|
90
|
+
|
|
91
|
+
if (remoteStatus.isLoading && !status) {
|
|
92
|
+
return <div className="p-8 text-gray-400">{t('remoteLoading')}</div>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<PageLayout className="space-y-6">
|
|
97
|
+
<PageHeader title={t('remotePageTitle')} description={t('remotePageDescription')} />
|
|
98
|
+
|
|
99
|
+
<div className="grid gap-6 xl:grid-cols-[1.35fr_0.95fr]">
|
|
100
|
+
<Card>
|
|
101
|
+
<CardHeader>
|
|
102
|
+
<CardTitle className="flex items-center gap-2">
|
|
103
|
+
<Activity className="h-4 w-4 text-primary" />
|
|
104
|
+
{t('remoteOverviewTitle')}
|
|
105
|
+
</CardTitle>
|
|
106
|
+
<CardDescription>{t('remoteOverviewDescription')}</CardDescription>
|
|
107
|
+
</CardHeader>
|
|
108
|
+
<CardContent className="space-y-5">
|
|
109
|
+
<div className="flex flex-wrap gap-2">
|
|
110
|
+
<StatusDot status={status?.account.loggedIn ? 'ready' : 'inactive'} label={status?.account.loggedIn ? t('remoteAccountConnected') : t('remoteAccountNotConnected')} />
|
|
111
|
+
<StatusDot status={serviceStatus.status} label={serviceStatus.label} />
|
|
112
|
+
<StatusDot status={runtimeStatus.status} label={runtimeStatus.label} />
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
116
|
+
<KeyValueRow label={t('remoteLocalOrigin')} value={status?.localOrigin} />
|
|
117
|
+
<KeyValueRow label={t('remotePublicPlatform')} value={status?.platformBase ?? status?.account.platformBase} />
|
|
118
|
+
<KeyValueRow label={t('remoteDeviceId')} value={status?.runtime?.deviceId} muted />
|
|
119
|
+
<KeyValueRow label={t('remoteLastConnectedAt')} value={status?.runtime?.lastConnectedAt ? formatDateTime(status.runtime.lastConnectedAt) : '-'} muted />
|
|
120
|
+
<KeyValueRow label={t('remoteLastError')} value={status?.runtime?.lastError} muted />
|
|
121
|
+
</div>
|
|
122
|
+
</CardContent>
|
|
123
|
+
</Card>
|
|
124
|
+
|
|
125
|
+
<Card>
|
|
126
|
+
<CardHeader>
|
|
127
|
+
<CardTitle className="flex items-center gap-2">
|
|
128
|
+
<Laptop className="h-4 w-4 text-primary" />
|
|
129
|
+
{t('remoteDeviceTitle')}
|
|
130
|
+
</CardTitle>
|
|
131
|
+
<CardDescription>{t('remoteDeviceDescription')}</CardDescription>
|
|
132
|
+
</CardHeader>
|
|
133
|
+
<CardContent className="space-y-4">
|
|
134
|
+
<div className="space-y-2">
|
|
135
|
+
<div className="flex items-center justify-between rounded-2xl border border-gray-200/70 px-4 py-3">
|
|
136
|
+
<div>
|
|
137
|
+
<p className="text-sm font-medium text-gray-900">{t('remoteEnabled')}</p>
|
|
138
|
+
<p className="mt-1 text-xs text-gray-500">{t('remoteEnabledHelp')}</p>
|
|
139
|
+
</div>
|
|
140
|
+
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div className="space-y-2">
|
|
145
|
+
<Label htmlFor="remote-device-name">{t('remoteDeviceName')}</Label>
|
|
146
|
+
<Input id="remote-device-name" value={deviceName} onChange={(event) => setDeviceName(event.target.value)} placeholder={t('remoteDeviceNamePlaceholder')} />
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div className="space-y-2">
|
|
150
|
+
<Label htmlFor="remote-platform-api-base">{t('remotePlatformApiBase')}</Label>
|
|
151
|
+
<Input
|
|
152
|
+
id="remote-platform-api-base"
|
|
153
|
+
value={platformApiBase}
|
|
154
|
+
onChange={(event) => setPlatformApiBase(event.target.value)}
|
|
155
|
+
placeholder="https://ai-gateway-api.nextclaw.io/v1"
|
|
156
|
+
/>
|
|
157
|
+
<p className="text-xs text-gray-500">{t('remotePlatformApiBaseHelp')}</p>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="flex flex-wrap gap-3">
|
|
161
|
+
<Button
|
|
162
|
+
onClick={() =>
|
|
163
|
+
settingsMutation.mutate({
|
|
164
|
+
enabled,
|
|
165
|
+
deviceName,
|
|
166
|
+
platformApiBase
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
disabled={settingsMutation.isPending}
|
|
170
|
+
>
|
|
171
|
+
{settingsMutation.isPending ? t('saving') : t('remoteSaveSettings')}
|
|
172
|
+
</Button>
|
|
173
|
+
<Button
|
|
174
|
+
variant="outline"
|
|
175
|
+
onClick={() => serviceMutation.mutate('restart')}
|
|
176
|
+
disabled={serviceMutation.isPending}
|
|
177
|
+
>
|
|
178
|
+
<RefreshCcw className="mr-2 h-4 w-4" />
|
|
179
|
+
{t('remoteRestartService')}
|
|
180
|
+
</Button>
|
|
181
|
+
</div>
|
|
182
|
+
<p className="text-xs text-gray-500">{t('remoteSaveHint')}</p>
|
|
183
|
+
</CardContent>
|
|
184
|
+
</Card>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
|
|
188
|
+
<Card>
|
|
189
|
+
<CardHeader>
|
|
190
|
+
<CardTitle className="flex items-center gap-2">
|
|
191
|
+
<KeyRound className="h-4 w-4 text-primary" />
|
|
192
|
+
{t('remoteAccountTitle')}
|
|
193
|
+
</CardTitle>
|
|
194
|
+
<CardDescription>{t('remoteAccountDescription')}</CardDescription>
|
|
195
|
+
</CardHeader>
|
|
196
|
+
<CardContent className="space-y-4">
|
|
197
|
+
{status?.account.loggedIn ? (
|
|
198
|
+
<>
|
|
199
|
+
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
200
|
+
<KeyValueRow label={t('remoteAccountEmail')} value={status.account.email} />
|
|
201
|
+
<KeyValueRow label={t('remoteAccountRole')} value={status.account.role} />
|
|
202
|
+
<KeyValueRow label={t('remoteApiBase')} value={status.account.apiBase} />
|
|
203
|
+
</div>
|
|
204
|
+
<Button variant="outline" onClick={() => logoutMutation.mutate()} disabled={logoutMutation.isPending}>
|
|
205
|
+
{logoutMutation.isPending ? t('remoteLoggingOut') : t('remoteLogout')}
|
|
206
|
+
</Button>
|
|
207
|
+
</>
|
|
208
|
+
) : (
|
|
209
|
+
<>
|
|
210
|
+
<div className="space-y-2">
|
|
211
|
+
<Label htmlFor="remote-email">{t('remoteEmail')}</Label>
|
|
212
|
+
<Input id="remote-email" value={email} onChange={(event) => setEmail(event.target.value)} placeholder="name@example.com" />
|
|
213
|
+
</div>
|
|
214
|
+
<div className="space-y-2">
|
|
215
|
+
<Label htmlFor="remote-password">{t('remotePassword')}</Label>
|
|
216
|
+
<Input id="remote-password" type="password" value={password} onChange={(event) => setPassword(event.target.value)} placeholder={t('remotePasswordPlaceholder')} />
|
|
217
|
+
</div>
|
|
218
|
+
<div className="space-y-2">
|
|
219
|
+
<Label htmlFor="remote-login-api-base">{t('remoteApiBase')}</Label>
|
|
220
|
+
<Input
|
|
221
|
+
id="remote-login-api-base"
|
|
222
|
+
value={loginApiBase}
|
|
223
|
+
onChange={(event) => setLoginApiBase(event.target.value)}
|
|
224
|
+
placeholder="https://ai-gateway-api.nextclaw.io/v1"
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
<div className="flex items-center justify-between rounded-2xl border border-gray-200/70 px-4 py-3">
|
|
228
|
+
<div>
|
|
229
|
+
<p className="text-sm font-medium text-gray-900">{t('remoteRegisterIfNeeded')}</p>
|
|
230
|
+
<p className="mt-1 text-xs text-gray-500">{t('remoteRegisterIfNeededHelp')}</p>
|
|
231
|
+
</div>
|
|
232
|
+
<Switch checked={register} onCheckedChange={setRegister} />
|
|
233
|
+
</div>
|
|
234
|
+
<Button
|
|
235
|
+
onClick={() =>
|
|
236
|
+
loginMutation.mutate({
|
|
237
|
+
email,
|
|
238
|
+
password,
|
|
239
|
+
apiBase: loginApiBase,
|
|
240
|
+
register
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
disabled={loginMutation.isPending || !email.trim() || !password}
|
|
244
|
+
>
|
|
245
|
+
{loginMutation.isPending ? t('remoteLoggingIn') : register ? t('remoteCreateAccount') : t('remoteLogin')}
|
|
246
|
+
</Button>
|
|
247
|
+
</>
|
|
248
|
+
)}
|
|
249
|
+
</CardContent>
|
|
250
|
+
</Card>
|
|
251
|
+
|
|
252
|
+
<Card>
|
|
253
|
+
<CardHeader>
|
|
254
|
+
<CardTitle className="flex items-center gap-2">
|
|
255
|
+
<ServerCog className="h-4 w-4 text-primary" />
|
|
256
|
+
{t('remoteServiceTitle')}
|
|
257
|
+
</CardTitle>
|
|
258
|
+
<CardDescription>{t('remoteServiceDescription')}</CardDescription>
|
|
259
|
+
</CardHeader>
|
|
260
|
+
<CardContent className="space-y-4">
|
|
261
|
+
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
262
|
+
<KeyValueRow label={t('remoteServicePid')} value={status?.service.pid} />
|
|
263
|
+
<KeyValueRow label={t('remoteServiceUiUrl')} value={status?.service.uiUrl} />
|
|
264
|
+
<KeyValueRow label={t('remoteServiceCurrentProcess')} value={status?.service.currentProcess ? t('yes') : t('no')} />
|
|
265
|
+
</div>
|
|
266
|
+
<div className="flex flex-wrap gap-3">
|
|
267
|
+
<Button variant="primary" onClick={() => serviceMutation.mutate('start')} disabled={serviceMutation.isPending}>
|
|
268
|
+
{t('remoteStartService')}
|
|
269
|
+
</Button>
|
|
270
|
+
<Button variant="outline" onClick={() => serviceMutation.mutate('restart')} disabled={serviceMutation.isPending}>
|
|
271
|
+
{t('remoteRestartService')}
|
|
272
|
+
</Button>
|
|
273
|
+
<Button variant="outline" onClick={() => serviceMutation.mutate('stop')} disabled={serviceMutation.isPending}>
|
|
274
|
+
{t('remoteStopService')}
|
|
275
|
+
</Button>
|
|
276
|
+
</div>
|
|
277
|
+
<p className="text-xs text-gray-500">{t('remoteServiceHint')}</p>
|
|
278
|
+
</CardContent>
|
|
279
|
+
</Card>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<Card>
|
|
283
|
+
<CardHeader>
|
|
284
|
+
<CardTitle className="flex items-center gap-2">
|
|
285
|
+
<ShieldCheck className="h-4 w-4 text-primary" />
|
|
286
|
+
{t('remoteDoctorTitle')}
|
|
287
|
+
</CardTitle>
|
|
288
|
+
<CardDescription>{t('remoteDoctorDescription')}</CardDescription>
|
|
289
|
+
</CardHeader>
|
|
290
|
+
<CardContent className="space-y-4">
|
|
291
|
+
<div className="flex flex-wrap gap-3">
|
|
292
|
+
<Button variant="outline" onClick={() => doctorMutation.mutate()} disabled={doctorMutation.isPending}>
|
|
293
|
+
<SquareTerminal className="mr-2 h-4 w-4" />
|
|
294
|
+
{doctorMutation.isPending ? t('remoteDoctorRunning') : t('remoteRunDoctor')}
|
|
295
|
+
</Button>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
{doctorMutation.data ? (
|
|
299
|
+
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
300
|
+
<KeyValueRow label={t('remoteDoctorGeneratedAt')} value={formatDateTime(doctorMutation.data.generatedAt)} muted />
|
|
301
|
+
<div className="mt-3 space-y-2">
|
|
302
|
+
{doctorMutation.data.checks.map((check) => (
|
|
303
|
+
<div key={check.name} className="rounded-xl border border-white bg-white px-3 py-3">
|
|
304
|
+
<div className="flex items-center justify-between gap-3">
|
|
305
|
+
<span className="text-sm font-medium text-gray-900">{check.name}</span>
|
|
306
|
+
<StatusDot status={check.ok ? 'ready' : 'warning'} label={check.ok ? t('remoteCheckPassed') : t('remoteCheckFailed')} />
|
|
307
|
+
</div>
|
|
308
|
+
<p className="mt-2 text-sm text-gray-600">{check.detail}</p>
|
|
309
|
+
</div>
|
|
310
|
+
))}
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
) : (
|
|
314
|
+
<p className="text-sm text-gray-500">{t('remoteDoctorEmpty')}</p>
|
|
315
|
+
)}
|
|
316
|
+
</CardContent>
|
|
317
|
+
</Card>
|
|
318
|
+
</PageLayout>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
@@ -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 };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import {
|
|
3
|
+
controlRemoteService,
|
|
4
|
+
fetchRemoteStatus,
|
|
5
|
+
fetchRemoteDoctor,
|
|
6
|
+
loginRemote,
|
|
7
|
+
logoutRemote,
|
|
8
|
+
updateRemoteSettings
|
|
9
|
+
} from '@/api/remote';
|
|
10
|
+
import { t } from '@/lib/i18n';
|
|
11
|
+
import { toast } from 'sonner';
|
|
12
|
+
|
|
13
|
+
export function useRemoteStatus() {
|
|
14
|
+
return useQuery({
|
|
15
|
+
queryKey: ['remote-status'],
|
|
16
|
+
queryFn: fetchRemoteStatus,
|
|
17
|
+
staleTime: 5000,
|
|
18
|
+
refetchOnWindowFocus: true
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useRemoteLogin() {
|
|
23
|
+
const queryClient = useQueryClient();
|
|
24
|
+
|
|
25
|
+
return useMutation({
|
|
26
|
+
mutationFn: loginRemote,
|
|
27
|
+
onSuccess: () => {
|
|
28
|
+
queryClient.invalidateQueries({ queryKey: ['remote-status'] });
|
|
29
|
+
toast.success(t('remoteLoginSuccess'));
|
|
30
|
+
},
|
|
31
|
+
onError: (error: Error) => {
|
|
32
|
+
toast.error(`${t('remoteLoginFailed')}: ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useRemoteLogout() {
|
|
38
|
+
const queryClient = useQueryClient();
|
|
39
|
+
|
|
40
|
+
return useMutation({
|
|
41
|
+
mutationFn: logoutRemote,
|
|
42
|
+
onSuccess: () => {
|
|
43
|
+
queryClient.invalidateQueries({ queryKey: ['remote-status'] });
|
|
44
|
+
toast.success(t('remoteLogoutSuccess'));
|
|
45
|
+
},
|
|
46
|
+
onError: (error: Error) => {
|
|
47
|
+
toast.error(`${t('remoteLogoutFailed')}: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useRemoteSettings() {
|
|
53
|
+
const queryClient = useQueryClient();
|
|
54
|
+
|
|
55
|
+
return useMutation({
|
|
56
|
+
mutationFn: updateRemoteSettings,
|
|
57
|
+
onSuccess: () => {
|
|
58
|
+
queryClient.invalidateQueries({ queryKey: ['remote-status'] });
|
|
59
|
+
toast.success(t('remoteSettingsSaved'));
|
|
60
|
+
},
|
|
61
|
+
onError: (error: Error) => {
|
|
62
|
+
toast.error(`${t('remoteSettingsSaveFailed')}: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function useRemoteDoctor() {
|
|
68
|
+
return useMutation({
|
|
69
|
+
mutationFn: fetchRemoteDoctor,
|
|
70
|
+
onSuccess: () => {
|
|
71
|
+
toast.success(t('remoteDoctorCompleted'));
|
|
72
|
+
},
|
|
73
|
+
onError: (error: Error) => {
|
|
74
|
+
toast.error(`${t('remoteDoctorFailed')}: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function useRemoteServiceControl() {
|
|
80
|
+
const queryClient = useQueryClient();
|
|
81
|
+
|
|
82
|
+
return useMutation({
|
|
83
|
+
mutationFn: controlRemoteService,
|
|
84
|
+
onSuccess: (result) => {
|
|
85
|
+
queryClient.invalidateQueries({ queryKey: ['remote-status'] });
|
|
86
|
+
toast.success(result.message);
|
|
87
|
+
},
|
|
88
|
+
onError: (error: Error) => {
|
|
89
|
+
toast.error(`${t('remoteServiceActionFailed')}: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -4,6 +4,30 @@ import { API_BASE } from '@/api/client';
|
|
|
4
4
|
import { useUiStore } from '@/stores/ui.store';
|
|
5
5
|
import type { QueryClient } from '@tanstack/react-query';
|
|
6
6
|
|
|
7
|
+
function shouldInvalidateConfigQuery(configPath: string) {
|
|
8
|
+
const normalized = configPath.trim().toLowerCase();
|
|
9
|
+
if (!normalized) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (normalized.startsWith('plugins') || normalized.startsWith('skills')) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function invalidateMarketplaceQueries(queryClient: QueryClient | undefined, configPath: string): void {
|
|
19
|
+
if (configPath.startsWith('plugins')) {
|
|
20
|
+
queryClient?.invalidateQueries({ queryKey: ['ncp-session-types'] });
|
|
21
|
+
queryClient?.invalidateQueries({ queryKey: ['marketplace-installed', 'plugin'] });
|
|
22
|
+
queryClient?.invalidateQueries({ queryKey: ['marketplace-items'] });
|
|
23
|
+
}
|
|
24
|
+
if (configPath.startsWith('mcp')) {
|
|
25
|
+
queryClient?.invalidateQueries({ queryKey: ['marketplace-mcp-installed'] });
|
|
26
|
+
queryClient?.invalidateQueries({ queryKey: ['marketplace-mcp-items'] });
|
|
27
|
+
queryClient?.invalidateQueries({ queryKey: ['marketplace-mcp-doctor'] });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
7
31
|
export function useWebSocket(queryClient?: QueryClient) {
|
|
8
32
|
const [ws, setWs] = useState<ConfigWebSocket | null>(null);
|
|
9
33
|
const { setConnectionStatus } = useUiStore();
|
|
@@ -83,17 +107,6 @@ export function useWebSocket(queryClient?: QueryClient) {
|
|
|
83
107
|
queryClient.invalidateQueries({ queryKey: ['ncp-session-messages'] });
|
|
84
108
|
};
|
|
85
109
|
|
|
86
|
-
const shouldInvalidateConfigQuery = (configPath: string) => {
|
|
87
|
-
const normalized = configPath.trim().toLowerCase();
|
|
88
|
-
if (!normalized) {
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
if (normalized.startsWith('plugins') || normalized.startsWith('skills')) {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
return true;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
110
|
setConnectionStatus('connecting');
|
|
98
111
|
|
|
99
112
|
client.on('connection.open', () => {
|
|
@@ -121,11 +134,7 @@ export function useWebSocket(queryClient?: QueryClient) {
|
|
|
121
134
|
if (configPath.startsWith('session')) {
|
|
122
135
|
invalidateSessionQueries();
|
|
123
136
|
}
|
|
124
|
-
|
|
125
|
-
queryClient?.invalidateQueries({ queryKey: ['ncp-session-types'] });
|
|
126
|
-
queryClient?.invalidateQueries({ queryKey: ['marketplace-installed', 'plugin'] });
|
|
127
|
-
queryClient?.invalidateQueries({ queryKey: ['marketplace-items'] });
|
|
128
|
-
}
|
|
137
|
+
invalidateMarketplaceQueries(queryClient, configPath);
|
|
129
138
|
});
|
|
130
139
|
|
|
131
140
|
client.on('run.updated', (event) => {
|