@lobehub/lobehub 2.0.0-next.290 → 2.0.0-next.292

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 (89) hide show
  1. package/.conductor/setup.sh +107 -0
  2. package/.cursor/rules/linear.mdc +53 -0
  3. package/.github/actions/desktop-build-setup/action.yml +29 -0
  4. package/.github/actions/desktop-upload-artifacts/action.yml +46 -0
  5. package/.github/workflows/release-desktop-beta.yml +76 -115
  6. package/.github/workflows/release-desktop-stable.yml +461 -0
  7. package/CHANGELOG.md +68 -0
  8. package/CLAUDE.md +2 -48
  9. package/apps/desktop/dev-app-update.yml +10 -0
  10. package/apps/desktop/electron-builder.mjs +40 -10
  11. package/apps/desktop/electron.vite.config.ts +3 -2
  12. package/apps/desktop/package.json +2 -1
  13. package/apps/desktop/scripts/update-test/README.md +222 -0
  14. package/apps/desktop/scripts/update-test/dev-app-update.local.yml +18 -0
  15. package/apps/desktop/scripts/update-test/generate-manifest.sh +277 -0
  16. package/apps/desktop/scripts/update-test/run-test.sh +105 -0
  17. package/apps/desktop/scripts/update-test/setup.sh +111 -0
  18. package/apps/desktop/scripts/update-test/start-server.sh +70 -0
  19. package/apps/desktop/scripts/update-test/stop-server.sh +33 -0
  20. package/apps/desktop/src/main/core/infrastructure/UpdaterManager.ts +120 -9
  21. package/apps/desktop/src/main/core/infrastructure/__tests__/UpdaterManager.test.ts +17 -1
  22. package/apps/desktop/src/main/env.ts +19 -11
  23. package/apps/desktop/src/main/modules/updater/configs.ts +14 -1
  24. package/changelog/v1.json +21 -0
  25. package/conductor.json +5 -0
  26. package/locales/en-US/chat.json +2 -0
  27. package/locales/en-US/subscription.json +2 -2
  28. package/locales/zh-CN/chat.json +2 -0
  29. package/locales/zh-CN/subscription.json +2 -2
  30. package/package.json +1 -1
  31. package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +16 -14
  32. package/packages/electron-client-ipc/src/useWatchBroadcast.ts +10 -4
  33. package/packages/model-bank/src/aiModels/qiniu.ts +6 -6
  34. package/packages/observability-otel/src/node.ts +39 -37
  35. package/scripts/electronWorkflow/mergeMacReleaseFiles.js +22 -8
  36. package/src/app/(backend)/api/version/route.ts +13 -0
  37. package/src/app/[variants]/(desktop)/desktop-onboarding/_layout/index.tsx +2 -1
  38. package/src/app/[variants]/(main)/_layout/index.tsx +2 -1
  39. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobScheduleConfig.tsx +0 -1
  40. package/src/app/[variants]/(main)/agent/cron/[cronId]/index.tsx +5 -5
  41. package/src/app/[variants]/(main)/agent/features/Conversation/ThreadHydration.tsx +3 -1
  42. package/src/app/[variants]/(main)/group/features/Conversation/ThreadHydration.tsx +3 -1
  43. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/Checker.tsx +15 -6
  44. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/index.tsx +68 -23
  45. package/src/app/[variants]/(mobile)/router/mobileRouter.config.tsx +1 -4
  46. package/src/app/[variants]/router/desktopRouter.config.tsx +1 -4
  47. package/src/components/HtmlPreview/PreviewDrawer.tsx +1 -1
  48. package/src/features/ChatInput/ChatInputProvider.tsx +1 -1
  49. package/src/features/Conversation/Messages/Assistant/components/MessageContent.tsx +9 -16
  50. package/src/features/Conversation/Messages/AssistantGroup/components/GroupItem.tsx +12 -2
  51. package/src/features/Conversation/Messages/Task/components/MessageContent.tsx +1 -0
  52. package/src/features/Conversation/Messages/Tool/Tool/index.tsx +10 -1
  53. package/src/features/Conversation/Messages/components/ContentLoading.tsx +64 -0
  54. package/src/features/Conversation/Messages/components/DisplayContent.tsx +4 -2
  55. package/src/features/{ElectronTitlebar/hooks → Electron/navigation}/useNavigationHistory.ts +1 -1
  56. package/src/features/{ElectronTitlebar/NavigationBar/index.tsx → Electron/titlebar/NavigationBar.tsx} +1 -1
  57. package/src/features/{ElectronTitlebar/NavigationBar → Electron/titlebar}/RecentlyViewed.tsx +1 -1
  58. package/src/features/{ElectronTitlebar/index.tsx → Electron/titlebar/TitleBar.tsx} +19 -9
  59. package/src/features/Electron/titlebar/WinControl.tsx +5 -0
  60. package/src/features/Electron/updater/UpdateModal.tsx +299 -0
  61. package/src/features/LibraryModal/AddFilesToKnowledgeBase/index.test.tsx +24 -0
  62. package/src/features/LibraryModal/AddFilesToKnowledgeBase/index.tsx +21 -24
  63. package/src/features/LibraryModal/CreateNew/index.tsx +18 -22
  64. package/src/features/OllamaModelDownloader/index.tsx +3 -3
  65. package/src/features/PluginDevModal/index.tsx +1 -1
  66. package/src/layout/GlobalProvider/AppTheme.tsx +1 -1
  67. package/src/libs/swr/index.ts +17 -23
  68. package/src/locales/default/chat.ts +2 -0
  69. package/src/store/aiInfra/slices/aiProvider/action.ts +68 -1
  70. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +2 -1
  71. package/src/store/chat/slices/portal/action.test.ts +0 -2
  72. package/src/store/chat/slices/portal/action.ts +17 -44
  73. package/src/store/chat/slices/thread/action.test.ts +4 -1
  74. package/src/store/chat/slices/thread/action.ts +6 -1
  75. package/src/components/FunctionModal/createModalHooks.ts +0 -48
  76. package/src/components/FunctionModal/index.ts +0 -1
  77. package/src/components/FunctionModal/style.tsx +0 -44
  78. package/src/features/ElectronTitlebar/UpdateModal.tsx +0 -274
  79. package/src/features/ElectronTitlebar/WinControl/index.tsx +0 -90
  80. /package/src/features/{ElectronTitlebar/Connection/index.tsx → Electron/connection/Connection.tsx} +0 -0
  81. /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/ConnectionMode.tsx +0 -0
  82. /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/Option.tsx +0 -0
  83. /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/RemoteStatus.tsx +0 -0
  84. /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/Waiting.tsx +0 -0
  85. /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/WaitingAnim.tsx +0 -0
  86. /package/src/features/{ElectronTitlebar/helpers → Electron/navigation}/routeMetadata.ts +0 -0
  87. /package/src/features/{ElectronTitlebar/hooks → Electron/system}/useWatchThemeUpdate.ts +0 -0
  88. /package/src/features/{ElectronTitlebar → Electron/titlebar}/SimpleTitleBar.tsx +0 -0
  89. /package/src/features/{ElectronTitlebar → Electron/updater}/UpdateNotification.tsx +0 -0
