@nextclaw/ui 0.6.14 → 0.7.0
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 +18 -0
- package/README.md +2 -0
- package/dist/assets/ChannelsList-DF2U-LY1.js +1 -0
- package/dist/assets/ChatPage-BX39y0U5.js +36 -0
- package/dist/assets/DocBrowser-B9ws5JL7.js +1 -0
- package/dist/assets/{LogoBadge-BxZJ9BJT.js → LogoBadge-DvGAzkZ3.js} +1 -1
- package/dist/assets/MarketplacePage-DG5mHWJ8.js +49 -0
- package/dist/assets/ModelConfig-BL_HsOsm.js +1 -0
- package/dist/assets/ProvidersList-CH5z00YT.js +1 -0
- package/dist/assets/RuntimeConfig-BplBgkwo.js +1 -0
- package/dist/assets/SearchConfig-BhaI0fUf.js +1 -0
- package/dist/assets/{SecretsConfig-9OABNssV.js → SecretsConfig-CFoimOh9.js} +2 -2
- package/dist/assets/SessionsConfig-BHTAYn9T.js +2 -0
- package/dist/assets/index-BLeJkJ0o.css +1 -0
- package/dist/assets/index-DK4TS5ev.js +8 -0
- package/dist/assets/index-X5J6Mm--.js +1 -0
- package/dist/assets/{index-CkqvHQAt.js → index-uMsNsQX6.js} +1 -1
- package/dist/assets/{label-BIjHWZUm.js → label-D8ly4a2P.js} +1 -1
- package/dist/assets/page-layout-BSYfvwbp.js +1 -0
- package/dist/assets/security-config-DlKEYHNN.js +1 -0
- package/dist/assets/{session-run-status-BZEH0QZp.js → session-run-status-TkIuGbVw.js} +1 -1
- package/dist/assets/skeleton-CWbsNx2h.js +1 -0
- package/dist/assets/{switch-CnGQpdTp.js → switch-Ce_g9lpN.js} +1 -1
- package/dist/assets/tabs-custom-Cf5azvT5.js +1 -0
- package/dist/assets/useConfirmDialog-A8Ek8Wu7.js +5 -0
- package/dist/assets/vendor-B7ozqnFC.js +412 -0
- package/dist/index.html +3 -3
- package/package.json +9 -10
- package/src/App.tsx +49 -27
- package/src/api/client.ts +1 -0
- package/src/api/config.ts +60 -0
- package/src/api/types.ts +29 -1
- package/src/api/websocket.ts +2 -0
- package/src/components/auth/login-page.tsx +69 -0
- package/src/components/chat/ChatConversationPanel.tsx +12 -54
- package/src/components/chat/ChatSidebar.tsx +7 -1
- package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +80 -0
- package/src/components/chat/adapters/chat-input-bar.adapter.ts +329 -0
- package/src/components/chat/adapters/chat-message.adapter.test.ts +137 -0
- package/src/components/chat/adapters/chat-message.adapter.ts +200 -0
- package/src/components/chat/chat-input/chat-input-bar.controller.test.tsx +128 -0
- package/src/components/chat/chat-input/chat-input-bar.controller.ts +105 -0
- package/src/components/chat/containers/chat-input-bar.container.tsx +270 -0
- package/src/components/chat/containers/chat-message-list.container.tsx +67 -0
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/managers/chat-thread.manager.ts +3 -1
- package/src/components/chat/nextclaw/index.ts +23 -0
- package/src/components/common/BrandHeader.tsx +4 -1
- package/src/components/common/StatusBadge.tsx +32 -20
- package/src/components/config/runtime-security-card.tsx +276 -0
- package/src/components/config/security-config.tsx +12 -0
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/components/marketplace/MarketplacePage.test.tsx +170 -0
- package/src/components/marketplace/MarketplacePage.tsx +77 -28
- package/src/hooks/use-auth.ts +111 -0
- package/src/hooks/useMarketplace.ts +9 -0
- package/src/hooks/useWebSocket.ts +53 -1
- package/src/lib/i18n.ts +72 -0
- package/src/test/setup.ts +16 -0
- package/tsconfig.json +3 -2
- package/vite.config.ts +2 -1
- package/vitest.config.ts +16 -0
- package/.eslintrc.cjs +0 -48
- package/dist/assets/ChannelsList-DiSnpiW0.js +0 -1
- package/dist/assets/ChatPage-DsaIrNHN.js +0 -36
- package/dist/assets/DocBrowser-CnfcptGM.js +0 -1
- package/dist/assets/MarketplacePage-BI_J_DBQ.js +0 -49
- package/dist/assets/ModelConfig-DfL8F4tN.js +0 -1
- package/dist/assets/ProvidersList-DpT_oFHZ.js +0 -1
- package/dist/assets/RuntimeConfig-BNYR_Iag.js +0 -1
- package/dist/assets/SearchConfig-TDBl7Fjh.js +0 -1
- package/dist/assets/SessionsConfig-BRwntUDz.js +0 -2
- package/dist/assets/card-BYnT3Mxo.js +0 -1
- package/dist/assets/index-BCfS4UY1.css +0 -1
- package/dist/assets/index-BnUxgevr.js +0 -8
- package/dist/assets/input-oaepEtqu.js +0 -1
- package/dist/assets/page-layout-B6JXiSQB.js +0 -1
- package/dist/assets/popover-LJQgv5l1.js +0 -1
- package/dist/assets/tabs-custom-CpSv7pDl.js +0 -1
- package/dist/assets/useConfirmDialog-pqAlPdQZ.js +0 -5
- package/dist/assets/vendor-BKtTvQYU.js +0 -407
- package/src/components/chat/ChatThread.tsx +0 -402
- package/src/components/chat/SkillsPicker.tsx +0 -137
- package/src/components/chat/chat-input/ChatInputBarView.tsx +0 -82
- package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +0 -83
- package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +0 -39
- package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +0 -31
- package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +0 -112
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +0 -24
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +0 -58
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +0 -56
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +0 -40
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputThinkingSelector.tsx +0 -74
- package/src/components/chat/chat-input/useChatInputBarController.ts +0 -322
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
} from '@/api/types';
|
|
12
12
|
import { fetchMarketplacePluginContent, fetchMarketplaceSkillContent } from '@/api/marketplace';
|
|
13
13
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
14
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
14
15
|
import { Tabs } from '@/components/ui/tabs-custom';
|
|
15
16
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
16
17
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
@@ -30,6 +31,7 @@ import { useEffect, useMemo, useState } from 'react';
|
|
|
30
31
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
31
32
|
|
|
32
33
|
const PAGE_SIZE = 12;
|
|
34
|
+
const SKELETON_CARD_COUNT = PAGE_SIZE;
|
|
33
35
|
|
|
34
36
|
type ScopeType = 'all' | 'installed';
|
|
35
37
|
|
|
@@ -362,16 +364,16 @@ function MarketplaceListCard(props: {
|
|
|
362
364
|
onInstall: (item: MarketplaceItemSummary) => void;
|
|
363
365
|
onManage: (action: MarketplaceManageAction, record: MarketplaceInstalledRecord) => void;
|
|
364
366
|
}) {
|
|
365
|
-
const record = props
|
|
367
|
+
const { item, record, localeFallbacks, installState, manageState, onOpen, onInstall, onManage } = props;
|
|
366
368
|
const pluginRecord = record?.type === 'plugin' ? record : undefined;
|
|
367
|
-
const type =
|
|
368
|
-
const title =
|
|
369
|
-
const summary = pickLocalizedText(
|
|
369
|
+
const type = item?.type ?? record?.type;
|
|
370
|
+
const title = item?.name ?? record?.label ?? record?.id ?? record?.spec ?? t('marketplaceUnknownItem');
|
|
371
|
+
const summary = pickLocalizedText(item?.summaryI18n, item?.summary, localeFallbacks)
|
|
370
372
|
|| (record ? t('marketplaceInstalledLocalSummary') : '');
|
|
371
|
-
const spec =
|
|
373
|
+
const spec = item?.install.spec ?? record?.spec ?? '';
|
|
372
374
|
|
|
373
375
|
const targetId = record?.id || record?.spec;
|
|
374
|
-
const busyForRecord = Boolean(targetId) &&
|
|
376
|
+
const busyForRecord = Boolean(targetId) && manageState.isPending && manageState.targetId === targetId;
|
|
375
377
|
|
|
376
378
|
const canToggle = Boolean(pluginRecord);
|
|
377
379
|
const canUninstallPlugin = record?.type === 'plugin' && record.origin !== 'bundled';
|
|
@@ -379,14 +381,14 @@ function MarketplaceListCard(props: {
|
|
|
379
381
|
const canUninstall = Boolean(canUninstallPlugin || canUninstallSkill);
|
|
380
382
|
|
|
381
383
|
const isDisabled = record ? (record.enabled === false || record.runtimeStatus === 'disabled') : false;
|
|
382
|
-
const installSpec =
|
|
383
|
-
const isInstalling = typeof installSpec === 'string' &&
|
|
384
|
+
const installSpec = item?.install.spec;
|
|
385
|
+
const isInstalling = typeof installSpec === 'string' && installState.installingSpecs.has(installSpec);
|
|
384
386
|
|
|
385
387
|
const displayType = type === 'plugin' ? t('marketplaceTypePlugin') : type === 'skill' ? t('marketplaceTypeSkill') : t('marketplaceTypeExtension');
|
|
386
388
|
|
|
387
389
|
return (
|
|
388
390
|
<article
|
|
389
|
-
onClick={
|
|
391
|
+
onClick={onOpen}
|
|
390
392
|
className="group bg-white border border-gray-200/40 hover:border-blue-300/80 rounded-2xl px-5 py-4 hover:shadow-md shadow-sm transition-all flex items-start gap-3.5 justify-between cursor-pointer"
|
|
391
393
|
>
|
|
392
394
|
<div className="flex gap-3 min-w-0 flex-1 h-full items-start">
|
|
@@ -434,11 +436,11 @@ function MarketplaceListCard(props: {
|
|
|
434
436
|
</div>
|
|
435
437
|
|
|
436
438
|
<div className="shrink-0 flex items-center h-full">
|
|
437
|
-
{
|
|
439
|
+
{item && !record && (
|
|
438
440
|
<button
|
|
439
441
|
onClick={(event) => {
|
|
440
442
|
event.stopPropagation();
|
|
441
|
-
|
|
443
|
+
onInstall(item);
|
|
442
444
|
}}
|
|
443
445
|
disabled={isInstalling}
|
|
444
446
|
className="inline-flex items-center gap-1.5 h-8 px-4 rounded-xl text-xs font-medium bg-primary text-white hover:bg-primary-600 disabled:opacity-50 transition-colors"
|
|
@@ -449,29 +451,29 @@ function MarketplaceListCard(props: {
|
|
|
449
451
|
|
|
450
452
|
{pluginRecord && canToggle && (
|
|
451
453
|
<button
|
|
452
|
-
disabled={
|
|
454
|
+
disabled={manageState.isPending}
|
|
453
455
|
onClick={(event) => {
|
|
454
456
|
event.stopPropagation();
|
|
455
|
-
|
|
457
|
+
onManage(isDisabled ? 'enable' : 'disable', pluginRecord);
|
|
456
458
|
}}
|
|
457
459
|
className="inline-flex items-center h-8 px-4 rounded-xl text-xs font-medium border border-gray-200/80 text-gray-600 bg-white hover:bg-gray-50 hover:border-gray-300 disabled:opacity-50 transition-colors"
|
|
458
460
|
>
|
|
459
|
-
{busyForRecord &&
|
|
460
|
-
? (
|
|
461
|
+
{busyForRecord && manageState.action !== 'uninstall'
|
|
462
|
+
? (manageState.action === 'enable' ? t('marketplaceEnabling') : t('marketplaceDisabling'))
|
|
461
463
|
: (isDisabled ? t('marketplaceEnable') : t('marketplaceDisable'))}
|
|
462
464
|
</button>
|
|
463
465
|
)}
|
|
464
466
|
|
|
465
467
|
{record && canUninstall && (
|
|
466
468
|
<button
|
|
467
|
-
disabled={
|
|
469
|
+
disabled={manageState.isPending}
|
|
468
470
|
onClick={(event) => {
|
|
469
471
|
event.stopPropagation();
|
|
470
|
-
|
|
472
|
+
onManage('uninstall', record);
|
|
471
473
|
}}
|
|
472
474
|
className="inline-flex items-center h-8 px-4 rounded-xl text-xs font-medium border border-rose-100 text-rose-500 bg-white hover:bg-rose-50 hover:border-rose-200 disabled:opacity-50 transition-colors"
|
|
473
475
|
>
|
|
474
|
-
{busyForRecord &&
|
|
476
|
+
{busyForRecord && manageState.action === 'uninstall' ? t('marketplaceRemoving') : t('marketplaceUninstall')}
|
|
475
477
|
</button>
|
|
476
478
|
)}
|
|
477
479
|
</div>
|
|
@@ -479,6 +481,38 @@ function MarketplaceListCard(props: {
|
|
|
479
481
|
);
|
|
480
482
|
}
|
|
481
483
|
|
|
484
|
+
function MarketplaceListSkeleton(props: {
|
|
485
|
+
count?: number;
|
|
486
|
+
}) {
|
|
487
|
+
const count = props.count ?? SKELETON_CARD_COUNT;
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
<>
|
|
491
|
+
{Array.from({ length: count }, (_, index) => (
|
|
492
|
+
<article
|
|
493
|
+
key={`marketplace-skeleton-${index}`}
|
|
494
|
+
className="rounded-2xl border border-gray-200/40 bg-white px-5 py-4 shadow-sm"
|
|
495
|
+
>
|
|
496
|
+
<div className="flex items-start gap-3.5 justify-between">
|
|
497
|
+
<div className="flex min-w-0 flex-1 gap-3">
|
|
498
|
+
<Skeleton className="h-10 w-10 shrink-0 rounded-xl" />
|
|
499
|
+
<div className="min-w-0 flex-1 space-y-2 pt-0.5">
|
|
500
|
+
<Skeleton className="h-4 w-32 max-w-[70%]" />
|
|
501
|
+
<div className="flex items-center gap-2">
|
|
502
|
+
<Skeleton className="h-3 w-12" />
|
|
503
|
+
<Skeleton className="h-3 w-24" />
|
|
504
|
+
</div>
|
|
505
|
+
<Skeleton className="h-3 w-full" />
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
<Skeleton className="h-8 w-20 shrink-0 rounded-xl" />
|
|
509
|
+
</div>
|
|
510
|
+
</article>
|
|
511
|
+
))}
|
|
512
|
+
</>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
482
516
|
function PaginationBar(props: {
|
|
483
517
|
page: number;
|
|
484
518
|
totalPages: number;
|
|
@@ -510,11 +544,11 @@ function PaginationBar(props: {
|
|
|
510
544
|
}
|
|
511
545
|
|
|
512
546
|
export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
547
|
+
const { forcedType } = props;
|
|
513
548
|
const navigate = useNavigate();
|
|
514
549
|
const params = useParams<{ type?: string }>();
|
|
515
550
|
const { language } = useI18n();
|
|
516
551
|
const docBrowser = useDocBrowser();
|
|
517
|
-
const forcedType = props.forcedType;
|
|
518
552
|
|
|
519
553
|
const routeType: MarketplaceRouteType | null = useMemo(() => {
|
|
520
554
|
if (forcedType === 'plugins' || forcedType === 'skills') {
|
|
@@ -650,10 +684,17 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
650
684
|
|
|
651
685
|
const total = scope === 'installed' ? installedEntries.length : (itemsQuery.data?.total ?? 0);
|
|
652
686
|
const totalPages = scope === 'installed' ? 1 : (itemsQuery.data?.totalPages ?? 0);
|
|
687
|
+
const showCatalogSkeleton = scope === 'all' && itemsQuery.isLoading && !itemsQuery.data;
|
|
688
|
+
const showInstalledSkeleton = scope === 'installed' && installedQuery.isLoading && !installedQuery.data;
|
|
689
|
+
const showListSkeleton = showCatalogSkeleton || showInstalledSkeleton;
|
|
690
|
+
const isListRefreshing = !showListSkeleton && (
|
|
691
|
+
(scope === 'all' && itemsQuery.isFetching)
|
|
692
|
+
|| (scope === 'installed' && installedQuery.isFetching)
|
|
693
|
+
);
|
|
653
694
|
|
|
654
695
|
const listSummary = useMemo(() => {
|
|
655
696
|
if (scope === 'installed') {
|
|
656
|
-
if (installedQuery.isLoading) {
|
|
697
|
+
if (installedQuery.isLoading && !installedQuery.data) {
|
|
657
698
|
return t('loading');
|
|
658
699
|
}
|
|
659
700
|
return `${installedEntries.length} ${t(copyKeys.installedCountSuffix)}`;
|
|
@@ -664,7 +705,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
664
705
|
}
|
|
665
706
|
|
|
666
707
|
return `${allItems.length} / ${total}`;
|
|
667
|
-
}, [scope, installedQuery.isLoading, installedEntries.length, itemsQuery.data, allItems.length, total, copyKeys.installedCountSuffix]);
|
|
708
|
+
}, [scope, installedQuery.data, installedQuery.isLoading, installedEntries.length, itemsQuery.data, allItems.length, total, copyKeys.installedCountSuffix]);
|
|
668
709
|
|
|
669
710
|
const installState: InstallState = { installingSpecs };
|
|
670
711
|
|
|
@@ -868,9 +909,17 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
868
909
|
</div>
|
|
869
910
|
)}
|
|
870
911
|
|
|
871
|
-
<div className="min-h-0 flex-1 overflow-y-auto custom-scrollbar pr-1">
|
|
872
|
-
<div
|
|
873
|
-
{
|
|
912
|
+
<div className="min-h-0 flex-1 overflow-y-auto custom-scrollbar pr-1" aria-busy={showListSkeleton || isListRefreshing}>
|
|
913
|
+
<div
|
|
914
|
+
data-testid={showListSkeleton ? 'marketplace-list-skeleton' : undefined}
|
|
915
|
+
className={cn(
|
|
916
|
+
'grid grid-cols-1 gap-3 lg:grid-cols-2 2xl:grid-cols-3 transition-opacity',
|
|
917
|
+
isListRefreshing ? 'opacity-70' : 'opacity-100'
|
|
918
|
+
)}
|
|
919
|
+
>
|
|
920
|
+
{showListSkeleton && <MarketplaceListSkeleton />}
|
|
921
|
+
|
|
922
|
+
{!showListSkeleton && scope === 'all' && allItems.map((item) => (
|
|
874
923
|
<MarketplaceListCard
|
|
875
924
|
key={item.id}
|
|
876
925
|
item={item}
|
|
@@ -884,7 +933,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
884
933
|
/>
|
|
885
934
|
))}
|
|
886
935
|
|
|
887
|
-
{scope === 'installed' && installedEntries.map((entry) => (
|
|
936
|
+
{!showListSkeleton && scope === 'installed' && installedEntries.map((entry) => (
|
|
888
937
|
<MarketplaceListCard
|
|
889
938
|
key={entry.key}
|
|
890
939
|
item={entry.item}
|
|
@@ -899,16 +948,16 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
899
948
|
))}
|
|
900
949
|
</div>
|
|
901
950
|
|
|
902
|
-
{scope === 'all' && !
|
|
951
|
+
{scope === 'all' && !showListSkeleton && !itemsQuery.isError && allItems.length === 0 && (
|
|
903
952
|
<div className="text-[13px] text-gray-500 py-8 text-center">{t(copyKeys.emptyData)}</div>
|
|
904
953
|
)}
|
|
905
|
-
{scope === 'installed' && !
|
|
954
|
+
{scope === 'installed' && !showListSkeleton && !installedQuery.isError && installedEntries.length === 0 && (
|
|
906
955
|
<div className="text-[13px] text-gray-500 py-8 text-center">{t(copyKeys.emptyInstalled)}</div>
|
|
907
956
|
)}
|
|
908
957
|
</div>
|
|
909
958
|
</section>
|
|
910
959
|
|
|
911
|
-
{scope === 'all' && (
|
|
960
|
+
{scope === 'all' && !showCatalogSkeleton && (
|
|
912
961
|
<div className="shrink-0">
|
|
913
962
|
<PaginationBar
|
|
914
963
|
page={page}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import {
|
|
3
|
+
fetchAuthStatus,
|
|
4
|
+
loginAuth,
|
|
5
|
+
logoutAuth,
|
|
6
|
+
setupAuth,
|
|
7
|
+
updateAuthEnabled,
|
|
8
|
+
updateAuthPassword
|
|
9
|
+
} from '@/api/config';
|
|
10
|
+
import { toast } from 'sonner';
|
|
11
|
+
import { t } from '@/lib/i18n';
|
|
12
|
+
|
|
13
|
+
export function useAuthStatus() {
|
|
14
|
+
return useQuery({
|
|
15
|
+
queryKey: ['auth-status'],
|
|
16
|
+
queryFn: fetchAuthStatus,
|
|
17
|
+
staleTime: 5_000,
|
|
18
|
+
retry: 3,
|
|
19
|
+
retryDelay: (attempt) => Math.min(1000 * attempt, 3000),
|
|
20
|
+
refetchOnWindowFocus: true
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function invalidateProtectedQueries(queryClient: ReturnType<typeof useQueryClient>): Promise<unknown[]> {
|
|
25
|
+
return Promise.all([
|
|
26
|
+
queryClient.invalidateQueries({ queryKey: ['auth-status'] }),
|
|
27
|
+
queryClient.invalidateQueries({ queryKey: ['app-meta'] }),
|
|
28
|
+
queryClient.invalidateQueries({ queryKey: ['config'] }),
|
|
29
|
+
queryClient.invalidateQueries({ queryKey: ['config-meta'] }),
|
|
30
|
+
queryClient.invalidateQueries({ queryKey: ['config-schema'] }),
|
|
31
|
+
queryClient.invalidateQueries({ queryKey: ['sessions'] }),
|
|
32
|
+
queryClient.invalidateQueries({ queryKey: ['session-history'] }),
|
|
33
|
+
queryClient.invalidateQueries({ queryKey: ['chat-runs'] }),
|
|
34
|
+
queryClient.invalidateQueries({ queryKey: ['cron-jobs'] })
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useSetupAuth() {
|
|
39
|
+
const queryClient = useQueryClient();
|
|
40
|
+
|
|
41
|
+
return useMutation({
|
|
42
|
+
mutationFn: setupAuth,
|
|
43
|
+
onSuccess: async () => {
|
|
44
|
+
await invalidateProtectedQueries(queryClient);
|
|
45
|
+
toast.success(t('authSetupSuccess'));
|
|
46
|
+
},
|
|
47
|
+
onError: (error: Error) => {
|
|
48
|
+
toast.error(`${t('authActionFailed')}: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useLoginAuth() {
|
|
54
|
+
const queryClient = useQueryClient();
|
|
55
|
+
|
|
56
|
+
return useMutation({
|
|
57
|
+
mutationFn: loginAuth,
|
|
58
|
+
onSuccess: async () => {
|
|
59
|
+
await invalidateProtectedQueries(queryClient);
|
|
60
|
+
toast.success(t('authLoginSuccess'));
|
|
61
|
+
},
|
|
62
|
+
onError: (error: Error) => {
|
|
63
|
+
toast.error(`${t('authActionFailed')}: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function useLogoutAuth() {
|
|
69
|
+
const queryClient = useQueryClient();
|
|
70
|
+
|
|
71
|
+
return useMutation({
|
|
72
|
+
mutationFn: logoutAuth,
|
|
73
|
+
onSuccess: async () => {
|
|
74
|
+
await invalidateProtectedQueries(queryClient);
|
|
75
|
+
toast.success(t('authLogoutSuccess'));
|
|
76
|
+
},
|
|
77
|
+
onError: (error: Error) => {
|
|
78
|
+
toast.error(`${t('authActionFailed')}: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function useUpdateAuthPassword() {
|
|
84
|
+
const queryClient = useQueryClient();
|
|
85
|
+
|
|
86
|
+
return useMutation({
|
|
87
|
+
mutationFn: updateAuthPassword,
|
|
88
|
+
onSuccess: async () => {
|
|
89
|
+
await invalidateProtectedQueries(queryClient);
|
|
90
|
+
toast.success(t('authPasswordUpdated'));
|
|
91
|
+
},
|
|
92
|
+
onError: (error: Error) => {
|
|
93
|
+
toast.error(`${t('authActionFailed')}: ${error.message}`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function useUpdateAuthEnabled() {
|
|
99
|
+
const queryClient = useQueryClient();
|
|
100
|
+
|
|
101
|
+
return useMutation({
|
|
102
|
+
mutationFn: updateAuthEnabled,
|
|
103
|
+
onSuccess: async (_, variables) => {
|
|
104
|
+
await invalidateProtectedQueries(queryClient);
|
|
105
|
+
toast.success(variables.enabled ? t('authEnabledSuccess') : t('authDisabledSuccess'));
|
|
106
|
+
},
|
|
107
|
+
onError: (error: Error) => {
|
|
108
|
+
toast.error(`${t('authActionFailed')}: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -16,6 +16,15 @@ export function useMarketplaceItems(params: MarketplaceListParams) {
|
|
|
16
16
|
return useQuery({
|
|
17
17
|
queryKey: ['marketplace-items', params],
|
|
18
18
|
queryFn: () => fetchMarketplaceItems(params),
|
|
19
|
+
placeholderData: (previousData, previousQuery) => {
|
|
20
|
+
const previousParams = previousQuery?.queryKey?.[1];
|
|
21
|
+
if (!previousParams || typeof previousParams !== 'object' || previousParams === null) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const previousType = 'type' in previousParams ? previousParams.type : undefined;
|
|
26
|
+
return previousType === params.type ? previousData : undefined;
|
|
27
|
+
},
|
|
19
28
|
staleTime: 15_000
|
|
20
29
|
});
|
|
21
30
|
}
|
|
@@ -37,6 +37,36 @@ export function useWebSocket(queryClient?: QueryClient) {
|
|
|
37
37
|
}
|
|
38
38
|
})();
|
|
39
39
|
const client = new ConfigWebSocket(wsUrl);
|
|
40
|
+
let isSocketOpen = false;
|
|
41
|
+
|
|
42
|
+
const probeHealth = async (): Promise<boolean> => {
|
|
43
|
+
const base = API_BASE?.replace(/\/$/, '') || window.location.origin;
|
|
44
|
+
const url = `${base}/api/health`;
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(url, { method: 'GET', cache: 'no-store' });
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const payload = await response.json() as {
|
|
51
|
+
ok?: boolean;
|
|
52
|
+
data?: {
|
|
53
|
+
status?: string;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
return payload.ok === true && payload.data?.status === 'ok';
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const syncConnectionStatusFromHealth = async () => {
|
|
63
|
+
if (isSocketOpen) {
|
|
64
|
+
setConnectionStatus('connected');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const healthy = await probeHealth();
|
|
68
|
+
setConnectionStatus(healthy ? 'connected' : 'disconnected');
|
|
69
|
+
};
|
|
40
70
|
|
|
41
71
|
const invalidateSessionQueries = (sessionKey?: string) => {
|
|
42
72
|
if (!queryClient) {
|
|
@@ -50,10 +80,23 @@ export function useWebSocket(queryClient?: QueryClient) {
|
|
|
50
80
|
queryClient.invalidateQueries({ queryKey: ['session-history'] });
|
|
51
81
|
};
|
|
52
82
|
|
|
83
|
+
setConnectionStatus('connecting');
|
|
84
|
+
|
|
53
85
|
client.on('connection.open', () => {
|
|
86
|
+
isSocketOpen = true;
|
|
54
87
|
setConnectionStatus('connected');
|
|
55
88
|
});
|
|
56
89
|
|
|
90
|
+
client.on('connection.close', () => {
|
|
91
|
+
isSocketOpen = false;
|
|
92
|
+
void syncConnectionStatusFromHealth();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
client.on('connection.error', () => {
|
|
96
|
+
isSocketOpen = false;
|
|
97
|
+
void syncConnectionStatusFromHealth();
|
|
98
|
+
});
|
|
99
|
+
|
|
57
100
|
client.on('config.updated', (event) => {
|
|
58
101
|
// Trigger refetch of config
|
|
59
102
|
if (queryClient) {
|
|
@@ -100,8 +143,17 @@ export function useWebSocket(queryClient?: QueryClient) {
|
|
|
100
143
|
|
|
101
144
|
client.connect();
|
|
102
145
|
setWs(client);
|
|
146
|
+
void syncConnectionStatusFromHealth();
|
|
147
|
+
const healthTimer = window.setInterval(() => {
|
|
148
|
+
void syncConnectionStatusFromHealth();
|
|
149
|
+
}, 10_000);
|
|
103
150
|
|
|
104
|
-
return () =>
|
|
151
|
+
return () => {
|
|
152
|
+
window.clearInterval(healthTimer);
|
|
153
|
+
isSocketOpen = false;
|
|
154
|
+
client.disconnect();
|
|
155
|
+
setConnectionStatus('disconnected');
|
|
156
|
+
};
|
|
105
157
|
}, [setConnectionStatus, queryClient]);
|
|
106
158
|
|
|
107
159
|
return ws;
|
package/src/lib/i18n.ts
CHANGED
|
@@ -134,6 +134,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
134
134
|
marketplace: { zh: '市场', en: 'Marketplace' },
|
|
135
135
|
advanced: { zh: '高级', en: 'Advanced' },
|
|
136
136
|
settings: { zh: '设置', en: 'Settings' },
|
|
137
|
+
security: { zh: '安全', en: 'Security' },
|
|
137
138
|
backToMain: { zh: '返回主界面', en: 'Back to Main' },
|
|
138
139
|
|
|
139
140
|
// Common
|
|
@@ -401,10 +402,80 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
401
402
|
textChunkLimit: { zh: '文本分块上限', en: 'Text Chunk Limit' },
|
|
402
403
|
invalidJson: { zh: 'JSON 格式无效', en: 'Invalid JSON' },
|
|
403
404
|
|
|
405
|
+
// Auth
|
|
406
|
+
authBrand: { zh: 'NextClaw UI', en: 'NextClaw UI' },
|
|
407
|
+
authLoginTitle: { zh: '管理员登录', en: 'Admin Sign In' },
|
|
408
|
+
authLoginDescription: {
|
|
409
|
+
zh: '认证已开启。登录后才能查看这台机器的 NextClaw UI。',
|
|
410
|
+
en: 'Authentication is enabled. Sign in to access this machine’s NextClaw UI.'
|
|
411
|
+
},
|
|
412
|
+
authUsername: { zh: '管理员用户名', en: 'Admin Username' },
|
|
413
|
+
authUsernamePlaceholder: { zh: '输入管理员用户名', en: 'Enter admin username' },
|
|
414
|
+
authPassword: { zh: '管理员密码', en: 'Admin Password' },
|
|
415
|
+
authPasswordPlaceholder: { zh: '输入管理员密码', en: 'Enter admin password' },
|
|
416
|
+
authConfirmPassword: { zh: '确认密码', en: 'Confirm Password' },
|
|
417
|
+
authConfirmPasswordPlaceholder: { zh: '再次输入密码', en: 'Enter password again' },
|
|
418
|
+
authLoginAction: { zh: '登录', en: 'Sign In' },
|
|
419
|
+
authLoggingIn: { zh: '登录中...', en: 'Signing in...' },
|
|
420
|
+
authLoggingOut: { zh: '退出中...', en: 'Signing out...' },
|
|
421
|
+
authActionFailed: { zh: '认证操作失败', en: 'Authentication action failed' },
|
|
422
|
+
authLoginSuccess: { zh: '登录成功', en: 'Signed in successfully' },
|
|
423
|
+
authLogoutSuccess: { zh: '已退出登录', en: 'Signed out successfully' },
|
|
424
|
+
authSetupSuccess: { zh: '认证已开启,当前标签页已自动登录,可直接继续使用', en: 'Authentication enabled. This tab is now signed in and ready to use.' },
|
|
425
|
+
authPasswordUpdated: { zh: '管理员密码已更新', en: 'Admin password updated' },
|
|
426
|
+
authEnabledSuccess: { zh: '认证已开启', en: 'Authentication enabled' },
|
|
427
|
+
authDisabledSuccess: { zh: '认证已关闭', en: 'Authentication disabled' },
|
|
428
|
+
authRetryStatus: { zh: '重试', en: 'Retry' },
|
|
429
|
+
authStatusLoadFailed: { zh: '无法获取认证状态,请检查 UI 服务是否正常。', en: 'Failed to load authentication status. Check whether the UI server is healthy.' },
|
|
430
|
+
|
|
404
431
|
// Runtime
|
|
405
432
|
runtimePageTitle: { zh: '路由与运行时', en: 'Routing & Runtime' },
|
|
406
433
|
runtimePageDescription: { zh: '对齐 OpenClaw 的多 Agent 路由:绑定规则、Agent 池、私聊范围。', en: 'Align multi-agent routing with OpenClaw: bindings, agent pool, and DM scope.' },
|
|
407
434
|
runtimeLoading: { zh: '加载运行时配置中...', en: 'Loading runtime settings...' },
|
|
435
|
+
authSecurityTitle: { zh: 'Security', en: 'Security' },
|
|
436
|
+
authSecurityDescription: {
|
|
437
|
+
zh: '保持本机控制台默认简单;只有在你需要远程暴露时,再给 UI 加一层登录门。',
|
|
438
|
+
en: 'Keep the local console simple by default, and add a lightweight login gate only when you expose the UI remotely.'
|
|
439
|
+
},
|
|
440
|
+
authSetupTitle: { zh: '开启轻量认证', en: 'Enable Lightweight Authentication' },
|
|
441
|
+
authSetupDescription: {
|
|
442
|
+
zh: '首次开启时设置单个管理员账号。完成后当前标签页会自动登录。',
|
|
443
|
+
en: 'Create the single admin account the first time you enable authentication. This tab will be signed in automatically.'
|
|
444
|
+
},
|
|
445
|
+
authSetupAction: { zh: '开启认证', en: 'Enable Authentication' },
|
|
446
|
+
authSettingUp: { zh: '开启中...', en: 'Enabling...' },
|
|
447
|
+
authPasswordMismatch: { zh: '两次输入的密码不一致', en: 'Passwords do not match' },
|
|
448
|
+
authPasswordMinLengthHint: {
|
|
449
|
+
zh: '密码至少 8 个字符。当前版本只支持单管理员账号。',
|
|
450
|
+
en: 'Passwords must be at least 8 characters. This version supports a single admin account only.'
|
|
451
|
+
},
|
|
452
|
+
authStatusLabel: { zh: '当前状态', en: 'Current Status' },
|
|
453
|
+
authStatusConfiguredUser: { zh: '管理员账号:{username}', en: 'Admin account: {username}' },
|
|
454
|
+
authUsernameFixedHelp: {
|
|
455
|
+
zh: '首版不提供修改用户名和多用户管理;如需重新定义账号,请后续扩展这套边界。',
|
|
456
|
+
en: 'This first version does not support renaming the admin account or managing multiple users.'
|
|
457
|
+
},
|
|
458
|
+
authEnableLabel: { zh: '要求登录', en: 'Require Login' },
|
|
459
|
+
authEnableOnHelp: {
|
|
460
|
+
zh: '已开启后,除健康检查与认证接口外,其余 UI API 和 WebSocket 都需要登录。',
|
|
461
|
+
en: 'When enabled, every UI API and WebSocket except health and auth endpoints requires login.'
|
|
462
|
+
},
|
|
463
|
+
authEnableOffHelp: {
|
|
464
|
+
zh: '当前保持即开即用。重新打开后,这个标签页会自动拿到新的登录会话。',
|
|
465
|
+
en: 'The UI is currently open for local-style use. Re-enabling will issue a fresh signed-in session to this tab.'
|
|
466
|
+
},
|
|
467
|
+
authPasswordSectionTitle: { zh: '修改管理员密码', en: 'Change Admin Password' },
|
|
468
|
+
authPasswordSectionDescription: {
|
|
469
|
+
zh: '更新密码后,旧会话会立即失效;当前标签页会自动续成新会话(仅在认证开启时)。',
|
|
470
|
+
en: 'Updating the password invalidates old sessions immediately. This tab gets a fresh session automatically while auth is enabled.'
|
|
471
|
+
},
|
|
472
|
+
authPasswordAction: { zh: '更新密码', en: 'Update Password' },
|
|
473
|
+
authPasswordUpdating: { zh: '更新中...', en: 'Updating...' },
|
|
474
|
+
authLogoutAction: { zh: '退出当前标签页', en: 'Sign Out This Tab' },
|
|
475
|
+
authSessionMemoryNotice: {
|
|
476
|
+
zh: '当前版本的会话只保存在服务端内存里。NextClaw UI 进程重启后,需要重新登录。',
|
|
477
|
+
en: 'Sessions are stored only in server memory for now. You will need to sign in again after the NextClaw UI process restarts.'
|
|
478
|
+
},
|
|
408
479
|
dmScope: { zh: '私聊范围', en: 'DM Scope' },
|
|
409
480
|
dmScopeHelp: { zh: '控制私聊会话如何隔离。', en: 'Control how direct-message sessions are isolated.' },
|
|
410
481
|
defaultContextTokens: { zh: '默认上下文 Token', en: 'Default Context Tokens' },
|
|
@@ -600,6 +671,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
600
671
|
chatToolOutput: { zh: '查看输出', en: 'View Output' },
|
|
601
672
|
chatToolNoOutput: { zh: '无输出(执行完成)', en: 'No output (completed)' },
|
|
602
673
|
chatReasoning: { zh: '查看推理内容', en: 'Show reasoning' },
|
|
674
|
+
chatUnknownPart: { zh: '未知消息片段', en: 'Unknown message part' },
|
|
603
675
|
chatCodeCopy: { zh: '复制代码', en: 'Copy' },
|
|
604
676
|
chatCodeCopied: { zh: '已复制', en: 'Copied' },
|
|
605
677
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
|
|
4
|
+
value: vi.fn(),
|
|
5
|
+
writable: true
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
class MockResizeObserver {
|
|
9
|
+
observe() {}
|
|
10
|
+
|
|
11
|
+
unobserve() {}
|
|
12
|
+
|
|
13
|
+
disconnect() {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
vi.stubGlobal('ResizeObserver', MockResizeObserver);
|
package/tsconfig.json
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
"jsx": "react-jsx",
|
|
9
9
|
"noEmit": true,
|
|
10
10
|
"allowImportingTsExtensions": true,
|
|
11
|
-
"types": ["vite/client"],
|
|
11
|
+
"types": ["vite/client", "vitest/globals"],
|
|
12
12
|
"baseUrl": ".",
|
|
13
13
|
"paths": {
|
|
14
14
|
"@/*": ["./src/*"],
|
|
15
|
-
"@nextclaw/agent-chat": ["../nextclaw-agent-chat/src/index.ts"]
|
|
15
|
+
"@nextclaw/agent-chat": ["../nextclaw-agent-chat/src/index.ts"],
|
|
16
|
+
"@nextclaw/agent-chat-ui": ["../nextclaw-agent-chat-ui/src/index.ts"]
|
|
16
17
|
}
|
|
17
18
|
},
|
|
18
19
|
"include": ["src"]
|
package/vite.config.ts
CHANGED
|
@@ -10,7 +10,8 @@ export default defineConfig({
|
|
|
10
10
|
resolve: {
|
|
11
11
|
alias: {
|
|
12
12
|
'@': path.resolve(__dirname, './src'),
|
|
13
|
-
'@nextclaw/agent-chat': path.resolve(__dirname, '../nextclaw-agent-chat/src/index.ts')
|
|
13
|
+
'@nextclaw/agent-chat': path.resolve(__dirname, '../nextclaw-agent-chat/src/index.ts'),
|
|
14
|
+
'@nextclaw/agent-chat-ui': path.resolve(__dirname, '../nextclaw-agent-chat-ui/src/index.ts')
|
|
14
15
|
}
|
|
15
16
|
},
|
|
16
17
|
server: {
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { defineConfig } from 'vitest/config';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
resolve: {
|
|
6
|
+
alias: {
|
|
7
|
+
'@': path.resolve(__dirname, './src'),
|
|
8
|
+
'@nextclaw/agent-chat': path.resolve(__dirname, '../nextclaw-agent-chat/src/index.ts')
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
test: {
|
|
12
|
+
environment: 'jsdom',
|
|
13
|
+
globals: true,
|
|
14
|
+
setupFiles: ['./src/test/setup.ts']
|
|
15
|
+
}
|
|
16
|
+
});
|
package/.eslintrc.cjs
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
root: true,
|
|
3
|
-
parser: "@typescript-eslint/parser",
|
|
4
|
-
plugins: ["@typescript-eslint"],
|
|
5
|
-
extends: [
|
|
6
|
-
"eslint:recommended",
|
|
7
|
-
"plugin:@typescript-eslint/recommended",
|
|
8
|
-
"plugin:react-hooks/recommended",
|
|
9
|
-
"prettier"
|
|
10
|
-
],
|
|
11
|
-
env: {
|
|
12
|
-
browser: true,
|
|
13
|
-
es2022: true,
|
|
14
|
-
node: true
|
|
15
|
-
},
|
|
16
|
-
settings: {
|
|
17
|
-
react: {
|
|
18
|
-
version: "detect"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
ignorePatterns: ["dist", "node_modules", "tailwind.config.js"],
|
|
22
|
-
rules: {
|
|
23
|
-
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
|
24
|
-
"react-hooks/rules-of-hooks": "error",
|
|
25
|
-
"react-hooks/exhaustive-deps": "warn",
|
|
26
|
-
"react-hooks/set-state-in-effect": "off",
|
|
27
|
-
"prefer-destructuring": ["warn", {
|
|
28
|
-
"VariableDeclarator": {
|
|
29
|
-
"array": false,
|
|
30
|
-
"object": true
|
|
31
|
-
},
|
|
32
|
-
"AssignmentExpression": {
|
|
33
|
-
"array": false,
|
|
34
|
-
"object": false
|
|
35
|
-
}
|
|
36
|
-
}],
|
|
37
|
-
"max-lines": ["warn", { "max": 800, "skipBlankLines": true, "skipComments": true }],
|
|
38
|
-
"max-lines-per-function": ["warn", { "max": 150, "skipBlankLines": true, "skipComments": true, "IIFEs": true }]
|
|
39
|
-
},
|
|
40
|
-
overrides: [
|
|
41
|
-
{
|
|
42
|
-
files: ["src/components/**/*.tsx", "src/App.tsx"],
|
|
43
|
-
rules: {
|
|
44
|
-
"max-lines-per-function": ["warn", { "max": 300, "skipBlankLines": true, "skipComments": true, "IIFEs": true }]
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
]
|
|
48
|
-
};
|