@lobehub/chat 1.81.3 → 1.81.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.github/workflows/release.yml +5 -0
  3. package/.github/workflows/test.yml +5 -0
  4. package/CHANGELOG.md +58 -0
  5. package/changelog/v1.json +21 -0
  6. package/locales/ar/common.json +2 -0
  7. package/locales/ar/electron.json +32 -0
  8. package/locales/ar/models.json +129 -3
  9. package/locales/ar/plugin.json +1 -0
  10. package/locales/ar/tool.json +25 -0
  11. package/locales/bg-BG/common.json +2 -0
  12. package/locales/bg-BG/electron.json +32 -0
  13. package/locales/bg-BG/models.json +129 -3
  14. package/locales/bg-BG/plugin.json +1 -0
  15. package/locales/bg-BG/tool.json +25 -0
  16. package/locales/de-DE/common.json +2 -0
  17. package/locales/de-DE/electron.json +32 -0
  18. package/locales/de-DE/models.json +129 -3
  19. package/locales/de-DE/plugin.json +1 -0
  20. package/locales/de-DE/tool.json +25 -0
  21. package/locales/en-US/common.json +2 -0
  22. package/locales/en-US/electron.json +32 -0
  23. package/locales/en-US/models.json +129 -3
  24. package/locales/en-US/plugin.json +1 -0
  25. package/locales/en-US/tool.json +25 -0
  26. package/locales/es-ES/common.json +2 -0
  27. package/locales/es-ES/electron.json +32 -0
  28. package/locales/es-ES/models.json +129 -3
  29. package/locales/es-ES/plugin.json +1 -0
  30. package/locales/es-ES/tool.json +25 -0
  31. package/locales/fa-IR/common.json +2 -0
  32. package/locales/fa-IR/electron.json +32 -0
  33. package/locales/fa-IR/models.json +129 -3
  34. package/locales/fa-IR/plugin.json +1 -0
  35. package/locales/fa-IR/tool.json +25 -0
  36. package/locales/fr-FR/common.json +2 -0
  37. package/locales/fr-FR/electron.json +32 -0
  38. package/locales/fr-FR/models.json +129 -3
  39. package/locales/fr-FR/plugin.json +1 -0
  40. package/locales/fr-FR/tool.json +25 -0
  41. package/locales/it-IT/common.json +2 -0
  42. package/locales/it-IT/electron.json +32 -0
  43. package/locales/it-IT/models.json +129 -3
  44. package/locales/it-IT/plugin.json +1 -0
  45. package/locales/it-IT/tool.json +25 -0
  46. package/locales/ja-JP/common.json +2 -0
  47. package/locales/ja-JP/electron.json +32 -0
  48. package/locales/ja-JP/models.json +129 -3
  49. package/locales/ja-JP/plugin.json +1 -0
  50. package/locales/ja-JP/tool.json +25 -0
  51. package/locales/ko-KR/common.json +2 -0
  52. package/locales/ko-KR/electron.json +32 -0
  53. package/locales/ko-KR/models.json +129 -3
  54. package/locales/ko-KR/plugin.json +1 -0
  55. package/locales/ko-KR/tool.json +25 -0
  56. package/locales/nl-NL/common.json +2 -0
  57. package/locales/nl-NL/electron.json +32 -0
  58. package/locales/nl-NL/models.json +129 -3
  59. package/locales/nl-NL/plugin.json +1 -0
  60. package/locales/nl-NL/tool.json +25 -0
  61. package/locales/pl-PL/common.json +2 -0
  62. package/locales/pl-PL/electron.json +32 -0
  63. package/locales/pl-PL/models.json +129 -3
  64. package/locales/pl-PL/plugin.json +1 -0
  65. package/locales/pl-PL/tool.json +25 -0
  66. package/locales/pt-BR/common.json +2 -0
  67. package/locales/pt-BR/electron.json +32 -0
  68. package/locales/pt-BR/models.json +129 -3
  69. package/locales/pt-BR/plugin.json +1 -0
  70. package/locales/pt-BR/tool.json +25 -0
  71. package/locales/ru-RU/common.json +2 -0
  72. package/locales/ru-RU/electron.json +32 -0
  73. package/locales/ru-RU/models.json +129 -3
  74. package/locales/ru-RU/plugin.json +1 -0
  75. package/locales/ru-RU/tool.json +25 -0
  76. package/locales/tr-TR/common.json +2 -0
  77. package/locales/tr-TR/electron.json +32 -0
  78. package/locales/tr-TR/models.json +129 -3
  79. package/locales/tr-TR/plugin.json +1 -0
  80. package/locales/tr-TR/tool.json +25 -0
  81. package/locales/vi-VN/common.json +2 -0
  82. package/locales/vi-VN/electron.json +32 -0
  83. package/locales/vi-VN/models.json +129 -3
  84. package/locales/vi-VN/plugin.json +1 -0
  85. package/locales/vi-VN/tool.json +25 -0
  86. package/locales/zh-CN/common.json +2 -0
  87. package/locales/zh-CN/electron.json +32 -0
  88. package/locales/zh-CN/models.json +134 -8
  89. package/locales/zh-CN/plugin.json +1 -0
  90. package/locales/zh-CN/tool.json +25 -0
  91. package/locales/zh-TW/common.json +2 -0
  92. package/locales/zh-TW/electron.json +32 -0
  93. package/locales/zh-TW/models.json +129 -3
  94. package/locales/zh-TW/plugin.json +1 -0
  95. package/locales/zh-TW/tool.json +25 -0
  96. package/package.json +4 -3
  97. package/packages/electron-client-ipc/src/events/index.ts +5 -5
  98. package/packages/electron-client-ipc/src/events/localFile.ts +22 -0
  99. package/packages/electron-client-ipc/src/events/{file.ts → upload.ts} +1 -1
  100. package/packages/electron-client-ipc/src/types/index.ts +2 -1
  101. package/packages/electron-client-ipc/src/types/localFile.ts +52 -0
  102. package/packages/file-loaders/README.md +63 -0
  103. package/packages/file-loaders/package.json +42 -0
  104. package/packages/file-loaders/src/index.ts +2 -0
  105. package/packages/file-loaders/src/loadFile.ts +206 -0
  106. package/packages/file-loaders/src/loaders/docx/__snapshots__/index.test.ts.snap +74 -0
  107. package/packages/file-loaders/src/loaders/docx/fixtures/test.docx +0 -0
  108. package/packages/file-loaders/src/loaders/docx/index.test.ts +41 -0
  109. package/packages/file-loaders/src/loaders/docx/index.ts +73 -0
  110. package/packages/file-loaders/src/loaders/excel/__snapshots__/index.test.ts.snap +58 -0
  111. package/packages/file-loaders/src/loaders/excel/fixtures/test.xlsx +0 -0
  112. package/packages/file-loaders/src/loaders/excel/index.test.ts +47 -0
  113. package/packages/file-loaders/src/loaders/excel/index.ts +121 -0
  114. package/packages/file-loaders/src/loaders/index.ts +19 -0
  115. package/packages/file-loaders/src/loaders/pdf/__snapshots__/index.test.ts.snap +98 -0
  116. package/packages/file-loaders/src/loaders/pdf/index.test.ts +49 -0
  117. package/packages/file-loaders/src/loaders/pdf/index.ts +133 -0
  118. package/packages/file-loaders/src/loaders/pptx/__snapshots__/index.test.ts.snap +40 -0
  119. package/packages/file-loaders/src/loaders/pptx/fixtures/test.pptx +0 -0
  120. package/packages/file-loaders/src/loaders/pptx/index.test.ts +47 -0
  121. package/packages/file-loaders/src/loaders/pptx/index.ts +186 -0
  122. package/packages/file-loaders/src/loaders/text/__snapshots__/index.test.ts.snap +15 -0
  123. package/packages/file-loaders/src/loaders/text/fixtures/test.txt +2 -0
  124. package/packages/file-loaders/src/loaders/text/index.test.ts +38 -0
  125. package/packages/file-loaders/src/loaders/text/index.ts +53 -0
  126. package/packages/file-loaders/src/types.ts +200 -0
  127. package/packages/file-loaders/src/utils/isTextReadableFile.ts +68 -0
  128. package/packages/file-loaders/src/utils/parser-utils.ts +112 -0
  129. package/packages/file-loaders/test/__snapshots__/loaders.test.ts.snap +93 -0
  130. package/packages/file-loaders/test/fixtures/test.csv +4 -0
  131. package/packages/file-loaders/test/fixtures/test.docx +0 -0
  132. package/packages/file-loaders/test/fixtures/test.epub +0 -0
  133. package/packages/file-loaders/test/fixtures/test.md +3 -0
  134. package/packages/file-loaders/test/fixtures/test.pptx +0 -0
  135. package/packages/file-loaders/test/fixtures/test.txt +3 -0
  136. package/packages/file-loaders/test/loaders.test.ts +39 -0
  137. package/scripts/prebuild.mts +5 -1
  138. package/src/app/(backend)/trpc/desktop/[trpc]/route.ts +26 -0
  139. package/src/features/Conversation/Messages/Assistant/Tool/Render/Arguments/ObjectEntity.tsx +81 -0
  140. package/src/features/Conversation/Messages/Assistant/Tool/Render/Arguments/ValueCell.tsx +43 -0
  141. package/src/features/Conversation/Messages/Assistant/Tool/Render/Arguments/index.tsx +120 -0
  142. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +75 -2
  143. package/src/features/Conversation/Messages/Assistant/Tool/Render/KeyValueEditor.tsx +214 -0
  144. package/src/features/User/UserPanel/useMenu.tsx +8 -1
  145. package/src/libs/agent-runtime/google/index.ts +3 -0
  146. package/src/libs/trpc/client/desktop.ts +14 -0
  147. package/src/locales/default/common.ts +2 -0
  148. package/src/locales/default/electron.ts +34 -0
  149. package/src/locales/default/index.ts +2 -0
  150. package/src/locales/default/tool.ts +25 -0
  151. package/src/server/routers/desktop/index.ts +9 -0
  152. package/src/server/routers/desktop/pgTable.ts +43 -0
  153. package/src/services/electron/autoUpdate.ts +17 -0
  154. package/src/services/electron/file.ts +31 -0
  155. package/src/services/electron/localFileService.ts +39 -0
  156. package/src/services/electron/remoteServer.ts +40 -0
  157. package/src/store/chat/index.ts +1 -1
  158. package/src/store/chat/slices/builtinTool/actions/index.ts +3 -1
  159. package/src/store/chat/slices/builtinTool/actions/localFile.ts +129 -0
  160. package/src/store/chat/slices/builtinTool/initialState.ts +2 -0
  161. package/src/store/chat/slices/builtinTool/selectors.ts +2 -0
  162. package/src/store/chat/slices/plugin/action.ts +3 -3
  163. package/src/store/chat/store.ts +2 -0
  164. package/src/store/electron/actions/sync.ts +117 -0
  165. package/src/store/electron/index.ts +1 -0
  166. package/src/store/electron/initialState.ts +18 -0
  167. package/src/store/electron/selectors/index.ts +1 -0
  168. package/src/store/electron/selectors/sync.ts +9 -0
  169. package/src/store/electron/store.ts +29 -0
  170. package/src/tools/index.ts +8 -0
  171. package/src/tools/local-files/Render/ListFiles/Result.tsx +42 -0
  172. package/src/tools/local-files/Render/ListFiles/index.tsx +68 -0
  173. package/src/tools/local-files/Render/ReadLocalFile/ReadFileSkeleton.tsx +50 -0
  174. package/src/tools/local-files/Render/ReadLocalFile/ReadFileView.tsx +197 -0
  175. package/src/tools/local-files/Render/ReadLocalFile/index.tsx +31 -0
  176. package/src/tools/local-files/Render/ReadLocalFile/style.ts +37 -0
  177. package/src/tools/local-files/Render/SearchFiles/Result.tsx +42 -0
  178. package/src/tools/local-files/Render/SearchFiles/SearchQuery/SearchView.tsx +77 -0
  179. package/src/tools/local-files/Render/SearchFiles/SearchQuery/index.tsx +72 -0
  180. package/src/tools/local-files/Render/SearchFiles/index.tsx +32 -0
  181. package/src/tools/local-files/Render/index.tsx +36 -0
  182. package/src/tools/local-files/components/FileItem.tsx +117 -0
  183. package/src/tools/local-files/index.ts +149 -0
  184. package/src/tools/local-files/systemRole.ts +46 -0
  185. package/src/tools/local-files/type.ts +33 -0
  186. package/src/tools/renders.ts +3 -0
  187. package/packages/electron-client-ipc/src/events/search.ts +0 -4
  188. package/src/features/Conversation/Messages/Assistant/Tool/Render/Arguments.tsx +0 -165
  189. /package/packages/electron-client-ipc/src/types/{file.ts → upload.ts} +0 -0