@@ -8,7 +8,6 @@ import { CollapsedMessage } from '../../AssistantGroup/components/CollapsedMessa
8
8
  import DisplayContent from '../../components/DisplayContent';
9
9
  import FileChunks from '../../components/FileChunks';
10
10
  import ImageFileListViewer from '../../components/ImageFileListViewer';
11
- import IntentUnderstanding from '../../components/IntentUnderstanding';
12
11
  import Reasoning from '../../components/Reasoning';
13
12
  import SearchGrounding from '../../components/SearchGrounding';
14
13
  import { useMarkdown } from '../useMarkdown';
@@ -23,9 +22,6 @@ const MessageContent = memo<UIChatMessage>(
23
22
 
24
23
  const isToolCallGenerating = generating && (content === LOADING_FLAT || !content) && !!tools;
25
24
 
26
- // TODO: Need to implement isIntentUnderstanding selector in ConversationStore if needed
27
- const isIntentUnderstanding = false;
28
-
29
25
  const showSearch = !!search && !!search.citations?.length;
30
26
  const showImageItems = !!imageList && imageList.length > 0;
31
27
 
@@ -46,18 +42,15 @@ const MessageContent = memo<UIChatMessage>(
46
42
  )}
47
43
  {showFileChunks && <FileChunks data={chunksList} />}
