@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +8 -8
  3. package/README.zh-CN.md +8 -8
  4. package/locales/ar/error.json +5 -1
  5. package/locales/bg-BG/error.json +4 -0
  6. package/locales/de-DE/error.json +4 -0
  7. package/locales/en-US/error.json +4 -0
  8. package/locales/es-ES/error.json +4 -0
  9. package/locales/fr-FR/error.json +4 -0
  10. package/locales/it-IT/error.json +4 -0
  11. package/locales/ja-JP/error.json +4 -0
  12. package/locales/ko-KR/error.json +4 -0
  13. package/locales/nl-NL/error.json +4 -0
  14. package/locales/pl-PL/error.json +4 -0
  15. package/locales/pt-BR/error.json +4 -0
  16. package/locales/ru-RU/error.json +4 -0
  17. package/locales/tr-TR/error.json +4 -0
  18. package/locales/vi-VN/error.json +4 -0
  19. package/locales/zh-CN/error.json +4 -0
  20. package/locales/zh-TW/error.json +5 -1
  21. package/package.json +1 -1
  22. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/LocalFiles.tsx +1 -1
  23. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx +6 -1
  24. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +6 -3
  25. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files.tsx +1 -1
  26. package/src/components/FileList/EditableFileList.tsx +17 -5
  27. package/src/components/FileList/FileListViewer.tsx +19 -0
  28. package/src/components/FileList/index.ts +2 -0
  29. package/src/components/FileList/type.tsx +7 -0
  30. package/src/components/ImageItem/index.tsx +75 -0
  31. package/src/features/Conversation/Messages/User.tsx +2 -2
  32. package/src/features/FileList/EditableFileList.tsx +31 -0
  33. package/src/features/FileList/FileListPreviewer.tsx +17 -0
  34. package/src/features/FileList/index.tsx +2 -0
  35. package/src/libs/swr/index.ts +33 -2
  36. package/src/locales/default/error.ts +1 -0
  37. package/src/services/__tests__/chat.test.ts +4 -1
  38. package/src/services/file/client.test.ts +2 -0
  39. package/src/services/file/client.ts +2 -0
  40. package/src/store/agent/slices/chat/action.ts +3 -4
  41. package/src/store/file/slices/images/action.test.ts +10 -5
  42. package/src/store/file/slices/images/action.ts +87 -22
  43. package/src/store/file/slices/images/initialState.ts +2 -0
  44. package/src/store/file/{selectors.test.ts → slices/images/selectors.test.ts} +4 -1
  45. package/src/store/file/slices/images/selectors.ts +7 -1
  46. package/src/store/global/action.ts +2 -2
  47. package/src/store/user/slices/common/action.ts +2 -2
  48. package/src/tools/dalle/Render/Item/Image.tsx +1 -1
  49. package/src/{components/FileList → tools/dalle/Render/Item}/ImageFileItem.tsx +1 -1
  50. package/src/types/files.ts +1 -0
  51. package/src/components/FileList/index.tsx +0 -22
  52. /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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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 | Description |
