@lobehub/chat 0.147.20 → 0.147.22

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 (41) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +8 -8
  3. package/README.zh-CN.md +8 -8
  4. package/locales/ar/chat.json +3 -0
  5. package/locales/bg-BG/chat.json +3 -0
  6. package/locales/de-DE/chat.json +3 -0
  7. package/locales/en-US/chat.json +3 -0
  8. package/locales/es-ES/chat.json +3 -0
  9. package/locales/fr-FR/chat.json +3 -0
  10. package/locales/it-IT/chat.json +3 -0
  11. package/locales/ja-JP/chat.json +3 -0
  12. package/locales/ko-KR/chat.json +3 -0
  13. package/locales/nl-NL/chat.json +3 -0
  14. package/locales/pl-PL/chat.json +3 -0
  15. package/locales/pt-BR/chat.json +3 -0
  16. package/locales/ru-RU/chat.json +3 -0
  17. package/locales/tr-TR/chat.json +3 -0
  18. package/locales/vi-VN/chat.json +3 -0
  19. package/locales/zh-CN/chat.json +4 -1
  20. package/locales/zh-TW/chat.json +3 -0
  21. package/package.json +2 -2
  22. package/public/favicon-32x32.ico +0 -0
  23. package/public/favicon.ico +0 -0
  24. package/public/icons/apple-touch-icon.png +0 -0
  25. package/src/app/chat/(desktop)/features/ChatInput/Footer/DragUpload.tsx +3 -3
  26. package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +2 -2
  27. package/src/app/metadata.ts +3 -5
  28. package/src/features/ChatInput/ActionBar/FileUpload.tsx +3 -3
  29. package/src/features/ChatInput/Topic/index.tsx +6 -2
  30. package/src/features/Conversation/components/ChatItem/index.tsx +8 -3
  31. package/src/libs/swr/index.ts +9 -0
  32. package/src/locales/default/chat.ts +5 -2
  33. package/src/store/chat/slices/message/action.ts +80 -42
  34. package/src/store/chat/slices/message/initialState.ts +1 -1
  35. package/src/store/chat/slices/message/reducer.ts +32 -1
  36. package/src/store/chat/slices/topic/action.test.ts +25 -2
  37. package/src/store/chat/slices/topic/action.ts +24 -7
  38. package/src/store/chat/slices/topic/reducer.test.ts +141 -0
  39. package/src/store/chat/slices/topic/reducer.ts +67 -0
  40. package/src/store/session/slices/session/action.ts +4 -5
  41. package/vercel.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.147.22](https://github.com/lobehub/lobe-chat/compare/v0.147.21...v0.147.22)
6
+
7
+ <sup>Released on **2024-04-19**</sup>
8
+
9
+ <br/>
10
+
11
+ <details>
12
+ <summary><kbd>Improvements and Fixes</kbd></summary>
13
+
14
+ </details>
15
+
16
+ <div align="right">
17
+
18
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
19
+
20
+ </div>
21
+
22
+ ### [Version 0.147.21](https://github.com/lobehub/lobe-chat/compare/v0.147.20...v0.147.21)
23
+
24
+ <sup>Released on **2024-04-19**</sup>
25
+
26
+ #### 💄 Styles
27
+
28
+ - **misc**: Optimized file upload buttons and prompts.
29
+
30
+ <br/>
31
+
32
+ <details>
33
+ <summary><kbd>Improvements and Fixes</kbd></summary>
34
+
35
+ #### Styles
36
+
37
+ - **misc**: Optimized file upload buttons and prompts, closes [#2050](https://github.com/lobehub/lobe-chat/issues/2050) ([c23087e](https://github.com/lobehub/lobe-chat/commit/c23087e))
38
+
39
+ </details>
40
+
41
+ <div align="right">
42
+
43
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
44
+
45
+ </div>
46
+
5
47
  ### [Version 0.147.20](https://github.com/lobehub/lobe-chat/compare/v0.147.19...v0.147.20)
6
48
 
7
49
  <sup>Released on **2024-04-18**</sup>
package/README.md CHANGED
@@ -260,14 +260,14 @@ Our marketplace is not just a showcase platform but also a collaborative space.
260
260
 
261
261
  <!-- AGENT LIST -->
262
262
 
263
- | Recent Submits | Description |
264
- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
265
- | [Biology Professor](https://chat-preview.lobehub.com/market?agent=bio-professor)<br/><sup>By **[luxiangze](https://github.com/luxiangze)** on **2024-04-13**</sup> | As a biology professor, you will receive questions and concepts about biology. Please explain these questions and concepts in specific and concise language, and try to use practical examples to illustrate them to help your audience better understand. Please ensure that your explanations are accurate and clear, and try to encourage creative and flexible answers. Respond in Chinese.<br/>`biology` |
266
- | [Master of Fortune Telling](https://chat-preview.lobehub.com/market?agent=fortune-teller)<br/><sup>By **[kamilkenrich](https://github.com/kamilkenrich)** on **2024-04-13**</sup> | Specializes in numerology, divination, astrology, and blood type analysis<br/>`numerology` `divination` `astrology` `psychology` `blood-type` `constellation` |
267
- | [Healthy Recipe Recommender](https://chat-preview.lobehub.com/market?agent=healthy-recipe-recommender)<br/><sup>By **[Greasen](https://github.com/Greasen)** on **2024-04-11**</sup> | Customized nutrition meals, scientific combinations, healthy eating, your personal nutritionist.<br/>`recipes` `fitness-meals` `nutrition-meals` |
268
- | [SmartWeather Assistant](https://chat-preview.lobehub.com/market?agent=personal-weather-consultant)<br/><sup>By **[Greasen](https://github.com/Greasen)** on **2024-04-11**</sup> | SmartWeather Assistant, your personal weather advisor, fashion guide, and positive energy booster!<br/>`weather` `assistant` `fashion` |
269
-
270
- > 📊 Total agents: [<kbd>**238**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
263
+ | Recent Submits | Description |
264
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
265
+ | [Chinese Academic Paper Editor](https://chat-preview.lobehub.com/market?agent=chinese-paper-polishing)<br/><sup>By **[y22emc2](https://github.com/y22emc2)** on **2024-04-15**</sup> | As an assistant for improving Chinese academic paper writing, your task is to enhance the provided text in terms of spelling, grammar, clarity, conciseness, and overall readability, to improve the text's academic standards and literary quality. This includes breaking down long sentences, reducing repetition, and providing improvement suggestions. Please provide the corrected version of the text first, then list the modifications in the markdown table below, along with the reasons for the changes.<br/>`academic-paper-writing` `proofreading` `text-editing` |
266
+ | [Biology Professor](https://chat-preview.lobehub.com/market?agent=bio-professor)<br/><sup>By **[luxiangze](https://github.com/luxiangze)** on **2024-04-13**</sup> | As a biology professor, you will receive questions and concepts about biology. Please explain these questions and concepts in specific and concise language, and try to use practical examples to illustrate them to help your audience better understand. Please ensure that your explanations are accurate and clear, and try to encourage creative and flexible answers. Respond in Chinese.<br/>`biology` |
267
+ | [Master of Fortune Telling](https://chat-preview.lobehub.com/market?agent=fortune-teller)<br/><sup>By **[kamilkenrich](https://github.com/kamilkenrich)** on **2024-04-13**</sup> | Specializes in numerology, divination, astrology, and blood type analysis<br/>`numerology` `divination` `astrology` `psychology` `blood-type` `constellation` |
268
+ | [High School Science Study Assistant](https://chat-preview.lobehub.com/market?agent=highschool-master)<br/><sup>By **[cnliucheng](https://github.com/cnliucheng)** on **2024-04-13**</sup> | I am a study assistant AI specially designed for Chinese high school students. Whether you encounter difficulties in physics, chemistry, mathematics, or biology, I can provide detailed answers and explanations for you. Not only that, I can also recommend suitable practice questions based on your learning progress to help you consolidate knowledge and improve learning efficiency. Additionally, I will use LaTeX format as much as possible to present the solution process and formulas.<br/>`high-school-study` `science-assistant` `problem-solving` `learning-progress` `la-te-x` |
269
+
270
+ > 📊 Total agents: [<kbd>**240**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
271
271
 
272
272
  <!-- AGENT LIST -->
273
273
 
package/README.zh-CN.md CHANGED
@@ -248,14 +248,14 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
248
248
 
249
249
  <!-- AGENT LIST -->
250
250
 
251
- | 最近新增 | 助手说明 |
252
- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
253
- | [生物学教授](https://chat-preview.lobehub.com/market?agent=bio-professor)<br/><sup>By **[luxiangze](https://github.com/luxiangze)** on **2024-04-13**</sup> | 作为一位生物学教授,您将收到关于生物学的问题和概念,请用具体而简明的语言解释这些问题和概念,并尽量使用实际例子进行说明,以帮助您的听众更好地理解。请确保您的解释准确而清晰,并试图鼓励创造性和灵活性的答案。用中文回答<br/>`生物` |
254
- | [命理大师](https://chat-preview.lobehub.com/market?agent=fortune-teller)<br/><sup>By **[kamilkenrich](https://github.com/kamilkenrich)** on **2024-04-13**</sup> | 擅长命理学、占卜预测、占星术和血型学分析<br/>`命理学、占卜、占星学、心理学、血型、星座` |
255
- | [健康食谱推荐师](https://chat-preview.lobehub.com/market?agent=healthy-recipe-recommender)<br/><sup>By **[Greasen](https://github.com/Greasen)** on **2024-04-11**</sup> | 精准定制营养餐,科学搭配,健康饮食,你的私人营养师。<br/>`食谱、健身餐、营养餐` |
256
- | [智预小助手](https://chat-preview.lobehub.com/market?agent=personal-weather-consultant)<br/><sup>By **[Greasen](https://github.com/Greasen)** on **2024-04-11**</sup> | 智预小助手,您的私人天气顾问,穿搭指南,正能量加油器!<br/>`天气` `助理、穿搭` |
257
-
258
- > 📊 Total agents: [<kbd>**238**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
251
+ | 最近新增 | 助手说明 |
252
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
253
+ | [中文论文编辑师](https://chat-preview.lobehub.com/market?agent=chinese-paper-polishing)<br/><sup>By **[y22emc2](https://github.com/y22emc2)** on **2024-04-15**</sup> | 作为一名中文学术论文写作改进助理,你的任务是改进所提供文本的拼写、语法、清晰、简洁和整体可读性,提高文本的学术规范性、文学性,同时分解长句,减少重复,并提供改进建议。请先提供文本的更正版本,然后在 markdown 表格中列出修改的内容,并给出修改的理由。<br/>`学术论文写作` `校对` `文本编辑` |
254
+ | [生物学教授](https://chat-preview.lobehub.com/market?agent=bio-professor)<br/><sup>By **[luxiangze](https://github.com/luxiangze)** on **2024-04-13**</sup> | 作为一位生物学教授,您将收到关于生物学的问题和概念,请用具体而简明的语言解释这些问题和概念,并尽量使用实际例子进行说明,以帮助您的听众更好地理解。请确保您的解释准确而清晰,并试图鼓励创造性和灵活性的答案。用中文回答<br/>`生物` |
255
+ | [命理大师](https://chat-preview.lobehub.com/market?agent=fortune-teller)<br/><sup>By **[kamilkenrich](https://github.com/kamilkenrich)** on **2024-04-13**</sup> | 擅长命理学、占卜预测、占星术和血型学分析<br/>`命理学、占卜、占星学、心理学、血型、星座` |
256
+ | [高中理科学习助手](https://chat-preview.lobehub.com/market?agent=highschool-master)<br/><sup>By **[cnliucheng](https://github.com/cnliucheng)** on **2024-04-13**</sup> | 我是一个专门为中国高中生设计的学习辅助 AI。无论你在物理、化学、数学还是生物上遇到难题,我都能为你提供详细的解答和解析。不仅如此,我还能根据你的学习进度,推荐合适的练习题,帮助我巩固知识,提升学习效率。同时,我会尽可能使用 LaTeX 格式来呈现解题过程和公式<br/>`高中学习` `理科辅助` `题目解答` `学习进度` `la-te-x` |
257
+
258
+ > 📊 Total agents: [<kbd>**240**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
259
259
 
260
260
  <!-- AGENT LIST -->
261
261
 
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "تحديث معلومات المساعد",
120
120
  "upload": {
121
+ "actionFiletip": "تحميل المستند",
121
122
  "actionTooltip": "تحميل الصورة",
122
123
  "disabled": "النموذج الحالي لا يدعم التعرف على الرؤية، يرجى تغيير النموذج المستخدم",
123
124
  "dragDesc": "اسحب الملفات إلى هنا، يدعم تحميل عدة صور. اضغط على Shift لإرسال الصور مباشرة",
125
+ "dragFileDesc": "اسحب الصور والملفات هنا لدعم تحميل صور وملفات متعددة. اضغط باستمرار على Shift لإرسال صورة أو ملف مباشرة",
126
+ "dragFileTitle": "تحميل المستند",
124
127
  "dragTitle": "تحميل الصورة"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "Актуализирай информацията за агента",
120
120
  "upload": {
121
+ "actionFiletip": "Загрузите файл",
121
122
  "actionTooltip": "Качи изображение",
122
123
  "disabled": "Текущият модел не поддържа визуално разпознаване. Моля, превключи моделите, за да използваш тази функция.",
123
124
  "dragDesc": "Плъзни и пусни файлове тук, поддържа се качване на няколко изображения. Задръж Shift, за да изпратиш изображения директно.",
125
+ "dragFileDesc": "Перетащите сюда изображения и файлы, чтобы можно было загрузить несколько изображений и файлов. Удерживайте нажатой клавишу Shift, чтобы отправить изображение или файл напрямую.",
126
+ "dragFileTitle": "Загрузите файл",
124
127
  "dragTitle": "Качи изображение"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "Assistenteninformationen aktualisieren",
120
120
  "upload": {
121
+ "actionFiletip": "Laden Sie die Datei hoch",
121
122
  "actionTooltip": "Bild hochladen",
122
123
  "disabled": "Das aktuelle Modell unterstützt keine visuelle Erkennung. Bitte wechseln Sie das Modell, um es zu verwenden.",
123
124
  "dragDesc": "Dateien hierher ziehen, um mehrere Bilder hochzuladen. Halte die Umschalttaste gedrückt, um Bilder direkt zu senden.",
125
+ "dragFileDesc": "Ziehen Sie Bilder und Dateien hierher, um das Hochladen mehrerer Bilder und Dateien zu unterstützen. Halten Sie die Umschalttaste gedrückt, um ein Bild oder eine Datei direkt zu senden.",
126
+ "dragFileTitle": "Laden Sie die Datei hoch",
124
127
  "dragTitle": "Bild hochladen"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "Update Agent Information",
120
120
  "upload": {
121
+ "actionFiletip": "Update File",
121
122
  "actionTooltip": "Upload Image",
122
123
  "disabled": "The current model does not support visual recognition. Please switch models to use this feature.",
123
124
  "dragDesc": "Drag and drop files here, support uploading multiple images. Hold down Shift to send images directly.",
125
+ "dragFileDesc": "Drag pictures and files here to support uploading multiple pictures and files. Hold down Shift to send an image or file directly.",
126
+ "dragFileTitle": "Update File",
124
127
  "dragTitle": "Upload Image"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "Actualizar información del asistente",
120
120
  "upload": {
121
+ "actionFiletip": "Sube el archivo",
121
122
  "actionTooltip": "Subir imagen",
122
123
  "disabled": "El modelo actual no admite reconocimiento visual. Por favor, cambia de modelo para usar esta función",
123
124
  "dragDesc": "Arrastra y suelta archivos aquí, admite la carga de varias imágenes. Mantén presionada la tecla Shift para enviar las imágenes directamente.",
125
+ "dragFileDesc": "Arrastre imágenes y archivos aquí para admitir la carga de varias imágenes y archivos. Mantenga presionada la tecla Mayús para enviar una imagen o un archivo directamente.",
126
+ "dragFileTitle": "Sube el archivo",
124
127
  "dragTitle": "Subir imagen"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "Mettre à jour les informations de l'agent",
120
120
  "upload": {
121
+ "actionFiletip": "Télécharger le fichier",
121
122
  "actionTooltip": "Télécharger une image",
122
123
  "disabled": "Le modèle actuel ne prend pas en charge la reconnaissance visuelle. Veuillez changer de modèle pour utiliser cette fonctionnalité.",
123
124
  "dragDesc": "Faites glisser les fichiers ici pour télécharger plusieurs images. Maintenez la touche Maj enfoncée pour envoyer directement les images.",
125
+ "dragFileDesc": "Faites glisser les images et les fichiers ici pour prendre en charge le téléchargement de plusieurs images et fichiers. Maintenez la touche Shift enfoncée pour envoyer directement une image ou un fichier.",
126
+ "dragFileTitle": "Télécharger le fichier",
124
127
  "dragTitle": "Télécharger une image"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "Aggiorna informazioni assistente",
120
120
  "upload": {
121
+ "actionFiletip": "Carica il file",
121
122
  "actionTooltip": "Carica immagine",
122
123
  "disabled": "Il modello attuale non supporta il riconoscimento visivo, si prega di cambiare modello prima di utilizzarlo",
123
124
  "dragDesc": "Trascina i file qui, supporta il caricamento di più immagini. Tieni premuto Shift per inviare direttamente le immagini",
125
+ "dragFileDesc": "Trascina qui immagini e file per supportare il caricamento di più immagini e file. Tieni premuto Maiusc per inviare direttamente un'immagine o un file",
126
+ "dragFileTitle": "Carica il file",
124
127
  "dragTitle": "Carica immagine"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "エージェント情報を更新",
120
120
  "upload": {
121
+ "actionFiletip": "ファイルをアップロードする",
121
122
  "actionTooltip": "画像をアップロード",
122
123
  "disabled": "現在のモデルはビジュアル認識をサポートしていません。モデルを切り替えて使用してください",
123
124
  "dragDesc": "ファイルをここにドラッグしてください。複数の画像をアップロードできます。Shift キーを押しながら画像を直接送信することもできます。",
125
+ "dragFileDesc": "複数の写真やファイルのアップロードをサポートするには、ここに写真やファイルをドラッグします。 Shift キーを押しながら画像またはファイルを直接送信します。",
126
+ "dragFileTitle": "ファイルをアップロードする",
124
127
  "dragTitle": "画像をアップロード"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "도우미 정보 업데이트",
120
120
  "upload": {
121
+ "actionFiletip": "파일 업로드",
121
122
  "actionTooltip": "이미지 업로드",
122
123
  "disabled": "현재 모델은 시각 인식을 지원하지 않습니다. 모델을 전환한 후 사용해주세요.",
123
124
  "dragDesc": "여기로 파일을 끌어다 놓거나 여러 이미지를 업로드할 수 있습니다. Shift를 누른 채로 이미지를 직접 보내기",
125
+ "dragFileDesc": "여러 사진과 파일 업로드를 지원하려면 여기로 사진과 파일을 드래그하세요. 이미지나 파일을 직접 보내려면 Shift 키를 누르세요",
126
+ "dragFileTitle": "파일 업로드",
124
127
  "dragTitle": "이미지 업로드"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "Assistentgegevens bijwerken",
120
120
  "upload": {
121
+ "actionFiletip": "Upload het bestand",
121
122
  "actionTooltip": "Upload afbeelding",
122
123
  "disabled": "Het huidige model ondersteunt geen visuele herkenning. Schakel over naar een ander model om dit te gebruiken.",
123
124
  "dragDesc": "Sleep bestanden hierheen om meerdere afbeeldingen te uploaden. Houd Shift ingedrukt om afbeeldingen direct te verzenden",
125
+ "dragFileDesc": "Sleep afbeeldingen en bestanden hierheen om het uploaden van meerdere afbeeldingen en bestanden te ondersteunen. Houd Shift ingedrukt om een ​​afbeelding of bestand rechtstreeks te verzenden",
126
+ "dragFileTitle": "Upload het bestand",
124
127
  "dragTitle": "Upload afbeelding"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "Aktualizuj informacje asystenta",
120
120
  "upload": {
121
+ "actionFiletip": "Prześlij plik",
121
122
  "actionTooltip": "Prześlij obraz",
122
123
  "disabled": "Obecny model nie obsługuje rozpoznawania wizyjnego. Proszę przełączyć model.",
123
124
  "dragDesc": "Przeciągnij pliki tutaj, obsługiwane jest przesyłanie wielu obrazów. Naciśnij klawisz Shift, aby wysłać obraz bezpośrednio",
125
+ "dragFileDesc": "Przeciągnij zdjęcia i pliki tutaj, aby umożliwić przesyłanie wielu zdjęć i plików. Przytrzymaj klawisz Shift, aby bezpośrednio wysłać obraz lub plik",
126
+ "dragFileTitle": "Prześlij plik",
124
127
  "dragTitle": "Prześlij obraz"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "Atualizar Informações do Assistente",
120
120
  "upload": {
121
+ "actionFiletip": "Enviar arquivo",
121
122
  "actionTooltip": "Enviar Imagem",
122
123
  "disabled": "O modelo atual não suporta reconhecimento visual. Por favor, altere o modelo antes de usar.",
123
124
  "dragDesc": "Arraste os arquivos aqui, suporta o envio de várias imagens. Pressione Shift para enviar as imagens diretamente.",
125
+ "dragFileDesc": "Arraste fotos e arquivos aqui para suportar o upload de várias fotos e arquivos. Mantenha pressionada a tecla Shift para enviar uma imagem ou arquivo diretamente.",
126
+ "dragFileTitle": "Enviar arquivo",
124
127
  "dragTitle": "Enviar Imagem"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "Обновить информацию помощника",
120
120
  "upload": {
121
+ "actionFiletip": "Загрузите файл",
121
122
  "actionTooltip": "Загрузить изображение",
122
123
  "disabled": "Текущая модель не поддерживает визуальное распознавание. Пожалуйста, выберите другую модель.",
123
124
  "dragDesc": "Перетащите файлы сюда, поддерживается загрузка нескольких изображений. Удерживайте Shift для отправки изображений",
125
+ "dragFileDesc": "Перетащите сюда изображения и файлы, чтобы можно было загрузить несколько изображений и файлов. Удерживайте нажатой клавишу Shift, чтобы отправить изображение или файл напрямую",
126
+ "dragFileTitle": "Загрузите файл",
124
127
  "dragTitle": "Загрузить изображение"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "Asistan Bilgilerini Güncelle",
120
120
  "upload": {
121
+ "actionFiletip": "Dosyayı Yükle",
121
122
  "actionTooltip": "Resim Yükle",
122
123
  "disabled": "Geçerli model görüntü tanıma desteğini desteklemiyor, lütfen modeli değiştirerek kullanın",
123
124
  "dragDesc": "Dosyaları buraya sürükleyip bırakın, birden fazla resim yüklemeyi destekler. Resimleri doğrudan göndermek için Shift tuşunu basılı tutun.",
125
+ "dragFileDesc": "Birden fazla resim ve dosya yüklemeyi desteklemek için resimleri ve dosyaları buraya sürükleyin. Bir görüntüyü veya dosyayı doğrudan göndermek için Shift tuşunu basılı tutun.",
126
+ "dragFileTitle": "Dosyayı Yükle",
124
127
  "dragTitle": "Resim Yükle"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "Cập nhật thông tin trợ lý",
120
120
  "upload": {
121
+ "actionFiletip": "Tải lên tập tin",
121
122
  "actionTooltip": "Tải lên hình ảnh",
122
123
  "disabled": "Mô hình hiện tại không hỗ trợ nhận diện hình ảnh, vui lòng chuyển đổi mô hình trước khi sử dụng",
123
124
  "dragDesc": "Kéo và thả tệp vào đây, hỗ trợ tải lên nhiều hình ảnh. Giữ phím Shift để gửi hình ảnh trực tiếp",
125
+ "dragFileDesc": "Kéo ảnh và tệp vào đây để hỗ trợ tải lên nhiều ảnh và tệp. Giữ phím Shift để gửi hình ảnh hoặc tập tin trực tiếp",
126
+ "dragFileTitle": "Tải lên tập tin",
124
127
  "dragTitle": "Tải lên hình ảnh"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "更新助理信息",
120
120
  "upload": {
121
+ "actionFiletip": "上传文件",
121
122
  "actionTooltip": "上传图片",
122
- "disabled": "当前模型不支持视觉识别,请切换模型后使用",
123
+ "disabled": "当前模型不支持视觉识别和文件分析,请切换模型后使用",
123
124
  "dragDesc": "拖拽文件到这里,支持上传多个图片。按住 Shift 直接发送图片",
125
+ "dragFileDesc": "拖拽图片和文件到这里,支持上传多个图片和文件。按住 Shift 直接发送图片或文件",
126
+ "dragFileTitle": "上传文件",
124
127
  "dragTitle": "上传图片"
125
128
  }
126
129
  }
@@ -118,9 +118,12 @@
118
118
  },
119
119
  "updateAgent": "更新助理信息",
120
120
  "upload": {
121
+ "actionFiletip": "上傳文件",
121
122
  "actionTooltip": "上傳圖片",
122
123
  "disabled": "當前模型不支援視覺識別,請切換模型後使用",
123
124
  "dragDesc": "拖拽文件到這裡,支持上傳多個圖片。按住 Shift 直接發送圖片",
125
+ "dragFileDesc": "拖曳圖片和文件至此,支援上傳多張圖片和文件。按住 Shift 直接傳送圖片或文件",
126
+ "dragFileTitle": "上傳文件",
124
127
  "dragTitle": "上傳圖片"
125
128
  }
126
129
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.147.20",
3
+ "version": "0.147.22",
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",
@@ -86,7 +86,7 @@
86
86
  "@aws-sdk/client-bedrock-runtime": "^3.549.0",
87
87
  "@azure/openai": "^1.0.0-beta.12",
88
88
  "@cfworker/json-schema": "^1.12.8",
89
- "@google/generative-ai": "^0.5.0",
89
+ "@google/generative-ai": "^0.7.0",
90
90
  "@icons-pack/react-simple-icons": "^9.4.0",
91
91
  "@lobehub/chat-plugin-sdk": "latest",
92
92
  "@lobehub/chat-plugins-gateway": "latest",
Binary file
Binary file
@@ -151,7 +151,7 @@ const DragUpload = memo(() => {
151
151
  window.removeEventListener('drop', handleDrop);
152
152
  window.removeEventListener('paste', handlePaste);
153
153
  };
154
- }, [handleDrop, handlePaste]);
154
+ }, [handleDragEnter, handleDragOver, handleDragLeave, handleDrop, handlePaste]);
155
155
 
156
156
  return (
157
157
  isDragging && (
@@ -164,8 +164,8 @@ const DragUpload = memo(() => {
164
164
  <Icon icon={FileText} size={{ fontSize: 64, strokeWidth: 1 }} />
165
165
  </Flexbox>
166
166
  <Flexbox align={'center'} gap={8} style={{ textAlign: 'center' }}>
167
- <Flexbox className={styles.title}>{t('upload.dragTitle')}</Flexbox>
168
- <Flexbox className={styles.desc}>{t('upload.dragDesc')}</Flexbox>
167
+ <Flexbox className={styles.title}>{t(enabledFiles ? 'upload.dragFileTitle' : 'upload.dragTitle')}</Flexbox>
168
+ <Flexbox className={styles.desc}>{t(enabledFiles ? 'upload.dragFileDesc' : 'upload.dragDesc')}</Flexbox>
169
169
  </Flexbox>
170
170
  </Center>
171
171
  </div>
@@ -114,8 +114,8 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
114
114
  modal.confirm({
115
115
  centered: true,
116
116
  okButtonProps: { danger: true },
117
- onOk: () => {
118
- removeTopic(id);
117
+ onOk: async () => {
118
+ await removeTopic(id);
119
119
  },
120
120
  title: t('topic.confirmRemoveTopic', { ns: 'chat' }),
121
121
  });
@@ -22,11 +22,9 @@ const metadata: Metadata = {
22
22
  },
23
23
  description,
24
24
  icons: {
25
- apple:
26
- 'https://registry.npmmirror.com/@lobehub/assets-favicons/latest/files/assets/apple-touch-icon.png',
27
- icon: 'https://registry.npmmirror.com/@lobehub/assets-favicons/latest/files/assets/favicon-32x32.png',
28
- shortcut:
29
- 'https://registry.npmmirror.com/@lobehub/assets-favicons/latest/files/assets/favicon.ico',
25
+ apple:'icons/apple-touch-icon.png',
26
+ icon:'favicon.ico',
27
+ shortcut:'favicon-32x32.ico',
30
28
  },
31
29
  manifest: noManifest ? undefined : '/manifest.json',
32
30
  metadataBase: new URL(SITE_URL),
@@ -1,7 +1,7 @@
1
1
  import { ActionIcon, Icon } from '@lobehub/ui';
2
2
  import { Upload } from 'antd';
3
3
  import { useTheme } from 'antd-style';
4
- import { LucideImage, LucideLoader2 } from 'lucide-react';
4
+ import { LucideImage, FileUp, LucideLoader2 } from 'lucide-react';
5
5
  import { memo, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Center } from 'react-layout-kit';
@@ -51,9 +51,9 @@ const FileUpload = memo(() => {
51
51
  ) : (
52
52
  <ActionIcon
53
53
  disable={!canUpload}
54
- icon={LucideImage}
54
+ icon={enabledFiles ? FileUp : LucideImage}
55
55
  placement={'bottom'}
56
- title={t(canUpload ? 'upload.actionTooltip' : 'upload.disabled')}
56
+ title={t(canUpload ? (enabledFiles ? 'upload.actionFiletip' : 'upload.actionTooltip') : 'upload.disabled')}
57
57
  />
58
58
  )}
59
59
  </Upload>
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import HotKeys from '@/components/HotKeys';
9
9
  import { PREFIX_KEY, SAVE_TOPIC_KEY } from '@/const/hotkeys';
10
+ import { useActionSWR } from '@/libs/swr';
10
11
  import { useChatStore } from '@/store/chat';
11
12
 
12
13
  const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
@@ -16,20 +17,23 @@ const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
16
17
  s.openNewTopicOrSaveTopic,
17
18
  ]);
18
19
 
20
+ const { mutate, isValidating } = useActionSWR('openNewTopicOrSaveTopic', openNewTopicOrSaveTopic);
21
+
19
22
  const icon = hasTopic ? LucideMessageSquarePlus : LucideGalleryVerticalEnd;
20
23
  const Render = mobile ? ActionIcon : Button;
21
24
  const iconRender: any = mobile ? icon : <Icon icon={icon} />;
22
25
  const desc = t(hasTopic ? 'topic.openNewTopic' : 'topic.saveCurrentMessages');
23
26
 
24
27
  const hotkeys = [PREFIX_KEY, SAVE_TOPIC_KEY].join('+');
25
- useHotkeys(hotkeys, openNewTopicOrSaveTopic, {
28
+
29
+ useHotkeys(hotkeys, () => mutate(), {
26
30
  enableOnFormTags: true,
27
31
  preventDefault: true,
28
32
  });
29
33
 
30
34
  return (
31
35
  <Tooltip title={<HotKeys desc={desc} keys={hotkeys} />}>
32
- <Render aria-label={desc} icon={iconRender} onClick={openNewTopicOrSaveTopic} />
36
+ <Render aria-label={desc} icon={iconRender} loading={isValidating} onClick={() => mutate()} />
33
37
  </Tooltip>
34
38
  );
35
39
  });
@@ -19,6 +19,9 @@ import ActionsBar from './ActionsBar';
19
19
  import HistoryDivider from './HistoryDivider';
20
20
 
21
21
  const useStyles = createStyles(({ css, prefixCls }) => ({
22
+ loading: css`
23
+ opacity: 0.6;
24
+ `,
22
25
  message: css`
23
26
  // prevent the textarea too long
24
27
  .${prefixCls}-input {
@@ -35,7 +38,7 @@ export interface ChatListItemProps {
35
38
  const Item = memo<ChatListItemProps>(({ index, id }) => {
36
39
  const fontSize = useGlobalStore((s) => settingsSelectors.currentSettings(s).fontSize);
37
40
  const { t } = useTranslation('common');
38
- const { styles } = useStyles();
41
+ const { styles, cx } = useStyles();
39
42
  const [editing, setEditing] = useState(false);
40
43
  const [type = 'chat'] = useSessionStore((s) => {
41
44
  const config = agentSelectors.currentAgentConfig(s);
@@ -54,10 +57,12 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
54
57
  const historyLength = useChatStore((s) => chatSelectors.currentChats(s).length);
55
58
 
56
59
  const [loading, updateMessageContent] = useChatStore((s) => [
57
- s.chatLoadingId === id,
60
+ s.chatLoadingId === id || s.messageLoadingIds.includes(id),
58
61
  s.modifyMessageContent,
59
62
  ]);
60
63
 
64
+ const [isMessageLoading] = useChatStore((s) => [s.messageLoadingIds.includes(id)]);
65
+
61
66
  const onAvatarsClick = useAvatarsClick();
62
67
 
63
68
  const RenderMessage = useCallback(
@@ -110,7 +115,7 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
110
115
  <ChatItem
111
116
  actions={<ActionsBar index={index} setEditing={setEditing} />}
112
117
  avatar={item.meta}
113
- className={styles.message}
118
+ className={cx(styles.message, isMessageLoading && styles.loading)}
114
119
  editing={editing}
115
120
  error={error}
116
121
  errorMessage={<ErrorMessageExtra data={item} />}
@@ -32,3 +32,12 @@ export const useActionSWR: SWRHook = (key, fetch, config) =>
32
32
  revalidateOnReconnect: false,
33
33
  ...config,
34
34
  });
35
+
36
+ export interface SWRRefreshParams<T, A = (...args: any[]) => any> {
37
+ action: A;
38
+ optimisticData?: (data: T | undefined) => T;
39
+ }
40
+
41
+ export type SWRefreshMethod<T> = <A extends (...args: any[]) => Promise<any>>(
42
+ params?: SWRRefreshParams<T, A>,
43
+ ) => ReturnType<A>;
@@ -120,9 +120,12 @@ export default {
120
120
  },
121
121
  updateAgent: '更新助理信息',
122
122
  upload: {
123
+ actionFiletip: '上传文件',
123
124
  actionTooltip: '上传图片',
124
- disabled: '当前模型不支持视觉识别,请切换模型后使用',
125
+ disabled: '当前模型不支持视觉识别和文件分析,请切换模型后使用',
125
126
  dragDesc: '拖拽文件到这里,支持上传多个图片。按住 Shift 直接发送图片',
126
- dragTitle: '上传图片',
127
+ dragFileDesc: '拖拽图片和文件到这里,支持上传多个图片和文件。按住 Shift 直接发送图片或文件',
128
+ dragFileTitle: '上传文件',
129
+ dragTitle: '上传图片'
127
130
  },
128
131
  };
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
2
2
  // Disable the auto sort key eslint rule to make the code more logic and readable
3
3
  import { copyToClipboard } from '@lobehub/ui';
4
+ import { produce } from 'immer';
4
5
  import { template } from 'lodash-es';
5
6
  import { SWRResponse, mutate } from 'swr';
6
7
  import { StateCreator } from 'zustand/vanilla';
@@ -19,6 +20,7 @@ import { agentSelectors } from '@/store/session/selectors';
19
20
  import { ChatMessage } from '@/types/message';
20
21
  import { TraceEventPayloads } from '@/types/trace';
21
22
  import { setNamespace } from '@/utils/storeDebug';
23
+ import { nanoid } from '@/utils/uuid';
22
24
 
23
25
  import { chatSelectors } from '../../selectors';
24
26
  import { MessageDispatch, messagesReducer } from './reducer';
@@ -97,6 +99,7 @@ export interface ChatMessageAction {
97
99
  id?: string,
98
100
  action?: string,
99
101
  ) => AbortController | undefined;
102
+ toggleMessageLoading: (loading: boolean, id: string) => void;
100
103
  refreshMessages: () => Promise<void>;
101
104
  // TODO: 后续 smoothMessage 实现考虑落到 sse 这一层
102
105
  createSmoothMessage: (id: string) => {
@@ -111,6 +114,7 @@ export interface ChatMessageAction {
111
114
  * @param content
112
115
  */
113
116
  internalUpdateMessageContent: (id: string, content: string) => Promise<void>;
117
+ internalCreateMessage: (params: CreateMessageParams) => Promise<string>;
114
118
  internalResendMessage: (id: string, traceId?: string) => Promise<void>;
115
119
  internalTraceMessage: (id: string, payload: TraceEventPayloads) => Promise<void>;
116
120
  }
@@ -130,6 +134,7 @@ export const chatMessage: StateCreator<
130
134
  ChatMessageAction
131
135
  > = (set, get) => ({
132
136
  deleteMessage: async (id) => {
137
+ get().dispatchMessage({ type: 'deleteMessage', id });
133
138
  await messageService.removeMessage(id);
134
139
  await get().refreshMessages();
135
140
  },
@@ -167,43 +172,6 @@ export const chatMessage: StateCreator<
167
172
  await messageService.removeAllMessages();
168
173
  await refreshMessages();
169
174
  },
170
- internalResendMessage: async (messageId, traceId) => {
171
- // 1. 构造所有相关的历史记录
172
- const chats = chatSelectors.currentChats(get());
173
-
174
- const currentIndex = chats.findIndex((c) => c.id === messageId);
175
- if (currentIndex < 0) return;
176
-
177
- const currentMessage = chats[currentIndex];
178
-
179
- let contextMessages: ChatMessage[] = [];
180
-
181
- switch (currentMessage.role) {
182
- case 'function':
183
- case 'user': {
184
- contextMessages = chats.slice(0, currentIndex + 1);
185
- break;
186
- }
187
- case 'assistant': {
188
- // 消息是 AI 发出的因此需要找到它的 user 消息
189
- const userId = currentMessage.parentId;
190
- const userIndex = chats.findIndex((c) => c.id === userId);
191
- // 如果消息没有 parentId,那么同 user/function 模式
192
- contextMessages = chats.slice(0, userIndex < 0 ? currentIndex + 1 : userIndex + 1);
193
- break;
194
- }
195
- }
196
-
197
- if (contextMessages.length <= 0) return;
198
-
199
- const { coreProcessMessage } = get();
200
-
201
- const latestMsg = contextMessages.filter((s) => s.role === 'user').at(-1);
202
-
203
- if (!latestMsg) return;
204
-
205
- await coreProcessMessage(contextMessages, latestMsg.id, traceId);
206
- },
207
175
  sendMessage: async ({ message, files, onlyAddUserMessage }) => {
208
176
  const { coreProcessMessage, activeTopicId, activeId } = get();
209
177
  if (!activeId) return;
@@ -223,8 +191,7 @@ export const chatMessage: StateCreator<
223
191
  topicId: activeTopicId,
224
192
  };
225
193
 
226
- const id = await messageService.createMessage(newMessage);
227
- await get().refreshMessages();
194
+ const id = await get().internalCreateMessage(newMessage);
228
195
 
229
196
  // if only add user message, then stop
230
197
  if (onlyAddUserMessage) return;
@@ -315,8 +282,7 @@ export const chatMessage: StateCreator<
315
282
  topicId: activeTopicId, // if there is activeTopicId,then add it to topicId
316
283
  };
317
284
 
318
- const mid = await messageService.createMessage(assistantMessage);
319
- await refreshMessages();
285
+ const mid = await get().internalCreateMessage(assistantMessage);
320
286
 
321
287
  // 2. fetch the AI response
322
288
  const { isFunctionCall, content, functionCallAtEnd, functionCallContent, traceId } =
@@ -344,7 +310,7 @@ export const chatMessage: StateCreator<
344
310
  traceId,
345
311
  };
346
312
 
347
- functionId = await messageService.createMessage(functionMessage);
313
+ functionId = await get().internalCreateMessage(functionMessage);
348
314
  }
349
315
 
350
316
  await refreshMessages();
@@ -533,6 +499,62 @@ export const chatMessage: StateCreator<
533
499
  window.removeEventListener('beforeunload', preventLeavingFn);
534
500
  }
535
501
  },
502
+ toggleMessageLoading: (loading, id) => {
503
+ set(
504
+ {
505
+ messageLoadingIds: produce(get().messageLoadingIds, (draft) => {
506
+ if (loading) {
507
+ draft.push(id);
508
+ } else {
509
+ const index = draft.indexOf(id);
510
+
511
+ if (index >= 0) draft.splice(index, 1);
512
+ }
513
+ }),
514
+ },
515
+ false,
516
+ 'toggleMessageLoading',
517
+ );
518
+ },
519
+
520
+ internalResendMessage: async (messageId, traceId) => {
521
+ // 1. 构造所有相关的历史记录
522
+ const chats = chatSelectors.currentChats(get());
523
+
524
+ const currentIndex = chats.findIndex((c) => c.id === messageId);
525
+ if (currentIndex < 0) return;
526
+
527
+ const currentMessage = chats[currentIndex];
528
+
529
+ let contextMessages: ChatMessage[] = [];
530
+
531
+ switch (currentMessage.role) {
532
+ case 'function':
533
+ case 'user': {
534
+ contextMessages = chats.slice(0, currentIndex + 1);
535
+ break;
536
+ }
537
+ case 'assistant': {
538
+ // 消息是 AI 发出的因此需要找到它的 user 消息
539
+ const userId = currentMessage.parentId;
540
+ const userIndex = chats.findIndex((c) => c.id === userId);
541
+ // 如果消息没有 parentId,那么同 user/function 模式
542
+ contextMessages = chats.slice(0, userIndex < 0 ? currentIndex + 1 : userIndex + 1);
543
+ break;
544
+ }
545
+ }
546
+
547
+ if (contextMessages.length <= 0) return;
548
+
549
+ const { coreProcessMessage } = get();
550
+
551
+ const latestMsg = contextMessages.filter((s) => s.role === 'user').at(-1);
552
+
553
+ if (!latestMsg) return;
554
+
555
+ await coreProcessMessage(contextMessages, latestMsg.id, traceId);
556
+ },
557
+
536
558
  internalUpdateMessageContent: async (id, content) => {
537
559
  const { dispatchMessage, refreshMessages } = get();
538
560
 
@@ -545,6 +567,22 @@ export const chatMessage: StateCreator<
545
567
  await refreshMessages();
546
568
  },
547
569
 
570
+ internalCreateMessage: async (message) => {
571
+ const { dispatchMessage, refreshMessages, toggleMessageLoading } = get();
572
+
573
+ // use optimistic update to avoid the slow waiting
574
+ const tempId = 'tmp_' + nanoid();
575
+ dispatchMessage({ type: 'createMessage', id: tempId, value: message });
576
+
577
+ toggleMessageLoading(true, tempId);
578
+ const id = await messageService.createMessage(message);
579
+
580
+ await refreshMessages();
581
+ toggleMessageLoading(false, tempId);
582
+
583
+ return id;
584
+ },
585
+
548
586
  createSmoothMessage: (id) => {
549
587
  const { dispatchMessage } = get();
550
588
 
@@ -9,7 +9,7 @@ export interface ChatMessageState {
9
9
  activeId: string;
10
10
  chatLoadingId?: string;
11
11
  inputMessage: string;
12
- messageLoadingIds: [];
12
+ messageLoadingIds: string[];
13
13
  messages: ChatMessage[];
14
14
  /**
15
15
  * whether messages have fetched
@@ -1,6 +1,7 @@
1
1
  import isEqual from 'fast-deep-equal';
2
2
  import { produce } from 'immer';
3
3
 
4
+ import { CreateMessageParams } from '@/services/message';
4
5
  import { ChatMessage } from '@/types/message';
5
6
  import { merge } from '@/utils/merge';
6
7
 
@@ -10,6 +11,15 @@ interface UpdateMessage {
10
11
  type: 'updateMessage';
11
12
  value: ChatMessage[keyof ChatMessage];
12
13
  }
14
+ interface CreateMessage {
15
+ id: string;
16
+ type: 'createMessage';
17
+ value: CreateMessageParams;
18
+ }
19
+ interface DeleteMessage {
20
+ id: string;
21
+ type: 'deleteMessage';
22
+ }
13
23
 
14
24
  interface UpdatePluginState {
15
25
  id: string;
@@ -24,7 +34,12 @@ interface UpdateMessageExtra {
24
34
  value: any;
25
35
  }
26
36
 
27
- export type MessageDispatch = UpdateMessage | UpdatePluginState | UpdateMessageExtra;
37
+ export type MessageDispatch =
38
+ | CreateMessage
39
+ | UpdateMessage
40
+ | UpdatePluginState
41
+ | UpdateMessageExtra
42
+ | DeleteMessage;
28
43
 
29
44
  export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch): ChatMessage[] => {
30
45
  switch (payload.type) {
@@ -76,6 +91,22 @@ export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch):
76
91
  });
77
92
  }
78
93
 
94
+ case 'createMessage': {
95
+ return produce(state, (draftState) => {
96
+ const { value, id } = payload;
97
+
98
+ draftState.push({ ...value, createdAt: Date.now(), id, meta: {}, updatedAt: Date.now() });
99
+ });
100
+ }
101
+ case 'deleteMessage': {
102
+ return produce(state, (draft) => {
103
+ const { id } = payload;
104
+
105
+ const index = draft.findIndex((m) => m.id === id);
106
+
107
+ if (index >= 0) draft.splice(index, 1);
108
+ });
109
+ }
79
110
  default: {
80
111
  throw new Error('暂未实现的 type,请检查 reducer');
81
112
  }
@@ -149,7 +149,9 @@ describe('topic action', () => {
149
149
  });
150
150
 
151
151
  // Check if mutate has been called with the active session ID
152
- expect(mutate).toHaveBeenCalledWith(['SWR_USE_FETCH_TOPIC', activeId]);
152
+ expect(mutate).toHaveBeenCalledWith(['SWR_USE_FETCH_TOPIC', activeId], undefined, {
153
+ populateCache: false,
154
+ });
153
155
  });
154
156
 
155
157
  it('should handle errors during refreshing topics', async () => {
@@ -314,7 +316,7 @@ describe('topic action', () => {
314
316
  const activeId = 'test-session-id';
315
317
 
316
318
  await act(async () => {
317
- useChatStore.setState({ activeId });
319
+ useChatStore.setState({ activeId, activeTopicId: topicId });
318
320
  });
319
321
 
320
322
  const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
@@ -329,6 +331,27 @@ describe('topic action', () => {
329
331
  expect(refreshTopicSpy).toHaveBeenCalled();
330
332
  expect(switchTopicSpy).toHaveBeenCalled();
331
333
  });
334
+ it('should remove a specific topic and its messages, then not refresh the topic list', async () => {
335
+ const topicId = 'topic-1';
336
+ const { result } = renderHook(() => useChatStore());
337
+ const activeId = 'test-session-id';
338
+
339
+ await act(async () => {
340
+ useChatStore.setState({ activeId });
341
+ });
342
+
343
+ const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
344
+ const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
345
+
346
+ await act(async () => {
347
+ await result.current.removeTopic(topicId);
348
+ });
349
+
350
+ expect(messageService.removeMessages).toHaveBeenCalledWith(activeId, topicId);
351
+ expect(topicService.removeTopic).toHaveBeenCalledWith(topicId);
352
+ expect(refreshTopicSpy).toHaveBeenCalled();
353
+ expect(switchTopicSpy).not.toHaveBeenCalled();
354
+ });
332
355
  });
333
356
  describe('removeUnstarredTopic', () => {
334
357
  it('should remove unstarred topics and refresh the topic list', async () => {
@@ -9,11 +9,11 @@ import { StateCreator } from 'zustand/vanilla';
9
9
  import { chainSummaryTitle } from '@/chains/summaryTitle';
10
10
  import { LOADING_FLAT } from '@/const/message';
11
11
  import { TraceNameMap } from '@/const/trace';
12
- import { useClientDataSWR } from '@/libs/swr';
12
+ import { SWRefreshMethod, useClientDataSWR } from '@/libs/swr';
13
13
  import { chatService } from '@/services/chat';
14
14
  import { messageService } from '@/services/message';
15
15
  import { topicService } from '@/services/topic';
16
- import { ChatStore } from '@/store/chat';
16
+ import type { ChatStore } from '@/store/chat';
17
17
  import { ChatMessage } from '@/types/message';
18
18
  import { ChatTopic } from '@/types/topic';
19
19
  import { setNamespace } from '@/utils/storeDebug';
@@ -29,7 +29,7 @@ const SWR_USE_SEARCH_TOPIC = 'SWR_USE_SEARCH_TOPIC';
29
29
  export interface ChatTopicAction {
30
30
  favoriteTopic: (id: string, favState: boolean) => Promise<void>;
31
31
  openNewTopicOrSaveTopic: () => Promise<void>;
32
- refreshTopic: () => Promise<void>;
32
+ refreshTopic: SWRefreshMethod<ChatTopic[]>;
33
33
  removeAllTopics: () => Promise<void>;
34
34
  removeSessionTopics: () => Promise<void>;
35
35
  removeTopic: (id: string) => Promise<void>;
@@ -78,6 +78,17 @@ export const chatTopic: StateCreator<
78
78
  messages: messages.map((m) => m.id),
79
79
  });
80
80
  await refreshTopic();
81
+ // TODO: 优化为乐观更新
82
+ // const params: CreateTopicParams = {
83
+ // sessionId: activeId,
84
+ // title: t('topic.defaultTitle', { ns: 'chat' }),
85
+ // messages: messages.map((m) => m.id),
86
+ // };
87
+
88
+ // const topicId = await refreshTopic({
89
+ // action: async () => topicService.createTopic(params),
90
+ // optimisticData: (data) => topicReducer(data, { type: 'addTopic', value: params }),
91
+ // });
81
92
 
82
93
  // 2. auto summary topic Title
83
94
  // we don't need to wait for summary, just let it run async
@@ -189,9 +200,10 @@ export const chatTopic: StateCreator<
189
200
  await refreshTopic();
190
201
  },
191
202
  removeTopic: async (id) => {
192
- const { activeId, switchTopic, refreshTopic } = get();
203
+ const { activeId, activeTopicId, switchTopic, refreshTopic } = get();
193
204
 
194
205
  // remove messages in the topic
206
+ // TODO: Need to remove because server service don't need to call it
195
207
  await messageService.removeMessages(activeId, id);
196
208
 
197
209
  // remove topic
@@ -199,7 +211,7 @@ export const chatTopic: StateCreator<
199
211
  await refreshTopic();
200
212
 
201
213
  // switch bach to default topic
202
- switchTopic();
214
+ if (activeTopicId === id) switchTopic();
203
215
  },
204
216
  removeUnstarredTopic: async () => {
205
217
  const { refreshTopic, switchTopic } = get();
@@ -226,7 +238,12 @@ export const chatTopic: StateCreator<
226
238
  updateTopicLoading: (id) => {
227
239
  set({ topicLoadingId: id }, false, n('updateTopicLoading'));
228
240
  },
229
- refreshTopic: async () => {
230
- await mutate([SWR_USE_FETCH_TOPIC, get().activeId]);
241
+ // TODO: I don't know why this ts error, so have to ignore it
242
+ // @ts-ignore
243
+ refreshTopic: async (params) => {
244
+ return mutate([SWR_USE_FETCH_TOPIC, get().activeId], params?.action, {
245
+ optimisticData: params?.optimisticData,
246
+ populateCache: false,
247
+ });
231
248
  },
232
249
  });
@@ -0,0 +1,141 @@
1
+ import { produce } from 'immer';
2
+ import { expect } from 'vitest';
3
+
4
+ import { ChatTopic } from '@/types/topic';
5
+
6
+ import { ChatTopicDispatch, topicReducer } from './reducer';
7
+
8
+ describe('topicReducer', () => {
9
+ let state: ChatTopic[];
10
+
11
+ beforeEach(() => {
12
+ state = [];
13
+ });
14
+
15
+ describe('addTopic', () => {
16
+ it('should add a new ChatTopic object to state', () => {
17
+ const payload: ChatTopicDispatch = {
18
+ type: 'addTopic',
19
+ value: {
20
+ title: 'Test Topic',
21
+ sessionId: '',
22
+ },
23
+ };
24
+
25
+ const newState = topicReducer(state, payload);
26
+
27
+ expect(newState[0].id).toBeDefined();
28
+ });
29
+ });
30
+
31
+ describe('updateTopic', () => {
32
+ it('should update the ChatTopic object in state', () => {
33
+ const topic: ChatTopic = {
34
+ id: '1',
35
+ title: 'Test Topic',
36
+ createdAt: Date.now(),
37
+ updatedAt: Date.now(),
38
+ };
39
+
40
+ state.push(topic);
41
+
42
+ const payload: ChatTopicDispatch = {
43
+ type: 'updateTopic',
44
+ id: '1',
45
+ key: 'title',
46
+ value: 'Updated Topic',
47
+ };
48
+
49
+ const newState = topicReducer(state, payload);
50
+
51
+ expect(newState[0].title).toBe('Updated Topic');
52
+ });
53
+
54
+ it('should update the ChatTopic object with correct properties', () => {
55
+ const topic: ChatTopic = {
56
+ id: '1',
57
+ title: 'Test Topic',
58
+ createdAt: Date.now() - 1,
59
+ updatedAt: Date.now() - 1, // 设定比当前时间前面一点
60
+ };
61
+
62
+ state.push(topic);
63
+
64
+ const payload: ChatTopicDispatch = {
65
+ type: 'updateTopic',
66
+ id: '1',
67
+ key: 'title',
68
+ value: 'Updated Topic',
69
+ };
70
+
71
+ const newState = topicReducer(state, payload);
72
+
73
+ expect(newState[0].updatedAt).toBeGreaterThan(topic.updatedAt);
74
+ });
75
+ });
76
+
77
+ describe('deleteTopic', () => {
78
+ it('should delete the specified ChatTopic object from state', () => {
79
+ const topic: ChatTopic = {
80
+ id: '1',
81
+ title: 'Test Topic',
82
+ createdAt: Date.now(),
83
+ updatedAt: Date.now(),
84
+ };
85
+
86
+ state.push(topic);
87
+
88
+ const payload: ChatTopicDispatch = {
89
+ type: 'deleteTopic',
90
+ id: '1',
91
+ };
92
+
93
+ const newState = topicReducer(state, payload);
94
+
95
+ expect(newState).toEqual([]);
96
+ });
97
+ });
98
+
99
+ describe('default', () => {
100
+ it('should return the original state object', () => {
101
+ const payload = {
102
+ type: 'unknown',
103
+ } as unknown as ChatTopicDispatch;
104
+
105
+ const newState = topicReducer(state, payload);
106
+
107
+ expect(newState).toBe(state);
108
+ });
109
+ });
110
+
111
+ describe('produce', () => {
112
+ it('should generate immutable state object', () => {
113
+ const payload: ChatTopicDispatch = {
114
+ type: 'addTopic',
115
+ value: {
116
+ title: 'Test Topic',
117
+ sessionId: '1',
118
+ },
119
+ };
120
+
121
+ const newState = topicReducer(state, payload);
122
+
123
+ expect(newState).not.toBe(state);
124
+ });
125
+
126
+ it('should not modify the original state object', () => {
127
+ const payload: ChatTopicDispatch = {
128
+ type: 'addTopic',
129
+ value: {
130
+ title: 'Test Topic',
131
+
132
+ sessionId: '123',
133
+ },
134
+ };
135
+
136
+ const newState = topicReducer(state, payload);
137
+
138
+ expect(state).toEqual([]);
139
+ });
140
+ });
141
+ });
@@ -0,0 +1,67 @@
1
+ import { produce } from 'immer';
2
+
3
+ import { CreateTopicParams } from '@/services/topic/type';
4
+ import { ChatTopic } from '@/types/topic';
5
+
6
+ interface AddChatTopicAction {
7
+ type: 'addTopic';
8
+ value: CreateTopicParams;
9
+ }
10
+
11
+ interface UpdateChatTopicAction {
12
+ id: string;
13
+ key: keyof ChatTopic;
14
+ type: 'updateTopic';
15
+ value: any;
16
+ }
17
+
18
+ interface DeleteChatTopicAction {
19
+ id: string;
20
+ type: 'deleteTopic';
21
+ }
22
+
23
+ export type ChatTopicDispatch = AddChatTopicAction | UpdateChatTopicAction | DeleteChatTopicAction;
24
+
25
+ export const topicReducer = (state: ChatTopic[] = [], payload: ChatTopicDispatch): ChatTopic[] => {
26
+ switch (payload.type) {
27
+ case 'addTopic': {
28
+ return produce(state, (draftState) => {
29
+ draftState.unshift({
30
+ ...payload.value,
31
+ createdAt: Date.now(),
32
+ id: Date.now().toString(),
33
+ sessionId: payload.value.sessionId ? payload.value.sessionId : undefined,
34
+ updatedAt: Date.now(),
35
+ });
36
+ });
37
+ }
38
+
39
+ case 'updateTopic': {
40
+ return produce(state, (draftState) => {
41
+ const { key, value, id } = payload;
42
+ const topicIndex = draftState.findIndex((topic) => topic.id === id);
43
+
44
+ if (topicIndex !== -1) {
45
+ const updatedTopic = { ...draftState[topicIndex] };
46
+ // @ts-ignore
47
+ updatedTopic[key] = value;
48
+ draftState[topicIndex] = updatedTopic;
49
+ updatedTopic.updatedAt = Date.now();
50
+ }
51
+ });
52
+ }
53
+
54
+ case 'deleteTopic': {
55
+ return produce(state, (draftState) => {
56
+ const topicIndex = draftState.findIndex((topic) => topic.id === payload.id);
57
+ if (topicIndex !== -1) {
58
+ draftState.splice(topicIndex, 1);
59
+ }
60
+ });
61
+ }
62
+
63
+ default: {
64
+ return state;
65
+ }
66
+ }
67
+ };
@@ -6,7 +6,7 @@ import { StateCreator } from 'zustand/vanilla';
6
6
 
7
7
  import { message } from '@/components/AntdStaticMethods';
8
8
  import { INBOX_SESSION_ID } from '@/const/session';
9
- import { useClientDataSWR } from '@/libs/swr';
9
+ import { SWRRefreshParams, useClientDataSWR } from '@/libs/swr';
10
10
  import { sessionService } from '@/services/session';
11
11
  import { useGlobalStore } from '@/store/global';
12
12
  import { settingsSelectors } from '@/store/global/selectors';
@@ -51,10 +51,7 @@ export interface SessionAction {
51
51
  /**
52
52
  * re-fetch the data
53
53
  */
54
- refreshSessions: (params?: {
55
- action: () => Promise<void>;
56
- optimisticData?: (data: ChatSessionList) => ChatSessionList;
57
- }) => Promise<void>;
54
+ refreshSessions: (params?: SWRRefreshParams<ChatSessionList>) => Promise<void>;
58
55
 
59
56
  /**
60
57
  * remove session
@@ -145,6 +142,8 @@ export const createSessionSlice: StateCreator<
145
142
  },
146
143
  // 乐观更新
147
144
  optimisticData: produce((draft) => {
145
+ if (!draft) return;
146
+
148
147
  const session = draft.all.find((i) => i.id === sessionId);
149
148
  if (!session) return;
150
149
 
package/vercel.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "installCommand": "bun install"
2
+ "installCommand": "npx bun@1.1.3 install"
3
3
  }