48
44
  {showReasoning && <Reasoning {...props.reasoning} id={id} />}
49
- {isIntentUnderstanding ? (
50
- <IntentUnderstanding />
51
- ) : (
52
- <DisplayContent
53
- content={content}
54
- hasImages={showImageItems}
55
- isMultimodal={metadata?.isMultimodal}
56
- isToolCallGenerating={isToolCallGenerating}
57
- markdownProps={markdownProps}
58
- tempDisplayContent={metadata?.tempDisplayContent}
59
- />
60
- )}
45
+ <DisplayContent
46
+ content={content}
47
+ hasImages={showImageItems}
48
+ id={id}
49
+ isMultimodal={metadata?.isMultimodal}
50
+ isToolCallGenerating={isToolCallGenerating}
51
+ markdownProps={markdownProps}
52
+ tempDisplayContent={metadata?.tempDisplayContent}
53
+ />
61
54
  {showImageItems && <ImageFileListViewer items={imageList} />}
62
55
  </Flexbox>
63
56
  );
@@ -25,10 +25,20 @@ const GroupItem = memo<GroupItemProps>(
25
25
  toggleMessageEditing(item.id, true);
26
26
  }}
27
27
  >
28
- <ContentBlock {...item} assistantId={assistantId} disableEditing={disableEditing} error={error} />
28
+ <ContentBlock
29
+ {...item}
30
+ assistantId={assistantId}
31
+ disableEditing={disableEditing}
32
+ error={error}
33
+ />
29
34
  </Flexbox>
30
35
  ) : (
31
- <ContentBlock {...item} assistantId={assistantId} disableEditing={disableEditing} error={error} />
36
+ <ContentBlock
37
+ {...item}
38
+ assistantId={assistantId}
39
+ disableEditing={disableEditing}
40
+ error={error}
41
+ />
32
42
  );
33
43
  },
34
44
  isEqual,
