@lobehub/chat 0.162.7 → 0.162.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +8 -8
- package/README.zh-CN.md +8 -8
- package/locales/ar/error.json +5 -1
- package/locales/bg-BG/error.json +4 -0
- package/locales/de-DE/error.json +4 -0
- package/locales/en-US/error.json +4 -0
- package/locales/es-ES/error.json +4 -0
- package/locales/fr-FR/error.json +4 -0
- package/locales/it-IT/error.json +4 -0
- package/locales/ja-JP/error.json +4 -0
- package/locales/ko-KR/error.json +4 -0
- package/locales/nl-NL/error.json +4 -0
- package/locales/pl-PL/error.json +4 -0
- package/locales/pt-BR/error.json +4 -0
- package/locales/ru-RU/error.json +4 -0
- package/locales/tr-TR/error.json +4 -0
- package/locales/vi-VN/error.json +4 -0
- package/locales/zh-CN/error.json +4 -0
- package/locales/zh-TW/error.json +5 -1
- package/package.json +1 -1
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/LocalFiles.tsx +1 -1
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx +6 -1
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +6 -3
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files.tsx +1 -1
- package/src/components/FileList/EditableFileList.tsx +17 -5
- package/src/components/FileList/FileListViewer.tsx +19 -0
- package/src/components/FileList/index.ts +2 -0
- package/src/components/FileList/type.tsx +7 -0
- package/src/components/ImageItem/index.tsx +75 -0
- package/src/features/Conversation/Messages/User.tsx +2 -2
- package/src/features/FileList/EditableFileList.tsx +31 -0
- package/src/features/FileList/FileListPreviewer.tsx +17 -0
- package/src/features/FileList/index.tsx +2 -0
- package/src/libs/swr/index.ts +33 -2
- package/src/locales/default/error.ts +1 -0
- package/src/services/__tests__/chat.test.ts +4 -1
- package/src/services/file/client.test.ts +2 -0
- package/src/services/file/client.ts +2 -0
- package/src/store/agent/slices/chat/action.ts +3 -4
- package/src/store/file/slices/images/action.test.ts +10 -5
- package/src/store/file/slices/images/action.ts +87 -22
- package/src/store/file/slices/images/initialState.ts +2 -0
- package/src/store/file/{selectors.test.ts → slices/images/selectors.test.ts} +4 -1
- package/src/store/file/slices/images/selectors.ts +7 -1
- package/src/store/global/action.ts +2 -2
- package/src/store/user/slices/common/action.ts +2 -2
- package/src/tools/dalle/Render/Item/Image.tsx +1 -1
- package/src/{components/FileList → tools/dalle/Render/Item}/ImageFileItem.tsx +1 -1
- package/src/types/files.ts +1 -0
- package/src/components/FileList/index.tsx +0 -22
- /package/src/components/{FileList → ImageItem}/style.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 0.162.8](https://github.com/lobehub/lobe-chat/compare/v0.162.7...v0.162.8)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2024-05-28**</sup>
|
|
8
|
+
|
|
9
|
+
#### 💄 Styles
|
|
10
|
+
|
|
11
|
+
- **misc**: Add optimistic loading for image uploading.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### Styles
|
|
19
|
+
|
|
20
|
+
- **misc**: Add optimistic loading for image uploading, closes [#2700](https://github.com/lobehub/lobe-chat/issues/2700) ([f99c9ce](https://github.com/lobehub/lobe-chat/commit/f99c9ce))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 0.162.7](https://github.com/lobehub/lobe-chat/compare/v0.162.6...v0.162.7)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2024-05-28**</sup>
|
package/README.md
CHANGED
|
@@ -262,14 +262,14 @@ Our marketplace is not just a showcase platform but also a collaborative space.
|
|
|
262
262
|
|
|
263
263
|
<!-- AGENT LIST -->
|
|
264
264
|
|
|
265
|
-
| Recent Submits
|
|
266
|
-
|
|
|
267
|
-
| [
|
|
268
|
-
| [
|
|
269
|
-
| [
|
|
270
|
-
| [
|
|
271
|
-
|
|
272
|
-
> 📊 Total agents: [<kbd>**
|
|
265
|
+
| Recent Submits | Description |
|
|
266
|
+
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
267
|
+
| [Dart/Flutter Dev](https://chat-preview.lobehub.com/market?agent=dart-flutter)<br/><sup>By **[rezmeplxrf](https://github.com/rezmeplxrf)** on **2024-05-28**</sup> | Dart/Flutter Expert. Never nest more than 3 levels deep. Use riverpod, flutter_riverpod, riverpod_hook, flutter_hook for state management.<br/>`dart` `flutter` `development` `state-management` `riverpod` |
|
|
268
|
+
| [C# .NET Technology Expert](https://chat-preview.lobehub.com/market?agent=dotnet-expert)<br/><sup>By **[johnnyqian](https://github.com/johnnyqian)** on **2024-05-28**</sup> | C# .NET Technology Expert<br/>`net` `developer` `net-core` `azure` `c` `microsoft` `sql-server` `entity-framework` `ef` `ef-core` |
|
|
269
|
+
| [Daily Assistant](https://chat-preview.lobehub.com/market?agent=junior-helper)<br/><sup>By **[Qinks6](https://github.com/Qinks6)** on **2024-05-28**</sup> | A cute little helper that can search and draw<br/>`assistant` `search` `drawing` `information-retrieval` `user-interaction` |
|
|
270
|
+
| [Node.js Optimizer](https://chat-preview.lobehub.com/market?agent=node-js-devoloper)<br/><sup>By **[chrisuhg](https://github.com/chrisuhg)** on **2024-05-28**</sup> | Specializes in Node.js code review, performance optimization, asynchronous programming, error handling, code refactoring, dependency management, security enhancement, test coverage, and documentation writing.<br/>`node-js` `code-optimization` `performance-optimization` `asynchronous-programming` `error-handling` |
|
|
271
|
+
|
|
272
|
+
> 📊 Total agents: [<kbd>**279**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
|
|
273
273
|
|
|
274
274
|
<!-- AGENT LIST -->
|
|
275
275
|
|
package/README.zh-CN.md
CHANGED
|
@@ -250,14 +250,14 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
|
|
|
250
250
|
|
|
251
251
|
<!-- AGENT LIST -->
|
|
252
252
|
|
|
253
|
-
| 最近新增
|
|
254
|
-
|
|
|
255
|
-
| [
|
|
256
|
-
| [
|
|
257
|
-
| [
|
|
258
|
-
| [
|
|
259
|
-
|
|
260
|
-
> 📊 Total agents: [<kbd>**
|
|
253
|
+
| 最近新增 | 助手说明 |
|
|
254
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
255
|
+
| [Dart/Flutter Dev](https://chat-preview.lobehub.com/market?agent=dart-flutter)<br/><sup>By **[rezmeplxrf](https://github.com/rezmeplxrf)** on **2024-05-28**</sup> | Dart/Flutter 전문가. 3단계 이상 중첩하지 않음. 상태 관리에 riverpod, flutter_riverpod, riverpod_hook, flutter_hook 사용.<br/>`dart` `flutter` `개발` `상태-관리` `riverpod` |
|
|
256
|
+
| [C# .NET 技术专家](https://chat-preview.lobehub.com/market?agent=dotnet-expert)<br/><sup>By **[johnnyqian](https://github.com/johnnyqian)** on **2024-05-28**</sup> | C# .NET 技术专家<br/>`net` `developer` `net-core` `azure` `c` `microsoft` `sql-server` `entity-framework` `ef` `ef-core` |
|
|
257
|
+
| [日常小助手](https://chat-preview.lobehub.com/market?agent=junior-helper)<br/><sup>By **[Qinks6](https://github.com/Qinks6)** on **2024-05-28**</sup> | 一个能搜索、能画图的小可爱<br/>`助手` `搜索` `绘图` `信息查询` `用户交互` |
|
|
258
|
+
| [Node.js 优化师](https://chat-preview.lobehub.com/market?agent=node-js-devoloper)<br/><sup>By **[chrisuhg](https://github.com/chrisuhg)** on **2024-05-28**</sup> | 擅长 Node.js 代码审查、性能优化、异步编程、错误处理、代码重构、依赖管理、安全增强、测试覆盖率和文档编写。<br/>`node-js` `代码优化` `性能优化` `异步编程` `错误处理` |
|
|
259
|
+
|
|
260
|
+
> 📊 Total agents: [<kbd>**279**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
|
|
261
261
|
|
|
262
262
|
<!-- AGENT LIST -->
|
|
263
263
|
|
package/locales/ar/error.json
CHANGED
package/locales/bg-BG/error.json
CHANGED
package/locales/de-DE/error.json
CHANGED
|
@@ -136,5 +136,9 @@
|
|
|
136
136
|
"apiKey": "Benutzerdefinierter API-Schlüssel",
|
|
137
137
|
"password": "Passwort"
|
|
138
138
|
}
|
|
139
|
+
},
|
|
140
|
+
"upload": {
|
|
141
|
+
"desc": "Details: {{detail}}",
|
|
142
|
+
"title": "Dateiupload fehlgeschlagen. Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es später erneut."
|
|
139
143
|
}
|
|
140
144
|
}
|
package/locales/en-US/error.json
CHANGED
package/locales/es-ES/error.json
CHANGED
|
@@ -136,5 +136,9 @@
|
|
|
136
136
|
"apiKey": "Clave de API personalizada",
|
|
137
137
|
"password": "Contraseña"
|
|
138
138
|
}
|
|
139
|
+
},
|
|
140
|
+
"upload": {
|
|
141
|
+
"desc": "Detalles: {{detail}}",
|
|
142
|
+
"title": "Error al subir el archivo, por favor verifica la conexión a internet o inténtalo de nuevo más tarde"
|
|
139
143
|
}
|
|
140
144
|
}
|
package/locales/fr-FR/error.json
CHANGED
|
@@ -136,5 +136,9 @@
|
|
|
136
136
|
"apiKey": "Clé API personnalisée",
|
|
137
137
|
"password": "Mot de passe"
|
|
138
138
|
}
|
|
139
|
+
},
|
|
140
|
+
"upload": {
|
|
141
|
+
"desc": "Détails : {{detail}}",
|
|
142
|
+
"title": "Échec de l'envoi du fichier, veuillez vérifier votre connexion réseau ou réessayer plus tard"
|
|
139
143
|
}
|
|
140
144
|
}
|
package/locales/it-IT/error.json
CHANGED
package/locales/ja-JP/error.json
CHANGED
package/locales/ko-KR/error.json
CHANGED
package/locales/nl-NL/error.json
CHANGED
package/locales/pl-PL/error.json
CHANGED
package/locales/pt-BR/error.json
CHANGED
package/locales/ru-RU/error.json
CHANGED
package/locales/tr-TR/error.json
CHANGED
package/locales/vi-VN/error.json
CHANGED
package/locales/zh-CN/error.json
CHANGED
package/locales/zh-TW/error.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.162.
|
|
3
|
+
"version": "0.162.8",
|
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx
CHANGED
|
@@ -27,7 +27,11 @@ const useStyles = createStyles(({ css, prefixCls }) => {
|
|
|
27
27
|
|
|
28
28
|
const isMac = isMacOS();
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
interface SendMoreProps {
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SendMore = memo<SendMoreProps>(({ disabled }) => {
|
|
31
35
|
const { t } = useTranslation('chat');
|
|
32
36
|
|
|
33
37
|
const { styles } = useStyles();
|
|
@@ -55,6 +59,7 @@ const SendMore = memo(() => {
|
|
|
55
59
|
|
|
56
60
|
return (
|
|
57
61
|
<Dropdown
|
|
62
|
+
disabled={disabled}
|
|
58
63
|
menu={{
|
|
59
64
|
items: [
|
|
60
65
|
{
|
package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { useAgentStore } from '@/store/agent';
|
|
|
14
14
|
import { agentSelectors } from '@/store/agent/slices/chat';
|
|
15
15
|
import { useChatStore } from '@/store/chat';
|
|
16
16
|
import { chatSelectors } from '@/store/chat/selectors';
|
|
17
|
+
import { filesSelectors, useFileStore } from '@/store/file';
|
|
17
18
|
import { useUserStore } from '@/store/user';
|
|
18
19
|
import { modelProviderSelectors, preferenceSelectors } from '@/store/user/selectors';
|
|
19
20
|
import { isMacOS } from '@/utils/platform';
|
|
@@ -60,10 +61,11 @@ const Footer = memo<FooterProps>(({ setExpand }) => {
|
|
|
60
61
|
|
|
61
62
|
const { theme, styles } = useStyles();
|
|
62
63
|
|
|
63
|
-
const [
|
|
64
|
+
const [isAIGenerating, stopGenerateMessage] = useChatStore((s) => [
|
|
64
65
|
chatSelectors.isAIGenerating(s),
|
|
65
66
|
s.stopGenerateMessage,
|
|
66
67
|
]);
|
|
68
|
+
const isImageUploading = useFileStore(filesSelectors.isImageUploading);
|
|
67
69
|
|
|
68
70
|
const model = useAgentStore(agentSelectors.currentAgentModel);
|
|
69
71
|
|
|
@@ -123,7 +125,7 @@ const Footer = memo<FooterProps>(({ setExpand }) => {
|
|
|
123
125
|
</Flexbox>
|
|
124
126
|
<SaveTopic />
|
|
125
127
|
<Flexbox style={{ minWidth: 92 }}>
|
|
126
|
-
{
|
|
128
|
+
{isAIGenerating ? (
|
|
127
129
|
<Button
|
|
128
130
|
className={styles.loadingButton}
|
|
129
131
|
icon={<StopLoadingIcon />}
|
|
@@ -134,6 +136,7 @@ const Footer = memo<FooterProps>(({ setExpand }) => {
|
|
|
134
136
|
) : (
|
|
135
137
|
<Space.Compact>
|
|
136
138
|
<Button
|
|
139
|
+
disabled={isImageUploading}
|
|
137
140
|
onClick={() => {
|
|
138
141
|
sendMessage();
|
|
139
142
|
setExpand?.(false);
|
|
@@ -142,7 +145,7 @@ const Footer = memo<FooterProps>(({ setExpand }) => {
|
|
|
142
145
|
>
|
|
143
146
|
{t('input.send')}
|
|
144
147
|
</Button>
|
|
145
|
-
<SendMore />
|
|
148
|
+
<SendMore disabled={isImageUploading} />
|
|
146
149
|
</Space.Compact>
|
|
147
150
|
)}
|
|
148
151
|
</Flexbox>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { memo } from 'react';
|
|
2
2
|
import { Flexbox } from 'react-layout-kit';
|
|
3
3
|
|
|
4
|
-
import EditableFileList from '@/
|
|
4
|
+
import { EditableFileList } from '@/features/FileList';
|
|
5
5
|
import { useFileStore } from '@/store/file';
|
|
6
6
|
|
|
7
7
|
const Files = memo(() => {
|
|
@@ -3,18 +3,22 @@ import { useResponsive } from 'antd-style';
|
|
|
3
3
|
import { memo } from 'react';
|
|
4
4
|
import { Flexbox } from 'react-layout-kit';
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import ImageItem from '@/components/ImageItem';
|
|
7
|
+
|
|
8
|
+
import { ImageFileItem } from './type';
|
|
7
9
|
|
|
8
10
|
interface EditableFileListProps {
|
|
9
11
|
alwaysShowClose?: boolean;
|
|
10
12
|
editable?: boolean;
|
|
11
|
-
items:
|
|
13
|
+
items: ImageFileItem[];
|
|
14
|
+
onRemove?: (id: string) => void;
|
|
12
15
|
padding?: number | string;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
const EditableFileList = memo<EditableFileListProps>(
|
|
16
|
-
({ items, editable = true, alwaysShowClose, padding = 12 }) => {
|
|
18
|
+
export const EditableFileList = memo<EditableFileListProps>(
|
|
19
|
+
({ items, editable = true, alwaysShowClose, onRemove, padding = 12 }) => {
|
|
17
20
|
const { mobile } = useResponsive();
|
|
21
|
+
|
|
18
22
|
return (
|
|
19
23
|
<Flexbox
|
|
20
24
|
gap={mobile ? 4 : 6}
|
|
@@ -24,7 +28,15 @@ const EditableFileList = memo<EditableFileListProps>(
|
|
|
24
28
|
>
|
|
25
29
|
<ImageGallery>
|
|
26
30
|
{items.map((i) => (
|
|
27
|
-
<
|
|
31
|
+
<ImageItem
|
|
32
|
+
alt={i.alt}
|
|
33
|
+
alwaysShowClose={alwaysShowClose}
|
|
34
|
+
editable={editable}
|
|
35
|
+
key={i.id}
|
|
36
|
+
loading={i.loading}
|
|
37
|
+
onRemove={() => onRemove?.(i.id)}
|
|
38
|
+
url={i.url}
|
|
39
|
+
/>
|
|
28
40
|
))}
|
|
29
41
|
</ImageGallery>
|
|
30
42
|
</Flexbox>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ImageGallery } from '@lobehub/ui';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
|
|
4
|
+
import GalleyGrid from '@/components/GalleyGrid';
|
|
5
|
+
import ImageItem from '@/components/ImageItem';
|
|
6
|
+
|
|
7
|
+
import { ImageFileItem } from './type';
|
|
8
|
+
|
|
9
|
+
interface FileListProps {
|
|
10
|
+
items: ImageFileItem[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ImageFileListViewer = memo<FileListProps>(({ items }) => {
|
|
14
|
+
return (
|
|
15
|
+
<ImageGallery>
|
|
16
|
+
<GalleyGrid items={items} renderItem={ImageItem} />
|
|
17
|
+
</ImageGallery>
|
|
18
|
+
);
|
|
19
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ActionIcon, Image } from '@lobehub/ui';
|
|
2
|
+
import { createStyles } from 'antd-style';
|
|
3
|
+
import { Trash } from 'lucide-react';
|
|
4
|
+
import { CSSProperties, memo } from 'react';
|
|
5
|
+
|
|
6
|
+
import { usePlatform } from '@/hooks/usePlatform';
|
|
7
|
+
|
|
8
|
+
import { MIN_IMAGE_SIZE } from './style';
|
|
9
|
+
|
|
10
|
+
const useStyles = createStyles(({ css, token }) => ({
|
|
11
|
+
deleteButton: css`
|
|
12
|
+
color: #fff;
|
|
13
|
+
background: ${token.colorBgMask};
|
|
14
|
+
|
|
15
|
+
&:hover {
|
|
16
|
+
background: ${token.colorError};
|
|
17
|
+
}
|
|
18
|
+
`,
|
|
19
|
+
editableImage: css`
|
|
20
|
+
background: ${token.colorBgContainer};
|
|
21
|
+
box-shadow: 0 0 0 1px ${token.colorFill} inset;
|
|
22
|
+
`,
|
|
23
|
+
image: css`
|
|
24
|
+
margin-block: 0 !important;
|
|
25
|
+
`,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
interface FileItemProps {
|
|
29
|
+
alt?: string;
|
|
30
|
+
alwaysShowClose?: boolean;
|
|
31
|
+
className?: string;
|
|
32
|
+
editable?: boolean;
|
|
33
|
+
loading?: boolean;
|
|
34
|
+
onClick?: () => void;
|
|
35
|
+
onRemove?: () => void;
|
|
36
|
+
style?: CSSProperties;
|
|
37
|
+
url?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ImageItem = memo<FileItemProps>(
|
|
41
|
+
({ editable, alt, onRemove, url, loading, alwaysShowClose }) => {
|
|
42
|
+
const IMAGE_SIZE = editable ? MIN_IMAGE_SIZE : '100%';
|
|
43
|
+
const { styles, cx } = useStyles();
|
|
44
|
+
const { isSafari } = usePlatform();
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Image
|
|
48
|
+
actions={
|
|
49
|
+
editable && (
|
|
50
|
+
<ActionIcon
|
|
51
|
+
className={styles.deleteButton}
|
|
52
|
+
glass
|
|
53
|
+
icon={Trash}
|
|
54
|
+
onClick={(e) => {
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
onRemove?.();
|
|
57
|
+
}}
|
|
58
|
+
size={'small'}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
alt={alt || ''}
|
|
63
|
+
alwaysShowActions={alwaysShowClose}
|
|
64
|
+
height={isSafari ? 'auto' : '100%'}
|
|
65
|
+
isLoading={loading}
|
|
66
|
+
size={IMAGE_SIZE as any}
|
|
67
|
+
src={url}
|
|
68
|
+
style={{ height: isSafari ? 'auto' : '100%' }}
|
|
69
|
+
wrapperClassName={cx(styles.image, editable && styles.editableImage)}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
export default ImageItem;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ReactNode, memo } from 'react';
|
|
2
2
|
import { Flexbox } from 'react-layout-kit';
|
|
3
3
|
|
|
4
|
-
import FileList from '@/components/FileList';
|
|
5
4
|
import { LOADING_FLAT } from '@/const/message';
|
|
5
|
+
import { FileListPreviewer } from '@/features/FileList';
|
|
6
6
|
import { ChatMessage } from '@/types/message';
|
|
7
7
|
|
|
8
8
|
import BubblesLoading from '../components/BubblesLoading';
|
|
@@ -17,7 +17,7 @@ export const UserMessage = memo<
|
|
|
17
17
|
return (
|
|
18
18
|
<Flexbox gap={8} id={id}>
|
|
19
19
|
{editableContent}
|
|
20
|
-
{res.files && res.files?.length > 0 && <
|
|
20
|
+
{res.files && res.files?.length > 0 && <FileListPreviewer items={res.files} />}
|
|
21
21
|
</Flexbox>
|
|
22
22
|
);
|
|
23
23
|
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import isEqual from 'fast-deep-equal';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
|
|
4
|
+
import { EditableFileList as Base } from '@/components/FileList';
|
|
5
|
+
import { filesSelectors, useFileStore } from '@/store/file';
|
|
6
|
+
|
|
7
|
+
interface EditableFileListProps {
|
|
8
|
+
alwaysShowClose?: boolean;
|
|
9
|
+
editable?: boolean;
|
|
10
|
+
items: string[];
|
|
11
|
+
padding?: number | string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const EditableFileList = memo<EditableFileListProps>(
|
|
15
|
+
({ items, editable = true, alwaysShowClose, padding }) => {
|
|
16
|
+
const [removeFile] = useFileStore((s) => [s.removeFile]);
|
|
17
|
+
const list = useFileStore(filesSelectors.getImageDetailByList(items), isEqual);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Base
|
|
21
|
+
alwaysShowClose={alwaysShowClose}
|
|
22
|
+
editable={editable}
|
|
23
|
+
items={list}
|
|
24
|
+
onRemove={(id) => removeFile(id)}
|
|
25
|
+
padding={padding}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export default EditableFileList;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import isEqual from 'fast-deep-equal';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
|
|
4
|
+
import { ImageFileListViewer } from '@/components/FileList';
|
|
5
|
+
import { filesSelectors, useFileStore } from '@/store/file';
|
|
6
|
+
|
|
7
|
+
interface FileListProps {
|
|
8
|
+
items: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const FileListPreviewer = memo<FileListProps>(({ items }) => {
|
|
12
|
+
const useFetchFiles = useFileStore((s) => s.useFetchFiles);
|
|
13
|
+
const data = useFileStore(filesSelectors.getImageDetailByList(items), isEqual);
|
|
14
|
+
useFetchFiles(items);
|
|
15
|
+
|
|
16
|
+
return <ImageFileListViewer items={data} />;
|
|
17
|
+
});
|
package/src/libs/swr/index.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import useSWR, { SWRHook } from 'swr';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* This type of request method is relatively flexible data, which will be triggered on the first time
|
|
5
|
+
*
|
|
6
|
+
* Refresh rules have two types:
|
|
7
|
+
*
|
|
8
|
+
* - when the user refocuses, it will be refreshed outside the 5mins interval.
|
|
9
|
+
* - can be combined with refreshXXX methods to refresh data
|
|
10
|
+
*
|
|
11
|
+
* Suitable for messages, topics, sessions, and other data that users will interact with on the client.
|
|
12
|
+
*
|
|
13
|
+
* 这一类请求方法是相对灵活的数据,会在请求时触发
|
|
14
|
+
*
|
|
15
|
+
* 刷新规则有两种:
|
|
16
|
+
* - 当用户重新聚焦时,在 5mins 间隔外会重新刷新一次
|
|
17
|
+
* - 可以搭配 refreshXXX 这样的方法刷新数据
|
|
18
|
+
*
|
|
19
|
+
* 适用于 messages、topics、sessions 等用户会在客户端交互的数据
|
|
6
20
|
*/
|
|
7
21
|
// @ts-ignore
|
|
8
22
|
export const useClientDataSWR: SWRHook = (key, fetch, config) =>
|
|
@@ -11,6 +25,23 @@ export const useClientDataSWR: SWRHook = (key, fetch, config) =>
|
|
|
11
25
|
// Cause issue like this: https://github.com/lobehub/lobe-chat/issues/532
|
|
12
26
|
// we need to set it to 0.
|
|
13
27
|
dedupingInterval: 0,
|
|
28
|
+
focusThrottleInterval: 5 * 60 * 1000,
|
|
29
|
+
refreshWhenOffline: false,
|
|
30
|
+
revalidateOnFocus: true,
|
|
31
|
+
revalidateOnReconnect: true,
|
|
32
|
+
...config,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* This type of request method is relatively "dead" request mode, which will only be triggered on the first request.
|
|
37
|
+
* it suitable for first time request like `initUserState`
|
|
38
|
+
|
|
39
|
+
* 这一类请求方法是相对“死”的请求模式,只会在第一次请求时触发。
|
|
40
|
+
* 适用于第一次请求,例如 `initUserState`
|
|
41
|
+
*/
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
export const useOnlyFetchOnceSWR: SWRHook = (key, fetch, config) =>
|
|
44
|
+
useSWR(key, fetch, {
|
|
14
45
|
refreshWhenOffline: false,
|
|
15
46
|
revalidateOnFocus: false,
|
|
16
47
|
revalidateOnReconnect: false,
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
LobeAnthropicAI,
|
|
9
9
|
LobeAzureOpenAI,
|
|
10
10
|
LobeBedrockAI,
|
|
11
|
+
LobeDeepSeekAI,
|
|
11
12
|
LobeGoogleAI,
|
|
12
13
|
LobeGroq,
|
|
13
|
-
LobeDeepSeekAI,
|
|
14
14
|
LobeMistralAI,
|
|
15
15
|
LobeMoonshotAI,
|
|
16
16
|
LobeOllamaAI,
|
|
@@ -136,6 +136,7 @@ describe('ChatService', () => {
|
|
|
136
136
|
useFileStore.setState({
|
|
137
137
|
imagesMap: {
|
|
138
138
|
file1: {
|
|
139
|
+
id: 'file1',
|
|
139
140
|
name: 'abc.png',
|
|
140
141
|
saveMode: 'url',
|
|
141
142
|
fileType: 'image/png',
|
|
@@ -193,6 +194,7 @@ describe('ChatService', () => {
|
|
|
193
194
|
useFileStore.setState({
|
|
194
195
|
imagesMap: {
|
|
195
196
|
file1: {
|
|
197
|
+
id: 'file1',
|
|
196
198
|
name: 'abc.png',
|
|
197
199
|
saveMode: 'url',
|
|
198
200
|
fileType: 'image/png',
|
|
@@ -234,6 +236,7 @@ describe('ChatService', () => {
|
|
|
234
236
|
useFileStore.setState({
|
|
235
237
|
imagesMap: {
|
|
236
238
|
file1: {
|
|
239
|
+
id: 'file1',
|
|
237
240
|
name: 'abc.png',
|
|
238
241
|
saveMode: 'url',
|
|
239
242
|
fileType: 'image/png',
|
|
@@ -90,6 +90,7 @@ describe('FileService', () => {
|
|
|
90
90
|
|
|
91
91
|
expect(FileModel.findById).toHaveBeenCalledWith(fileId);
|
|
92
92
|
expect(result).toEqual({
|
|
93
|
+
id: '1',
|
|
93
94
|
base64Url: 'data:image/png;base64,AA==',
|
|
94
95
|
fileType: 'image/png',
|
|
95
96
|
name: 'test',
|
|
@@ -120,6 +121,7 @@ describe('FileService', () => {
|
|
|
120
121
|
expect(FileModel.findById).toHaveBeenCalledWith(fileId);
|
|
121
122
|
expect(result).toEqual({
|
|
122
123
|
fileType: 'image/png',
|
|
124
|
+
id: '1',
|
|
123
125
|
name: 'test.png',
|
|
124
126
|
saveMode: 'url',
|
|
125
127
|
url: 'https://example.com/test.png',
|
|
@@ -28,6 +28,7 @@ export class ClientService implements IFileService {
|
|
|
28
28
|
|
|
29
29
|
return {
|
|
30
30
|
fileType: item.fileType,
|
|
31
|
+
id,
|
|
31
32
|
name: item.metadata.filename,
|
|
32
33
|
saveMode: 'url',
|
|
33
34
|
url: urlJoin(fileEnv.NEXT_PUBLIC_S3_DOMAIN!, item.url!),
|
|
@@ -41,6 +42,7 @@ export class ClientService implements IFileService {
|
|
|
41
42
|
return {
|
|
42
43
|
base64Url: `data:${item.fileType};base64,${base64}`,
|
|
43
44
|
fileType: item.fileType,
|
|
45
|
+
id,
|
|
44
46
|
name: item.name,
|
|
45
47
|
saveMode: 'local',
|
|
46
48
|
url,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import isEqual from 'fast-deep-equal';
|
|
2
2
|
import { produce } from 'immer';
|
|
3
|
-
import
|
|
3
|
+
import { SWRResponse, mutate } from 'swr';
|
|
4
4
|
import { DeepPartial } from 'utility-types';
|
|
5
5
|
import { StateCreator } from 'zustand/vanilla';
|
|
6
6
|
|
|
7
7
|
import { INBOX_SESSION_ID } from '@/const/session';
|
|
8
8
|
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
|
9
|
-
import { useClientDataSWR } from '@/libs/swr';
|
|
9
|
+
import { useClientDataSWR, useOnlyFetchOnceSWR } from '@/libs/swr';
|
|
10
10
|
import { sessionService } from '@/services/session';
|
|
11
11
|
import { AgentState } from '@/store/agent/slices/chat/initialState';
|
|
12
12
|
import { useSessionStore } from '@/store/session';
|
|
@@ -112,7 +112,7 @@ export const createChatSlice: StateCreator<
|
|
|
112
112
|
},
|
|
113
113
|
),
|
|
114
114
|
useInitAgentStore: (defaultAgentConfig) =>
|
|
115
|
-
|
|
115
|
+
useOnlyFetchOnceSWR<DeepPartial<LobeAgentConfig>>(
|
|
116
116
|
'fetchInboxAgentConfig',
|
|
117
117
|
() => sessionService.getSessionConfig(INBOX_SESSION_ID),
|
|
118
118
|
{
|
|
@@ -130,7 +130,6 @@ export const createChatSlice: StateCreator<
|
|
|
130
130
|
get().internal_dispatchAgentMap(INBOX_SESSION_ID, data, 'initInbox');
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
|
-
revalidateOnFocus: false,
|
|
134
133
|
},
|
|
135
134
|
),
|
|
136
135
|
|
|
@@ -2,6 +2,7 @@ import { act, renderHook } from '@testing-library/react';
|
|
|
2
2
|
import useSWR from 'swr';
|
|
3
3
|
import { Mock, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
5
|
+
import { notification } from '@/components/AntdStaticMethods';
|
|
5
6
|
import { DB_File } from '@/database/client/schemas/files';
|
|
6
7
|
import { fileService } from '@/services/file';
|
|
7
8
|
import { uploadService } from '@/services/upload';
|
|
@@ -10,6 +11,13 @@ import { useFileStore as useStore } from '../../store';
|
|
|
10
11
|
|
|
11
12
|
vi.mock('zustand/traditional');
|
|
12
13
|
|
|
14
|
+
// Mock necessary modules and functions
|
|
15
|
+
vi.mock('@/components/AntdStaticMethods', () => ({
|
|
16
|
+
notification: {
|
|
17
|
+
error: vi.fn(),
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
|
|
13
21
|
// Mock for useSWR
|
|
14
22
|
vi.mock('swr', () => ({
|
|
15
23
|
default: vi.fn(),
|
|
@@ -120,7 +128,6 @@ describe('useFileStore:images', () => {
|
|
|
120
128
|
vi.spyOn(uploadService, 'uploadFile').mockRejectedValue(new Error(errorMessage));
|
|
121
129
|
|
|
122
130
|
// Mock console.error for testing
|
|
123
|
-
const consoleErrorMock = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
124
131
|
|
|
125
132
|
await act(async () => {
|
|
126
133
|
await result.current.uploadFile(testFile);
|
|
@@ -136,11 +143,9 @@ describe('useFileStore:images', () => {
|
|
|
136
143
|
});
|
|
137
144
|
// 由于上传失败,inputFilesList 应该没有变化
|
|
138
145
|
expect(result.current.inputFilesList).toEqual([]);
|
|
139
|
-
// 确保错误被正确记录
|
|
140
|
-
expect(consoleErrorMock).toHaveBeenCalledWith('upload error:', expect.any(Error));
|
|
141
146
|
|
|
142
|
-
//
|
|
143
|
-
|
|
147
|
+
// 确保错误提示被调用
|
|
148
|
+
expect(notification.error).toHaveBeenCalled();
|
|
144
149
|
});
|
|
145
150
|
|
|
146
151
|
it('uploadFile should upload the file and update inputFilesList', async () => {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { t } from 'i18next';
|
|
1
2
|
import { produce } from 'immer';
|
|
2
3
|
import useSWR, { SWRResponse } from 'swr';
|
|
3
4
|
import { StateCreator } from 'zustand/vanilla';
|
|
4
5
|
|
|
6
|
+
import { notification } from '@/components/AntdStaticMethods';
|
|
5
7
|
import { fileService } from '@/services/file';
|
|
6
8
|
import { uploadService } from '@/services/upload';
|
|
7
9
|
import { FilePreview } from '@/types/files';
|
|
@@ -13,13 +15,19 @@ const n = setNamespace('image');
|
|
|
13
15
|
|
|
14
16
|
export interface FileAction {
|
|
15
17
|
clearImageList: () => void;
|
|
18
|
+
|
|
19
|
+
internal_addFile: (id: string) => void;
|
|
20
|
+
internal_removeFile: (id: string) => void;
|
|
21
|
+
internal_toggleLoading: (id: string, loading: boolean) => void;
|
|
22
|
+
loadFileDetail: (id: string) => Promise<void>;
|
|
16
23
|
removeAllFiles: () => Promise<void>;
|
|
17
24
|
removeFile: (id: string) => Promise<void>;
|
|
18
25
|
|
|
19
26
|
setImageMapItem: (id: string, item: FilePreview) => void;
|
|
20
|
-
uploadFile: (file: File) => Promise<void>;
|
|
21
27
|
|
|
28
|
+
uploadFile: (file: File) => Promise<void>;
|
|
22
29
|
useFetchFile: (id: string) => SWRResponse<FilePreview>;
|
|
30
|
+
useFetchFiles: (ids: string[]) => SWRResponse<void>;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
export const createFileSlice: StateCreator<
|
|
@@ -31,18 +39,47 @@ export const createFileSlice: StateCreator<
|
|
|
31
39
|
clearImageList: () => {
|
|
32
40
|
set({ inputFilesList: [] }, false, n('clearImageList'));
|
|
33
41
|
},
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
internal_addFile: (id: string) => {
|
|
43
|
+
set(({ inputFilesList }) => ({ inputFilesList: [...inputFilesList, id] }), false, n('addFile'));
|
|
36
44
|
},
|
|
37
|
-
|
|
38
|
-
await fileService.removeFile(id);
|
|
39
|
-
|
|
45
|
+
internal_removeFile: (id: string) => {
|
|
40
46
|
set(
|
|
41
47
|
({ inputFilesList }) => ({ inputFilesList: inputFilesList.filter((i) => i !== id) }),
|
|
42
48
|
false,
|
|
43
49
|
n('removeFile'),
|
|
44
50
|
);
|
|
45
51
|
},
|
|
52
|
+
internal_toggleLoading: (id: string, loading) => {
|
|
53
|
+
if (loading) {
|
|
54
|
+
set(
|
|
55
|
+
({ uploadingIds }) => ({ uploadingIds: [...uploadingIds, id] }),
|
|
56
|
+
false,
|
|
57
|
+
'toggleLoading/loading',
|
|
58
|
+
);
|
|
59
|
+
} else {
|
|
60
|
+
set(
|
|
61
|
+
({ uploadingIds }) => ({ uploadingIds: uploadingIds.filter((i) => i !== id) }),
|
|
62
|
+
false,
|
|
63
|
+
'toggleLoading/stopLoading',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
loadFileDetail: async (id) => {
|
|
69
|
+
// initFile preview
|
|
70
|
+
const item = await fileService.getFile(id);
|
|
71
|
+
get().setImageMapItem(item.id, item);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
removeAllFiles: async () => {
|
|
75
|
+
await fileService.removeAllFiles();
|
|
76
|
+
},
|
|
77
|
+
removeFile: async (id) => {
|
|
78
|
+
get().internal_removeFile(id);
|
|
79
|
+
|
|
80
|
+
await fileService.removeFile(id);
|
|
81
|
+
},
|
|
82
|
+
|
|
46
83
|
setImageMapItem: (id, item) => {
|
|
47
84
|
set(
|
|
48
85
|
produce((draft) => {
|
|
@@ -54,29 +91,51 @@ export const createFileSlice: StateCreator<
|
|
|
54
91
|
n('setImageMapItem'),
|
|
55
92
|
);
|
|
56
93
|
},
|
|
94
|
+
|
|
57
95
|
uploadFile: async (file) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
96
|
+
const fileItem = {
|
|
97
|
+
createdAt: file.lastModified,
|
|
98
|
+
data: await file.arrayBuffer(),
|
|
99
|
+
fileType: file.type,
|
|
100
|
+
name: file.name,
|
|
101
|
+
saveMode: 'local' as const,
|
|
102
|
+
size: file.size,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// at first create a temp id for the file for optimistic rendering
|
|
106
|
+
const tempId = Date.now().toString();
|
|
107
|
+
get().internal_addFile(tempId);
|
|
108
|
+
|
|
109
|
+
get().internal_toggleLoading(tempId, true);
|
|
110
|
+
|
|
111
|
+
// create a local Url for display
|
|
112
|
+
const url = URL.createObjectURL(new Blob([fileItem.data!], { type: fileItem.fileType }));
|
|
113
|
+
get().setImageMapItem(tempId, { ...fileItem, id: tempId, url });
|
|
67
114
|
|
|
115
|
+
try {
|
|
116
|
+
// after finish upload, mark the `loading=false` to show the uploaded item
|
|
117
|
+
const result = await uploadService.uploadFile(fileItem);
|
|
68
118
|
const data = await fileService.createFile(result);
|
|
119
|
+
get().internal_toggleLoading(tempId, false);
|
|
69
120
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
121
|
+
// after finish upload, remove the temp id and add the final one
|
|
122
|
+
get().internal_removeFile(tempId);
|
|
123
|
+
get().internal_addFile(data.id);
|
|
124
|
+
|
|
125
|
+
// initFile preview
|
|
126
|
+
await get().loadFileDetail(data.id);
|
|
75
127
|
} catch (error) {
|
|
76
|
-
|
|
77
|
-
|
|
128
|
+
get().internal_removeFile(tempId);
|
|
129
|
+
get().internal_toggleLoading(tempId, false);
|
|
130
|
+
|
|
131
|
+
// show error message
|
|
132
|
+
notification.error({
|
|
133
|
+
description: t('upload.desc', { detail: error, ns: 'error' }),
|
|
134
|
+
message: t('upload.title', { ns: 'error' }),
|
|
135
|
+
});
|
|
78
136
|
}
|
|
79
137
|
},
|
|
138
|
+
|
|
80
139
|
useFetchFile: (id) =>
|
|
81
140
|
useSWR(id, async (id) => {
|
|
82
141
|
const item = await fileService.getFile(id);
|
|
@@ -85,4 +144,10 @@ export const createFileSlice: StateCreator<
|
|
|
85
144
|
|
|
86
145
|
return item;
|
|
87
146
|
}),
|
|
147
|
+
useFetchFiles: (ids) =>
|
|
148
|
+
useSWR(ids, async (ids) => {
|
|
149
|
+
const pools = ids.map(async (id) => get().loadFileDetail(id));
|
|
150
|
+
|
|
151
|
+
await Promise.all(pools);
|
|
152
|
+
}),
|
|
88
153
|
});
|
|
@@ -3,9 +3,11 @@ import { FilePreview } from '@/types/files';
|
|
|
3
3
|
export interface ImageFileState {
|
|
4
4
|
imagesMap: Record<string, FilePreview>;
|
|
5
5
|
inputFilesList: string[];
|
|
6
|
+
uploadingIds: string[];
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export const initialImageFileState: ImageFileState = {
|
|
9
10
|
imagesMap: {},
|
|
10
11
|
inputFilesList: [],
|
|
12
|
+
uploadingIds: [],
|
|
11
13
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import { FilesStoreState } from '
|
|
3
|
+
import { FilesStoreState } from '../../initialState';
|
|
4
4
|
import { filesSelectors } from './selectors';
|
|
5
5
|
|
|
6
6
|
describe('filesSelectors', () => {
|
|
@@ -11,6 +11,7 @@ describe('filesSelectors', () => {
|
|
|
11
11
|
state = {
|
|
12
12
|
imagesMap: {
|
|
13
13
|
'1': {
|
|
14
|
+
id: '1',
|
|
14
15
|
name: 'a',
|
|
15
16
|
fileType: 'image/png',
|
|
16
17
|
saveMode: 'local',
|
|
@@ -18,6 +19,7 @@ describe('filesSelectors', () => {
|
|
|
18
19
|
url: 'blob:abc',
|
|
19
20
|
},
|
|
20
21
|
'2': {
|
|
22
|
+
id: '2',
|
|
21
23
|
name: 'b',
|
|
22
24
|
fileType: 'image/png',
|
|
23
25
|
saveMode: 'url',
|
|
@@ -25,6 +27,7 @@ describe('filesSelectors', () => {
|
|
|
25
27
|
url: 'url2',
|
|
26
28
|
},
|
|
27
29
|
},
|
|
30
|
+
uploadingIds: [],
|
|
28
31
|
// 假设 '3' 是不存在的 ID
|
|
29
32
|
inputFilesList: ['1', '2', '3'],
|
|
30
33
|
};
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { FilesStoreState } from '../../initialState';
|
|
2
2
|
|
|
3
3
|
const getImageDetailByList = (list: string[]) => (s: FilesStoreState) =>
|
|
4
|
-
list
|
|
4
|
+
list
|
|
5
|
+
.map((i) => s.imagesMap[i])
|
|
6
|
+
.filter(Boolean)
|
|
7
|
+
.map((i) => ({ ...i, loading: s.uploadingIds.includes(i.id) }));
|
|
5
8
|
|
|
6
9
|
const imageDetailList = (s: FilesStoreState) => getImageDetailByList(s.inputFilesList)(s);
|
|
7
10
|
|
|
@@ -25,10 +28,13 @@ const getImageUrlOrBase64ByList = (idList: string[]) => (s: FilesStoreState) =>
|
|
|
25
28
|
|
|
26
29
|
const imageUrlOrBase64List = (s: FilesStoreState) => getImageUrlOrBase64ByList(s.inputFilesList)(s);
|
|
27
30
|
|
|
31
|
+
const isImageUploading = (s: FilesStoreState) => s.uploadingIds.length > 0;
|
|
32
|
+
|
|
28
33
|
export const filesSelectors = {
|
|
29
34
|
getImageDetailByList,
|
|
30
35
|
getImageUrlOrBase64ById,
|
|
31
36
|
getImageUrlOrBase64ByList,
|
|
32
37
|
imageDetailList,
|
|
33
38
|
imageUrlOrBase64List,
|
|
39
|
+
isImageUploading,
|
|
34
40
|
};
|
|
@@ -7,7 +7,7 @@ import type { StateCreator } from 'zustand/vanilla';
|
|
|
7
7
|
import { INBOX_SESSION_ID } from '@/const/session';
|
|
8
8
|
import { SESSION_CHAT_URL } from '@/const/url';
|
|
9
9
|
import { CURRENT_VERSION } from '@/const/version';
|
|
10
|
-
import {
|
|
10
|
+
import { useOnlyFetchOnceSWR } from '@/libs/swr';
|
|
11
11
|
import { globalService } from '@/services/global';
|
|
12
12
|
import type { GlobalStore } from '@/store/global/index';
|
|
13
13
|
import { merge } from '@/utils/merge';
|
|
@@ -93,7 +93,7 @@ export const globalActionSlice: StateCreator<
|
|
|
93
93
|
}),
|
|
94
94
|
|
|
95
95
|
useInitSystemStatus: () =>
|
|
96
|
-
|
|
96
|
+
useOnlyFetchOnceSWR<SystemStatus>(
|
|
97
97
|
'initSystemStatus',
|
|
98
98
|
() => get().statusStorage.getFromLocalStorage(),
|
|
99
99
|
{
|
|
@@ -3,6 +3,7 @@ import { DeepPartial } from 'utility-types';
|
|
|
3
3
|
import type { StateCreator } from 'zustand/vanilla';
|
|
4
4
|
|
|
5
5
|
import { DEFAULT_PREFERENCE } from '@/const/user';
|
|
6
|
+
import { useOnlyFetchOnceSWR } from '@/libs/swr';
|
|
6
7
|
import { userService } from '@/services/user';
|
|
7
8
|
import { ClientService } from '@/services/user/client';
|
|
8
9
|
import type { UserStore } from '@/store/user';
|
|
@@ -69,7 +70,7 @@ export const createCommonSlice: StateCreator<
|
|
|
69
70
|
),
|
|
70
71
|
|
|
71
72
|
useInitUserState: (isLogin, serverConfig, options) =>
|
|
72
|
-
|
|
73
|
+
useOnlyFetchOnceSWR<UserInitializationState>(
|
|
73
74
|
!!isLogin ? GET_USER_STATE_KEY : null,
|
|
74
75
|
() => userService.getUserState(),
|
|
75
76
|
{
|
|
@@ -123,7 +124,6 @@ export const createCommonSlice: StateCreator<
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
},
|
|
126
|
-
revalidateOnFocus: false,
|
|
127
127
|
},
|
|
128
128
|
),
|
|
129
129
|
});
|
|
@@ -4,7 +4,7 @@ import { memo } from 'react';
|
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { Flexbox } from 'react-layout-kit';
|
|
6
6
|
|
|
7
|
-
import ImageFileItem from '
|
|
7
|
+
import ImageFileItem from './ImageFileItem';
|
|
8
8
|
|
|
9
9
|
interface ImagePreviewProps {
|
|
10
10
|
imageId?: string;
|
|
@@ -6,7 +6,7 @@ import { CSSProperties, memo, useCallback } from 'react';
|
|
|
6
6
|
import { usePlatform } from '@/hooks/usePlatform';
|
|
7
7
|
import { useFileStore } from '@/store/file';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const MIN_IMAGE_SIZE = 64;
|
|
10
10
|
|
|
11
11
|
export const useStyles = createStyles(({ css, token }) => ({
|
|
12
12
|
deleteButton: css`
|
package/src/types/files.ts
CHANGED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { ImageGallery } from '@lobehub/ui';
|
|
2
|
-
import { memo, useMemo } from 'react';
|
|
3
|
-
|
|
4
|
-
import GalleyGrid from '@/components/GalleyGrid';
|
|
5
|
-
|
|
6
|
-
import ImageFileItem from './ImageFileItem';
|
|
7
|
-
|
|
8
|
-
interface FileListProps {
|
|
9
|
-
items: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const FileList = memo<FileListProps>(({ items }) => {
|
|
13
|
-
const data = useMemo(() => items.map((id) => ({ id })), [items]);
|
|
14
|
-
|
|
15
|
-
return (
|
|
16
|
-
<ImageGallery>
|
|
17
|
-
<GalleyGrid items={data} renderItem={ImageFileItem} />
|
|
18
|
-
</ImageGallery>
|
|
19
|
-
);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
export default FileList;
|
|
File without changes
|