@nextclaw/ui 0.12.3 → 0.12.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/dist/assets/{ChannelsList-DZWam3Ob.js → ChannelsList-C6-lh55g.js} +2 -2
- package/dist/assets/ChatPage-DOW0gPc2.js +45 -0
- package/dist/assets/DocBrowser-CGyeswYP.js +1 -0
- package/dist/assets/{DocBrowser-C7-1sXqo.js → DocBrowser-QUZ3nfmH.js} +1 -1
- package/dist/assets/{DocBrowserContext-DN5tjUoS.js → DocBrowserContext-CpiIfhJO.js} +1 -1
- package/dist/assets/{LogoBadge-DDS1sU_U.js → LogoBadge-BUK13xK5.js} +1 -1
- package/dist/assets/MarketplacePage-BDVwhIYE.js +1 -0
- package/dist/assets/MarketplacePage-LnKKL3xK.js +49 -0
- package/dist/assets/McpMarketplacePage-BG4T_Pcx.js +40 -0
- package/dist/assets/ModelConfig-LtWuogIw.js +1 -0
- package/dist/assets/ProviderScopedModelInput-DGn6sFEN.js +1 -0
- package/dist/assets/ProvidersList-ma-_MlLo.js +1 -0
- package/dist/assets/{RemoteAccessPage-COnjm8_x.js → RemoteAccessPage-ff15qO-c.js} +1 -1
- package/dist/assets/RuntimeConfig-TgPandXF.js +1 -0
- package/dist/assets/SearchConfig-C9iBt7pl.js +1 -0
- package/dist/assets/{SecretsConfig-Cefg1LFJ.js → SecretsConfig-Bew4EF2A.js} +2 -2
- package/dist/assets/{SessionsConfig-BZnmVTIu.js → SessionsConfig-2r2yAGZg.js} +2 -2
- package/dist/assets/{book-open-DvWqOode.js → book-open-CJG8Yz3U.js} +1 -1
- package/dist/assets/{chat-session-display-D4bYa0b8.js → chat-session-display-DkAC5OMC.js} +1 -1
- package/dist/assets/{chunk-JZWAC4HX-CxfKRD7X.js → chunk-JZWAC4HX-D5b3Iyas.js} +1 -1
- package/dist/assets/{config-BeGwf2Ao.js → config-zvnxSXSP.js} +1 -1
- package/dist/assets/{createLucideIcon-C7MmdIX3.js → createLucideIcon-_FMJqZw2.js} +1 -1
- package/dist/assets/{dist-B6VMuIQN.js → dist-B1fpOuON.js} +1 -1
- package/dist/assets/{dist-RWNFhxvR.js → dist-BCXX7FD-.js} +2 -2
- package/dist/assets/{external-link-U86Acd1t.js → external-link-b7gAJWYY.js} +1 -1
- package/dist/assets/{hash-D-OVfV3Z.js → hash-Bhy4TwfZ.js} +1 -1
- package/dist/assets/i18n-DJg9BPYk.js +1 -0
- package/dist/assets/index-BoJbxdvZ.css +1 -0
- package/dist/assets/index-CtlT4E9Y.js +6 -0
- package/dist/assets/infiniteQueryBehavior-CTcVlD9s.js +1 -0
- package/dist/assets/loader-circle-B60I0hEk.js +1 -0
- package/dist/assets/{logos-U1_qDA3U.js → logos-GMeYU9vc.js} +1 -1
- package/dist/assets/{page-layout-Z1klaUFW.js → page-layout-C8UbWuMt.js} +1 -1
- package/dist/assets/plus-CR7RfK3H.js +1 -0
- package/dist/assets/{popover-xWbqMnIN.js → popover-8HSx9wQj.js} +1 -1
- package/dist/assets/react-BB4jko2M.js +1 -0
- package/dist/assets/{refresh-ccw-JQh1lwq-.js → refresh-ccw-CA4_C7Zg.js} +1 -1
- package/dist/assets/{save-4VRlzkii.js → save-BtvMy4lk.js} +1 -1
- package/dist/assets/search-C60UA27E.js +1 -0
- package/dist/assets/security-config-BkFDYZ6j.js +1 -0
- package/dist/assets/{select-DF-AUoie.js → select-xp_Ac8ip.js} +1 -1
- package/dist/assets/skeleton-uxz_5h3A.js +1 -0
- package/dist/assets/{status-dot-Bq_8Ojvv.js → status-dot-Cn4Pp7DZ.js} +1 -1
- package/dist/assets/{switch-D7JF_RZ-.js → switch-BTi6UOij.js} +1 -1
- package/dist/assets/{tabs-custom-CLksZ2bO.js → tabs-custom-BiiN8DME.js} +1 -1
- package/dist/assets/{trash-2-VV8jvziy.js → trash-2-BpsF0N-r.js} +1 -1
- package/dist/assets/use-infinite-scroll-loader-C8jBv11-.js +1 -0
- package/dist/assets/{useConfirmDialog-CuQqiPx7.js → useConfirmDialog-BJIwUZjH.js} +1 -1
- package/dist/assets/{useMutation-DBTWPbTg.js → useMutation-BjBOKHj_.js} +1 -1
- package/dist/assets/x-BfTu-g7D.js +1 -0
- package/dist/index.html +19 -18
- package/package.json +5 -5
- package/src/account/components/account-panel.tsx +46 -4
- package/src/account/managers/account.manager.ts +19 -4
- package/src/api/remote.ts +9 -0
- package/src/api/remote.types.ts +5 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +183 -141
- package/src/components/chat/ChatSidebar.test.tsx +168 -28
- package/src/components/chat/ChatSidebar.tsx +103 -28
- package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +11 -11
- package/src/components/chat/adapters/chat-message.adapter.test.ts +43 -6
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +182 -44
- package/src/components/chat/adapters/chat-message.session-spawn-tool-card.test.ts +104 -0
- package/src/components/chat/chat-child-session-panel.tsx +103 -45
- package/src/components/chat/chat-page-runtime.test.ts +16 -19
- package/src/components/chat/chat-session-preference-sync.test.ts +13 -0
- package/src/components/chat/chat-session-preference-sync.ts +9 -7
- package/src/components/chat/chat-sidebar-list-mode-switch.tsx +43 -0
- package/src/components/chat/chat-sidebar-project-groups.tsx +152 -0
- package/src/components/chat/hooks/use-chat-session-project.test.tsx +5 -5
- package/src/components/chat/hooks/use-chat-session-project.ts +0 -5
- package/src/components/chat/hooks/use-chat-session-update.test.tsx +75 -0
- package/src/components/chat/hooks/use-chat-session-update.ts +4 -2
- package/src/components/chat/managers/chat-session-list.manager.test.ts +46 -6
- package/src/components/chat/managers/chat-session-list.manager.ts +19 -6
- package/src/components/chat/ncp/NcpChatPage.tsx +33 -38
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +3 -5
- package/src/components/chat/ncp/ncp-chat-page-data.ts +0 -1
- package/src/components/chat/ncp/ncp-chat.presenter.ts +2 -16
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +20 -7
- package/src/components/chat/session-header/chat-session-project-badge.test.tsx +16 -0
- package/src/components/chat/session-header/chat-session-project-badge.tsx +2 -2
- package/src/components/chat/stores/chat-session-list.store.ts +3 -0
- package/src/components/chat/useChatSessionTypeState.test.tsx +0 -3
- package/src/components/chat/useChatSessionTypeState.ts +3 -5
- package/src/components/config/ChannelsList.test.tsx +68 -0
- package/src/components/config/ChannelsList.tsx +22 -4
- package/src/components/config/ProvidersList.tsx +17 -3
- package/src/components/config/providers-list.test.tsx +68 -0
- package/src/components/layout/Sidebar.tsx +13 -13
- package/src/components/layout/sidebar.layout.test.tsx +32 -1
- package/src/components/marketplace/MarketplacePage.tsx +30 -30
- package/src/components/marketplace/marketplace-page-parts.tsx +16 -24
- package/src/components/marketplace/mcp/McpMarketplacePage.tsx +28 -26
- package/src/hooks/marketplace-list-pages.ts +27 -0
- package/src/hooks/use-infinite-scroll-loader.ts +88 -0
- package/src/hooks/useMarketplace.ts +14 -3
- package/src/hooks/useMcpMarketplace.ts +14 -3
- package/src/lib/i18n.chat.ts +3 -0
- package/src/lib/i18n.remote.ts +15 -0
- package/dist/assets/ChatPage-YBL7iJ1X.js +0 -43
- package/dist/assets/DocBrowser-DQjtSsY3.js +0 -1
- package/dist/assets/MarketplacePage-2tWWgwAb.js +0 -49
- package/dist/assets/MarketplacePage-BorWJftJ.js +0 -1
- package/dist/assets/McpMarketplacePage-N-fB4HID.js +0 -40
- package/dist/assets/ModelConfig-DvsBTUiE.js +0 -1
- package/dist/assets/ProviderScopedModelInput-D9woCARc.js +0 -1
- package/dist/assets/ProvidersList-D-qPGgC4.js +0 -1
- package/dist/assets/RuntimeConfig-BHpqcaHm.js +0 -1
- package/dist/assets/SearchConfig-DIT6M65Q.js +0 -1
- package/dist/assets/i18n-hM3v-3YG.js +0 -1
- package/dist/assets/index-CpxuJa9o.css +0 -1
- package/dist/assets/index-DHmCjcxq.js +0 -6
- package/dist/assets/label-CHJ1ATds.js +0 -1
- package/dist/assets/loader-circle-C8cpaL0w.js +0 -1
- package/dist/assets/marketplace-localization-CxSTG9wr.js +0 -1
- package/dist/assets/plus-CrkO1kob.js +0 -1
- package/dist/assets/react-3YE87-lE.js +0 -1
- package/dist/assets/search-EX-Papzl.js +0 -1
- package/dist/assets/security-config-DEgOD4VX.js +0 -1
- package/dist/assets/skeleton-B0mmt1vo.js +0 -1
- package/dist/assets/x-B4sxJkGY.js +0 -1
|
@@ -80,15 +80,25 @@ export function Sidebar({ mode }: SidebarProps) {
|
|
|
80
80
|
label: t('providers'),
|
|
81
81
|
icon: Sparkles,
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
target: '/channels',
|
|
85
|
+
label: t('channels'),
|
|
86
|
+
icon: MessageSquare,
|
|
87
|
+
},
|
|
83
88
|
{
|
|
84
89
|
target: '/search',
|
|
85
90
|
label: t('searchChannels'),
|
|
86
91
|
icon: Search,
|
|
87
92
|
},
|
|
88
93
|
{
|
|
89
|
-
target: '/
|
|
90
|
-
label: t('
|
|
91
|
-
icon:
|
|
94
|
+
target: '/marketplace/plugins',
|
|
95
|
+
label: t('marketplaceFilterPlugins'),
|
|
96
|
+
icon: Plug,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
target: '/marketplace/mcp',
|
|
100
|
+
label: t('marketplaceFilterMcp'),
|
|
101
|
+
icon: Wrench,
|
|
92
102
|
},
|
|
93
103
|
{
|
|
94
104
|
target: '/runtime',
|
|
@@ -114,16 +124,6 @@ export function Sidebar({ mode }: SidebarProps) {
|
|
|
114
124
|
target: '/secrets',
|
|
115
125
|
label: t('secrets'),
|
|
116
126
|
icon: KeyRound,
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
target: '/marketplace/plugins',
|
|
120
|
-
label: t('marketplaceFilterPlugins'),
|
|
121
|
-
icon: Plug,
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
target: '/marketplace/mcp',
|
|
125
|
-
label: t('marketplaceFilterMcp'),
|
|
126
|
-
icon: Wrench,
|
|
127
127
|
}
|
|
128
128
|
];
|
|
129
129
|
const navItems = isSettingsMode ? settingsNavItems : mainNavItems;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
1
|
+
import { render, screen, within } from '@testing-library/react';
|
|
2
2
|
import { MemoryRouter } from 'react-router-dom';
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { Sidebar } from '@/components/layout/Sidebar';
|
|
@@ -85,6 +85,37 @@ describe('Sidebar', () => {
|
|
|
85
85
|
expect(backLink.className).toContain('hover:bg-gray-200/60');
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
it('keeps the settings navigation in the expected product order', () => {
|
|
89
|
+
const { container } = render(
|
|
90
|
+
<MemoryRouter initialEntries={['/model']}>
|
|
91
|
+
<Sidebar mode="settings" />
|
|
92
|
+
</MemoryRouter>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const nav = container.querySelector('nav');
|
|
96
|
+
if (!(nav instanceof HTMLElement)) {
|
|
97
|
+
throw new Error('settings nav not found');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const linkTexts = within(nav)
|
|
101
|
+
.getAllByRole('link')
|
|
102
|
+
.map((link) => link.textContent?.trim() || '');
|
|
103
|
+
|
|
104
|
+
expect(linkTexts).toEqual([
|
|
105
|
+
'Model',
|
|
106
|
+
'Providers',
|
|
107
|
+
'Channels',
|
|
108
|
+
'Search Channels',
|
|
109
|
+
'Plugins',
|
|
110
|
+
'MCP',
|
|
111
|
+
'Routing & Runtime',
|
|
112
|
+
'Remote Access',
|
|
113
|
+
'Security',
|
|
114
|
+
'Sessions',
|
|
115
|
+
'Secrets'
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
|
|
88
119
|
it('keeps the footer utilities compact without changing the top header structure', () => {
|
|
89
120
|
render(
|
|
90
121
|
<MemoryRouter initialEntries={['/model']}>
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
import {
|
|
24
24
|
FilterPanel,
|
|
25
25
|
MarketplaceListSkeleton,
|
|
26
|
-
|
|
26
|
+
MarketplaceInfiniteScrollStatus
|
|
27
27
|
} from '@/components/marketplace/marketplace-page-parts';
|
|
28
28
|
import { buildLocaleFallbacks, pickLocalizedText } from '@/components/marketplace/marketplace-localization';
|
|
29
29
|
import { t } from '@/lib/i18n';
|
|
@@ -31,6 +31,7 @@ import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
|
31
31
|
import { cn } from '@/lib/utils';
|
|
32
32
|
import { useEffect, useMemo, useState } from 'react';
|
|
33
33
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
34
|
+
import { useInfiniteScrollLoader } from '@/hooks/use-infinite-scroll-loader';
|
|
34
35
|
|
|
35
36
|
const PAGE_SIZE = 12;
|
|
36
37
|
const SKELETON_CARD_COUNT = PAGE_SIZE;
|
|
@@ -452,32 +453,38 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
452
453
|
const [query, setQuery] = useState('');
|
|
453
454
|
const [scope, setScope] = useState<ScopeType>('all');
|
|
454
455
|
const [sort, setSort] = useState<MarketplaceSort>('relevance');
|
|
455
|
-
const [page, setPage] = useState(1);
|
|
456
456
|
const [installingSpecs, setInstallingSpecs] = useState<ReadonlySet<string>>(new Set());
|
|
457
457
|
const [managingTargets, setManagingTargets] = useState<ReadonlyMap<string, MarketplaceManageAction>>(new Map());
|
|
458
458
|
|
|
459
459
|
useEffect(() => {
|
|
460
460
|
const timer = setTimeout(() => {
|
|
461
|
-
setPage(1);
|
|
462
461
|
setQuery(searchText.trim());
|
|
463
462
|
}, 250);
|
|
464
463
|
return () => clearTimeout(timer);
|
|
465
464
|
}, [searchText]);
|
|
466
465
|
|
|
467
|
-
useEffect(() => {
|
|
468
|
-
setPage(1);
|
|
469
|
-
}, [typeFilter]);
|
|
470
|
-
|
|
471
466
|
const installedQuery = useMarketplaceInstalled(typeFilter);
|
|
472
467
|
|
|
473
468
|
const itemsQuery = useMarketplaceItems({
|
|
474
469
|
q: query || undefined,
|
|
475
470
|
type: typeFilter,
|
|
476
471
|
sort,
|
|
477
|
-
page,
|
|
478
472
|
pageSize: PAGE_SIZE
|
|
479
473
|
});
|
|
480
474
|
|
|
475
|
+
const infiniteScroll = useInfiniteScrollLoader({
|
|
476
|
+
disabled: scope !== 'all' || itemsQuery.isError || !itemsQuery.hasNextPage || itemsQuery.isFetchingNextPage,
|
|
477
|
+
onLoadMore: () => itemsQuery.fetchNextPage(),
|
|
478
|
+
watchValue: `${typeFilter}:${scope}:${query}:${sort}:${itemsQuery.data?.loadedItems ?? 0}:${itemsQuery.data?.loadedPages ?? 0}`
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
useEffect(() => {
|
|
482
|
+
const container = infiniteScroll.containerRef.current;
|
|
483
|
+
if (container && typeof container.scrollTo === 'function') {
|
|
484
|
+
container.scrollTo({ top: 0 });
|
|
485
|
+
}
|
|
486
|
+
}, [infiniteScroll.containerRef, query, scope, sort, typeFilter]);
|
|
487
|
+
|
|
481
488
|
const installMutation = useInstallMarketplaceItem();
|
|
482
489
|
const manageMutation = useManageMarketplaceItem();
|
|
483
490
|
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
@@ -529,7 +536,6 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
529
536
|
}, [installedRecords, typeFilter, catalogLookup, query, localeFallbacks]);
|
|
530
537
|
|
|
531
538
|
const total = scope === 'installed' ? installedEntries.length : (itemsQuery.data?.total ?? 0);
|
|
532
|
-
const totalPages = scope === 'installed' ? 1 : (itemsQuery.data?.totalPages ?? 0);
|
|
533
539
|
const showCatalogSkeleton = scope === 'all' && itemsQuery.isLoading && !itemsQuery.data;
|
|
534
540
|
const showInstalledSkeleton = scope === 'installed' && installedQuery.isLoading && !installedQuery.data;
|
|
535
541
|
const showListSkeleton = showCatalogSkeleton || showInstalledSkeleton;
|
|
@@ -727,10 +733,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
727
733
|
<Tabs
|
|
728
734
|
tabs={scopeTabs}
|
|
729
735
|
activeTab={scope}
|
|
730
|
-
onChange={(value) =>
|
|
731
|
-
setScope(value as ScopeType);
|
|
732
|
-
setPage(1);
|
|
733
|
-
}}
|
|
736
|
+
onChange={(value) => setScope(value as ScopeType)}
|
|
734
737
|
className="mb-4"
|
|
735
738
|
/>
|
|
736
739
|
|
|
@@ -740,10 +743,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
740
743
|
searchPlaceholder={t(copyKeys.searchPlaceholder)}
|
|
741
744
|
sort={sort}
|
|
742
745
|
onSearchTextChange={setSearchText}
|
|
743
|
-
onSortChange={
|
|
744
|
-
setPage(1);
|
|
745
|
-
setSort(value);
|
|
746
|
-
}}
|
|
746
|
+
onSortChange={setSort}
|
|
747
747
|
/>
|
|
748
748
|
|
|
749
749
|
<section className="flex min-h-0 flex-1 flex-col">
|
|
@@ -765,7 +765,11 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
765
765
|
</div>
|
|
766
766
|
)}
|
|
767
767
|
|
|
768
|
-
<div
|
|
768
|
+
<div
|
|
769
|
+
ref={infiniteScroll.containerRef}
|
|
770
|
+
className="min-h-0 flex-1 overflow-y-auto custom-scrollbar pr-1"
|
|
771
|
+
aria-busy={showListSkeleton || itemsQuery.isFetchingNextPage}
|
|
772
|
+
>
|
|
769
773
|
<div
|
|
770
774
|
data-testid={showListSkeleton ? 'marketplace-list-skeleton' : undefined}
|
|
771
775
|
className="grid grid-cols-1 gap-3 lg:grid-cols-2 2xl:grid-cols-3"
|
|
@@ -807,20 +811,16 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
807
811
|
{scope === 'installed' && !showListSkeleton && !installedQuery.isError && installedEntries.length === 0 && (
|
|
808
812
|
<div className="text-[13px] text-gray-500 py-8 text-center">{t(copyKeys.emptyInstalled)}</div>
|
|
809
813
|
)}
|
|
810
|
-
</div>
|
|
811
|
-
</section>
|
|
812
814
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
onNext={() => setPage((current) => (totalPages > 0 ? Math.min(totalPages, current + 1) : current + 1))}
|
|
821
|
-
/>
|
|
815
|
+
{scope === 'all' && !showCatalogSkeleton && !itemsQuery.isError && (
|
|
816
|
+
<MarketplaceInfiniteScrollStatus
|
|
817
|
+
hasMore={Boolean(itemsQuery.hasNextPage)}
|
|
818
|
+
loading={itemsQuery.isFetchingNextPage}
|
|
819
|
+
sentinelRef={infiniteScroll.sentinelRef}
|
|
820
|
+
/>
|
|
821
|
+
)}
|
|
822
822
|
</div>
|
|
823
|
-
|
|
823
|
+
</section>
|
|
824
824
|
<ConfirmDialog />
|
|
825
825
|
</PageLayout>
|
|
826
826
|
);
|
|
@@ -3,6 +3,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|
|
3
3
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
4
4
|
import { t } from '@/lib/i18n';
|
|
5
5
|
import { PackageSearch } from 'lucide-react';
|
|
6
|
+
import type { Ref } from 'react';
|
|
6
7
|
|
|
7
8
|
export function FilterPanel(props: {
|
|
8
9
|
scope: 'all' | 'installed';
|
|
@@ -71,32 +72,23 @@ export function MarketplaceListSkeleton(props: {
|
|
|
71
72
|
);
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
export function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
onPrev: () => void;
|
|
79
|
-
onNext: () => void;
|
|
75
|
+
export function MarketplaceInfiniteScrollStatus(props: {
|
|
76
|
+
hasMore: boolean;
|
|
77
|
+
loading: boolean;
|
|
78
|
+
sentinelRef: Ref<HTMLDivElement>;
|
|
80
79
|
}) {
|
|
80
|
+
if (!props.hasMore && !props.loading) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
81
84
|
return (
|
|
82
|
-
<div className="
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
</button>
|
|
90
|
-
<div className="text-sm text-gray-600 min-w-20 text-center">
|
|
91
|
-
{props.totalPages === 0 ? '0 / 0' : `${props.page} / ${props.totalPages}`}
|
|
92
|
-
</div>
|
|
93
|
-
<button
|
|
94
|
-
className="h-8 px-3 rounded-xl border border-gray-200/80 text-sm text-gray-600 disabled:opacity-40"
|
|
95
|
-
onClick={props.onNext}
|
|
96
|
-
disabled={props.totalPages === 0 || props.page >= props.totalPages || props.busy}
|
|
97
|
-
>
|
|
98
|
-
{t('next')}
|
|
99
|
-
</button>
|
|
85
|
+
<div className="py-4">
|
|
86
|
+
{props.hasMore && <div ref={props.sentinelRef} className="h-1 w-full" aria-hidden="true" />}
|
|
87
|
+
{props.loading && (
|
|
88
|
+
<div data-testid="marketplace-loading-more" className="pt-3 text-center text-xs text-gray-500">
|
|
89
|
+
{t('loading')}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
100
92
|
</div>
|
|
101
93
|
);
|
|
102
94
|
}
|
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
} from '@/components/marketplace/marketplace-localization';
|
|
37
37
|
import { t } from '@/lib/i18n';
|
|
38
38
|
import { cn } from '@/lib/utils';
|
|
39
|
+
import { MarketplaceInfiniteScrollStatus } from '@/components/marketplace/marketplace-page-parts';
|
|
40
|
+
import { useInfiniteScrollLoader } from '@/hooks/use-infinite-scroll-loader';
|
|
39
41
|
|
|
40
42
|
type ScopeType = 'catalog' | 'installed';
|
|
41
43
|
|
|
@@ -256,7 +258,6 @@ export function McpMarketplacePage() {
|
|
|
256
258
|
const [searchText, setSearchText] = useState('');
|
|
257
259
|
const [query, setQuery] = useState('');
|
|
258
260
|
const [sort, setSort] = useState<MarketplaceSort>('relevance');
|
|
259
|
-
const [page, setPage] = useState(1);
|
|
260
261
|
const [installingItem, setInstallingItem] = useState<MarketplaceItemSummary | null>(null);
|
|
261
262
|
const [doctorTarget, setDoctorTarget] = useState<string | null>(null);
|
|
262
263
|
const [doctorResult, setDoctorResult] = useState<MarketplaceMcpDoctorResult | null>(null);
|
|
@@ -267,7 +268,6 @@ export function McpMarketplacePage() {
|
|
|
267
268
|
|
|
268
269
|
useEffect(() => {
|
|
269
270
|
const timer = window.setTimeout(() => {
|
|
270
|
-
setPage(1);
|
|
271
271
|
setQuery(searchText.trim());
|
|
272
272
|
}, 250);
|
|
273
273
|
return () => window.clearTimeout(timer);
|
|
@@ -276,11 +276,23 @@ export function McpMarketplacePage() {
|
|
|
276
276
|
const itemsQuery = useMcpMarketplaceItems({
|
|
277
277
|
q: query || undefined,
|
|
278
278
|
sort,
|
|
279
|
-
page,
|
|
280
279
|
pageSize: PAGE_SIZE
|
|
281
280
|
});
|
|
282
281
|
const installedQuery = useMcpMarketplaceInstalled();
|
|
283
282
|
|
|
283
|
+
const infiniteScroll = useInfiniteScrollLoader({
|
|
284
|
+
disabled: scope !== 'catalog' || itemsQuery.isError || !itemsQuery.hasNextPage || itemsQuery.isFetchingNextPage,
|
|
285
|
+
onLoadMore: () => itemsQuery.fetchNextPage(),
|
|
286
|
+
watchValue: `${scope}:${query}:${sort}:${itemsQuery.data?.loadedItems ?? 0}:${itemsQuery.data?.loadedPages ?? 0}`
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
const container = infiniteScroll.containerRef.current;
|
|
291
|
+
if (container && typeof container.scrollTo === 'function') {
|
|
292
|
+
container.scrollTo({ top: 0 });
|
|
293
|
+
}
|
|
294
|
+
}, [infiniteScroll.containerRef, query, scope, sort]);
|
|
295
|
+
|
|
284
296
|
const installMutation = useInstallMcpMarketplaceItem();
|
|
285
297
|
const manageMutation = useManageMcpMarketplaceItem();
|
|
286
298
|
const doctorMutation = useMutation({
|
|
@@ -502,7 +514,11 @@ export function McpMarketplacePage() {
|
|
|
502
514
|
</span>
|
|
503
515
|
</div>
|
|
504
516
|
|
|
505
|
-
<div
|
|
517
|
+
<div
|
|
518
|
+
ref={infiniteScroll.containerRef}
|
|
519
|
+
className="min-h-0 flex-1 overflow-y-auto pr-1"
|
|
520
|
+
aria-busy={itemsQuery.isLoading || itemsQuery.isFetchingNextPage}
|
|
521
|
+
>
|
|
506
522
|
<div className={cn('grid grid-cols-1 gap-3 lg:grid-cols-2 2xl:grid-cols-3')}>
|
|
507
523
|
{scope === 'catalog' && itemsQuery.isLoading && Array.from({ length: 6 }, (_, index) => (
|
|
508
524
|
<div key={index} className="rounded-2xl border border-gray-200/70 bg-white p-4 shadow-sm">
|
|
@@ -533,30 +549,16 @@ export function McpMarketplacePage() {
|
|
|
533
549
|
{scope === 'installed' && installedRecords.length === 0 && (
|
|
534
550
|
<div className="py-8 text-center text-sm text-gray-500">{t('marketplaceNoInstalledMcp')}</div>
|
|
535
551
|
)}
|
|
536
|
-
</div>
|
|
537
|
-
</section>
|
|
538
552
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
{t('prev')}
|
|
547
|
-
</button>
|
|
548
|
-
<div className="min-w-20 text-center text-sm text-gray-600">
|
|
549
|
-
{itemsQuery.data?.totalPages ? `${page} / ${itemsQuery.data.totalPages}` : '0 / 0'}
|
|
550
|
-
</div>
|
|
551
|
-
<button
|
|
552
|
-
className="h-8 rounded-xl border border-gray-200/80 px-3 text-sm text-gray-600 disabled:opacity-40"
|
|
553
|
-
onClick={() => setPage((current) => current + 1)}
|
|
554
|
-
disabled={!itemsQuery.data?.totalPages || page >= itemsQuery.data.totalPages || itemsQuery.isFetching}
|
|
555
|
-
>
|
|
556
|
-
{t('next')}
|
|
557
|
-
</button>
|
|
553
|
+
{scope === 'catalog' && !itemsQuery.isError && (
|
|
554
|
+
<MarketplaceInfiniteScrollStatus
|
|
555
|
+
hasMore={Boolean(itemsQuery.hasNextPage)}
|
|
556
|
+
loading={itemsQuery.isFetchingNextPage}
|
|
557
|
+
sentinelRef={infiniteScroll.sentinelRef}
|
|
558
|
+
/>
|
|
559
|
+
)}
|
|
558
560
|
</div>
|
|
559
|
-
|
|
561
|
+
</section>
|
|
560
562
|
|
|
561
563
|
<InstallDialog
|
|
562
564
|
item={installingItem}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { InfiniteData } from '@tanstack/react-query';
|
|
2
|
+
import type { MarketplaceListView } from '@/api/types';
|
|
3
|
+
|
|
4
|
+
export type InfiniteMarketplaceListView = MarketplaceListView & {
|
|
5
|
+
pages: MarketplaceListView[];
|
|
6
|
+
loadedItems: number;
|
|
7
|
+
loadedPages: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function collapseMarketplaceListPages(
|
|
11
|
+
data: InfiniteData<MarketplaceListView> | undefined
|
|
12
|
+
): InfiniteMarketplaceListView | undefined {
|
|
13
|
+
if (!data || data.pages.length === 0) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const items = data.pages.flatMap((page) => page.items);
|
|
18
|
+
const lastPage = data.pages[data.pages.length - 1];
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
...lastPage,
|
|
22
|
+
items,
|
|
23
|
+
pages: data.pages,
|
|
24
|
+
loadedItems: items.length,
|
|
25
|
+
loadedPages: data.pages.length
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_THRESHOLD_PX = 160;
|
|
4
|
+
|
|
5
|
+
type UseInfiniteScrollLoaderParams = {
|
|
6
|
+
disabled: boolean;
|
|
7
|
+
onLoadMore: () => Promise<unknown> | unknown;
|
|
8
|
+
thresholdPx?: number;
|
|
9
|
+
watchValue?: string | number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function useInfiniteScrollLoader(params: UseInfiniteScrollLoaderParams) {
|
|
13
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
14
|
+
const sentinelRef = useRef<HTMLDivElement | null>(null);
|
|
15
|
+
const onLoadMoreRef = useRef(params.onLoadMore);
|
|
16
|
+
const loadingRef = useRef(false);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
onLoadMoreRef.current = params.onLoadMore;
|
|
20
|
+
}, [params.onLoadMore]);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (params.disabled) {
|
|
24
|
+
loadingRef.current = false;
|
|
25
|
+
}
|
|
26
|
+
}, [params.disabled]);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const container = containerRef.current;
|
|
30
|
+
const sentinel = sentinelRef.current;
|
|
31
|
+
const thresholdPx = params.thresholdPx ?? DEFAULT_THRESHOLD_PX;
|
|
32
|
+
|
|
33
|
+
if (params.disabled || !container || !sentinel) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const triggerLoadMore = () => {
|
|
38
|
+
if (loadingRef.current || params.disabled) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
loadingRef.current = true;
|
|
43
|
+
Promise.resolve(onLoadMoreRef.current()).finally(() => {
|
|
44
|
+
loadingRef.current = false;
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const maybeLoadMore = () => {
|
|
49
|
+
const remainingDistance = sentinel.getBoundingClientRect().top - container.getBoundingClientRect().bottom;
|
|
50
|
+
if (remainingDistance <= thresholdPx) {
|
|
51
|
+
triggerLoadMore();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (typeof IntersectionObserver === 'function') {
|
|
56
|
+
const observer = new IntersectionObserver(
|
|
57
|
+
(entries) => {
|
|
58
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
59
|
+
triggerLoadMore();
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
root: container,
|
|
64
|
+
rootMargin: `0px 0px ${thresholdPx}px 0px`
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
observer.observe(sentinel);
|
|
69
|
+
maybeLoadMore();
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
observer.disconnect();
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
container.addEventListener('scroll', maybeLoadMore, { passive: true });
|
|
77
|
+
maybeLoadMore();
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
container.removeEventListener('scroll', maybeLoadMore);
|
|
81
|
+
};
|
|
82
|
+
}, [params.disabled, params.thresholdPx, params.watchValue]);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
containerRef,
|
|
86
|
+
sentinelRef
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
1
|
+
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
2
|
import { toast } from 'sonner';
|
|
3
3
|
import { t } from '@/lib/i18n';
|
|
4
4
|
import {
|
|
@@ -20,11 +20,15 @@ import type {
|
|
|
20
20
|
MarketplaceItemType,
|
|
21
21
|
MarketplaceManageRequest
|
|
22
22
|
} from '@/api/types';
|
|
23
|
+
import { collapseMarketplaceListPages } from '@/hooks/marketplace-list-pages';
|
|
24
|
+
import { useMemo } from 'react';
|
|
23
25
|
|
|
24
26
|
export function useMarketplaceItems(params: MarketplaceListParams) {
|
|
25
|
-
|
|
27
|
+
const query = useInfiniteQuery({
|
|
26
28
|
queryKey: ['marketplace-items', params],
|
|
27
|
-
|
|
29
|
+
initialPageParam: 1,
|
|
30
|
+
queryFn: ({ pageParam }) => fetchMarketplaceItems({ ...params, page: pageParam }),
|
|
31
|
+
getNextPageParam: (lastPage) => (lastPage.page < lastPage.totalPages ? lastPage.page + 1 : undefined),
|
|
28
32
|
placeholderData: (previousData, previousQuery) => {
|
|
29
33
|
const previousParams = previousQuery?.queryKey?.[1];
|
|
30
34
|
if (!previousParams || typeof previousParams !== 'object' || previousParams === null) {
|
|
@@ -36,6 +40,13 @@ export function useMarketplaceItems(params: MarketplaceListParams) {
|
|
|
36
40
|
},
|
|
37
41
|
staleTime: 15_000
|
|
38
42
|
});
|
|
43
|
+
|
|
44
|
+
const data = useMemo(() => collapseMarketplaceListPages(query.data), [query.data]);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
...query,
|
|
48
|
+
data
|
|
49
|
+
};
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
export function useMarketplaceRecommendations(type: MarketplaceItemType, params: { scene?: string; limit?: number }) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
1
|
+
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
2
|
import { toast } from 'sonner';
|
|
3
3
|
import {
|
|
4
4
|
doctorMcpMarketplaceItem,
|
|
@@ -12,13 +12,24 @@ import {
|
|
|
12
12
|
type McpMarketplaceListParams
|
|
13
13
|
} from '@/api/mcp-marketplace';
|
|
14
14
|
import { t } from '@/lib/i18n';
|
|
15
|
+
import { collapseMarketplaceListPages } from '@/hooks/marketplace-list-pages';
|
|
16
|
+
import { useMemo } from 'react';
|
|
15
17
|
|
|
16
18
|
export function useMcpMarketplaceItems(params: McpMarketplaceListParams) {
|
|
17
|
-
|
|
19
|
+
const query = useInfiniteQuery({
|
|
18
20
|
queryKey: ['marketplace-mcp-items', params],
|
|
19
|
-
|
|
21
|
+
initialPageParam: 1,
|
|
22
|
+
queryFn: ({ pageParam }) => fetchMcpMarketplaceItems({ ...params, page: pageParam }),
|
|
23
|
+
getNextPageParam: (lastPage) => (lastPage.page < lastPage.totalPages ? lastPage.page + 1 : undefined),
|
|
20
24
|
staleTime: 15_000
|
|
21
25
|
});
|
|
26
|
+
|
|
27
|
+
const data = useMemo(() => collapseMarketplaceListPages(query.data), [query.data]);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
...query,
|
|
31
|
+
data
|
|
32
|
+
};
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
export function useMcpMarketplaceInstalled() {
|
package/src/lib/i18n.chat.ts
CHANGED
|
@@ -125,10 +125,13 @@ export const CHAT_LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
125
125
|
chatSidebarScheduledTasks: { zh: '定时任务', en: 'Scheduled Tasks' },
|
|
126
126
|
chatSidebarSkills: { zh: '技能', en: 'Skills' },
|
|
127
127
|
chatSidebarTaskRecords: { zh: '会话记录', en: 'Sessions' },
|
|
128
|
+
chatSidebarViewTime: { zh: '时间', en: 'Time' },
|
|
129
|
+
chatSidebarViewProject: { zh: '项目', en: 'Project' },
|
|
128
130
|
chatSidebarToday: { zh: '今天', en: 'Today' },
|
|
129
131
|
chatSidebarYesterday: { zh: '昨天', en: 'Yesterday' },
|
|
130
132
|
chatSidebarPrevious7Days: { zh: '近 7 天', en: 'Previous 7 Days' },
|
|
131
133
|
chatSidebarOlder: { zh: '更早', en: 'Older' },
|
|
134
|
+
chatSidebarProjectViewEmpty: { zh: '还没有绑定项目的会话', en: 'No project conversations yet' },
|
|
132
135
|
chatWelcomeTitle: { zh: '你好,有什么可以帮你的吗?', en: 'Hello, how can I help you?' },
|
|
133
136
|
chatWelcomeSubtitle: { zh: '开始一个新任务或选择已有对话', en: 'Start a new task or select an existing conversation' },
|
|
134
137
|
chatWelcomeCapability1Title: { zh: '智能对话', en: 'Smart Conversations' },
|
package/src/lib/i18n.remote.ts
CHANGED
|
@@ -168,6 +168,21 @@ export const REMOTE_LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
168
168
|
en: 'Authorize this device in your browser and connect it to the NextClaw platform.'
|
|
169
169
|
},
|
|
170
170
|
remoteAccountEmail: { zh: '邮箱', en: 'Email' },
|
|
171
|
+
remoteAccountUsername: { zh: '用户名', en: 'Username' },
|
|
172
|
+
remoteAccountUsernameRequiredTitle: { zh: '发布 skill 前需要先设置用户名', en: 'Set a username before publishing skills' },
|
|
173
|
+
remoteAccountUsernameRequiredDescription: {
|
|
174
|
+
zh: '平台账号已经登录,但还没有正式用户名。设置后,你的个人 skill 会使用 `@username/skill-name` 作为发布标识。',
|
|
175
|
+
en: 'Your platform account is already signed in, but it does not have a username yet. Once set, your personal skills will publish as `@username/skill-name`.'
|
|
176
|
+
},
|
|
177
|
+
remoteAccountUsernamePlaceholder: { zh: '例如:alice-dev', en: 'For example: alice-dev' },
|
|
178
|
+
remoteAccountUsernameSave: { zh: '保存用户名', en: 'Save Username' },
|
|
179
|
+
remoteAccountUsernameSaving: { zh: '保存中...', en: 'Saving...' },
|
|
180
|
+
remoteAccountUsernameSetSuccess: { zh: '用户名已保存', en: 'Username saved' },
|
|
181
|
+
remoteAccountUsernameSetFailed: { zh: '用户名保存失败', en: 'Failed to save username' },
|
|
182
|
+
remoteAccountUsernameLockedHelp: {
|
|
183
|
+
zh: '当前版本先支持首次设置用户名;设置后暂不支持改名。',
|
|
184
|
+
en: 'This version supports initial username setup only. Renaming is not available yet.'
|
|
185
|
+
},
|
|
171
186
|
remoteAccountRole: { zh: '角色', en: 'Role' },
|
|
172
187
|
remoteApiBase: { zh: 'API Base', en: 'API Base' },
|
|
173
188
|
remoteBrowserAuthTitle: { zh: '浏览器授权登录', en: 'Browser Authorization' },
|