@@ -52,6 +52,7 @@ const MessageContent = memo<UIChatMessage>(
52
52
  <DisplayContent
53
53
  content={content}
54
54
  hasImages={showImageItems}
55
+ id={id}
55
56
  isMultimodal={metadata?.isMultimodal}
56
57
  isToolCallGenerating={isToolCallGenerating}
57
58
  markdownProps={markdownProps}
@@ -33,7 +33,16 @@ export interface InspectorProps {
33
33
  * Tool message component - adapts Tool message data to use AssistantGroup/Tool components
34
34
  */
35
35
  const Tool = memo<InspectorProps>(
36
- ({ arguments: requestArgs, apiName, disableEditing, messageId, toolCallId, index, identifier, type }) => {
36
+ ({
37
+ arguments: requestArgs,
38
+ apiName,
39
+ disableEditing,
40
+ messageId,
41
+ toolCallId,
42
+ index,
43
+ identifier,
44
+ type,
45
+ }) => {
37
46
  const [showDebug, setShowDebug] = useState(false);
38
47
  const [showPluginRender, setShowPluginRender] = useState(false);
39
48
  const [expand, setExpand] = useState(true);
@@ -0,0 +1,64 @@
1
+ import { Flexbox, Text } from '@lobehub/ui';
2
+ import { memo, useEffect, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ import BubblesLoading from '@/components/BubblesLoading';
6
+ import { useChatStore } from '@/store/chat';
7
+ import { operationSelectors } from '@/store/chat/selectors';
8
+ import type { OperationType } from '@/store/chat/slices/operation/types';
9
+
10
+ const ELAPSED_TIME_THRESHOLD = 2100; // Show elapsed time after 2 seconds
11
+
12
+ interface ContentLoadingProps {
13
+ id: string;
14
+ }
15
+
16
+ const ContentLoading = memo<ContentLoadingProps>(({ id }) => {
17
+ const { t } = useTranslation('chat');
18
+ const operations = useChatStore(operationSelectors.getOperationsByMessage(id));
19
+ const [elapsedSeconds, setElapsedSeconds] = useState(0);
20
+
21
+ // Get the running operation
22
+ const runningOp = operations.find((op) => op.status === 'running');
23
+ const operationType = runningOp?.type as OperationType | undefined;
24
+ const startTime = runningOp?.metadata?.startTime;
25
+
26
+ // Track elapsed time, reset when operation type changes
27
+ useEffect(() => {
28
+ if (!startTime) {
29
+ setElapsedSeconds(0);
30
+ return;
31
+ }
32
+
33
+ const updateElapsed = () => {
34
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
35
+ setElapsedSeconds(elapsed);
36
+ };
37
+
38
+ updateElapsed();
39
+ const interval = setInterval(updateElapsed, 1000);
40
+
41
+ return () => clearInterval(interval);
42
+ }, [startTime, operationType]);
43
+
44
+ // Get localized label based on operation type
45
+ const operationLabel = operationType
46
+ ? (t(`operation.${operationType}` as any) as string)
47
+ : undefined;
48
+
49
+ const showElapsedTime = elapsedSeconds >= ELAPSED_TIME_THRESHOLD / 1000;
50
+
51
+ return (
52
+ <Flexbox align={'center'} horizontal>
53
+ <BubblesLoading />
54
+ {operationLabel && (
55
+ <Text type={'secondary'}>
56
+ {operationLabel}...
57
+ {showElapsedTime && ` (${elapsedSeconds}s)`}
58
+ </Text>
59
+ )}
60
+ </Flexbox>
61
+ );
62
+ });
63
+
64
+ export default ContentLoading;
@@ -2,17 +2,18 @@ import { deserializeParts } from '@lobechat/utils';
2
2
  import { type MarkdownProps } from '@lobehub/ui';
3
3
  import { memo } from 'react';
4
4
 
5
- import BubblesLoading from '@/components/BubblesLoading';
6
5
  import { LOADING_FLAT } from '@/const/message';
7
6
  import MarkdownMessage from '@/features/Conversation/Markdown';
8
7
 
9
8
  import { normalizeThinkTags, processWithArtifact } from '../../utils/markdown';
9
+ import ContentLoading from './ContentLoading';
10
10
  import { RichContentRenderer } from './RichContentRenderer';
11
11
 
12
12
  const DisplayContent = memo<{
13
13
  addIdOnDOM?: boolean;
14
14
  content: string;
15
15
  hasImages?: boolean;
16
+ id: string;
16
17
  isMultimodal?: boolean;
17
18
  isToolCallGenerating?: boolean;
18
19
  markdownProps?: Omit<MarkdownProps, 'className' | 'style' | 'children'>;
@@ -25,11 +26,12 @@ const DisplayContent = memo<{
25
26
  hasImages,
26
27
  isMultimodal,
27
28
  tempDisplayContent,
29
+ id,
28
30
  }) => {
29
31
  const message = normalizeThinkTags(processWithArtifact(content));
30
32
  if (isToolCallGenerating) return;
31
33
 
32
- if ((!content && !hasImages) || content === LOADING_FLAT) return <BubblesLoading />;
34
+ if ((!content && !hasImages) || content === LOADING_FLAT) return <ContentLoading id={id} />;
33
35
 
34
36
  const contentParts = isMultimodal ? deserializeParts(tempDisplayContent || content) : null;
35
37
 
@@ -7,7 +7,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
7
7
 
8
8
  import { useElectronStore } from '@/store/electron';
9
9
 
10
- import { getRouteMetadata } from '../helpers/routeMetadata';
10
+ import { getRouteMetadata } from './routeMetadata';
11
11
 
12
12
  /**
13
13
  * Hook to manage navigation history in Electron desktop app
@@ -10,7 +10,7 @@ import { systemStatusSelectors } from '@/store/global/selectors';
10
10
  import { electronStylish } from '@/styles/electron';
11
11
  import { isMacOS } from '@/utils/platform';
12
12
 
13
- import { useNavigationHistory } from '../hooks/useNavigationHistory';
13
+ import { useNavigationHistory } from '../navigation/useNavigationHistory';
14
14
  import RecentlyViewed from './RecentlyViewed';
15
15
 
16
16
  const isMac = isMacOS();
@@ -9,7 +9,7 @@ import { useNavigate } from 'react-router-dom';
9
9
  import { useElectronStore } from '@/store/electron';
10
10
  import type { HistoryEntry } from '@/store/electron/actions/navigationHistory';
11
11
 
12
- import { getRouteIcon } from '../helpers/routeMetadata';
12
+ import { getRouteIcon } from '../navigation/routeMetadata';
13
13
 
14
14
  const styles = createStaticStyles(({ css, cssVar }) => ({
15
15
  container: css`
@@ -1,18 +1,19 @@
1
1
  import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
2
+ import { useWatchBroadcast } from '@lobechat/electron-client-ipc';
2
3
  import { Flexbox } from '@lobehub/ui';
3
4
  import { Divider } from 'antd';
4
- import { memo, useMemo } from 'react';
5
+ import { memo, useMemo, useRef } from 'react';
5
6
 
6
7
  import { useElectronStore } from '@/store/electron';
7
8
  import { electronStylish } from '@/styles/electron';
8
9
  import { isMacOS } from '@/utils/platform';
9
10
 
10
- import Connection from './Connection';
11
+ import Connection from '../connection/Connection';
12
+ import { useWatchThemeUpdate } from '../system/useWatchThemeUpdate';
13
+ import { useUpdateModal } from '../updater/UpdateModal';
14
+ import { UpdateNotification } from '../updater/UpdateNotification';
11
15
  import NavigationBar from './NavigationBar';
12
- import { UpdateModal } from './UpdateModal';
13
- import { UpdateNotification } from './UpdateNotification';
14
16
  import WinControl from './WinControl';
15
- import { useWatchThemeUpdate } from './hooks/useWatchThemeUpdate';
16
17
 
17
18
  const isMac = isMacOS();
18
19
 
@@ -25,6 +26,19 @@ const TitleBar = memo(() => {
25
26
  initElectronAppState();
26
27
  useWatchThemeUpdate();
27
28
 
29
+ const { open: openUpdateModal } = useUpdateModal();
30
+ const updateModalOpenRef = useRef(false);
31
+
32
+ useWatchBroadcast('manualUpdateCheckStart', () => {
33
+ if (updateModalOpenRef.current) return;
34
+ updateModalOpenRef.current = true;
35
+ openUpdateModal({
36
+ onAfterClose: () => {
37
+ updateModalOpenRef.current = false;
38
+ },
39
+ });
40
+ });
41
+
28
42
  const showWinControl = isAppStateInit && !isMac;
29
43
 
30
44
  const padding = useMemo(() => {
@@ -59,12 +73,8 @@ const TitleBar = memo(() => {
59
73
  </>
60
74
  )}
61
75
  </Flexbox>
62
- <UpdateModal />
63
76
  </Flexbox>
64
77
  );
65
78
  });
66
79
 
67
80
  export default TitleBar;
68
-
69
- export { default as SimpleTitleBar } from './SimpleTitleBar';
70
- export { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
@@ -0,0 +1,5 @@
1
+ const WinControl = () => {
2
+ return <div style={{ width: 132 }} />;
3
+ };
4
+
5
+ export default WinControl;
@@ -0,0 +1,299 @@
1
+ import {
2
+ type ProgressInfo,
3
+ type UpdateInfo,
4
+ useWatchBroadcast,
5
+ } from '@lobechat/electron-client-ipc';
6
+ import { Button, Flexbox, type ModalInstance, createModal } from '@lobehub/ui';
7
+ import { App, Progress, Spin } from 'antd';
8
+ import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+
11
+ import { autoUpdateService } from '@/services/electron/autoUpdate';
12
+ import { formatSpeed } from '@/utils/format';
13
+
14
+ type UpdateStage = 'checking' | 'available' | 'latest' | 'downloading' | 'downloaded';
15
+
16
+ interface ModalUpdateOptions {
17
+ closable?: boolean;
18
+ keyboard?: boolean;
19
+ maskClosable?: boolean;
20
+ title?: React.ReactNode;
21
+ }
22
+
23
+ interface UpdateModalContentProps {
24
+ onClose: () => void;
25
+ setModalProps: (props: ModalUpdateOptions) => void;
26
+ }
27
+
28
+ const UpdateModalContent = memo<UpdateModalContentProps>(({ onClose, setModalProps }) => {
29
+ const { t } = useTranslation(['electron', 'common']);
30
+ const { modal } = App.useApp();
31
+ const errorHandledRef = useRef(false);
32
+ const isClosingRef = useRef(false);
33
+
34
+ const [stage, setStage] = useState<UpdateStage>('checking');
35
+ const [updateAvailableInfo, setUpdateAvailableInfo] = useState<UpdateInfo | null>(null);
36
+ const [downloadedInfo, setDownloadedInfo] = useState<UpdateInfo | null>(null);
37
+ const [progress, setProgress] = useState<ProgressInfo | null>(null);
38
+ const [latestVersionInfo, setLatestVersionInfo] = useState<UpdateInfo | null>(null);
39
+
40
+ useEffect(() => {
41
+ const isDownloading = stage === 'downloading';
42
+ const modalTitle = (() => {
43
+ switch (stage) {
44
+ case 'checking': {
45
+ return t('updater.checkingUpdate');
46
+ }
47
+ case 'available': {
48
+ return t('updater.newVersionAvailable');
49
+ }
50
+ case 'downloading': {
51
+ return t('updater.downloadingUpdate');
52
+ }
53
+ case 'downloaded': {
54
+ return t('updater.updateReady');
55
+ }
56
+ case 'latest': {
57
+ return t('updater.isLatestVersion');
58
+ }
59
+ default: {
60
+ return '';
61
+ }
62
+ }
63
+ })();
64
+
65
+ setModalProps({
66
+ closable: !isDownloading,
67
+ keyboard: !isDownloading,
68
+ maskClosable: !isDownloading,
69
+ title: modalTitle,
70
+ });
71
+ }, [setModalProps, stage, t]);
72
+
73
+ useWatchBroadcast('manualUpdateAvailable', (info: UpdateInfo) => {
74
+ if (isClosingRef.current) return;
75
+ setStage('available');
76
+ setUpdateAvailableInfo(info);
77
+ setDownloadedInfo(null);
78
+ setLatestVersionInfo(null);
79
+ });
80
+
81
+ useWatchBroadcast('manualUpdateNotAvailable', (info: UpdateInfo) => {
82
+ if (isClosingRef.current) return;
83
+ setStage('latest');
84
+ setLatestVersionInfo(info);
85
+ setUpdateAvailableInfo(null);
86
+ setDownloadedInfo(null);
87
+ setProgress(null);
88
+ });
89
+
90
+ useWatchBroadcast('updateDownloadStart', () => {
91
+ if (isClosingRef.current) return;
92
+ setStage('downloading');
93
+ setProgress({ bytesPerSecond: 0, percent: 0, total: 0, transferred: 0 });
94
+ setUpdateAvailableInfo(null);
95
+ setLatestVersionInfo(null);
96
+ });
97
+
98
+ useWatchBroadcast('updateDownloadProgress', (progressInfo: ProgressInfo) => {
99
+ if (isClosingRef.current) return;
100
+ setProgress(progressInfo);
101
+ });
102
+
103
+ useWatchBroadcast('updateDownloaded', (info: UpdateInfo) => {
104
+ if (isClosingRef.current) return;
105
+ setStage('downloaded');
106
+ setDownloadedInfo(info);
107
+ setProgress(null);
108
+ setUpdateAvailableInfo(null);
109
+ setLatestVersionInfo(null);
110
+ });
111
+
112
+ useWatchBroadcast('updateError', (message: string) => {
113
+ if (isClosingRef.current || errorHandledRef.current) return;
114
+ errorHandledRef.current = true;
115
+ isClosingRef.current = true;
116
+ onClose();
117
+ modal.error({ content: message, title: t('updater.updateError') });
118
+ });
119
+
120
+ const closeModal = () => {
121
+ if (isClosingRef.current) return;
122
+ errorHandledRef.current = true;
123
+ isClosingRef.current = true;
124
+ onClose();
125
+ };
126
+
127
+ const handleDownload = () => {
128
+ if (!updateAvailableInfo) return;
129
+ autoUpdateService.downloadUpdate();
130
+ };
131
+
132
+ const handleInstallNow = () => {
133
+ autoUpdateService.installNow();
134
+ closeModal();
135
+ };
136
+
137
+ const handleInstallLater = () => {
138
+ autoUpdateService.installLater();
139
+ closeModal();
140
+ };
141
+
142
+ const renderReleaseNotes = (notes?: UpdateInfo['releaseNotes']) => {
143
+ if (!notes) return null;
144
+ return (
145
+ <div
146
+ dangerouslySetInnerHTML={{ __html: notes as string }}
147
+ style={{
148
+ borderRadius: 4,
149
+ marginTop: 8,
150
+ maxHeight: 300,
151
+ overflow: 'auto',
152
+ padding: '8px 12px',
153
+ }}
154
+ />
155
+ );
156
+ };
157
+
158
+ const renderBody = () => {
159
+ switch (stage) {
160
+ case 'checking': {
161
+ return (
162
+ <Spin spinning>
163
+ <div style={{ padding: '20px', textAlign: 'center' }}>
164
+ {t('updater.checkingUpdateDesc')}
165
+ </div>
166
+ </Spin>
167
+ );
168
+ }
169
+ case 'available': {
170
+ return (
171
+ <>
172
+ <h4>
173
+ {t('updater.newVersionAvailableDesc', { version: updateAvailableInfo?.version })}
174
+ </h4>
175
+ {renderReleaseNotes(updateAvailableInfo?.releaseNotes)}
176
+ </>
177
+ );
178
+ }
179
+ case 'downloading': {
180
+ const percent = progress ? Math.round(progress.percent) : 0;
181
+ return (
182
+ <div style={{ padding: '20px 0' }}>
183
+ <Progress percent={percent} status="active" />
184
+ <div style={{ fontSize: 12, marginTop: 8, textAlign: 'center' }}>
185
+ {t('updater.downloadingUpdateDesc', { percent })}
186
+ {progress && progress.bytesPerSecond > 0 && (
187
+ <span>{formatSpeed(progress.bytesPerSecond)}</span>
188
+ )}
189
+ </div>
190
+ </div>
191
+ );
192
+ }
193
+ case 'downloaded': {
194
+ return (
195
+ <>
196
+ <h4>{t('updater.updateReadyDesc', { version: downloadedInfo?.version })}</h4>
197
+ {renderReleaseNotes(downloadedInfo?.releaseNotes)}
198
+ </>
199
+ );
200
+ }
201
+ case 'latest': {
202
+ return <p>{t('updater.isLatestVersionDesc', { version: latestVersionInfo?.version })}</p>;
203
+ }
204
+ default: {
205
+ return null;
206
+ }
207
+ }
208
+ };
209
+
210
+ const renderActions = () => {
211
+ if (stage === 'downloading') return null;
212
+
213
+ let actions: React.ReactNode[] = [];
214
+
215
+ if (stage === 'checking') {
216
+ actions = [
217
+ <Button key="cancel" onClick={closeModal}>
218
+ {t('cancel', { ns: 'common' })}
219
+ </Button>,
220
+ ];
221
+ }
222
+
223
+ if (stage === 'available') {
224
+ actions = [
225
+ <Button key="cancel" onClick={closeModal}>
226
+ {t('cancel', { ns: 'common' })}
227
+ </Button>,
228
+ <Button key="download" onClick={handleDownload} type="primary">
229
+ {t('updater.downloadNewVersion')}
230
+ </Button>,
231
+ ];
232
+ }
233
+
234
+ if (stage === 'downloaded') {
235
+ actions = [
236
+ <Button key="later" onClick={handleInstallLater}>
237
+ {t('updater.installLater')}
238
+ </Button>,
239
+ <Button key="now" onClick={handleInstallNow} type="primary">
240
+ {t('updater.restartAndInstall')}
241
+ </Button>,
242
+ ];
243
+ }
244
+
245
+ if (stage === 'latest') {
246
+ actions = [
247
+ <Button key="ok" onClick={closeModal} type="primary">
248
+ {t('ok', { ns: 'common' })}
249
+ </Button>,
250
+ ];
251
+ }
252
+
253
+ if (actions.length === 0) return null;
254
+
255
+ return (
256
+ <Flexbox gap={8} horizontal justify="end">
257
+ {actions}
258
+ </Flexbox>
259
+ );
260
+ };
261
+
262
+ return (
263
+ <Flexbox gap={16} style={{ padding: 16 }}>
264
+ <div>{renderBody()}</div>
265
+ {renderActions()}
266
+ </Flexbox>
267
+ );
268
+ });
269
+
270
+ UpdateModalContent.displayName = 'UpdateModalContent';
271
+
272
+ interface UpdateModalOpenProps {
273
+ onAfterClose?: () => void;
274
+ }
275
+
276
+ export const useUpdateModal = () => {
277
+ const instanceRef = useRef<ModalInstance | null>(null);
278
+
279
+ const open = useCallback((props?: UpdateModalOpenProps) => {
280
+ const setModalProps = (nextProps: ModalUpdateOptions) => {
281
+ instanceRef.current?.update?.(nextProps);
282
+ };
283
+
284
+ const handleClose = () => {
285
+ instanceRef.current?.close();
286
+ };
287
+
288
+ instanceRef.current = createModal({
289
+ afterClose: props?.onAfterClose,
290
+ children: <UpdateModalContent onClose={handleClose} setModalProps={setModalProps} />,
291
+ footer: null,
292
+ keyboard: true,
293
+ maskClosable: true,
294
+ title: '',
295
+ });
296
+ }, []);
297
+
298
+ return { open };
299
+ };
@@ -0,0 +1,24 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { useAddFilesToKnowledgeBaseModal } from './index';
5
+
6
+ const mockCreateModal = vi.hoisted(() => vi.fn());
7
+
8
+ vi.mock('@lobehub/ui', () => ({
9
+ Flexbox: () => null,
10
+ Icon: () => null,
11
+ createModal: mockCreateModal,
12
+ useModalContext: () => ({ close: vi.fn() }),
13
+ }));
14
+
15
+ describe('useAddFilesToKnowledgeBaseModal', () => {
16
+ it('should forward onClose to createModal afterClose', () => {
17
+ const onClose = vi.fn();
18
+ const { result } = renderHook(() => useAddFilesToKnowledgeBaseModal());
19
+
20
+ result.current.open({ fileIds: ['file-1'], onClose });
21
+
22
+ expect(mockCreateModal).toHaveBeenCalledWith(expect.objectContaining({ afterClose: onClose }));
23
+ });
24
+ });