@nextclaw/ui 0.5.14 → 0.5.16
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-Cz3uXqRf.js +1 -0
- package/dist/assets/CronConfig-Cp52VzLN.js +1 -0
- package/dist/assets/DocBrowser-f4DlcaoQ.js +1 -0
- package/dist/assets/MarketplacePage-lqsZ93qA.js +1 -0
- package/dist/assets/ModelConfig-Drz5yiFE.js +1 -0
- package/dist/assets/ProvidersList-BYuBSHLz.js +1 -0
- package/dist/assets/RuntimeConfig-24TfpNpu.js +1 -0
- package/dist/assets/SessionsConfig-Da9p8YBD.js +2 -0
- package/dist/assets/action-link-7uv6J28y.js +1 -0
- package/dist/assets/card-D-XgwaXI.js +1 -0
- package/dist/assets/config-hints-CApS3K_7.js +1 -0
- package/dist/assets/dialog-yW7kJ2ni.js +5 -0
- package/dist/assets/index-C8ImW-Sf.js +2 -0
- package/dist/assets/index-DdpR1fdj.css +1 -0
- package/dist/assets/label-DR4VTdER.js +1 -0
- package/dist/assets/page-layout-fYkwpvEI.js +1 -0
- package/dist/assets/switch-Z8BKkE23.js +1 -0
- package/dist/assets/tabs-custom-C-Ex-MdR.js +1 -0
- package/dist/assets/useConfig-BC17j2tK.js +1 -0
- package/dist/assets/useConfirmDialog-Bydoiiwm.js +1 -0
- package/dist/assets/vendor-Bhv7yx8z.js +347 -0
- package/dist/index.html +3 -2
- package/package.json +1 -1
- package/src/App.tsx +25 -14
- package/src/api/marketplace.ts +20 -20
- package/src/api/types.ts +2 -2
- package/src/components/layout/AppLayout.tsx +15 -3
- package/src/components/layout/Sidebar.tsx +9 -4
- package/src/components/marketplace/MarketplacePage.tsx +82 -49
- package/src/hooks/useMarketplace.ts +19 -11
- package/src/lib/i18n.ts +29 -13
- package/vite.config.ts +2 -2
- package/dist/assets/index-CZnQJKDU.js +0 -347
- package/dist/assets/index-CfkfeqC5.css +0 -1
package/dist/index.html
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
8
|
<title>NextClaw - 系统配置</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-C8ImW-Sf.js"></script>
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-Bhv7yx8z.js">
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DdpR1fdj.css">
|
|
11
12
|
</head>
|
|
12
13
|
|
|
13
14
|
<body>
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
|
+
import { lazy, Suspense } from 'react';
|
|
1
2
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
3
|
import { AppLayout } from '@/components/layout/AppLayout';
|
|
3
|
-
import { ModelConfig } from '@/components/config/ModelConfig';
|
|
4
|
-
import { ProvidersList } from '@/components/config/ProvidersList';
|
|
5
|
-
import { ChannelsList } from '@/components/config/ChannelsList';
|
|
6
|
-
import { RuntimeConfig } from '@/components/config/RuntimeConfig';
|
|
7
|
-
import { SessionsConfig } from '@/components/config/SessionsConfig';
|
|
8
|
-
import { CronConfig } from '@/components/config/CronConfig';
|
|
9
|
-
import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
|
|
10
4
|
import { useWebSocket } from '@/hooks/useWebSocket';
|
|
11
5
|
import { Toaster } from 'sonner';
|
|
12
6
|
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
|
@@ -20,6 +14,22 @@ const queryClient = new QueryClient({
|
|
|
20
14
|
}
|
|
21
15
|
});
|
|
22
16
|
|
|
17
|
+
const ModelConfigPage = lazy(async () => ({ default: (await import('@/components/config/ModelConfig')).ModelConfig }));
|
|
18
|
+
const ProvidersListPage = lazy(async () => ({ default: (await import('@/components/config/ProvidersList')).ProvidersList }));
|
|
19
|
+
const ChannelsListPage = lazy(async () => ({ default: (await import('@/components/config/ChannelsList')).ChannelsList }));
|
|
20
|
+
const RuntimeConfigPage = lazy(async () => ({ default: (await import('@/components/config/RuntimeConfig')).RuntimeConfig }));
|
|
21
|
+
const SessionsConfigPage = lazy(async () => ({ default: (await import('@/components/config/SessionsConfig')).SessionsConfig }));
|
|
22
|
+
const CronConfigPage = lazy(async () => ({ default: (await import('@/components/config/CronConfig')).CronConfig }));
|
|
23
|
+
const MarketplacePage = lazy(async () => ({ default: (await import('@/components/marketplace/MarketplacePage')).MarketplacePage }));
|
|
24
|
+
|
|
25
|
+
function RouteFallback() {
|
|
26
|
+
return <div className="h-full w-full animate-pulse rounded-2xl border border-border/40 bg-card/40" />;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function LazyRoute({ children }: { children: JSX.Element }) {
|
|
30
|
+
return <Suspense fallback={<RouteFallback />}>{children}</Suspense>;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
function AppContent() {
|
|
24
34
|
useWebSocket(queryClient); // Initialize WebSocket connection
|
|
25
35
|
const location = useLocation();
|
|
@@ -29,13 +39,14 @@ function AppContent() {
|
|
|
29
39
|
<AppLayout>
|
|
30
40
|
<div key={location.pathname} className="animate-fade-in w-full h-full">
|
|
31
41
|
<Routes>
|
|
32
|
-
<Route path="/model" element={<
|
|
33
|
-
<Route path="/providers" element={<
|
|
34
|
-
<Route path="/channels" element={<
|
|
35
|
-
<Route path="/runtime" element={<
|
|
36
|
-
<Route path="/sessions" element={<
|
|
37
|
-
<Route path="/cron" element={<
|
|
38
|
-
<Route path="/marketplace" element={<
|
|
42
|
+
<Route path="/model" element={<LazyRoute><ModelConfigPage /></LazyRoute>} />
|
|
43
|
+
<Route path="/providers" element={<LazyRoute><ProvidersListPage /></LazyRoute>} />
|
|
44
|
+
<Route path="/channels" element={<LazyRoute><ChannelsListPage /></LazyRoute>} />
|
|
45
|
+
<Route path="/runtime" element={<LazyRoute><RuntimeConfigPage /></LazyRoute>} />
|
|
46
|
+
<Route path="/sessions" element={<LazyRoute><SessionsConfigPage /></LazyRoute>} />
|
|
47
|
+
<Route path="/cron" element={<LazyRoute><CronConfigPage /></LazyRoute>} />
|
|
48
|
+
<Route path="/marketplace" element={<Navigate to="/marketplace/plugins" replace />} />
|
|
49
|
+
<Route path="/marketplace/:type" element={<LazyRoute><MarketplacePage /></LazyRoute>} />
|
|
39
50
|
<Route path="/" element={<Navigate to="/model" replace />} />
|
|
40
51
|
<Route path="*" element={<Navigate to="/model" replace />} />
|
|
41
52
|
</Routes>
|
package/src/api/marketplace.ts
CHANGED
|
@@ -13,23 +13,25 @@ import type {
|
|
|
13
13
|
} from './types';
|
|
14
14
|
|
|
15
15
|
export type MarketplaceListParams = {
|
|
16
|
+
type: MarketplaceItemType;
|
|
16
17
|
q?: string;
|
|
17
|
-
type?: MarketplaceItemType;
|
|
18
18
|
tag?: string;
|
|
19
19
|
sort?: MarketplaceSort;
|
|
20
20
|
page?: number;
|
|
21
21
|
pageSize?: number;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
function toMarketplaceTypeSegment(type: MarketplaceItemType): 'plugins' | 'skills' {
|
|
25
|
+
return type === 'plugin' ? 'plugins' : 'skills';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function fetchMarketplaceItems(params: MarketplaceListParams): Promise<MarketplaceListView> {
|
|
25
29
|
const query = new URLSearchParams();
|
|
30
|
+
const segment = toMarketplaceTypeSegment(params.type);
|
|
26
31
|
|
|
27
32
|
if (params.q?.trim()) {
|
|
28
33
|
query.set('q', params.q.trim());
|
|
29
34
|
}
|
|
30
|
-
if (params.type) {
|
|
31
|
-
query.set('type', params.type);
|
|
32
|
-
}
|
|
33
35
|
if (params.tag?.trim()) {
|
|
34
36
|
query.set('tag', params.tag.trim());
|
|
35
37
|
}
|
|
@@ -44,7 +46,9 @@ export async function fetchMarketplaceItems(params: MarketplaceListParams = {}):
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
const suffix = query.toString();
|
|
47
|
-
const response = await api.get<MarketplaceListView>(
|
|
49
|
+
const response = await api.get<MarketplaceListView>(
|
|
50
|
+
suffix ? `/api/marketplace/${segment}/items?${suffix}` : `/api/marketplace/${segment}/items`
|
|
51
|
+
);
|
|
48
52
|
if (!response.ok) {
|
|
49
53
|
throw new Error(response.error.message);
|
|
50
54
|
}
|
|
@@ -52,17 +56,10 @@ export async function fetchMarketplaceItems(params: MarketplaceListParams = {}):
|
|
|
52
56
|
return response.data;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
export async function fetchMarketplaceItem(slug: string, type
|
|
56
|
-
const
|
|
57
|
-
if (type) {
|
|
58
|
-
query.set('type', type);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const suffix = query.toString();
|
|
59
|
+
export async function fetchMarketplaceItem(slug: string, type: MarketplaceItemType): Promise<MarketplaceItemView> {
|
|
60
|
+
const segment = toMarketplaceTypeSegment(type);
|
|
62
61
|
const response = await api.get<MarketplaceItemView>(
|
|
63
|
-
|
|
64
|
-
? `/api/marketplace/items/${encodeURIComponent(slug)}?${suffix}`
|
|
65
|
-
: `/api/marketplace/items/${encodeURIComponent(slug)}`
|
|
62
|
+
`/api/marketplace/${segment}/items/${encodeURIComponent(slug)}`
|
|
66
63
|
);
|
|
67
64
|
if (!response.ok) {
|
|
68
65
|
throw new Error(response.error.message);
|
|
@@ -95,7 +92,8 @@ export async function fetchMarketplaceRecommendations(params: {
|
|
|
95
92
|
}
|
|
96
93
|
|
|
97
94
|
export async function installMarketplaceItem(request: MarketplaceInstallRequest): Promise<MarketplaceInstallResult> {
|
|
98
|
-
const
|
|
95
|
+
const segment = toMarketplaceTypeSegment(request.type);
|
|
96
|
+
const response = await api.post<MarketplaceInstallResult>(`/api/marketplace/${segment}/install`, request);
|
|
99
97
|
if (!response.ok) {
|
|
100
98
|
throw new Error(response.error.message);
|
|
101
99
|
}
|
|
@@ -103,8 +101,9 @@ export async function installMarketplaceItem(request: MarketplaceInstallRequest)
|
|
|
103
101
|
return response.data;
|
|
104
102
|
}
|
|
105
103
|
|
|
106
|
-
export async function fetchMarketplaceInstalled(): Promise<MarketplaceInstalledView> {
|
|
107
|
-
const
|
|
104
|
+
export async function fetchMarketplaceInstalled(type: MarketplaceItemType): Promise<MarketplaceInstalledView> {
|
|
105
|
+
const segment = toMarketplaceTypeSegment(type);
|
|
106
|
+
const response = await api.get<MarketplaceInstalledView>(`/api/marketplace/${segment}/installed`);
|
|
108
107
|
if (!response.ok) {
|
|
109
108
|
throw new Error(response.error.message);
|
|
110
109
|
}
|
|
@@ -112,7 +111,8 @@ export async function fetchMarketplaceInstalled(): Promise<MarketplaceInstalledV
|
|
|
112
111
|
}
|
|
113
112
|
|
|
114
113
|
export async function manageMarketplaceItem(request: MarketplaceManageRequest): Promise<MarketplaceManageResult> {
|
|
115
|
-
const
|
|
114
|
+
const segment = toMarketplaceTypeSegment(request.type);
|
|
115
|
+
const response = await api.post<MarketplaceManageResult>(`/api/marketplace/${segment}/manage`, request);
|
|
116
116
|
if (!response.ok) {
|
|
117
117
|
throw new Error(response.error.message);
|
|
118
118
|
}
|
package/src/api/types.ts
CHANGED
|
@@ -355,9 +355,9 @@ export type MarketplaceInstalledRecord = {
|
|
|
355
355
|
};
|
|
356
356
|
|
|
357
357
|
export type MarketplaceInstalledView = {
|
|
358
|
+
type: MarketplaceItemType;
|
|
358
359
|
total: number;
|
|
359
|
-
|
|
360
|
-
skillSpecs: string[];
|
|
360
|
+
specs: string[];
|
|
361
361
|
records: MarketplaceInstalledRecord[];
|
|
362
362
|
};
|
|
363
363
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { lazy, Suspense } from 'react';
|
|
1
2
|
import { Sidebar } from './Sidebar';
|
|
2
|
-
import { DocBrowserProvider,
|
|
3
|
+
import { DocBrowserProvider, useDocBrowser } from '@/components/doc-browser/DocBrowserContext';
|
|
4
|
+
import { useDocLinkInterceptor } from '@/components/doc-browser/useDocLinkInterceptor';
|
|
5
|
+
|
|
6
|
+
const DocBrowser = lazy(async () => ({ default: (await import('@/components/doc-browser/DocBrowser')).DocBrowser }));
|
|
3
7
|
|
|
4
8
|
interface AppLayoutProps {
|
|
5
9
|
children: React.ReactNode;
|
|
@@ -21,9 +25,17 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
21
25
|
</main>
|
|
22
26
|
</div>
|
|
23
27
|
{/* Doc Browser: docked mode renders inline, floating mode renders as overlay */}
|
|
24
|
-
{isOpen && mode === 'docked' &&
|
|
28
|
+
{isOpen && mode === 'docked' && (
|
|
29
|
+
<Suspense fallback={null}>
|
|
30
|
+
<DocBrowser />
|
|
31
|
+
</Suspense>
|
|
32
|
+
)}
|
|
25
33
|
</div>
|
|
26
|
-
{isOpen && mode === 'floating' &&
|
|
34
|
+
{isOpen && mode === 'floating' && (
|
|
35
|
+
<Suspense fallback={null}>
|
|
36
|
+
<DocBrowser />
|
|
37
|
+
</Suspense>
|
|
38
|
+
)}
|
|
27
39
|
</div>
|
|
28
40
|
);
|
|
29
41
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn } from '@/lib/utils';
|
|
2
2
|
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
|
|
3
3
|
import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
|
|
4
|
-
import { Cpu, GitBranch, History, MessageSquare, Sparkles, BookOpen,
|
|
4
|
+
import { Cpu, GitBranch, History, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette } from 'lucide-react';
|
|
5
5
|
import { NavLink } from 'react-router-dom';
|
|
6
6
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
7
7
|
import { useI18n } from '@/components/providers/I18nProvider';
|
|
@@ -62,9 +62,14 @@ export function Sidebar() {
|
|
|
62
62
|
icon: AlarmClock,
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
|
-
target: '/marketplace',
|
|
66
|
-
label: t('
|
|
67
|
-
icon:
|
|
65
|
+
target: '/marketplace/plugins',
|
|
66
|
+
label: t('marketplaceFilterPlugins'),
|
|
67
|
+
icon: Plug,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
target: '/marketplace/skills',
|
|
71
|
+
label: t('marketplaceFilterSkills'),
|
|
72
|
+
icon: BrainCircuit,
|
|
68
73
|
}
|
|
69
74
|
];
|
|
70
75
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/* eslint-disable max-lines-per-function */
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
MarketplaceInstalledRecord,
|
|
4
|
+
MarketplaceItemSummary,
|
|
5
|
+
MarketplaceManageAction,
|
|
6
|
+
MarketplaceSort,
|
|
7
|
+
MarketplaceItemType
|
|
8
|
+
} from '@/api/types';
|
|
3
9
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
4
10
|
import { Tabs } from '@/components/ui/tabs-custom';
|
|
5
11
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
@@ -15,10 +21,10 @@ import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
|
15
21
|
import { cn } from '@/lib/utils';
|
|
16
22
|
import { PackageSearch } from 'lucide-react';
|
|
17
23
|
import { useEffect, useMemo, useState } from 'react';
|
|
24
|
+
import { useNavigate, useParams } from 'react-router-dom';
|
|
18
25
|
|
|
19
26
|
const PAGE_SIZE = 12;
|
|
20
27
|
|
|
21
|
-
type FilterType = 'all' | 'plugin' | 'skill';
|
|
22
28
|
type ScopeType = 'all' | 'installed';
|
|
23
29
|
|
|
24
30
|
type InstallState = {
|
|
@@ -38,6 +44,8 @@ type InstalledRenderEntry = {
|
|
|
38
44
|
item?: MarketplaceItemSummary;
|
|
39
45
|
};
|
|
40
46
|
|
|
47
|
+
type MarketplaceRouteType = 'plugins' | 'skills';
|
|
48
|
+
|
|
41
49
|
function normalizeMarketplaceKey(value: string | undefined): string {
|
|
42
50
|
return (value ?? '').trim().toLowerCase();
|
|
43
51
|
}
|
|
@@ -168,10 +176,9 @@ function ItemIcon({ name, fallback }: { name?: string; fallback: string }) {
|
|
|
168
176
|
function FilterPanel(props: {
|
|
169
177
|
scope: ScopeType;
|
|
170
178
|
searchText: string;
|
|
171
|
-
|
|
179
|
+
searchPlaceholder: string;
|
|
172
180
|
sort: MarketplaceSort;
|
|
173
181
|
onSearchTextChange: (value: string) => void;
|
|
174
|
-
onTypeFilterChange: (value: FilterType) => void;
|
|
175
182
|
onSortChange: (value: MarketplaceSort) => void;
|
|
176
183
|
}) {
|
|
177
184
|
return (
|
|
@@ -182,33 +189,11 @@ function FilterPanel(props: {
|
|
|
182
189
|
<input
|
|
183
190
|
value={props.searchText}
|
|
184
191
|
onChange={(event) => props.onSearchTextChange(event.target.value)}
|
|
185
|
-
placeholder={
|
|
192
|
+
placeholder={props.searchPlaceholder}
|
|
186
193
|
className="w-full h-9 border border-gray-200/80 rounded-xl pl-9 pr-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/40"
|
|
187
194
|
/>
|
|
188
195
|
</div>
|
|
189
196
|
|
|
190
|
-
<div className="inline-flex h-9 rounded-xl bg-gray-100/80 p-1 shrink-0">
|
|
191
|
-
{([
|
|
192
|
-
{ value: 'all', label: t('marketplaceFilterAll') },
|
|
193
|
-
{ value: 'plugin', label: t('marketplaceFilterPlugins') },
|
|
194
|
-
{ value: 'skill', label: t('marketplaceFilterSkills') },
|
|
195
|
-
] as const).map((opt) => (
|
|
196
|
-
<button
|
|
197
|
-
key={opt.value}
|
|
198
|
-
type="button"
|
|
199
|
-
onClick={() => props.onTypeFilterChange(opt.value)}
|
|
200
|
-
className={cn(
|
|
201
|
-
'px-3 rounded-lg text-sm font-medium transition-all whitespace-nowrap',
|
|
202
|
-
props.typeFilter === opt.value
|
|
203
|
-
? 'bg-white text-gray-900 shadow-sm'
|
|
204
|
-
: 'text-gray-500 hover:text-gray-700'
|
|
205
|
-
)}
|
|
206
|
-
>
|
|
207
|
-
{opt.label}
|
|
208
|
-
</button>
|
|
209
|
-
))}
|
|
210
|
-
</div>
|
|
211
|
-
|
|
212
197
|
{props.scope === 'all' && (
|
|
213
198
|
<Select value={props.sort} onValueChange={(v) => props.onSortChange(v as MarketplaceSort)}>
|
|
214
199
|
<SelectTrigger className="h-9 w-[150px] shrink-0 rounded-lg">
|
|
@@ -367,10 +352,57 @@ function PaginationBar(props: {
|
|
|
367
352
|
}
|
|
368
353
|
|
|
369
354
|
export function MarketplacePage() {
|
|
355
|
+
const navigate = useNavigate();
|
|
356
|
+
const params = useParams<{ type?: string }>();
|
|
357
|
+
|
|
358
|
+
const routeType: MarketplaceRouteType | null = useMemo(() => {
|
|
359
|
+
if (params.type === 'plugins' || params.type === 'skills') {
|
|
360
|
+
return params.type;
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}, [params.type]);
|
|
364
|
+
|
|
365
|
+
useEffect(() => {
|
|
366
|
+
if (!routeType) {
|
|
367
|
+
navigate('/marketplace/plugins', { replace: true });
|
|
368
|
+
}
|
|
369
|
+
}, [routeType, navigate]);
|
|
370
|
+
|
|
371
|
+
const typeFilter: MarketplaceItemType = routeType === 'skills' ? 'skill' : 'plugin';
|
|
372
|
+
const isPluginModule = typeFilter === 'plugin';
|
|
373
|
+
const copyKeys = isPluginModule
|
|
374
|
+
? {
|
|
375
|
+
pageTitle: 'marketplacePluginsPageTitle',
|
|
376
|
+
pageDescription: 'marketplacePluginsPageDescription',
|
|
377
|
+
tabMarketplace: 'marketplaceTabMarketplacePlugins',
|
|
378
|
+
tabInstalled: 'marketplaceTabInstalledPlugins',
|
|
379
|
+
searchPlaceholder: 'marketplaceSearchPlaceholderPlugins',
|
|
380
|
+
sectionCatalog: 'marketplaceSectionPlugins',
|
|
381
|
+
sectionInstalled: 'marketplaceSectionInstalledPlugins',
|
|
382
|
+
errorLoadData: 'marketplaceErrorLoadingPluginsData',
|
|
383
|
+
errorLoadInstalled: 'marketplaceErrorLoadingInstalledPlugins',
|
|
384
|
+
emptyData: 'marketplaceNoPlugins',
|
|
385
|
+
emptyInstalled: 'marketplaceNoInstalledPlugins',
|
|
386
|
+
installedCountSuffix: 'marketplaceInstalledPluginsCountSuffix'
|
|
387
|
+
}
|
|
388
|
+
: {
|
|
389
|
+
pageTitle: 'marketplaceSkillsPageTitle',
|
|
390
|
+
pageDescription: 'marketplaceSkillsPageDescription',
|
|
391
|
+
tabMarketplace: 'marketplaceTabMarketplaceSkills',
|
|
392
|
+
tabInstalled: 'marketplaceTabInstalledSkills',
|
|
393
|
+
searchPlaceholder: 'marketplaceSearchPlaceholderSkills',
|
|
394
|
+
sectionCatalog: 'marketplaceSectionSkills',
|
|
395
|
+
sectionInstalled: 'marketplaceSectionInstalledSkills',
|
|
396
|
+
errorLoadData: 'marketplaceErrorLoadingSkillsData',
|
|
397
|
+
errorLoadInstalled: 'marketplaceErrorLoadingInstalledSkills',
|
|
398
|
+
emptyData: 'marketplaceNoSkills',
|
|
399
|
+
emptyInstalled: 'marketplaceNoInstalledSkills',
|
|
400
|
+
installedCountSuffix: 'marketplaceInstalledSkillsCountSuffix'
|
|
401
|
+
};
|
|
402
|
+
|
|
370
403
|
const [searchText, setSearchText] = useState('');
|
|
371
404
|
const [query, setQuery] = useState('');
|
|
372
405
|
const [scope, setScope] = useState<ScopeType>('all');
|
|
373
|
-
const [typeFilter, setTypeFilter] = useState<FilterType>('all');
|
|
374
406
|
const [sort, setSort] = useState<MarketplaceSort>('relevance');
|
|
375
407
|
const [page, setPage] = useState(1);
|
|
376
408
|
|
|
@@ -382,11 +414,15 @@ export function MarketplacePage() {
|
|
|
382
414
|
return () => clearTimeout(timer);
|
|
383
415
|
}, [searchText]);
|
|
384
416
|
|
|
385
|
-
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
setPage(1);
|
|
419
|
+
}, [typeFilter]);
|
|
420
|
+
|
|
421
|
+
const installedQuery = useMarketplaceInstalled(typeFilter);
|
|
386
422
|
|
|
387
423
|
const itemsQuery = useMarketplaceItems({
|
|
388
424
|
q: query || undefined,
|
|
389
|
-
type: typeFilter
|
|
425
|
+
type: typeFilter,
|
|
390
426
|
sort,
|
|
391
427
|
page,
|
|
392
428
|
pageSize: PAGE_SIZE
|
|
@@ -418,7 +454,7 @@ export function MarketplacePage() {
|
|
|
418
454
|
|
|
419
455
|
const installedEntries = useMemo<InstalledRenderEntry[]>(() => {
|
|
420
456
|
const entries = installedRecords
|
|
421
|
-
.filter((record) =>
|
|
457
|
+
.filter((record) => record.type === typeFilter)
|
|
422
458
|
.map((record) => ({
|
|
423
459
|
key: `${record.type}:${record.spec}:${record.id ?? ''}`,
|
|
424
460
|
record,
|
|
@@ -450,7 +486,7 @@ export function MarketplacePage() {
|
|
|
450
486
|
if (installedQuery.isLoading) {
|
|
451
487
|
return t('loading');
|
|
452
488
|
}
|
|
453
|
-
return `${installedEntries.length} ${t(
|
|
489
|
+
return `${installedEntries.length} ${t(copyKeys.installedCountSuffix)}`;
|
|
454
490
|
}
|
|
455
491
|
|
|
456
492
|
if (!itemsQuery.data) {
|
|
@@ -458,7 +494,7 @@ export function MarketplacePage() {
|
|
|
458
494
|
}
|
|
459
495
|
|
|
460
496
|
return `${allItems.length} / ${total}`;
|
|
461
|
-
}, [scope, installedQuery.isLoading, installedEntries.length, itemsQuery.data, allItems.length, total]);
|
|
497
|
+
}, [scope, installedQuery.isLoading, installedEntries.length, itemsQuery.data, allItems.length, total, copyKeys.installedCountSuffix]);
|
|
462
498
|
|
|
463
499
|
const installState: InstallState = {
|
|
464
500
|
isPending: installMutation.isPending,
|
|
@@ -471,11 +507,10 @@ export function MarketplacePage() {
|
|
|
471
507
|
action: manageMutation.variables?.action
|
|
472
508
|
};
|
|
473
509
|
|
|
474
|
-
const
|
|
475
|
-
{ id: 'all', label: t(
|
|
476
|
-
{ id: 'installed', label: t(
|
|
510
|
+
const scopeTabs = [
|
|
511
|
+
{ id: 'all', label: t(copyKeys.tabMarketplace) },
|
|
512
|
+
{ id: 'installed', label: t(copyKeys.tabInstalled), count: installedQuery.data?.total ?? 0 }
|
|
477
513
|
];
|
|
478
|
-
|
|
479
514
|
const handleInstall = (item: MarketplaceItemSummary) => {
|
|
480
515
|
if (installMutation.isPending) {
|
|
481
516
|
return;
|
|
@@ -515,10 +550,10 @@ export function MarketplacePage() {
|
|
|
515
550
|
|
|
516
551
|
return (
|
|
517
552
|
<PageLayout>
|
|
518
|
-
<PageHeader title={t(
|
|
553
|
+
<PageHeader title={t(copyKeys.pageTitle)} description={t(copyKeys.pageDescription)} />
|
|
519
554
|
|
|
520
555
|
<Tabs
|
|
521
|
-
tabs={
|
|
556
|
+
tabs={scopeTabs}
|
|
522
557
|
activeTab={scope}
|
|
523
558
|
onChange={(value) => {
|
|
524
559
|
setScope(value as ScopeType);
|
|
@@ -530,13 +565,9 @@ export function MarketplacePage() {
|
|
|
530
565
|
<FilterPanel
|
|
531
566
|
scope={scope}
|
|
532
567
|
searchText={searchText}
|
|
533
|
-
|
|
568
|
+
searchPlaceholder={t(copyKeys.searchPlaceholder)}
|
|
534
569
|
sort={sort}
|
|
535
570
|
onSearchTextChange={setSearchText}
|
|
536
|
-
onTypeFilterChange={(value) => {
|
|
537
|
-
setPage(1);
|
|
538
|
-
setTypeFilter(value);
|
|
539
|
-
}}
|
|
540
571
|
onSortChange={(value) => {
|
|
541
572
|
setPage(1);
|
|
542
573
|
setSort(value);
|
|
@@ -545,18 +576,20 @@ export function MarketplacePage() {
|
|
|
545
576
|
|
|
546
577
|
<section>
|
|
547
578
|
<div className="flex items-center justify-between mb-3">
|
|
548
|
-
<h3 className="text-[14px] font-semibold text-gray-900">
|
|
579
|
+
<h3 className="text-[14px] font-semibold text-gray-900">
|
|
580
|
+
{scope === 'installed' ? t(copyKeys.sectionInstalled) : t(copyKeys.sectionCatalog)}
|
|
581
|
+
</h3>
|
|
549
582
|
<span className="text-[12px] text-gray-500">{listSummary}</span>
|
|
550
583
|
</div>
|
|
551
584
|
|
|
552
585
|
{scope === 'all' && itemsQuery.isError && (
|
|
553
586
|
<div className="p-4 rounded-xl bg-rose-50 border border-rose-200 text-rose-700 text-sm">
|
|
554
|
-
{t(
|
|
587
|
+
{t(copyKeys.errorLoadData)}: {itemsQuery.error.message}
|
|
555
588
|
</div>
|
|
556
589
|
)}
|
|
557
590
|
{scope === 'installed' && installedQuery.isError && (
|
|
558
591
|
<div className="p-4 rounded-xl bg-rose-50 border border-rose-200 text-rose-700 text-sm">
|
|
559
|
-
{t(
|
|
592
|
+
{t(copyKeys.errorLoadInstalled)}: {installedQuery.error.message}
|
|
560
593
|
</div>
|
|
561
594
|
)}
|
|
562
595
|
|
|
@@ -587,10 +620,10 @@ export function MarketplacePage() {
|
|
|
587
620
|
</div>
|
|
588
621
|
|
|
589
622
|
{scope === 'all' && !itemsQuery.isLoading && !itemsQuery.isError && allItems.length === 0 && (
|
|
590
|
-
<div className="text-[13px] text-gray-500 py-8 text-center">{t(
|
|
623
|
+
<div className="text-[13px] text-gray-500 py-8 text-center">{t(copyKeys.emptyData)}</div>
|
|
591
624
|
)}
|
|
592
625
|
{scope === 'installed' && !installedQuery.isLoading && !installedQuery.isError && installedEntries.length === 0 && (
|
|
593
|
-
<div className="text-[13px] text-gray-500 py-8 text-center">{t(
|
|
626
|
+
<div className="text-[13px] text-gray-500 py-8 text-center">{t(copyKeys.emptyInstalled)}</div>
|
|
594
627
|
)}
|
|
595
628
|
</section>
|
|
596
629
|
|
|
@@ -31,16 +31,16 @@ export function useMarketplaceRecommendations(params: { scene?: string; limit?:
|
|
|
31
31
|
export function useMarketplaceItem(slug: string | null, type?: MarketplaceItemType) {
|
|
32
32
|
return useQuery({
|
|
33
33
|
queryKey: ['marketplace-item', slug, type],
|
|
34
|
-
queryFn: () => fetchMarketplaceItem(slug as string, type),
|
|
35
|
-
enabled: Boolean(slug),
|
|
34
|
+
queryFn: () => fetchMarketplaceItem(slug as string, type as MarketplaceItemType),
|
|
35
|
+
enabled: Boolean(slug && type),
|
|
36
36
|
staleTime: 30_000
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export function useMarketplaceInstalled() {
|
|
40
|
+
export function useMarketplaceInstalled(type: MarketplaceItemType) {
|
|
41
41
|
return useQuery({
|
|
42
|
-
queryKey: ['marketplace-installed'],
|
|
43
|
-
queryFn: fetchMarketplaceInstalled,
|
|
42
|
+
queryKey: ['marketplace-installed', type],
|
|
43
|
+
queryFn: () => fetchMarketplaceInstalled(type),
|
|
44
44
|
staleTime: 10_000
|
|
45
45
|
});
|
|
46
46
|
}
|
|
@@ -51,10 +51,13 @@ export function useInstallMarketplaceItem() {
|
|
|
51
51
|
return useMutation({
|
|
52
52
|
mutationFn: (request: MarketplaceInstallRequest) => installMarketplaceItem(request),
|
|
53
53
|
onSuccess: (result) => {
|
|
54
|
-
queryClient.invalidateQueries({ queryKey: ['marketplace-installed'] });
|
|
55
|
-
queryClient.refetchQueries({ queryKey: ['marketplace-installed'], type: 'active' });
|
|
54
|
+
queryClient.invalidateQueries({ queryKey: ['marketplace-installed', result.type] });
|
|
55
|
+
queryClient.refetchQueries({ queryKey: ['marketplace-installed', result.type], type: 'active' });
|
|
56
56
|
queryClient.refetchQueries({ queryKey: ['marketplace-items'], type: 'active' });
|
|
57
|
-
|
|
57
|
+
const fallback = result.type === 'plugin'
|
|
58
|
+
? t('marketplaceInstallSuccessPlugin')
|
|
59
|
+
: t('marketplaceInstallSuccessSkill');
|
|
60
|
+
toast.success(result.message || fallback);
|
|
58
61
|
},
|
|
59
62
|
onError: (error: Error) => {
|
|
60
63
|
toast.error(error.message || t('marketplaceInstallFailed'));
|
|
@@ -68,11 +71,16 @@ export function useManageMarketplaceItem() {
|
|
|
68
71
|
return useMutation({
|
|
69
72
|
mutationFn: (request: MarketplaceManageRequest) => manageMarketplaceItem(request),
|
|
70
73
|
onSuccess: (result) => {
|
|
71
|
-
queryClient.invalidateQueries({ queryKey: ['marketplace-installed'] });
|
|
74
|
+
queryClient.invalidateQueries({ queryKey: ['marketplace-installed', result.type] });
|
|
72
75
|
queryClient.invalidateQueries({ queryKey: ['marketplace-items'] });
|
|
73
|
-
queryClient.refetchQueries({ queryKey: ['marketplace-installed'], type: 'active' });
|
|
76
|
+
queryClient.refetchQueries({ queryKey: ['marketplace-installed', result.type], type: 'active' });
|
|
74
77
|
queryClient.refetchQueries({ queryKey: ['marketplace-items'], type: 'active' });
|
|
75
|
-
|
|
78
|
+
const fallback = result.action === 'enable'
|
|
79
|
+
? t('marketplaceEnableSuccess')
|
|
80
|
+
: result.action === 'disable'
|
|
81
|
+
? t('marketplaceDisableSuccess')
|
|
82
|
+
: t('marketplaceUninstallSuccess');
|
|
83
|
+
toast.success(result.message || fallback);
|
|
76
84
|
},
|
|
77
85
|
onError: (error: Error) => {
|
|
78
86
|
toast.error(error.message || t('marketplaceOperationFailed'));
|
package/src/lib/i18n.ts
CHANGED
|
@@ -385,12 +385,16 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
385
385
|
cronRunForceConfirm: { zh: '任务已禁用,仍要立即执行', en: 'Cron job disabled. Force run now' },
|
|
386
386
|
|
|
387
387
|
// Marketplace
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
388
|
+
marketplacePluginsPageTitle: { zh: '插件市场', en: 'Plugin Marketplace' },
|
|
389
|
+
marketplacePluginsPageDescription: { zh: '安装、启用与管理插件。', en: 'Install, enable, and manage plugins.' },
|
|
390
|
+
marketplaceSkillsPageTitle: { zh: '技能市场', en: 'Skill Marketplace' },
|
|
391
|
+
marketplaceSkillsPageDescription: { zh: '安装与管理技能。', en: 'Install and manage skills.' },
|
|
392
|
+
marketplaceTabMarketplacePlugins: { zh: '插件市场', en: 'Plugin Market' },
|
|
393
|
+
marketplaceTabMarketplaceSkills: { zh: '技能市场', en: 'Skill Market' },
|
|
394
|
+
marketplaceTabInstalledPlugins: { zh: '已安装插件', en: 'Installed Plugins' },
|
|
395
|
+
marketplaceTabInstalledSkills: { zh: '已安装技能', en: 'Installed Skills' },
|
|
396
|
+
marketplaceSearchPlaceholderPlugins: { zh: '搜索插件...', en: 'Search plugins...' },
|
|
397
|
+
marketplaceSearchPlaceholderSkills: { zh: '搜索技能...', en: 'Search skills...' },
|
|
394
398
|
marketplaceFilterPlugins: { zh: '插件', en: 'Plugins' },
|
|
395
399
|
marketplaceFilterSkills: { zh: '技能', en: 'Skills' },
|
|
396
400
|
marketplaceSortRelevance: { zh: '相关性', en: 'Relevance' },
|
|
@@ -408,20 +412,32 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
408
412
|
marketplaceDisabling: { zh: '禁用中...', en: 'Disabling...' },
|
|
409
413
|
marketplaceUninstall: { zh: '卸载', en: 'Uninstall' },
|
|
410
414
|
marketplaceRemoving: { zh: '卸载中...', en: 'Removing...' },
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
415
|
+
marketplaceSectionPlugins: { zh: '插件列表', en: 'Plugin Catalog' },
|
|
416
|
+
marketplaceSectionSkills: { zh: '技能列表', en: 'Skill Catalog' },
|
|
417
|
+
marketplaceSectionInstalledPlugins: { zh: '已安装插件', en: 'Installed Plugins' },
|
|
418
|
+
marketplaceSectionInstalledSkills: { zh: '已安装技能', en: 'Installed Skills' },
|
|
419
|
+
marketplaceErrorLoadingPluginsData: { zh: '加载插件市场数据失败', en: 'Failed to load plugin marketplace data' },
|
|
420
|
+
marketplaceErrorLoadingSkillsData: { zh: '加载技能市场数据失败', en: 'Failed to load skill marketplace data' },
|
|
421
|
+
marketplaceErrorLoadingInstalledPlugins: { zh: '加载已安装插件失败', en: 'Failed to load installed plugins' },
|
|
422
|
+
marketplaceErrorLoadingInstalledSkills: { zh: '加载已安装技能失败', en: 'Failed to load installed skills' },
|
|
423
|
+
marketplaceNoPlugins: { zh: '未找到插件。', en: 'No plugins found.' },
|
|
424
|
+
marketplaceNoSkills: { zh: '未找到技能。', en: 'No skills found.' },
|
|
425
|
+
marketplaceNoInstalledPlugins: { zh: '未找到已安装插件。', en: 'No installed plugins found.' },
|
|
426
|
+
marketplaceNoInstalledSkills: { zh: '未找到已安装技能。', en: 'No installed skills found.' },
|
|
417
427
|
marketplaceUninstallTitle: { zh: '确认卸载', en: 'Uninstall' },
|
|
418
428
|
marketplaceUninstallDescription: {
|
|
419
429
|
zh: '该操作会移除扩展,后续可在市场中重新安装。',
|
|
420
430
|
en: 'This will remove the extension. You can install it again from the marketplace.'
|
|
421
431
|
},
|
|
432
|
+
marketplaceInstallSuccessPlugin: { zh: '插件安装成功', en: 'Plugin installed successfully' },
|
|
433
|
+
marketplaceInstallSuccessSkill: { zh: '技能安装成功', en: 'Skill installed successfully' },
|
|
434
|
+
marketplaceEnableSuccess: { zh: '启用成功', en: 'Enabled successfully' },
|
|
435
|
+
marketplaceDisableSuccess: { zh: '禁用成功', en: 'Disabled successfully' },
|
|
436
|
+
marketplaceUninstallSuccess: { zh: '卸载成功', en: 'Uninstalled successfully' },
|
|
422
437
|
marketplaceInstallFailed: { zh: '安装失败', en: 'Install failed' },
|
|
423
438
|
marketplaceOperationFailed: { zh: '操作失败', en: 'Operation failed' },
|
|
424
|
-
|
|
439
|
+
marketplaceInstalledPluginsCountSuffix: { zh: '个已安装插件', en: 'installed plugins' },
|
|
440
|
+
marketplaceInstalledSkillsCountSuffix: { zh: '个已安装技能', en: 'installed skills' },
|
|
425
441
|
|
|
426
442
|
// Status
|
|
427
443
|
connected: { zh: '已连接', en: 'Connected' },
|
package/vite.config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineConfig } from 'vite';
|
|
1
|
+
import { defineConfig, splitVendorChunkPlugin } from 'vite';
|
|
2
2
|
import react from '@vitejs/plugin-react';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ const apiBase = process.env.VITE_API_BASE ?? 'http://127.0.0.1:18792';
|
|
|
6
6
|
const wsBase = apiBase.replace(/^http/i, 'ws');
|
|
7
7
|
|
|
8
8
|
export default defineConfig({
|
|
9
|
-
plugins: [react()],
|
|
9
|
+
plugins: [react(), splitVendorChunkPlugin()],
|
|
10
10
|
resolve: {
|
|
11
11
|
alias: {
|
|
12
12
|
'@': path.resolve(__dirname, './src')
|