266
- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
267
- | [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` |
268
- | [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` |
269
- | [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` |
270
- | [Foreign Colleague Evaluation Assistant](https://chat-preview.lobehub.com/market?agent=praise-assistant)<br/><sup>By **[johnnyqian](https://github.com/johnnyqian)** on **2024-05-27**</sup> | Give positive feedback to your colleagues<br/>`foreign-company` `evaluate` `review` `software-engineer` `praise` |
271
-
272
- > 📊 Total agents: [<kbd>**278**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
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
- | [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` |
256
- | [日常小助手](https://chat-preview.lobehub.com/market?agent=junior-helper)<br/><sup>By **[Qinks6](https://github.com/Qinks6)** on **2024-05-28**</sup> | 一个能搜索、能画图的小可爱<br/>`助手` `搜索` `绘图` `信息查询` `用户交互` |
257
- | [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` `代码优化` `性能优化` `异步编程` `错误处理` |
258
- | [外企同事评价助手](https://chat-preview.lobehub.com/market?agent=praise-assistant)<br/><sup>By **[johnnyqian](https://github.com/johnnyqian)** on **2024-05-27**</sup> | 给你的同事好评<br/>`foreign-company` `evaluate` `review` `software-engineer` `praise` |
259
-
260
- > 📊 Total agents: [<kbd>**278**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
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
 
@@ -136,5 +136,9 @@
136
136
  "apiKey": "مفتاح واجهة برمجة التطبيقات المخصص",
137
137
  "password": "كلمة المرور"
138
138
  }
139
+ },
140
+ "upload": {
141
+ "desc": "التفاصيل: {{detail}}",
142
+ "title": "فشل تحميل الملف، يرجى التحقق من الاتصال بالشبكة أو المحاولة لاحقًا"
139
143
  }
140
- }
144
+ }
@@ -136,5 +136,9 @@
136
136
  "apiKey": "Персонализиран API ключ",
137
137
  "password": "Парола"
138
138
  }
139
+ },
140
+ "upload": {
141
+ "desc": "Подробности: {{detail}}",
142
+ "title": "Неуспешно качване на файл, моля проверете интернет връзката или опитайте по-късно"
139
143
  }
140
144
  }
@@ -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
  }
@@ -136,5 +136,9 @@
136
136
  "apiKey": "Custom API Key",
137
137
  "password": "Password"
138
138
  }
139
+ },
140
+ "upload": {
141
+ "desc": "Details: {{detail}}",
142
+ "title": "File upload failed. Please check your network connection or try again later"
139
143
  }
140
144
  }
@@ -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
  }
@@ -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
  }
@@ -136,5 +136,9 @@
136
136
  "apiKey": "Chiave API personalizzata",
137
137
  "password": "Password"
138
138
  }
139
+ },
140
+ "upload": {
141
+ "desc": "Dettagli: {{detail}}",
142
+ "title": "Caricamento del file fallito, controlla la connessione di rete o riprova più tardi"
139
143
  }
140
144
  }
@@ -136,5 +136,9 @@
136
136
  "apiKey": "カスタムAPIキー",
137
137
  "password": "パスワード"
138
138
  }
139
+ },
140
+ "upload": {
141
+ "desc": "詳細: {{detail}}",
142
+ "title": "ファイルのアップロードに失敗しました。ネットワーク接続を確認するか、後でもう一度お試しください"
139
143
  }
140
144
  }
@@ -136,5 +136,9 @@
136
136
  "apiKey": "사용자 정의 API Key",
137
137
  "password": "비밀번호"
138
138
  }
139
+ },
140
+ "upload": {
141
+ "desc": "상세 내용: {{detail}}",
142
+ "title": "파일 업로드 실패, 네트워크 연결을 확인하거나 나중에 다시 시도해주세요"
139
143
  }
140
144
  }
@@ -136,5 +136,9 @@
136
136
  "apiKey": "Custom API Key",
137
137
  "password": "Password"
138
138
  }
139
+ },
140
+ "upload": {
141
+ "desc": "Details: {{detail}}",
142
+ "title": "Bestand uploaden mislukt, controleer uw internetverbinding of probeer het later opnieuw"
139
143
  }
140
144
  }
@@ -136,5 +136,9 @@
136
136
  "apiKey": "Niestandardowy klucz API",
137
137
  "password": "Hasło"
138
138
  }
139
+ },
140
+ "upload": {
141
+ "desc": "Szczegóły: {{detail}}",
142
+ "title": "Nie udało się przesłać pliku. Sprawdź połączenie sieciowe lub spróbuj ponownie później"
139
143
  }
140
144
  }
@@ -136,5 +136,9 @@
136
136
  "apiKey": "Chave de API personalizada",
137
137
  "password": "Senha"
138
138
  }
139
+ },
140
+ "upload": {
141
+ "desc": "Detalhes: {{detail}}",
142
+ "title": "Falha ao enviar o arquivo, verifique a conexão de rede ou tente novamente mais tarde"
139
143
  }
140
144
  }
@@ -136,5 +136,9 @@
136
136
  "apiKey": "Пользовательский ключ API",
137
137
  "password": "Пароль"
138
138
  }
139
+ },
140
+ "upload": {
141
+ "desc": "Подробности: {{detail}}",
142
+ "title": "Ошибка загрузки файла. Проверьте подключение к сети или попробуйте позже"
139
143
  }
140
144
  }
@@ -136,5 +136,9 @@
136
136
  "apiKey": "Özel API Anahtarı",
137
137
  "password": "Şifre"
138
138
  }
139
+ },
140
+ "upload": {
141
+ "desc": "Detay: {{detail}}",
142
+ "title": "Dosya yükleme başarısız, lütfen ağ bağlantınızı kontrol edin veya daha sonra tekrar deneyin"
139
143
  }
140
144
  }
@@ -136,5 +136,9 @@
136
136
  "apiKey": "Khóa API tùy chỉnh",
137
137
  "password": "Mật khẩu"
138
138
  }
139
+ },
140
+ "upload": {
141
+ "desc": "Chi tiết: {{detail}}",
142
+ "title": "Tải lên tệp thất bại, vui lòng kiểm tra kết nối mạng hoặc thử lại sau"
139
143
  }
140
144
  }
@@ -136,5 +136,9 @@
136
136
  "apiKey": "自定义 API Key",
137
137
  "password": "密码"
138
138
  }
139
+ },
140
+ "upload": {
141
+ "desc": "详情: {{detail}}",
142
+ "title": "文件上传失败,请检查网络连接或稍后再试"
139
143
  }
140
144
  }
@@ -136,5 +136,9 @@
136
136
  "apiKey": "自定義 API Key",
137
137
  "password": "密碼"
138
138
  }
139
+ },
140
+ "upload": {
141
+ "desc": "詳情: {{detail}}",
142
+ "title": "檔案上傳失敗,請檢查網路連線或稍後再試"
139
143
  }
140
- }
144
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.162.7",
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",
@@ -1,6 +1,6 @@
1
1
  import { memo } from 'react';
2
2
 
3
- import EditableFileList from '@/components/FileList/EditableFileList';
3
+ import { EditableFileList } from '@/features/FileList';
4
4
  import { useFileStore } from '@/store/file';
5
5
 
6
6
  export const LocalFiles = memo(() => {
@@ -27,7 +27,11 @@ const useStyles = createStyles(({ css, prefixCls }) => {
27
27
 
28
28
  const isMac = isMacOS();
29
29
 
30
- const SendMore = memo(() => {
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
  {
@@ -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 [loading, stopGenerateMessage] = useChatStore((s) => [
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
- {loading ? (
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 '@/components/FileList/EditableFileList';
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 ImageFileItem from './ImageFileItem';
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: string[];
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
- <ImageFileItem alwaysShowClose={alwaysShowClose} editable={editable} id={i} key={i} />
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,2 @@
1
+ export { EditableFileList } from './EditableFileList';
2
+ export { ImageFileListViewer } from './FileListViewer';
@@ -0,0 +1,7 @@
1
+ export interface ImageFileItem {
2
+ alt?: string;
3
+ id: string;
4
+ loading?: boolean;
5
+ onRemove?: (id: string) => void;
6
+ url: string;
7
+ }
@@ -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 && <FileList items={res.files} />}
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
+ });
@@ -0,0 +1,2 @@
1
+ export { EditableFileList } from './EditableFileList';
2
+ export { FileListPreviewer } from './FileListPreviewer';
@@ -1,8 +1,22 @@
1
1
  import useSWR, { SWRHook } from 'swr';
2
2
 
3
3
  /**
4
- * 这一类请求方法是比较「死」的请求模式,只会在第一次请求时触发。不会自动刷新,刷新需要搭配 refreshXXX 这样的方法实现,
5
- * 适用于 messages、topics、sessions 等由用户在客户端交互产生的数据。
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,
@@ -164,4 +164,5 @@ export default {
164
164
  password: '密码',
165
165
  },
166
166
  },
167
+ upload: { desc: '详情: {{detail}}', title: '文件上传失败,请检查网络连接或稍后再试' },
167
168
  };
@@ -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 useSWR, { SWRResponse, mutate } from 'swr';
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
- useSWR<DeepPartial<LobeAgentConfig>>(
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
- // Cleanup mock
143
- consoleErrorMock.mockRestore();
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
- removeAllFiles: async () => {
35
- await fileService.removeAllFiles();
42
+ internal_addFile: (id: string) => {
43
+ set(({ inputFilesList }) => ({ inputFilesList: [...inputFilesList, id] }), false, n('addFile'));
36
44
  },
37
- removeFile: async (id) => {
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
- try {
59
- const result = await uploadService.uploadFile({
60
- createdAt: file.lastModified,
61
- data: await file.arrayBuffer(),
62
- fileType: file.type,
63
- name: file.name,
64
- saveMode: 'local',
65
- size: file.size,
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
- set(
71
- ({ inputFilesList }) => ({ inputFilesList: [...inputFilesList, data.id] }),
72
- false,
73
- n('uploadFile'),
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
- console.error('upload error:', error);
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 './initialState';
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.map((i) => s.imagesMap[i]).filter(Boolean);
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 { useClientDataSWR } from '@/libs/swr';
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
- useClientDataSWR<SystemStatus>(
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
- useSWR<UserInitializationState>(
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 '@/components/FileList/ImageFileItem';
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
- import { MIN_IMAGE_SIZE } from './style';
9
+ const MIN_IMAGE_SIZE = 64;
10
10
 
11
11
  export const useStyles = createStyles(({ css, token }) => ({
12
12
  deleteButton: css`
@@ -4,6 +4,7 @@ export interface FilePreview {
4
4
  base64Url?: string;
5
5
  data?: ArrayBuffer;
6
6
  fileType: string;
7
+ id: string;
7
8
  name: string;
8
9
  saveMode: 'local' | 'url';
9
10
  url: string;
@@ -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