@@ -0,0 +1,50 @@
1
+ import { Skeleton } from 'antd';
2
+ import { createStyles } from 'antd-style';
3
+ import React, { memo } from 'react';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+ const useStyles = createStyles(({ css, token }) => ({
7
+ container: css`
8
+ padding: 8px;
9
+ border: 1px solid ${token.colorBorderSecondary};
10
+ border-radius: ${token.borderRadiusLG}px;
11
+ `,
12
+ header: css`
13
+ margin-block-end: 4px;
14
+ `,
15
+ meta: css`
16
+ font-size: 12px;
17
+ `,
18
+ path: css`
19
+ margin-block-start: 4px;
20
+ `,
21
+ }));
22
+
23
+ const ReadFileSkeleton = memo(() => {
24
+ const { styles } = useStyles();
25
+
26
+ return (
27
+ <Flexbox className={styles.container}>
28
+ <Flexbox
29
+ align={'center'}
30
+ className={styles.header}
31
+ gap={24}
32
+ horizontal
33
+ justify={'space-between'}
34
+ >
35
+ <Flexbox align={'center'} flex={1} gap={8} horizontal style={{ overflow: 'hidden' }}>
36
+ <Skeleton.Avatar active shape="square" size={24} style={{ borderRadius: 4 }} />
37
+ <Skeleton.Input active size="small" style={{ flex: 1, minWidth: 100 }} />
38
+ </Flexbox>
39
+ <Flexbox align={'center'} className={styles.meta} gap={16}>
40
+ <Skeleton.Input active size="small" style={{ maxWidth: 40 }} />
41
+ </Flexbox>
42
+ </Flexbox>
43
+
44
+ {/* Path */}
45
+ <Skeleton.Input active block className={styles.path} size="small" />
46
+ </Flexbox>
47
+ );
48
+ });
49
+
50
+ export default ReadFileSkeleton;
@@ -0,0 +1,197 @@
1
+ import { LocalReadFileResult } from '@lobechat/electron-client-ipc';
2
+ import { ActionIcon, Icon, Markdown } from '@lobehub/ui';
3
+ import { Typography } from 'antd';
4
+ import { createStyles } from 'antd-style';
5
+ import { AlignLeft, Asterisk, ChevronDownIcon, ExternalLink, FolderOpen } from 'lucide-react';
6
+ import React, { memo, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Flexbox } from 'react-layout-kit';
9
+
10
+ import FileIcon from '@/components/FileIcon';
11
+ import { localFileService } from '@/services/electron/localFileService';
12
+
13
+ const useStyles = createStyles(({ css, token, cx }) => ({
14
+ actions: cx(
15
+ 'local-file-actions',
16
+ css`
17
+ cursor: pointer;
18
+ color: ${token.colorTextTertiary};
19
+ opacity: 0;
20
+ transition: opacity 0.2s ${token.motionEaseInOut};
21
+ `,
22
+ ),
23
+ container: css`
24
+ padding: 8px;
25
+ border: 1px solid ${token.colorBorderSecondary};
26
+ border-radius: ${token.borderRadiusLG}px;
27
+ transition: all 0.2s ${token.motionEaseInOut};
28
+
29
+ &:hover {
30
+ border-color: ${token.colorBorder};
31
+
32
+ .local-file-actions {
33
+ opacity: 1;
34
+ }
35
+ }
36
+ `,
37
+ fileName: css`
38
+ flex: 1;
39
+ margin-inline-start: 8px;
40
+ color: ${token.colorTextSecondary};
41
+
42
+ &:hover {
43
+ color: ${token.colorText};
44
+ }
45
+ `,
46
+ header: css`
47
+ cursor: pointer;
48
+ `,
49
+ meta: css`
50
+ font-size: 12px;
51
+ color: ${token.colorTextTertiary};
52
+ `,
53
+ path: css`
54
+ margin-block-start: 4px;
55
+ padding-inline: 4px;
56
+
57
+ font-size: 12px;
58
+ color: ${token.colorTextSecondary};
59
+ word-break: break-all;
60
+ `,
61
+ previewBox: css`
62
+ position: relative;
63
+
64
+ overflow: hidden;
65
+
66
+ margin-block-start: 8px;
67
+ padding: 8px;
68
+ border-radius: 8px;
69
+
70
+ background: ${token.colorFillQuaternary};
71
+ `,
72
+ previewText: css`
73
+ font-family: ${token.fontFamilyCode};
74
+ font-size: 12px;
75
+ line-height: 1.6;
76
+ word-break: break-all;
77
+ white-space: pre-wrap;
78
+ `,
79
+ }));
80
+
81
+ // Assuming the result object might include the original path and an optional warning
82
+ interface ReadFileViewProps extends LocalReadFileResult {
83
+ path: string; // The full path requested
84
+ }
85
+
86
+ const ReadFileView = memo<ReadFileViewProps>(
87
+ ({
88
+ filename,
89
+ path,
90
+ fileType,
91
+ charCount,
92
+ lineCount, // Assuming the 250 is total lines?
93
+ content, // The actual content preview
94
+ }) => {
95
+ const { t } = useTranslation('tool');
96
+ const { styles } = useStyles();
97
+ const [isExpanded, setIsExpanded] = useState(false);
98
+
99
+ const handleToggleExpand = () => {
100
+ setIsExpanded(!isExpanded);
101
+ };
102
+
103
+ const handleOpenFile = (e: React.MouseEvent) => {
104
+ e.stopPropagation();
105
+ localFileService.openLocalFile({ path });
106
+ };
107
+
108
+ const handleOpenFolder = (e: React.MouseEvent) => {
109
+ e.stopPropagation();
110
+ localFileService.openLocalFolder({ isDirectory: false, path });
111
+ };
112
+
113
+ return (
114
+ <Flexbox className={styles.container}>
115
+ <Flexbox
116
+ align={'center'}
117
+ className={styles.header}
118
+ horizontal
119
+ justify={'space-between'}
120
+ onClick={handleToggleExpand}
121
+ >
122
+ <Flexbox align={'center'} flex={1} gap={0} horizontal style={{ overflow: 'hidden' }}>
123
+ <FileIcon fileName={filename} fileType={fileType} size={24} variant={'pure'} />
124
+ <Flexbox horizontal>
125
+ <Typography.Text className={styles.fileName} ellipsis>
126
+ {filename}
127
+ </Typography.Text>
128
+ {/* Actions on Hover */}
129
+ <Flexbox className={styles.actions} gap={8} horizontal style={{ marginLeft: 8 }}>
130
+ <ActionIcon
131
+ icon={ExternalLink}
132
+ onClick={handleOpenFile}
133
+ size="small"
134
+ title={t('localFiles.openFile')}
135
+ />
136
+ <ActionIcon
137
+ icon={FolderOpen}
138
+ onClick={handleOpenFolder}
139
+ size="small"
140
+ title={t('localFiles.openFolder')}
141
+ />
142
+ </Flexbox>
143
+ </Flexbox>
144
+ </Flexbox>
145
+ <Flexbox align={'center'} className={styles.meta} gap={8} horizontal>
146
+ <Flexbox align={'center'} gap={4} horizontal>
147
+ <Icon icon={Asterisk} size={'small'} />
148
+ <span>{charCount}</span>
149
+ </Flexbox>
150
+ <Flexbox align={'center'} gap={4} horizontal>
151
+ <Icon icon={AlignLeft} size={'small'} />
152
+ <span>
153
+ {content?.split('\n').length || 0} / {lineCount}
154
+ </span>
155
+ {/* Display preview lines / total lines */}
156
+ </Flexbox>
157
+ <ActionIcon
158
+ active={isExpanded}
159
+ icon={ChevronDownIcon}
160
+ onClick={handleToggleExpand}
161
+ size="small"
162
+ style={{
163
+ marginLeft: 8,
164
+ transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
165
+ transition: 'transform 0.2s',
166
+ }}
167
+ />
168
+ </Flexbox>
169
+ </Flexbox>
170
+
171
+ {/* Path */}
172
+ <Typography.Text className={styles.path} ellipsis type={'secondary'}>
173
+ {path}
174
+ </Typography.Text>
175
+
176
+ {/* Content Preview (Collapsible) */}
177
+ {isExpanded && (
178
+ <Flexbox className={styles.previewBox}>
179
+ {fileType === 'md' ? (
180
+ <Markdown style={{ maxHeight: 240, overflow: 'auto' }}>{content}</Markdown>
181
+ ) : (
182
+ <Typography.Paragraph
183
+ className={styles.previewText}
184
+ ellipsis={{ expandable: true, rows: 10, symbol: t('localFiles.read.more') }}
185
+ style={{ maxHeight: 240, overflow: 'auto' }}
186
+ >
187
+ {content}
188
+ </Typography.Paragraph>
189
+ )}
190
+ </Flexbox>
191
+ )}
192
+ </Flexbox>
193
+ );
194
+ },
195
+ );
196
+
197
+ export default ReadFileView;
@@ -0,0 +1,31 @@
1
+ import { LocalReadFileParams } from '@lobechat/electron-client-ipc';
2
+ import { memo } from 'react';
3
+
4
+ import { useChatStore } from '@/store/chat';
5
+ import { chatToolSelectors } from '@/store/chat/slices/builtinTool/selectors';
6
+ import { LocalReadFileState } from '@/tools/local-files/type';
7
+ import { ChatMessagePluginError } from '@/types/message';
8
+
9
+ import ReadFileSkeleton from './ReadFileSkeleton';
10
+ import ReadFileView from './ReadFileView';
11
+
12
+ interface ReadFileQueryProps {
13
+ args: LocalReadFileParams;
14
+ messageId: string;
15
+ pluginError: ChatMessagePluginError;
16
+ pluginState: LocalReadFileState;
17
+ }
18
+
19
+ const ReadFileQuery = memo<ReadFileQueryProps>(({ args, pluginState, messageId }) => {
20
+ const loading = useChatStore(chatToolSelectors.isSearchingLocalFiles(messageId));
21
+
22
+ if (loading) {
23
+ return <ReadFileSkeleton />;
24
+ }
25
+
26
+ if (!args?.path || !pluginState) return null;
27
+
28
+ return <ReadFileView {...pluginState.fileContent} path={args.path} />;
29
+ });
30
+
31
+ export default ReadFileQuery;
@@ -0,0 +1,37 @@
1
+ import { createStyles } from 'antd-style';
2
+
3
+ export const useStyles = createStyles(({ css, token }) => ({
4
+ container: css`
5
+ overflow: hidden;
6
+
7
+ max-width: 100%;
8
+ padding: 12px;
9
+ border: 1px solid ${token.colorBorderSecondary};
10
+ border-radius: ${token.borderRadius}px;
11
+ `,
12
+ fileName: css`
13
+ color: ${token.colorTextSecondary};
14
+ `,
15
+ meta: css`
16
+ font-size: 10px;
17
+ color: ${token.colorTextSecondary};
18
+ `,
19
+ metaItem: css`
20
+ white-space: nowrap;
21
+ `,
22
+ path: css`
23
+ font-size: 12px;
24
+ line-height: 1;
25
+ `,
26
+ previewBox: css`
27
+ padding-block: 8px;
28
+ padding-inline: 12px;
29
+ border-radius: ${token.borderRadiusSM}px;
30
+ background: ${token.colorFillTertiary};
31
+ `,
32
+ previewText: css`
33
+ font-family: ${token.fontFamilyCode};
34
+ font-size: 12px;
35
+ color: ${token.colorTextSecondary};
36
+ `,
37
+ }));
@@ -0,0 +1,42 @@
1
+ import { Skeleton } from 'antd';
2
+ import { memo } from 'react';
3
+ import { Flexbox } from 'react-layout-kit';
4
+
5
+ import { useChatStore } from '@/store/chat';
6
+ import { chatToolSelectors } from '@/store/chat/selectors';
7
+ import FileItem from '@/tools/local-files/components/FileItem';
8
+ import { FileResult } from '@/tools/local-files/type';
9
+ import { ChatMessagePluginError } from '@/types/message';
10
+
11
+ interface SearchFilesProps {
12
+ messageId: string;
13
+ pluginError: ChatMessagePluginError;
14
+ searchResults?: FileResult[];
15
+ }
16
+
17
+ const SearchFiles = memo<SearchFilesProps>(({ searchResults = [], messageId }) => {
18
+ const loading = useChatStore(chatToolSelectors.isSearchingLocalFiles(messageId));
19
+
20
+ if (loading) {
21
+ return (
22
+ <Flexbox gap={4}>
23
+ <Skeleton.Button active block style={{ height: 16 }} />
24
+ <Skeleton.Button active block style={{ height: 16 }} />
25
+ <Skeleton.Button active block style={{ height: 16 }} />
26
+ <Skeleton.Button active block style={{ height: 16 }} />
27
+ </Flexbox>
28
+ );
29
+ }
30
+
31
+ return (
32
+ <Flexbox gap={2} style={{ maxHeight: 260, overflow: 'scroll' }}>
33
+ {searchResults.map((item) => (
34
+ <FileItem key={item.path} {...item} />
35
+ ))}
36
+ </Flexbox>
37
+ );
38
+ });
39
+
40
+ SearchFiles.displayName = 'SearchFiles';
41
+
42
+ export default SearchFiles;
@@ -0,0 +1,77 @@
1
+ import { Icon } from '@lobehub/ui';
2
+ import { Skeleton } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import { SearchIcon } from 'lucide-react';
5
+ import { memo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { useIsMobile } from '@/hooks/useIsMobile';
10
+ import { shinyTextStylish } from '@/styles/loading';
11
+
12
+ const useStyles = createStyles(({ css, token }) => ({
13
+ font: css`
14
+ font-size: 12px;
15
+ color: ${token.colorTextTertiary};
16
+ `,
17
+ query: css`
18
+ cursor: pointer;
19
+
20
+ padding-block: 4px;
21
+ padding-inline: 8px;
22
+ border-radius: 8px;
23
+
24
+ font-size: 12px;
25
+ color: ${token.colorTextSecondary};
26
+
27
+ &:hover {
28
+ background: ${token.colorFillTertiary};
29
+ }
30
+ `,
31
+ shinyText: shinyTextStylish(token),
32
+ }));
33
+
34
+ interface SearchBarProps {
35
+ defaultQuery: string;
36
+ onEditingChange: (editing: boolean) => void;
37
+ resultsNumber: number;
38
+ searching?: boolean;
39
+ }
40
+
41
+ const SearchBar = memo<SearchBarProps>(
42
+ ({ defaultQuery, resultsNumber, onEditingChange, searching }) => {
43
+ const { t } = useTranslation('tool');
44
+ const isMobile = useIsMobile();
45
+ const { styles, cx } = useStyles();
46
+ return (
47
+ <Flexbox
48
+ align={isMobile ? 'flex-start' : 'center'}
49
+ distribution={'space-between'}
50
+ gap={isMobile ? 8 : 40}
51
+ height={isMobile ? undefined : 32}
52
+ horizontal={!isMobile}
53
+ >
54
+ <Flexbox
55
+ align={'center'}
56
+ className={cx(styles.query, searching && styles.shinyText)}
57
+ gap={8}
58
+ horizontal
59
+ onClick={() => {
60
+ onEditingChange(true);
61
+ }}
62
+ >
63
+ <Icon icon={SearchIcon} />
64
+ {defaultQuery}
65
+ </Flexbox>
66
+
67
+ <Flexbox align={'center'} horizontal>
68
+ <>
69
+ <div className={styles.font}>{t('search.searchResult')}</div>
70
+ {searching ? <Skeleton.Button active size={'small'} /> : resultsNumber}
71
+ </>
72
+ </Flexbox>
73
+ </Flexbox>
74
+ );
75
+ },
76
+ );
77
+ export default SearchBar;
@@ -0,0 +1,72 @@
1
+ import { LocalSearchFilesParams } from '@lobechat/electron-client-ipc';
2
+ import { ActionIcon, Icon } from '@lobehub/ui';
3
+ import { Button, Input, Space } from 'antd';
4
+ import { SearchIcon, XIcon } from 'lucide-react';
5
+ import { memo, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { useChatStore } from '@/store/chat';
10
+ import { chatToolSelectors } from '@/store/chat/selectors';
11
+ import { LocalFileSearchState } from '@/tools/local-files/type';
12
+
13
+ import SearchView from './SearchView';
14
+
15
+ interface SearchQueryViewProps {
16
+ args: LocalSearchFilesParams;
17
+
18
+ messageId: string;
19
+ pluginState?: LocalFileSearchState;
20
+ }
21
+
22
+ const SearchQueryView = memo<SearchQueryViewProps>(({ messageId, args, pluginState }) => {
23
+ const { t } = useTranslation('tool');
24
+ const loading = useChatStore(chatToolSelectors.isSearchingLocalFiles(messageId));
25
+ const reSearchLocalFiles = useChatStore((s) => s.reSearchLocalFiles);
26
+ const searchResults = pluginState?.searchResults || [];
27
+
28
+ const [editing, setEditing] = useState(false);
29
+ const [query, setQuery] = useState(args.keywords);
30
+
31
+ const updateAndSearch = async () => {
32
+ const data: LocalSearchFilesParams = { keywords: query };
33
+
34
+ await reSearchLocalFiles(messageId, data);
35
+ };
36
+
37
+ return editing ? (
38
+ <Flexbox align={'center'} flex={1} gap={8} height={32} horizontal>
39
+ <Space.Compact style={{ width: '100%' }}>
40
+ <Input
41
+ autoFocus
42
+ onChange={(e) => {
43
+ setQuery(e.target.value);
44
+ }}
45
+ onPressEnter={updateAndSearch}
46
+ placeholder={t('search.searchBar.placeholder')}
47
+ style={{ minWidth: 400 }}
48
+ value={query}
49
+ variant={'filled'}
50
+ />
51
+ <Button
52
+ icon={<Icon icon={SearchIcon} />}
53
+ loading={loading}
54
+ onClick={updateAndSearch}
55
+ type={'primary'}
56
+ >
57
+ {t('search.searchBar.button')}
58
+ </Button>
59
+ </Space.Compact>
60
+ <ActionIcon icon={XIcon} onClick={() => setEditing(false)} />
61
+ </Flexbox>
62
+ ) : (
63
+ <SearchView
64
+ defaultQuery={args?.keywords}
65
+ onEditingChange={setEditing}
66
+ resultsNumber={searchResults.length}
67
+ searching={loading || !pluginState}
68
+ />
69
+ );
70
+ });
71
+
72
+ export default SearchQueryView;
@@ -0,0 +1,32 @@
1
+ import { LocalSearchFilesParams } from '@lobechat/electron-client-ipc';
2
+ import { memo } from 'react';
3
+
4
+ import { LocalFileSearchState } from '@/tools/local-files/type';
5
+ import { ChatMessagePluginError } from '@/types/message';
6
+
7
+ import SearchResult from './Result';
8
+ import SearchQuery from './SearchQuery';
9
+
10
+ interface SearchFilesProps {
11
+ args: LocalSearchFilesParams;
12
+ messageId: string;
13
+ pluginError: ChatMessagePluginError;
14
+ pluginState?: LocalFileSearchState;
15
+ }
16
+
17
+ const SearchFiles = memo<SearchFilesProps>(({ messageId, pluginError, args, pluginState }) => {
18
+ return (
19
+ <>
20
+ <SearchQuery args={args} messageId={messageId} pluginState={pluginState} />
21
+ <SearchResult
22
+ messageId={messageId}
23
+ pluginError={pluginError}
24
+ searchResults={pluginState?.searchResults}
25
+ />
26
+ </>
27
+ );
28
+ });
29
+
30
+ SearchFiles.displayName = 'SearchFiles';
31
+
32
+ export default SearchFiles;
@@ -0,0 +1,36 @@
1
+ import { LocalFileItem } from '@lobechat/electron-client-ipc';
2
+ import { memo } from 'react';
3
+
4
+ import { LocalFilesApiName } from '@/tools/local-files';
5
+ import { BuiltinRenderProps } from '@/types/tool';
6
+
7
+ import ListFiles from './ListFiles';
8
+ import ReadLocalFile from './ReadLocalFile';
9
+ import SearchFiles from './SearchFiles';
10
+
11
+ const RenderMap = {
12
+ [LocalFilesApiName.searchLocalFiles]: SearchFiles,
13
+ [LocalFilesApiName.listLocalFiles]: ListFiles,
14
+ [LocalFilesApiName.readLocalFile]: ReadLocalFile,
15
+ };
16
+
17
+ const LocalFilesRender = memo<BuiltinRenderProps<LocalFileItem[]>>(
18
+ ({ pluginState, apiName, messageId, pluginError, args }) => {
19
+ const Render = RenderMap[apiName as any];
20
+
21
+ if (!Render) return;
22
+
23
+ return (
24
+ <Render
25
+ args={args}
26
+ messageId={messageId}
27
+ pluginError={pluginError}
28
+ pluginState={pluginState}
29
+ />
30
+ );
31
+ },
32
+ );
33
+
34
+ LocalFilesRender.displayName = 'LocalFilesRender';
35
+
36
+ export default LocalFilesRender;
@@ -0,0 +1,117 @@
1
+ import { LocalFileItem } from '@lobechat/electron-client-ipc';
2
+ import { ActionIcon, FileTypeIcon } from '@lobehub/ui';
3
+ import { createStyles } from 'antd-style';
4
+ import dayjs from 'dayjs';
5
+ import { FolderOpen } from 'lucide-react';
6
+ import React, { memo, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Flexbox } from 'react-layout-kit';
9
+
10
+ import FileIcon from '@/components/FileIcon';
11
+ import { localFileService } from '@/services/electron/localFileService';
12
+ import { formatSize } from '@/utils/format';
13
+
14
+ const useStyles = createStyles(({ css, token }) => ({
15
+ container: css`
16
+ border-radius: 4px;
17
+ color: ${token.colorTextSecondary};
18
+
19
+ :hover {
20
+ color: ${token.colorText};
21
+ background: ${token.colorFillTertiary};
22
+ }
23
+ `,
24
+ path: css`
25
+ overflow: hidden;
26
+
27
+ font-size: 10px;
28
+ line-height: 1;
29
+ color: ${token.colorTextDescription};
30
+ text-overflow: ellipsis;
31
+ white-space: nowrap;
32
+ `,
33
+ size: css`
34
+ min-width: 50px;
35
+
36
+ font-family: ${token.fontFamilyCode};
37
+ font-size: 10px;
38
+ color: ${token.colorTextTertiary};
39
+ text-align: end;
40
+ `,
41
+ title: css`
42
+ overflow: hidden;
43
+ display: block;
44
+
45
+ color: inherit;
46
+ text-overflow: ellipsis;
47
+ white-space: nowrap;
48
+ `,
49
+ }));
50
+
51
+ interface FileItemProps extends LocalFileItem {
52
+ showTime?: boolean;
53
+ }
54
+ const FileItem = memo<FileItemProps>(
55
+ ({ isDirectory, name, path, size, type, showTime = false, createdTime }) => {
56
+ const { t } = useTranslation('tool');
57
+ const { styles } = useStyles();
58
+ const [isHovering, setIsHovering] = useState(false);
59
+
60
+ return (
61
+ <Flexbox
62
+ align={'center'}
63
+ className={styles.container}
64
+ gap={12}
65
+ horizontal
66
+ onClick={() => {
67
+ if (isDirectory) {
68
+ localFileService.openLocalFolder({ isDirectory, path });
69
+ } else {
70
+ localFileService.openLocalFile({ path });
71
+ }
72
+ }}
73
+ onMouseEnter={() => setIsHovering(true)}
74
+ onMouseLeave={() => setIsHovering(false)}
75
+ padding={'2px 8px'}
76
+ style={{ cursor: 'pointer', fontSize: 12, width: '100%' }}
77
+ >
78
+ {isDirectory ? (
79
+ <FileTypeIcon size={16} type={'folder'} variant={'mono'} />
80
+ ) : (
81
+ <FileIcon fileName={name} fileType={type} size={16} variant={'pure'} />
82
+ )}
83
+ <Flexbox
84
+ align={'baseline'}
85
+ gap={4}
86
+ horizontal
87
+ style={{ overflow: 'hidden', width: '100%' }}
88
+ >
89
+ <div className={styles.title}>{name}</div>
90
+ {showTime ? (
91
+ <div className={styles.path}>{dayjs(createdTime).format('MMM DD hh:mm')}</div>
92
+ ) : (
93
+ <div className={styles.path}>{path}</div>
94
+ )}
95
+ </Flexbox>
96
+ {isHovering ? (
97
+ <Flexbox direction={'horizontal-reverse'} gap={8} style={{ minWidth: 50 }}>
98
+ <ActionIcon
99
+ icon={FolderOpen}
100
+ onClick={(e) => {
101
+ e.stopPropagation();
102
+ localFileService.openLocalFolder({ isDirectory, path });
103
+ }}
104
+ size={'small'}
105
+ style={{ height: 16, width: 16 }}
106
+ title={t('localFiles.openFolder')}
107
+ />
108
+ </Flexbox>
109
+ ) : (
110
+ <span className={styles.size}>{formatSize(size)}</span>
111
+ )}
112
+ </Flexbox>
113
+ );
114
+ },
115
+ );
116
+
117
+ export default FileItem;