@nextclaw/ui 0.6.15 → 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.
Files changed (88) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -0
  3. package/dist/assets/ChannelsList-DF2U-LY1.js +1 -0
  4. package/dist/assets/ChatPage-BX39y0U5.js +36 -0
  5. package/dist/assets/DocBrowser-B9ws5JL7.js +1 -0
  6. package/dist/assets/{LogoBadge-Cer0jX6t.js → LogoBadge-DvGAzkZ3.js} +1 -1
  7. package/dist/assets/MarketplacePage-DG5mHWJ8.js +49 -0
  8. package/dist/assets/ModelConfig-BL_HsOsm.js +1 -0
  9. package/dist/assets/ProvidersList-CH5z00YT.js +1 -0
  10. package/dist/assets/RuntimeConfig-BplBgkwo.js +1 -0
  11. package/dist/assets/SearchConfig-BhaI0fUf.js +1 -0
  12. package/dist/assets/{SecretsConfig-BnGVZiv4.js → SecretsConfig-CFoimOh9.js} +2 -2
  13. package/dist/assets/SessionsConfig-BHTAYn9T.js +2 -0
  14. package/dist/assets/index-BLeJkJ0o.css +1 -0
  15. package/dist/assets/index-DK4TS5ev.js +8 -0
  16. package/dist/assets/index-X5J6Mm--.js +1 -0
  17. package/dist/assets/{index-CkqvHQAt.js → index-uMsNsQX6.js} +1 -1
  18. package/dist/assets/{label-DkL14Jvl.js → label-D8ly4a2P.js} +1 -1
  19. package/dist/assets/page-layout-BSYfvwbp.js +1 -0
  20. package/dist/assets/security-config-DlKEYHNN.js +1 -0
  21. package/dist/assets/{session-run-status-tZ4ISNj-.js → session-run-status-TkIuGbVw.js} +1 -1
  22. package/dist/assets/skeleton-CWbsNx2h.js +1 -0
  23. package/dist/assets/{switch-CgbPbIX3.js → switch-Ce_g9lpN.js} +1 -1
  24. package/dist/assets/tabs-custom-Cf5azvT5.js +1 -0
  25. package/dist/assets/useConfirmDialog-A8Ek8Wu7.js +5 -0
  26. package/dist/assets/vendor-B7ozqnFC.js +412 -0
  27. package/dist/index.html +3 -3
  28. package/package.json +9 -5
  29. package/src/App.tsx +49 -27
  30. package/src/api/client.ts +1 -0
  31. package/src/api/config.ts +60 -0
  32. package/src/api/types.ts +26 -0
  33. package/src/components/auth/login-page.tsx +69 -0
  34. package/src/components/chat/ChatConversationPanel.tsx +12 -54
  35. package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +80 -0
  36. package/src/components/chat/adapters/chat-input-bar.adapter.ts +329 -0
  37. package/src/components/chat/adapters/chat-message.adapter.test.ts +137 -0
  38. package/src/components/chat/adapters/chat-message.adapter.ts +200 -0
  39. package/src/components/chat/chat-input/chat-input-bar.controller.test.tsx +128 -0
  40. package/src/components/chat/chat-input/chat-input-bar.controller.ts +105 -0
  41. package/src/components/chat/containers/chat-input-bar.container.tsx +270 -0
  42. package/src/components/chat/containers/chat-message-list.container.tsx +67 -0
  43. package/src/components/chat/index.ts +1 -0
  44. package/src/components/chat/managers/chat-thread.manager.ts +3 -1
  45. package/src/components/chat/nextclaw/index.ts +23 -0
  46. package/src/components/config/runtime-security-card.tsx +276 -0
  47. package/src/components/config/security-config.tsx +12 -0
  48. package/src/components/layout/Sidebar.tsx +6 -1
  49. package/src/components/marketplace/MarketplacePage.test.tsx +170 -0
  50. package/src/components/marketplace/MarketplacePage.tsx +77 -28
  51. package/src/hooks/use-auth.ts +111 -0
  52. package/src/hooks/useMarketplace.ts +9 -0
  53. package/src/lib/i18n.ts +72 -0
  54. package/src/test/setup.ts +16 -0
  55. package/tsconfig.json +3 -2
  56. package/vite.config.ts +2 -1
  57. package/vitest.config.ts +16 -0
  58. package/dist/assets/ChannelsList-DzeVn-JC.js +0 -1
  59. package/dist/assets/ChatPage-BiFhIm1-.js +0 -36
  60. package/dist/assets/DocBrowser-By3lF9yN.js +0 -1
  61. package/dist/assets/MarketplacePage-EZxALdIz.js +0 -49
  62. package/dist/assets/ModelConfig-AchYxLft.js +0 -1
  63. package/dist/assets/ProvidersList-BsD-4kKX.js +0 -1
  64. package/dist/assets/RuntimeConfig-sKOERbFD.js +0 -1
  65. package/dist/assets/SearchConfig-DAfvDwX6.js +0 -1
  66. package/dist/assets/SessionsConfig-CzvrKDRs.js +0 -2
  67. package/dist/assets/card-BAM7vbMg.js +0 -1
  68. package/dist/assets/index-D9rRqOi8.css +0 -1
  69. package/dist/assets/index-DJZ5y7t1.js +0 -8
  70. package/dist/assets/input-BoelTiYL.js +0 -1
  71. package/dist/assets/page-layout-CERNdqzB.js +0 -1
  72. package/dist/assets/popover-uwYz3Chm.js +0 -1
  73. package/dist/assets/tabs-custom-pDyl95el.js +0 -1
  74. package/dist/assets/useConfirmDialog-DyP6Ac75.js +0 -5
  75. package/dist/assets/vendor-BKtTvQYU.js +0 -407
  76. package/src/components/chat/ChatThread.tsx +0 -402
  77. package/src/components/chat/SkillsPicker.tsx +0 -137
  78. package/src/components/chat/chat-input/ChatInputBarView.tsx +0 -82
  79. package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +0 -83
  80. package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +0 -39
  81. package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +0 -31
  82. package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +0 -112
  83. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +0 -24
  84. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +0 -58
  85. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +0 -56
  86. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +0 -40
  87. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputThinkingSelector.tsx +0 -74
  88. 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.record;
367
+ const { item, record, localeFallbacks, installState, manageState, onOpen, onInstall, onManage } = props;
366
368
  const pluginRecord = record?.type === 'plugin' ? record : undefined;
367
- const type = props.item?.type ?? record?.type;
368
- const title = props.item?.name ?? record?.label ?? record?.id ?? record?.spec ?? t('marketplaceUnknownItem');
369
- const summary = pickLocalizedText(props.item?.summaryI18n, props.item?.summary, props.localeFallbacks)
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 = props.item?.install.spec ?? record?.spec ?? '';
373
+ const spec = item?.install.spec ?? record?.spec ?? '';
372
374
 
373
375
  const targetId = record?.id || record?.spec;
374
- const busyForRecord = Boolean(targetId) && props.manageState.isPending && props.manageState.targetId === 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 = props.item?.install.spec;
383
- const isInstalling = typeof installSpec === 'string' && props.installState.installingSpecs.has(installSpec);
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={props.onOpen}
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
- {props.item && !record && (
439
+ {item && !record && (
438
440
  <button
439
441
  onClick={(event) => {
440
442
  event.stopPropagation();
441
- props.onInstall(props.item as MarketplaceItemSummary);
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={props.manageState.isPending}
454
+ disabled={manageState.isPending}
453
455
  onClick={(event) => {
454
456
  event.stopPropagation();
455
- props.onManage(isDisabled ? 'enable' : 'disable', pluginRecord);
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 && props.manageState.action !== 'uninstall'
460
- ? (props.manageState.action === 'enable' ? t('marketplaceEnabling') : t('marketplaceDisabling'))
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={props.manageState.isPending}
469
+ disabled={manageState.isPending}
468
470
  onClick={(event) => {
469
471
  event.stopPropagation();
470
- props.onManage('uninstall', record);
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 && props.manageState.action === 'uninstall' ? t('marketplaceRemoving') : t('marketplaceUninstall')}
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 className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-3">
873
- {scope === 'all' && allItems.map((item) => (
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' && !itemsQuery.isLoading && !itemsQuery.isError && allItems.length === 0 && (
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' && !installedQuery.isLoading && !installedQuery.isError && installedEntries.length === 0 && (
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
  }
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: {
@@ -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
+ });
@@ -1 +0,0 @@
1
- import{r as v,j as a,an as Z,D as ee,d as T,K as ae,ad as te,aQ as se,aR as ne,aS as le,x as re,q as oe,ai as ce,s as ie}from"./vendor-BKtTvQYU.js";import{t as e,c as I,O as me,u as q,a as $,b as H,Q as pe,R as de,S as be,e as ue,f as xe,g as ye,h as ge}from"./index-DJZ5y7t1.js";import{B as E,P as he,a as fe}from"./page-layout-CERNdqzB.js";import{I as D}from"./input-BoelTiYL.js";import{L as we}from"./label-DkL14Jvl.js";import{S as ve}from"./switch-CgbPbIX3.js";import{L as K,S as J}from"./LogoBadge-Cer0jX6t.js";import{h as O}from"./config-hints-CApS3K_7.js";import{c as je,b as ke,a as Se,C as Ce}from"./config-layout-BHnOoweL.js";import{T as Ne}from"./tabs-custom-pDyl95el.js";function Pe({value:t,onChange:m,className:i,placeholder:r=""}){const[o,u]=v.useState(""),d=x=>{x.key==="Enter"&&o.trim()?(x.preventDefault(),m([...t,o.trim()]),u("")):x.key==="Backspace"&&!o&&t.length>0&&m(t.slice(0,-1))},g=x=>{m(t.filter((j,h)=>h!==x))};return a.jsxs("div",{className:I("flex flex-wrap gap-2 p-2 border rounded-md min-h-[42px]",i),children:[t.map((x,j)=>a.jsxs("span",{className:"inline-flex items-center gap-1 px-2 py-1 bg-primary text-primary-foreground rounded text-sm",children:[x,a.jsx("button",{type:"button",onClick:()=>g(j),className:"hover:text-red-300 transition-colors",children:a.jsx(Z,{className:"h-3 w-3"})})]},j)),a.jsx("input",{type:"text",value:o,onChange:x=>u(x.target.value),onKeyDown:d,className:"flex-1 outline-none min-w-[100px] bg-transparent text-sm",placeholder:r||e("enterTag")})]})}function z(t){var r,o;const m=me();return((r=t.tutorialUrls)==null?void 0:r[m])||((o=t.tutorialUrls)==null?void 0:o.default)||t.tutorialUrl}const Ie={telegram:"telegram.svg",slack:"slack.svg",discord:"discord.svg",whatsapp:"whatsapp.svg",qq:"qq.svg",feishu:"feishu.svg",dingtalk:"dingtalk.svg",wecom:"wecom.svg",mochat:"mochat.svg",email:"email.svg"};function Fe(t,m){const i=m.toLowerCase(),r=t[i];return r?`/logos/${r}`:null}function Q(t){return Fe(Ie,t)}const R=[{value:"pairing",label:"pairing"},{value:"allowlist",label:"allowlist"},{value:"open",label:"open"},{value:"disabled",label:"disabled"}],B=[{value:"open",label:"open"},{value:"allowlist",label:"allowlist"},{value:"disabled",label:"disabled"}],Te=[{value:"off",label:"off"},{value:"partial",label:"partial"},{value:"block",label:"block"},{value:"progress",label:"progress"}],De=t=>t.includes("token")||t.includes("secret")||t.includes("password")?a.jsx(ae,{className:"h-3.5 w-3.5 text-gray-500"}):t.includes("url")||t.includes("host")?a.jsx(te,{className:"h-3.5 w-3.5 text-gray-500"}):t.includes("email")||t.includes("mail")?a.jsx(se,{className:"h-3.5 w-3.5 text-gray-500"}):t.includes("id")||t.includes("from")?a.jsx(ne,{className:"h-3.5 w-3.5 text-gray-500"}):t==="enabled"||t==="consentGranted"?a.jsx(le,{className:"h-3.5 w-3.5 text-gray-500"}):a.jsx(re,{className:"h-3.5 w-3.5 text-gray-500"});function G(){return{telegram:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"token",type:"password",label:e("botToken")},{name:"allowFrom",type:"tags",label:e("allowFrom")},{name:"proxy",type:"text",label:e("proxy")},{name:"accountId",type:"text",label:e("accountId")},{name:"dmPolicy",type:"select",label:e("dmPolicy"),options:R},{name:"groupPolicy",type:"select",label:e("groupPolicy"),options:B},{name:"groupAllowFrom",type:"tags",label:e("groupAllowFrom")},{name:"requireMention",type:"boolean",label:e("requireMention")},{name:"mentionPatterns",type:"tags",label:e("mentionPatterns")},{name:"groups",type:"json",label:e("groupRulesJson")}],discord:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"token",type:"password",label:e("botToken")},{name:"allowBots",type:"boolean",label:e("allowBotMessages")},{name:"allowFrom",type:"tags",label:e("allowFrom")},{name:"gatewayUrl",type:"text",label:e("gatewayUrl")},{name:"intents",type:"number",label:e("intents")},{name:"proxy",type:"text",label:e("proxy")},{name:"mediaMaxMb",type:"number",label:e("attachmentMaxSizeMb")},{name:"streaming",type:"select",label:e("streamingMode"),options:Te},{name:"draftChunk",type:"json",label:e("draftChunkingJson")},{name:"textChunkLimit",type:"number",label:e("textChunkLimit")},{name:"accountId",type:"text",label:e("accountId")},{name:"dmPolicy",type:"select",label:e("dmPolicy"),options:R},{name:"groupPolicy",type:"select",label:e("groupPolicy"),options:B},{name:"groupAllowFrom",type:"tags",label:e("groupAllowFrom")},{name:"requireMention",type:"boolean",label:e("requireMention")},{name:"mentionPatterns",type:"tags",label:e("mentionPatterns")},{name:"groups",type:"json",label:e("groupRulesJson")}],whatsapp:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"bridgeUrl",type:"text",label:e("bridgeUrl")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],feishu:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"appId",type:"text",label:e("appId")},{name:"appSecret",type:"password",label:e("appSecret")},{name:"encryptKey",type:"password",label:e("encryptKey")},{name:"verificationToken",type:"password",label:e("verificationToken")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],dingtalk:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"clientId",type:"text",label:e("clientId")},{name:"clientSecret",type:"password",label:e("clientSecret")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],wecom:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"corpId",type:"text",label:e("corpId")},{name:"agentId",type:"text",label:e("agentId")},{name:"secret",type:"password",label:e("secret")},{name:"token",type:"password",label:e("token")},{name:"callbackPort",type:"number",label:e("callbackPort")},{name:"callbackPath",type:"text",label:e("callbackPath")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],slack:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"mode",type:"text",label:e("mode")},{name:"webhookPath",type:"text",label:e("webhookPath")},{name:"allowBots",type:"boolean",label:e("allowBotMessages")},{name:"botToken",type:"password",label:e("botToken")},{name:"appToken",type:"password",label:e("appToken")}],email:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"consentGranted",type:"boolean",label:e("consentGranted")},{name:"imapHost",type:"text",label:e("imapHost")},{name:"imapPort",type:"number",label:e("imapPort")},{name:"imapUsername",type:"text",label:e("imapUsername")},{name:"imapPassword",type:"password",label:e("imapPassword")},{name:"fromAddress",type:"email",label:e("fromAddress")}],mochat:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"baseUrl",type:"text",label:e("baseUrl")},{name:"clawToken",type:"password",label:e("clawToken")},{name:"agentUserId",type:"text",label:e("agentUserId")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],qq:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"appId",type:"text",label:e("appId")},{name:"secret",type:"password",label:e("appSecret")},{name:"markdownSupport",type:"boolean",label:e("markdownSupport")},{name:"allowFrom",type:"tags",label:e("allowFrom")}]}}function A(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function Y(t,m){const i={...t};for(const[r,o]of Object.entries(m)){const u=i[r];if(A(u)&&A(o)){i[r]=Y(u,o);continue}i[r]=o}return i}function Ae(t,m){const i=t.split("."),r={};let o=r;for(let u=0;u<i.length-1;u+=1){const d=i[u];o[d]={},o=o[d]}return o[i[i.length-1]]=m,r}function Le({channelName:t}){var _,U;const{data:m}=q(),{data:i}=$(),{data:r}=H(),o=pe(),u=de(),[d,g]=v.useState({}),[x,j]=v.useState({}),[h,f]=v.useState(null),k=t?m==null?void 0:m.channels[t]:null,w=t?G()[t]??[]:[],c=r==null?void 0:r.uiHints,p=t?`channels.${t}`:null,S=((_=r==null?void 0:r.actions)==null?void 0:_.filter(s=>s.scope===p))??[],C=t&&(((U=O(`channels.${t}`,c))==null?void 0:U.label)??t),P=i==null?void 0:i.channels.find(s=>s.name===t),F=P?z(P):void 0;v.useEffect(()=>{if(k){g({...k});const s={};(t?G()[t]??[]:[]).filter(l=>l.type==="json").forEach(l=>{const y=k[l.name];s[l.name]=JSON.stringify(y??{},null,2)}),j(s)}else g({}),j({})},[k,t]);const N=(s,n)=>{g(l=>({...l,[s]:n}))},L=s=>{if(s.preventDefault(),!t)return;const n={...d};for(const l of w){if(l.type!=="password")continue;const y=n[l.name];(typeof y!="string"||y.length===0)&&delete n[l.name]}for(const l of w){if(l.type!=="json")continue;const y=x[l.name]??"";try{n[l.name]=y.trim()?JSON.parse(y):{}}catch{T.error(`${e("invalidJson")}: ${l.name}`);return}}o.mutate({channel:t,data:n})},V=s=>{if(!s||!t)return;const n=s.channels;if(!A(n))return;const l=n[t];A(l)&&g(y=>Y(y,l))},W=async s=>{if(!(!t||!p)){f(s.id);try{let n={...d};s.saveBeforeRun&&(n={...n,...s.savePatch??{}},g(n),await o.mutateAsync({channel:t,data:n}));const l=await u.mutateAsync({actionId:s.id,data:{scope:p,draftConfig:Ae(p,n)}});V(l.patch),l.ok?T.success(l.message||e("success")):T.error(l.message||e("error"))}catch(n){const l=n instanceof Error?n.message:String(n);T.error(`${e("error")}: ${l}`)}finally{f(null)}}};if(!t||!P||!k)return a.jsx("div",{className:je,children:a.jsxs("div",{children:[a.jsx("h3",{className:"text-base font-semibold text-gray-900",children:e("channelsSelectTitle")}),a.jsx("p",{className:"mt-2 text-sm text-gray-500",children:e("channelsSelectDescription")})]})});const M=!!k.enabled;return a.jsxs("div",{className:ke,children:[a.jsx("div",{className:"border-b border-gray-100 px-6 py-5",children:a.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[a.jsxs("div",{className:"min-w-0",children:[a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx(K,{name:t,src:Q(t),className:I("h-9 w-9 rounded-lg border",M?"border-primary/30 bg-white":"border-gray-200/70 bg-white"),imgClassName:"h-5 w-5 object-contain",fallback:a.jsx("span",{className:"text-sm font-semibold uppercase text-gray-500",children:t[0]})}),a.jsx("h3",{className:"truncate text-lg font-semibold text-gray-900 capitalize",children:C})]}),a.jsx("p",{className:"mt-2 text-sm text-gray-500",children:e("channelsFormDescription")}),F&&a.jsxs("a",{href:F,className:"mt-2 inline-flex items-center gap-1.5 text-xs text-primary transition-colors hover:text-primary-hover",children:[a.jsx(ee,{className:"h-3.5 w-3.5"}),e("channelsGuideTitle")]})]}),a.jsx(J,{status:M?"active":"inactive",label:M?e("statusActive"):e("statusInactive")})]})}),a.jsxs("form",{onSubmit:L,className:"flex min-h-0 flex-1 flex-col",children:[a.jsx("div",{className:"min-h-0 flex-1 space-y-6 overflow-y-auto overscroll-contain px-6 py-5",children:w.map(s=>{const n=t?O(`channels.${t}.${s.name}`,c):void 0,l=(n==null?void 0:n.label)??s.label,y=n==null?void 0:n.placeholder;return a.jsxs("div",{className:"space-y-2.5",children:[a.jsxs(we,{htmlFor:s.name,className:"flex items-center gap-2 text-sm font-medium text-gray-900",children:[De(s.name),l]}),s.type==="boolean"&&a.jsxs("div",{className:"flex items-center justify-between rounded-xl bg-gray-50 p-3",children:[a.jsx("span",{className:"text-sm text-gray-500",children:d[s.name]?e("enabled"):e("disabled")}),a.jsx(ve,{id:s.name,checked:d[s.name]||!1,onCheckedChange:b=>N(s.name,b),className:"data-[state=checked]:bg-emerald-500"})]}),(s.type==="text"||s.type==="email")&&a.jsx(D,{id:s.name,type:s.type,value:d[s.name]||"",onChange:b=>N(s.name,b.target.value),placeholder:y,className:"rounded-xl"}),s.type==="password"&&a.jsx(D,{id:s.name,type:"password",value:d[s.name]||"",onChange:b=>N(s.name,b.target.value),placeholder:y??e("leaveBlankToKeepUnchanged"),className:"rounded-xl"}),s.type==="number"&&a.jsx(D,{id:s.name,type:"number",value:d[s.name]||0,onChange:b=>N(s.name,parseInt(b.target.value,10)||0),placeholder:y,className:"rounded-xl"}),s.type==="tags"&&a.jsx(Pe,{value:d[s.name]||[],onChange:b=>N(s.name,b)}),s.type==="select"&&a.jsxs(be,{value:d[s.name]||"",onValueChange:b=>N(s.name,b),children:[a.jsx(ue,{className:"rounded-xl",children:a.jsx(xe,{})}),a.jsx(ye,{children:(s.options??[]).map(b=>a.jsx(ge,{value:b.value,children:b.label},b.value))})]}),s.type==="json"&&a.jsx("textarea",{id:s.name,value:x[s.name]??"{}",onChange:b=>j(X=>({...X,[s.name]:b.target.value})),className:"min-h-[120px] w-full resize-none rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"})]},s.name)})}),a.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3 border-t border-gray-100 px-6 py-4",children:[a.jsx("div",{className:"flex flex-wrap items-center gap-2",children:S.filter(s=>s.trigger==="manual").map(s=>a.jsx(E,{type:"button",onClick:()=>W(s),disabled:o.isPending||!!h,variant:"secondary",children:h===s.id?e("connecting"):s.title},s.id))}),a.jsx(E,{type:"submit",disabled:o.isPending||!!h,children:o.isPending?e("saving"):e("save")})]})]})]})}const Me={telegram:"channelDescTelegram",slack:"channelDescSlack",email:"channelDescEmail",webhook:"channelDescWebhook",discord:"channelDescDiscord",feishu:"channelDescFeishu"};function Ke(){const{data:t}=q(),{data:m}=$(),{data:i}=H(),[r,o]=v.useState("enabled"),[u,d]=v.useState(),[g,x]=v.useState(""),j=i==null?void 0:i.uiHints,h=m==null?void 0:m.channels,f=t==null?void 0:t.channels,k=[{id:"enabled",label:e("channelsTabEnabled"),count:(h??[]).filter(c=>{var p;return(p=f==null?void 0:f[c.name])==null?void 0:p.enabled}).length},{id:"all",label:e("channelsTabAll"),count:(h??[]).length}],w=v.useMemo(()=>{const c=g.trim().toLowerCase();return(h??[]).filter(p=>{var C;const S=((C=f==null?void 0:f[p.name])==null?void 0:C.enabled)||!1;return r==="enabled"?S:!0}).filter(p=>c?(p.displayName||p.name).toLowerCase().includes(c)||p.name.toLowerCase().includes(c):!0)},[r,f,h,g]);return v.useEffect(()=>{if(w.length===0){d(void 0);return}w.some(p=>p.name===u)||d(w[0].name)},[w,u]),!t||!m?a.jsx("div",{className:"p-8 text-gray-400",children:e("channelsLoading")}):a.jsxs(he,{className:"xl:flex xl:h-full xl:min-h-0 xl:flex-col xl:pb-0",children:[a.jsx(fe,{title:e("channelsPageTitle"),description:e("channelsPageDescription")}),a.jsxs("div",{className:I(Ce,"xl:min-h-0 xl:flex-1"),children:[a.jsxs("section",{className:Se,children:[a.jsx("div",{className:"border-b border-gray-100 px-4 pt-4",children:a.jsx(Ne,{tabs:k,activeTab:r,onChange:o,className:"mb-0"})}),a.jsx("div",{className:"border-b border-gray-100 px-4 py-3",children:a.jsxs("div",{className:"relative",children:[a.jsx(oe,{className:"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"}),a.jsx(D,{value:g,onChange:c=>x(c.target.value),placeholder:e("channelsFilterPlaceholder"),className:"h-10 rounded-xl pl-9"})]})}),a.jsxs("div",{className:"min-h-0 flex-1 space-y-2 overflow-y-auto overscroll-contain p-3",children:[w.map(c=>{const p=t.channels[c.name],S=(p==null?void 0:p.enabled)||!1,C=O(`channels.${c.name}`,j),P=z(c),F=(C==null?void 0:C.help)||e(Me[c.name]||"channelDescriptionDefault"),N=u===c.name;return a.jsx("button",{type:"button",onClick:()=>d(c.name),className:I("w-full rounded-xl border p-2.5 text-left transition-all",N?"border-primary/30 bg-primary-50/40 shadow-sm":"border-gray-200/70 bg-white hover:border-gray-300 hover:bg-gray-50/70"),children:a.jsxs("div",{className:"flex items-start justify-between gap-3",children:[a.jsxs("div",{className:"flex min-w-0 items-center gap-3",children:[a.jsx(K,{name:c.name,src:Q(c.name),className:I("h-10 w-10 rounded-lg border",S?"border-primary/30 bg-white":"border-gray-200/70 bg-white"),imgClassName:"h-5 w-5 object-contain",fallback:a.jsx("span",{className:"text-sm font-semibold uppercase text-gray-500",children:c.name[0]})}),a.jsxs("div",{className:"min-w-0",children:[a.jsx("p",{className:"truncate text-sm font-semibold text-gray-900",children:c.displayName||c.name}),a.jsx("p",{className:"line-clamp-1 text-[11px] text-gray-500",children:F})]})]}),a.jsxs("div",{className:"flex items-center gap-2",children:[P&&a.jsx("a",{href:P,onClick:L=>L.stopPropagation(),className:"inline-flex h-7 w-7 items-center justify-center rounded-md text-gray-300 transition-colors hover:bg-gray-100/70 hover:text-gray-500",title:e("channelsGuideTitle"),children:a.jsx(ce,{className:"h-3.5 w-3.5"})}),a.jsx(J,{status:S?"active":"inactive",label:S?e("statusActive"):e("statusInactive"),className:"min-w-[56px] justify-center"})]})]})},c.name)}),w.length===0&&a.jsxs("div",{className:"flex h-full min-h-[220px] flex-col items-center justify-center rounded-xl border border-dashed border-gray-200 bg-gray-50/70 py-10 text-center",children:[a.jsx("div",{className:"mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-white",children:a.jsx(ie,{className:"h-5 w-5 text-gray-300"})}),a.jsx("p",{className:"text-sm font-medium text-gray-700",children:e("channelsNoMatch")})]})]})]}),a.jsx(Le,{channelName:u})]})]})}export{Ke as ChannelsList};