@lobehub/chat 1.98.1 → 1.99.0

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 (456) hide show
  1. package/.cursor/rules/backend-architecture.mdc +93 -17
  2. package/.cursor/rules/cursor-ux.mdc +45 -35
  3. package/.cursor/rules/project-introduce.mdc +72 -6
  4. package/.cursor/rules/rules-attach.mdc +16 -7
  5. package/.eslintrc.js +10 -0
  6. package/CHANGELOG.md +52 -0
  7. package/apps/desktop/README.md +7 -0
  8. package/apps/desktop/electron-builder.js +5 -0
  9. package/apps/desktop/package.json +2 -1
  10. package/apps/desktop/src/main/const/dir.ts +3 -0
  11. package/apps/desktop/src/main/controllers/UploadFileCtr.ts +13 -8
  12. package/apps/desktop/src/main/core/App.ts +8 -0
  13. package/apps/desktop/src/main/core/StaticFileServerManager.ts +221 -0
  14. package/apps/desktop/src/main/services/fileSrv.ts +231 -44
  15. package/apps/desktop/src/main/utils/next-electron-rsc.ts +36 -5
  16. package/changelog/v1.json +18 -0
  17. package/docs/development/database-schema.dbml +70 -0
  18. package/locales/ar/common.json +2 -0
  19. package/locales/ar/components.json +35 -0
  20. package/locales/ar/error.json +2 -0
  21. package/locales/ar/image.json +100 -0
  22. package/locales/ar/metadata.json +4 -0
  23. package/locales/ar/modelProvider.json +1 -0
  24. package/locales/ar/models.json +51 -9
  25. package/locales/ar/plugin.json +22 -0
  26. package/locales/ar/providers.json +3 -0
  27. package/locales/ar/setting.json +5 -0
  28. package/locales/bg-BG/common.json +2 -0
  29. package/locales/bg-BG/components.json +35 -0
  30. package/locales/bg-BG/error.json +2 -0
  31. package/locales/bg-BG/image.json +100 -0
  32. package/locales/bg-BG/metadata.json +4 -0
  33. package/locales/bg-BG/modelProvider.json +1 -0
  34. package/locales/bg-BG/models.json +51 -9
  35. package/locales/bg-BG/plugin.json +22 -0
  36. package/locales/bg-BG/providers.json +3 -0
  37. package/locales/bg-BG/setting.json +5 -0
  38. package/locales/de-DE/common.json +2 -0
  39. package/locales/de-DE/components.json +35 -0
  40. package/locales/de-DE/error.json +2 -0
  41. package/locales/de-DE/image.json +100 -0
  42. package/locales/de-DE/metadata.json +4 -0
  43. package/locales/de-DE/modelProvider.json +1 -0
  44. package/locales/de-DE/models.json +51 -9
  45. package/locales/de-DE/plugin.json +22 -0
  46. package/locales/de-DE/providers.json +3 -0
  47. package/locales/de-DE/setting.json +5 -0
  48. package/locales/en-US/common.json +2 -0
  49. package/locales/en-US/components.json +35 -0
  50. package/locales/en-US/error.json +2 -0
  51. package/locales/en-US/image.json +100 -0
  52. package/locales/en-US/metadata.json +4 -0
  53. package/locales/en-US/modelProvider.json +1 -0
  54. package/locales/en-US/models.json +51 -9
  55. package/locales/en-US/plugin.json +22 -0
  56. package/locales/en-US/providers.json +3 -0
  57. package/locales/en-US/setting.json +5 -0
  58. package/locales/es-ES/common.json +2 -0
  59. package/locales/es-ES/components.json +35 -0
  60. package/locales/es-ES/error.json +2 -0
  61. package/locales/es-ES/image.json +100 -0
  62. package/locales/es-ES/metadata.json +4 -0
  63. package/locales/es-ES/modelProvider.json +1 -0
  64. package/locales/es-ES/models.json +51 -9
  65. package/locales/es-ES/plugin.json +22 -0
  66. package/locales/es-ES/providers.json +3 -0
  67. package/locales/es-ES/setting.json +5 -0
  68. package/locales/fa-IR/common.json +2 -0
  69. package/locales/fa-IR/components.json +35 -0
  70. package/locales/fa-IR/error.json +2 -0
  71. package/locales/fa-IR/image.json +100 -0
  72. package/locales/fa-IR/metadata.json +4 -0
  73. package/locales/fa-IR/modelProvider.json +1 -0
  74. package/locales/fa-IR/models.json +51 -9
  75. package/locales/fa-IR/plugin.json +22 -0
  76. package/locales/fa-IR/providers.json +3 -0
  77. package/locales/fa-IR/setting.json +5 -0
  78. package/locales/fr-FR/common.json +2 -0
  79. package/locales/fr-FR/components.json +35 -0
  80. package/locales/fr-FR/error.json +2 -0
  81. package/locales/fr-FR/image.json +100 -0
  82. package/locales/fr-FR/metadata.json +4 -0
  83. package/locales/fr-FR/modelProvider.json +1 -0
  84. package/locales/fr-FR/models.json +51 -9
  85. package/locales/fr-FR/plugin.json +22 -0
  86. package/locales/fr-FR/providers.json +3 -0
  87. package/locales/fr-FR/setting.json +5 -0
  88. package/locales/it-IT/common.json +2 -0
  89. package/locales/it-IT/components.json +35 -0
  90. package/locales/it-IT/error.json +2 -0
  91. package/locales/it-IT/image.json +100 -0
  92. package/locales/it-IT/metadata.json +4 -0
  93. package/locales/it-IT/modelProvider.json +1 -0
  94. package/locales/it-IT/models.json +51 -9
  95. package/locales/it-IT/plugin.json +22 -0
  96. package/locales/it-IT/providers.json +3 -0
  97. package/locales/it-IT/setting.json +5 -0
  98. package/locales/ja-JP/common.json +2 -0
  99. package/locales/ja-JP/components.json +35 -0
  100. package/locales/ja-JP/error.json +2 -0
  101. package/locales/ja-JP/image.json +100 -0
  102. package/locales/ja-JP/metadata.json +4 -0
  103. package/locales/ja-JP/modelProvider.json +1 -0
  104. package/locales/ja-JP/models.json +51 -9
  105. package/locales/ja-JP/plugin.json +22 -0
  106. package/locales/ja-JP/providers.json +3 -0
  107. package/locales/ja-JP/setting.json +5 -0
  108. package/locales/ko-KR/common.json +2 -0
  109. package/locales/ko-KR/components.json +35 -0
  110. package/locales/ko-KR/error.json +2 -0
  111. package/locales/ko-KR/image.json +100 -0
  112. package/locales/ko-KR/metadata.json +4 -0
  113. package/locales/ko-KR/modelProvider.json +1 -0
  114. package/locales/ko-KR/models.json +51 -9
  115. package/locales/ko-KR/plugin.json +22 -0
  116. package/locales/ko-KR/providers.json +3 -0
  117. package/locales/ko-KR/setting.json +5 -0
  118. package/locales/nl-NL/common.json +2 -0
  119. package/locales/nl-NL/components.json +35 -0
  120. package/locales/nl-NL/error.json +2 -0
  121. package/locales/nl-NL/image.json +100 -0
  122. package/locales/nl-NL/metadata.json +4 -0
  123. package/locales/nl-NL/modelProvider.json +1 -0
  124. package/locales/nl-NL/models.json +51 -9
  125. package/locales/nl-NL/plugin.json +22 -0
  126. package/locales/nl-NL/providers.json +3 -0
  127. package/locales/nl-NL/setting.json +5 -0
  128. package/locales/pl-PL/common.json +2 -0
  129. package/locales/pl-PL/components.json +35 -0
  130. package/locales/pl-PL/error.json +2 -0
  131. package/locales/pl-PL/image.json +100 -0
  132. package/locales/pl-PL/metadata.json +4 -0
  133. package/locales/pl-PL/modelProvider.json +1 -0
  134. package/locales/pl-PL/models.json +51 -9
  135. package/locales/pl-PL/plugin.json +22 -0
  136. package/locales/pl-PL/providers.json +3 -0
  137. package/locales/pl-PL/setting.json +5 -0
  138. package/locales/pt-BR/common.json +2 -0
  139. package/locales/pt-BR/components.json +35 -0
  140. package/locales/pt-BR/error.json +2 -0
  141. package/locales/pt-BR/image.json +100 -0
  142. package/locales/pt-BR/metadata.json +4 -0
  143. package/locales/pt-BR/modelProvider.json +1 -0
  144. package/locales/pt-BR/models.json +51 -9
  145. package/locales/pt-BR/plugin.json +22 -0
  146. package/locales/pt-BR/providers.json +3 -0
  147. package/locales/pt-BR/setting.json +5 -0
  148. package/locales/ru-RU/common.json +2 -0
  149. package/locales/ru-RU/components.json +35 -0
  150. package/locales/ru-RU/error.json +2 -0
  151. package/locales/ru-RU/image.json +100 -0
  152. package/locales/ru-RU/metadata.json +4 -0
  153. package/locales/ru-RU/modelProvider.json +1 -0
  154. package/locales/ru-RU/models.json +51 -9
  155. package/locales/ru-RU/plugin.json +22 -0
  156. package/locales/ru-RU/providers.json +3 -0
  157. package/locales/ru-RU/setting.json +5 -0
  158. package/locales/tr-TR/common.json +2 -0
  159. package/locales/tr-TR/components.json +35 -0
  160. package/locales/tr-TR/error.json +2 -0
  161. package/locales/tr-TR/image.json +100 -0
  162. package/locales/tr-TR/metadata.json +4 -0
  163. package/locales/tr-TR/modelProvider.json +1 -0
  164. package/locales/tr-TR/models.json +51 -9
  165. package/locales/tr-TR/plugin.json +22 -0
  166. package/locales/tr-TR/providers.json +3 -0
  167. package/locales/tr-TR/setting.json +5 -0
  168. package/locales/vi-VN/common.json +2 -0
  169. package/locales/vi-VN/components.json +35 -0
  170. package/locales/vi-VN/error.json +2 -0
  171. package/locales/vi-VN/image.json +100 -0
  172. package/locales/vi-VN/metadata.json +4 -0
  173. package/locales/vi-VN/modelProvider.json +1 -0
  174. package/locales/vi-VN/models.json +51 -9
  175. package/locales/vi-VN/plugin.json +22 -0
  176. package/locales/vi-VN/providers.json +3 -0
  177. package/locales/vi-VN/setting.json +5 -0
  178. package/locales/zh-CN/common.json +2 -0
  179. package/locales/zh-CN/components.json +35 -0
  180. package/locales/zh-CN/error.json +2 -0
  181. package/locales/zh-CN/image.json +100 -0
  182. package/locales/zh-CN/metadata.json +4 -0
  183. package/locales/zh-CN/modelProvider.json +1 -0
  184. package/locales/zh-CN/models.json +51 -9
  185. package/locales/zh-CN/plugin.json +22 -0
  186. package/locales/zh-CN/providers.json +3 -0
  187. package/locales/zh-CN/setting.json +5 -0
  188. package/locales/zh-TW/common.json +2 -0
  189. package/locales/zh-TW/components.json +35 -0
  190. package/locales/zh-TW/error.json +2 -0
  191. package/locales/zh-TW/image.json +100 -0
  192. package/locales/zh-TW/metadata.json +4 -0
  193. package/locales/zh-TW/modelProvider.json +1 -0
  194. package/locales/zh-TW/models.json +51 -9
  195. package/locales/zh-TW/plugin.json +22 -0
  196. package/locales/zh-TW/providers.json +3 -0
  197. package/locales/zh-TW/setting.json +5 -0
  198. package/package.json +11 -4
  199. package/packages/electron-server-ipc/src/events/file.ts +3 -1
  200. package/packages/electron-server-ipc/src/types/file.ts +15 -0
  201. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.tsx +11 -1
  202. package/src/app/[variants]/(main)/image/@menu/components/AspectRatioSelect/index.tsx +73 -0
  203. package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +39 -0
  204. package/src/app/[variants]/(main)/image/@menu/components/SizeSelect/index.tsx +89 -0
  205. package/src/app/[variants]/(main)/image/@menu/default.tsx +11 -0
  206. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/AspectRatioSelect.tsx +24 -0
  207. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/DimensionControlGroup.tsx +107 -0
  208. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageNum.tsx +290 -0
  209. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx +504 -0
  210. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrl.tsx +18 -0
  211. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrlsUpload.tsx +19 -0
  212. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect.tsx +155 -0
  213. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/ImageManageModal.tsx +415 -0
  214. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +732 -0
  215. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SeedNumberInput.tsx +24 -0
  216. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSelect.tsx +17 -0
  217. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSliderInput.tsx +15 -0
  218. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/StepsSliderInput.tsx +11 -0
  219. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/constants.ts +1 -0
  220. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +93 -0
  221. package/src/app/[variants]/(main)/image/@topic/default.tsx +17 -0
  222. package/src/app/[variants]/(main)/image/@topic/features/Topics/NewTopicButton.tsx +64 -0
  223. package/src/app/[variants]/(main)/image/@topic/features/Topics/SkeletonList.tsx +34 -0
  224. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItem.tsx +136 -0
  225. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItemContainer.tsx +91 -0
  226. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicList.tsx +57 -0
  227. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicUrlSync.tsx +37 -0
  228. package/src/app/[variants]/(main)/image/@topic/features/Topics/index.tsx +19 -0
  229. package/src/app/[variants]/(main)/image/NotSupportClient.tsx +153 -0
  230. package/src/app/[variants]/(main)/image/_layout/Desktop/Container.tsx +35 -0
  231. package/src/app/[variants]/(main)/image/_layout/Desktop/RegisterHotkeys.tsx +10 -0
  232. package/src/app/[variants]/(main)/image/_layout/Desktop/index.tsx +30 -0
  233. package/src/app/[variants]/(main)/image/_layout/Mobile/index.tsx +14 -0
  234. package/src/app/[variants]/(main)/image/_layout/type.ts +7 -0
  235. package/src/app/[variants]/(main)/image/features/GenerationFeed/BatchItem.tsx +196 -0
  236. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ActionButtons.tsx +60 -0
  237. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ElapsedTime.tsx +90 -0
  238. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ErrorState.tsx +65 -0
  239. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/LoadingState.tsx +43 -0
  240. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/SuccessState.tsx +49 -0
  241. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/index.tsx +156 -0
  242. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/styles.ts +51 -0
  243. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/types.ts +39 -0
  244. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.ts +11 -0
  245. package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +97 -0
  246. package/src/app/[variants]/(main)/image/features/ImageWorkspace/Content.tsx +48 -0
  247. package/src/app/[variants]/(main)/image/features/ImageWorkspace/EmptyState.tsx +37 -0
  248. package/src/app/[variants]/(main)/image/features/ImageWorkspace/SkeletonList.tsx +50 -0
  249. package/src/app/[variants]/(main)/image/features/ImageWorkspace/index.tsx +23 -0
  250. package/src/app/[variants]/(main)/image/features/PromptInput/Title.tsx +38 -0
  251. package/src/app/[variants]/(main)/image/features/PromptInput/index.tsx +114 -0
  252. package/src/app/[variants]/(main)/image/layout.tsx +19 -0
  253. package/src/app/[variants]/(main)/image/loading.tsx +3 -0
  254. package/src/app/[variants]/(main)/image/page.tsx +47 -0
  255. package/src/app/[variants]/(main)/settings/system-agent/index.tsx +2 -1
  256. package/src/chains/summaryGenerationTitle.ts +25 -0
  257. package/src/components/ImageItem/index.tsx +9 -6
  258. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/Bedrock.tsx +3 -4
  259. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/ProviderApiKeyForm.tsx +5 -4
  260. package/src/components/InvalidAPIKey/APIKeyForm/index.tsx +108 -0
  261. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/useApiKey.ts +2 -1
  262. package/src/components/InvalidAPIKey/index.tsx +30 -0
  263. package/src/components/KeyValueEditor/index.tsx +203 -0
  264. package/src/components/KeyValueEditor/utils.ts +42 -0
  265. package/src/config/aiModels/fal.ts +52 -0
  266. package/src/config/aiModels/index.ts +3 -0
  267. package/src/config/aiModels/openai.ts +20 -6
  268. package/src/config/llm.ts +6 -0
  269. package/src/config/modelProviders/fal.ts +21 -0
  270. package/src/config/modelProviders/index.ts +3 -0
  271. package/src/config/paramsSchemas/fal/flux-kontext-dev.ts +8 -0
  272. package/src/config/paramsSchemas/fal/flux-pro-kontext.ts +11 -0
  273. package/src/config/paramsSchemas/fal/flux-schnell.ts +9 -0
  274. package/src/config/paramsSchemas/fal/imagen4.ts +10 -0
  275. package/src/config/paramsSchemas/openai/gpt-image-1.ts +10 -0
  276. package/src/const/hotkeys.ts +2 -2
  277. package/src/const/image.ts +6 -0
  278. package/src/const/settings/systemAgent.ts +1 -0
  279. package/src/database/client/migrations.json +27 -0
  280. package/src/database/migrations/0026_add_autovacuum_tuning.sql +2 -0
  281. package/src/database/migrations/0027_ai_image.sql +47 -0
  282. package/src/database/migrations/meta/0027_snapshot.json +6003 -0
  283. package/src/database/migrations/meta/_journal.json +7 -0
  284. package/src/database/models/__tests__/asyncTask.test.ts +7 -5
  285. package/src/database/models/__tests__/file.test.ts +287 -0
  286. package/src/database/models/__tests__/generation.test.ts +786 -0
  287. package/src/database/models/__tests__/generationBatch.test.ts +614 -0
  288. package/src/database/models/__tests__/generationTopic.test.ts +411 -0
  289. package/src/database/models/aiModel.ts +2 -0
  290. package/src/database/models/asyncTask.ts +1 -1
  291. package/src/database/models/file.ts +28 -20
  292. package/src/database/models/generation.ts +197 -0
  293. package/src/database/models/generationBatch.ts +212 -0
  294. package/src/database/models/generationTopic.ts +131 -0
  295. package/src/database/repositories/aiInfra/index.test.ts +151 -1
  296. package/src/database/repositories/aiInfra/index.ts +28 -19
  297. package/src/database/repositories/tableViewer/index.test.ts +1 -1
  298. package/src/database/schemas/file.ts +8 -0
  299. package/src/database/schemas/generation.ts +127 -0
  300. package/src/database/schemas/index.ts +1 -0
  301. package/src/database/schemas/relations.ts +45 -1
  302. package/src/database/type.ts +2 -0
  303. package/src/database/utils/idGenerator.ts +3 -0
  304. package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +39 -0
  305. package/src/features/Conversation/Error/InvalidAccessCode.tsx +2 -2
  306. package/src/features/Conversation/Error/index.tsx +3 -3
  307. package/src/features/ImageSidePanel/index.tsx +83 -0
  308. package/src/features/ImageTopicPanel/index.tsx +79 -0
  309. package/src/features/PluginDevModal/MCPManifestForm/CollapsibleSection.tsx +62 -0
  310. package/src/features/PluginDevModal/MCPManifestForm/QuickImportSection.tsx +158 -0
  311. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +99 -155
  312. package/src/features/PluginStore/McpList/Detail/Settings/index.tsx +5 -2
  313. package/src/hooks/useDownloadImage.ts +31 -0
  314. package/src/hooks/useFetchGenerationTopics.ts +13 -0
  315. package/src/hooks/useHotkeys/imageScope.ts +48 -0
  316. package/src/libs/mcp/client.ts +55 -22
  317. package/src/libs/mcp/types.ts +42 -6
  318. package/src/libs/model-runtime/BaseAI.ts +3 -1
  319. package/src/libs/model-runtime/ModelRuntime.test.ts +80 -0
  320. package/src/libs/model-runtime/ModelRuntime.ts +15 -1
  321. package/src/libs/model-runtime/UniformRuntime/index.ts +4 -1
  322. package/src/libs/model-runtime/fal/index.test.ts +442 -0
  323. package/src/libs/model-runtime/fal/index.ts +88 -0
  324. package/src/libs/model-runtime/openai/index.test.ts +396 -2
  325. package/src/libs/model-runtime/openai/index.ts +129 -3
  326. package/src/libs/model-runtime/runtimeMap.ts +2 -0
  327. package/src/libs/model-runtime/types/image.ts +25 -0
  328. package/src/libs/model-runtime/types/type.ts +1 -0
  329. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +10 -0
  330. package/src/libs/standard-parameters/index.ts +1 -0
  331. package/src/libs/standard-parameters/meta-schema.test.ts +214 -0
  332. package/src/libs/standard-parameters/meta-schema.ts +147 -0
  333. package/src/libs/swr/index.ts +1 -0
  334. package/src/libs/trpc/async/asyncAuth.ts +29 -8
  335. package/src/libs/trpc/async/context.ts +42 -4
  336. package/src/libs/trpc/async/index.ts +17 -4
  337. package/src/libs/trpc/async/init.ts +8 -0
  338. package/src/libs/trpc/client/lambda.ts +19 -2
  339. package/src/locales/default/common.ts +2 -0
  340. package/src/locales/default/components.ts +35 -0
  341. package/src/locales/default/error.ts +2 -0
  342. package/src/locales/default/image.ts +100 -0
  343. package/src/locales/default/index.ts +2 -0
  344. package/src/locales/default/metadata.ts +4 -0
  345. package/src/locales/default/modelProvider.ts +2 -0
  346. package/src/locales/default/plugin.ts +22 -0
  347. package/src/locales/default/setting.ts +5 -0
  348. package/src/middleware.ts +1 -0
  349. package/src/server/modules/ElectronIPCClient/index.ts +9 -1
  350. package/src/server/modules/S3/index.ts +15 -0
  351. package/src/server/routers/async/caller.ts +9 -1
  352. package/src/server/routers/async/image.ts +253 -0
  353. package/src/server/routers/async/index.ts +2 -0
  354. package/src/server/routers/lambda/aiProvider.test.ts +1 -0
  355. package/src/server/routers/lambda/generation.test.ts +267 -0
  356. package/src/server/routers/lambda/generation.ts +86 -0
  357. package/src/server/routers/lambda/generationBatch.test.ts +376 -0
  358. package/src/server/routers/lambda/generationBatch.ts +56 -0
  359. package/src/server/routers/lambda/generationTopic.test.ts +508 -0
  360. package/src/server/routers/lambda/generationTopic.ts +93 -0
  361. package/src/server/routers/lambda/image.ts +248 -0
  362. package/src/server/routers/lambda/index.ts +8 -0
  363. package/src/server/routers/tools/mcp.ts +15 -0
  364. package/src/server/services/file/__tests__/index.test.ts +135 -0
  365. package/src/server/services/file/impls/local.test.ts +153 -52
  366. package/src/server/services/file/impls/local.ts +70 -46
  367. package/src/server/services/file/impls/s3.test.ts +114 -0
  368. package/src/server/services/file/impls/s3.ts +40 -0
  369. package/src/server/services/file/impls/type.ts +10 -0
  370. package/src/server/services/file/index.ts +14 -0
  371. package/src/server/services/generation/index.ts +239 -0
  372. package/src/server/services/mcp/index.ts +20 -2
  373. package/src/services/__tests__/generation.test.ts +40 -0
  374. package/src/services/__tests__/generationBatch.test.ts +36 -0
  375. package/src/services/__tests__/generationTopic.test.ts +72 -0
  376. package/src/services/electron/file.ts +3 -1
  377. package/src/services/generation.ts +16 -0
  378. package/src/services/generationBatch.ts +25 -0
  379. package/src/services/generationTopic.ts +28 -0
  380. package/src/services/image.ts +33 -0
  381. package/src/services/mcp.ts +12 -7
  382. package/src/services/upload.ts +43 -9
  383. package/src/store/aiInfra/slices/aiProvider/action.ts +25 -5
  384. package/src/store/aiInfra/slices/aiProvider/initialState.ts +1 -0
  385. package/src/store/aiInfra/slices/aiProvider/selectors.ts +3 -0
  386. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +5 -5
  387. package/src/store/chat/slices/message/action.ts +2 -2
  388. package/src/store/chat/slices/translate/action.ts +1 -1
  389. package/src/store/global/initialState.ts +9 -0
  390. package/src/store/global/selectors/systemStatus.ts +8 -0
  391. package/src/store/image/index.ts +2 -0
  392. package/src/store/image/initialState.ts +25 -0
  393. package/src/store/image/selectors.ts +4 -0
  394. package/src/store/image/slices/createImage/action.test.ts +330 -0
  395. package/src/store/image/slices/createImage/action.ts +134 -0
  396. package/src/store/image/slices/createImage/initialState.ts +9 -0
  397. package/src/store/image/slices/createImage/selectors.test.ts +114 -0
  398. package/src/store/image/slices/createImage/selectors.ts +9 -0
  399. package/src/store/image/slices/generationBatch/action.test.ts +495 -0
  400. package/src/store/image/slices/generationBatch/action.ts +303 -0
  401. package/src/store/image/slices/generationBatch/initialState.ts +13 -0
  402. package/src/store/image/slices/generationBatch/reducer.test.ts +568 -0
  403. package/src/store/image/slices/generationBatch/reducer.ts +101 -0
  404. package/src/store/image/slices/generationBatch/selectors.test.ts +307 -0
  405. package/src/store/image/slices/generationBatch/selectors.ts +36 -0
  406. package/src/store/image/slices/generationConfig/action.test.ts +351 -0
  407. package/src/store/image/slices/generationConfig/action.ts +295 -0
  408. package/src/store/image/slices/generationConfig/hooks.test.ts +304 -0
  409. package/src/store/image/slices/generationConfig/hooks.ts +118 -0
  410. package/src/store/image/slices/generationConfig/index.ts +1 -0
  411. package/src/store/image/slices/generationConfig/initialState.ts +37 -0
  412. package/src/store/image/slices/generationConfig/selectors.test.ts +204 -0
  413. package/src/store/image/slices/generationConfig/selectors.ts +25 -0
  414. package/src/store/image/slices/generationTopic/action.test.ts +687 -0
  415. package/src/store/image/slices/generationTopic/action.ts +319 -0
  416. package/src/store/image/slices/generationTopic/index.ts +2 -0
  417. package/src/store/image/slices/generationTopic/initialState.ts +14 -0
  418. package/src/store/image/slices/generationTopic/reducer.test.ts +198 -0
  419. package/src/store/image/slices/generationTopic/reducer.ts +66 -0
  420. package/src/store/image/slices/generationTopic/selectors.test.ts +103 -0
  421. package/src/store/image/slices/generationTopic/selectors.ts +15 -0
  422. package/src/store/image/store.ts +42 -0
  423. package/src/store/image/utils/size.ts +51 -0
  424. package/src/store/tool/slices/customPlugin/action.ts +10 -1
  425. package/src/store/tool/slices/mcpStore/action.ts +6 -4
  426. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +4 -0
  427. package/src/store/user/slices/settings/selectors/systemAgent.ts +2 -0
  428. package/src/types/aiModel.ts +8 -3
  429. package/src/types/aiProvider.ts +1 -0
  430. package/src/types/asyncTask.ts +2 -0
  431. package/src/types/files/index.ts +5 -0
  432. package/src/types/generation/index.ts +80 -0
  433. package/src/types/hotkey.ts +2 -0
  434. package/src/types/plugins/mcp.ts +2 -6
  435. package/src/types/tool/plugin.ts +8 -0
  436. package/src/types/user/settings/keyVaults.ts +5 -0
  437. package/src/types/user/settings/systemAgent.ts +1 -0
  438. package/src/utils/client/downloadFile.ts +33 -4
  439. package/src/utils/number.test.ts +105 -0
  440. package/src/utils/number.ts +25 -0
  441. package/src/utils/server/__tests__/geo.test.ts +6 -3
  442. package/src/utils/storeDebug.test.ts +152 -0
  443. package/src/utils/storeDebug.ts +16 -7
  444. package/src/utils/time.test.ts +259 -0
  445. package/src/utils/time.ts +18 -0
  446. package/src/utils/units.ts +61 -0
  447. package/src/utils/url.test.ts +358 -9
  448. package/src/utils/url.ts +105 -3
  449. package/{vitest.server.config.ts → vitest.config.server.ts} +3 -0
  450. package/.cursor/rules/i18n/i18n-auto-attached.mdc +0 -6
  451. package/src/features/Conversation/Error/APIKeyForm/index.tsx +0 -105
  452. package/src/features/Conversation/Error/InvalidAPIKey.tsx +0 -16
  453. package/src/features/PluginDevModal/MCPManifestForm/EnvEditor.tsx +0 -227
  454. /package/.cursor/rules/{i18n/i18n.mdc → i18n.mdc} +0 -0
  455. /package/src/app/[variants]/(main)/settings/system-agent/features/{createForm.tsx → SystemAgentForm.tsx} +0 -0
  456. /package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/LoadingContext.ts +0 -0
@@ -7,7 +7,7 @@ import { isDeprecatedEdition, isDesktop, isUsePgliteDB } from '@/const/version';
7
7
  import { useClientDataSWR } from '@/libs/swr';
8
8
  import { aiProviderService } from '@/services/aiProvider';
9
9
  import { AiInfraStore } from '@/store/aiInfra/store';
10
- import { ModelAbilities } from '@/types/aiModel';
10
+ import { AIImageModelCard, ModelAbilities } from '@/types/aiModel';
11
11
  import {
12
12
  AiProviderDetailItem,
13
13
  AiProviderListItem,
@@ -15,6 +15,7 @@ import {
15
15
  AiProviderSortMap,
16
16
  AiProviderSourceEnum,
17
17
  CreateAiProviderParams,
18
+ EnabledProvider,
18
19
  UpdateAiProviderConfigParams,
19
20
  UpdateAiProviderParams,
20
21
  } from '@/types/aiProvider';
@@ -175,11 +176,20 @@ export const createAiProviderSlice: StateCreator<
175
176
  if (isLogin) return aiProviderService.getAiProviderRuntimeState();
176
177
 
177
178
  const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels');
179
+ const enabledAiProviders: EnabledProvider[] = DEFAULT_MODEL_PROVIDER_LIST.filter(
180
+ (provider) => provider.enabled,
181
+ ).map((item) => ({ id: item.id, name: item.name, source: 'builtin' }));
182
+ const allModels = LOBE_DEFAULT_MODEL_LIST;
178
183
  return {
179
- enabledAiModels: LOBE_DEFAULT_MODEL_LIST.filter((m) => m.enabled),
180
- enabledAiProviders: DEFAULT_MODEL_PROVIDER_LIST.filter(
181
- (provider) => provider.enabled,
182
- ).map((item) => ({ id: item.id, name: item.name, source: 'builtin' })),
184
+ enabledAiModels: allModels.filter((m) => m.enabled),
185
+ enabledAiProviders: enabledAiProviders,
186
+ enabledImageAiProviders: enabledAiProviders
187
+ .filter((provider) => {
188
+ return allModels.some(
189
+ (model) => model.providerId === provider.id && model.type === 'image',
190
+ );
191
+ })
192
+ .map((item) => ({ id: item.id, name: item.name, source: 'builtin' })),
183
193
  runtimeConfig: {},
184
194
  };
185
195
  },
@@ -196,6 +206,9 @@ export const createAiProviderSlice: StateCreator<
196
206
  contextWindowTokens: model.contextWindowTokens,
197
207
  displayName: model.displayName ?? '',
198
208
  id: model.id,
209
+ ...(model.type === 'image' && {
210
+ parameters: (model as AIImageModelCard).parameters,
211
+ }),
199
212
  }));
200
213
 
201
214
  return uniqBy(models, 'id');
@@ -207,6 +220,12 @@ export const createAiProviderSlice: StateCreator<
207
220
  children: getModelListByType(provider.id, 'chat'),
208
221
  name: provider.name || provider.id,
209
222
  }));
223
+
224
+ const enabledImageModelList = data.enabledImageAiProviders.map((provider) => ({
225
+ ...provider,
226
+ children: getModelListByType(provider.id, 'image'),
227
+ name: provider.name || provider.id,
228
+ }));
210
229
  const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels');
211
230
 
212
231
  set(
@@ -216,6 +235,7 @@ export const createAiProviderSlice: StateCreator<
216
235
  enabledAiModels: data.enabledAiModels,
217
236
  enabledAiProviders: data.enabledAiProviders,
218
237
  enabledChatModelList,
238
+ enabledImageModelList,
219
239
  },
220
240
  false,
221
241
  'useFetchAiProviderRuntimeState',
@@ -19,6 +19,7 @@ export interface AIProviderState {
19
19
  enabledAiProviders?: EnabledProvider[];
20
20
  // used for select
21
21
  enabledChatModelList?: EnabledProviderWithModels[];
22
+ enabledImageModelList?: EnabledProviderWithModels[];
22
23
  initAiProviderList: boolean;
23
24
  providerSearchKeyword: string;
24
25
  }
@@ -10,6 +10,8 @@ const enabledAiProviderList = (s: AIProviderStoreState) =>
10
10
  const disabledAiProviderList = (s: AIProviderStoreState) =>
11
11
  s.aiProviderList.filter((item) => !item.enabled);
12
12
 
13
+ const enabledImageModelList = (s: AIProviderStoreState) => s.enabledImageModelList || [];
14
+
13
15
  const isProviderEnabled = (id: string) => (s: AIProviderStoreState) =>
14
16
  enabledAiProviderList(s).some((i) => i.id === id);
15
17
 
@@ -113,6 +115,7 @@ export const aiProviderSelectors = {
113
115
  activeProviderConfig,
114
116
  disabledAiProviderList,
115
117
  enabledAiProviderList,
118
+ enabledImageModelList,
116
119
  isActiveProviderApiKeyNotEmpty,
117
120
  isActiveProviderEndpointNotEmpty,
118
121
  isAiProviderConfigLoading,
@@ -24,7 +24,7 @@ import { WebBrowsingManifest } from '@/tools/web-browsing';
24
24
  import { ChatMessage, CreateMessageParams, SendMessageParams } from '@/types/message';
25
25
  import { ChatImageItem } from '@/types/message/image';
26
26
  import { MessageSemanticSearchChunk } from '@/types/rag';
27
- import { setNamespace } from '@/utils/storeDebug';
27
+ import { Action, setNamespace } from '@/utils/storeDebug';
28
28
 
29
29
  import { chatSelectors, topicSelectors } from '../../../selectors';
30
30
 
@@ -104,7 +104,7 @@ export interface AIGenerateAction {
104
104
  internal_toggleChatLoading: (
105
105
  loading: boolean,
106
106
  id?: string,
107
- action?: string,
107
+ action?: Action,
108
108
  ) => AbortController | undefined;
109
109
  /**
110
110
  * Controls the streaming state of tool calling processes, updating the UI accordingly
@@ -383,7 +383,7 @@ export const generateAIChat: StateCreator<
383
383
  const abortController = get().internal_toggleChatLoading(
384
384
  true,
385
385
  assistantId,
386
- n('generateMessage(start)', { messageId: assistantId, messages }) as string,
386
+ n('generateMessage(start)', { messageId: assistantId, messages }),
387
387
  );
388
388
 
389
389
  get().internal_toggleSearchWorkflow(true, assistantId);
@@ -434,7 +434,7 @@ export const generateAIChat: StateCreator<
434
434
  get().internal_toggleChatLoading(
435
435
  false,
436
436
  assistantId,
437
- n('generateMessage(start)', { messageId: assistantId, messages }) as string,
437
+ n('generateMessage(start)', { messageId: assistantId, messages }),
438
438
  );
439
439
  get().internal_toggleSearchWorkflow(false, assistantId);
440
440
 
@@ -502,7 +502,7 @@ export const generateAIChat: StateCreator<
502
502
  const abortController = internal_toggleChatLoading(
503
503
  true,
504
504
  messageId,
505
- n('generateMessage(start)', { messageId, messages }) as string,
505
+ n('generateMessage(start)', { messageId, messages }),
506
506
  );
507
507
 
508
508
  const agentConfig = agentSelectors.currentAgentConfig(getAgentStoreState());
@@ -25,7 +25,7 @@ import {
25
25
  import { ChatImageItem } from '@/types/message/image';
26
26
  import { GroundingSearch } from '@/types/search';
27
27
  import { TraceEventPayloads } from '@/types/trace';
28
- import { setNamespace } from '@/utils/storeDebug';
28
+ import { Action, setNamespace } from '@/utils/storeDebug';
29
29
  import { nanoid } from '@/utils/uuid';
30
30
 
31
31
  import type { ChatStoreState } from '../../initialState';
@@ -130,7 +130,7 @@ export interface ChatMessageAction {
130
130
  key: keyof ChatStoreState,
131
131
  loading: boolean,
132
132
  id?: string,
133
- action?: string,
133
+ action?: Action,
134
134
  ) => AbortController | undefined;
135
135
  }
136
136
 
@@ -54,7 +54,7 @@ export const chatTranslate: StateCreator<
54
54
  // create translate extra
55
55
  await updateMessageTranslate(id, { content: '', from: '', to: targetLang });
56
56
 
57
- internal_toggleChatLoading(true, id, n('translateMessage(start)', { id }) as string);
57
+ internal_toggleChatLoading(true, id, n('translateMessage(start)', { id }));
58
58
 
59
59
  let content = '';
60
60
  let from = '';
@@ -10,6 +10,7 @@ export enum SidebarTabKey {
10
10
  Chat = 'chat',
11
11
  Discover = 'discover',
12
12
  Files = 'files',
13
+ Image = 'image',
13
14
  Me = 'me',
14
15
  Setting = 'settings',
15
16
  }
@@ -50,6 +51,8 @@ export interface SystemStatus {
50
51
  filePanelWidth: number;
51
52
  hidePWAInstaller?: boolean;
52
53
  hideThreadLimitAlert?: boolean;
54
+ imagePanelWidth: number;
55
+ imageTopicPanelWidth?: number;
53
56
  inputHeight: number;
54
57
  /**
55
58
  * 应用初始化时不启用 PGLite,只有当用户手动开启时才启用
@@ -65,6 +68,8 @@ export interface SystemStatus {
65
68
  showChatSideBar?: boolean;
66
69
  showFilePanel?: boolean;
67
70
  showHotkeyHelper?: boolean;
71
+ showImagePanel?: boolean;
72
+ showImageTopicPanel?: boolean;
68
73
  showSessionPanel?: boolean;
69
74
  showSystemRole?: boolean;
70
75
  systemRoleExpandedMap: Record<string, boolean>;
@@ -104,6 +109,8 @@ export const INITIAL_STATUS = {
104
109
  filePanelWidth: 320,
105
110
  hidePWAInstaller: false,
106
111
  hideThreadLimitAlert: false,
112
+ imagePanelWidth: 320,
113
+ imageTopicPanelWidth: 80,
107
114
  inputHeight: 200,
108
115
  mobileShowTopic: false,
109
116
  portalWidth: 400,
@@ -111,6 +118,8 @@ export const INITIAL_STATUS = {
111
118
  showChatSideBar: true,
112
119
  showFilePanel: true,
113
120
  showHotkeyHelper: false,
121
+ showImagePanel: true,
122
+ showImageTopicPanel: true,
114
123
  showSessionPanel: true,
115
124
  showSystemRole: false,
116
125
  systemRoleExpandedMap: {},
@@ -14,6 +14,8 @@ const mobileShowPortal = (s: GlobalState) => s.status.mobileShowPortal;
14
14
  const showChatSideBar = (s: GlobalState) => !s.status.zenMode && s.status.showChatSideBar;
15
15
  const showSessionPanel = (s: GlobalState) => !s.status.zenMode && s.status.showSessionPanel;
16
16
  const showFilePanel = (s: GlobalState) => s.status.showFilePanel;
17
+ const showImagePanel = (s: GlobalState) => s.status.showImagePanel;
18
+ const showImageTopicPanel = (s: GlobalState) => s.status.showImageTopicPanel;
17
19
  const hidePWAInstaller = (s: GlobalState) => s.status.hidePWAInstaller;
18
20
  const isShowCredit = (s: GlobalState) => s.status.isShowCredit;
19
21
  const themeMode = (s: GlobalState) => s.status.themeMode || 'auto';
@@ -24,6 +26,8 @@ const inZenMode = (s: GlobalState) => s.status.zenMode;
24
26
  const sessionWidth = (s: GlobalState) => s.status.sessionsWidth;
25
27
  const portalWidth = (s: GlobalState) => s.status.portalWidth || 400;
26
28
  const filePanelWidth = (s: GlobalState) => s.status.filePanelWidth;
29
+ const imagePanelWidth = (s: GlobalState) => s.status.imagePanelWidth;
30
+ const imageTopicPanelWidth = (s: GlobalState) => s.status.imageTopicPanelWidth;
27
31
  const inputHeight = (s: GlobalState) => s.status.inputHeight;
28
32
  const threadInputHeight = (s: GlobalState) => s.status.threadInputHeight;
29
33
 
@@ -62,6 +66,8 @@ export const systemStatusSelectors = {
62
66
  filePanelWidth,
63
67
  getAgentSystemRoleExpanded,
64
68
  hidePWAInstaller,
69
+ imagePanelWidth,
70
+ imageTopicPanelWidth,
65
71
  inZenMode,
66
72
  inputHeight,
67
73
  isDBInited,
@@ -78,6 +84,8 @@ export const systemStatusSelectors = {
78
84
  showChatHeader,
79
85
  showChatSideBar,
80
86
  showFilePanel,
87
+ showImagePanel,
88
+ showImageTopicPanel,
81
89
  showSessionPanel,
82
90
  showSystemRole,
83
91
  systemStatus,
@@ -0,0 +1,2 @@
1
+ export type { ImageStore } from './store';
2
+ export { getImageStoreState, useImageStore } from './store';
@@ -0,0 +1,25 @@
1
+ import { CreateImageState, initialCreateImageState } from './slices/createImage/initialState';
2
+ import {
3
+ GenerationBatchState,
4
+ initialGenerationBatchState,
5
+ } from './slices/generationBatch/initialState';
6
+ import {
7
+ GenerationConfigState,
8
+ initialGenerationConfigState,
9
+ } from './slices/generationConfig/initialState';
10
+ import {
11
+ GenerationTopicState,
12
+ initialGenerationTopicState,
13
+ } from './slices/generationTopic/initialState';
14
+
15
+ export type ImageStoreState = GenerationConfigState &
16
+ GenerationTopicState &
17
+ GenerationBatchState &
18
+ CreateImageState;
19
+
20
+ export const initialState: ImageStoreState = {
21
+ ...initialGenerationConfigState,
22
+ ...initialGenerationTopicState,
23
+ ...initialGenerationBatchState,
24
+ ...initialCreateImageState,
25
+ };
@@ -0,0 +1,4 @@
1
+ export * from './slices/createImage/selectors';
2
+ export * from './slices/generationBatch/selectors';
3
+ export * from './slices/generationConfig/selectors';
4
+ export * from './slices/generationTopic/selectors';
@@ -0,0 +1,330 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { imageService } from '@/services/image';
5
+ import { useImageStore } from '@/store/image';
6
+
7
+ // Mock external dependencies
8
+ vi.mock('@/services/image', () => ({
9
+ imageService: {
10
+ createImage: vi.fn().mockResolvedValue({
11
+ success: true,
12
+ data: {
13
+ batch: {
14
+ generationTopicId: 'test-topic-id',
15
+ provider: 'test-provider',
16
+ model: 'test-model',
17
+ prompt: 'test prompt',
18
+ width: 1024,
19
+ height: 1024,
20
+ userId: 'test-user',
21
+ id: 'batch-id',
22
+ accessedAt: new Date(),
23
+ createdAt: new Date(),
24
+ updatedAt: new Date(),
25
+ ratio: null,
26
+ config: {},
27
+ },
28
+ generations: [],
29
+ },
30
+ }),
31
+ },
32
+ }));
33
+
34
+ const mockImageService = vi.mocked(imageService);
35
+
36
+ describe('CreateImageAction', () => {
37
+ beforeEach(() => {
38
+ vi.clearAllMocks();
39
+ // Reset to initial state with proper defaults
40
+ const initialState = useImageStore.getState();
41
+ useImageStore.setState({
42
+ ...initialState,
43
+ isCreating: false,
44
+ isCreatingWithNewTopic: false,
45
+ activeGenerationTopicId: 'active-topic-id',
46
+ parameters: { prompt: 'test prompt', width: 1024, height: 1024 },
47
+ provider: 'test-provider',
48
+ model: 'test-model',
49
+ imageNum: 4,
50
+ generationBatchesMap: {
51
+ 'active-topic-id': [
52
+ {
53
+ id: 'batch-id',
54
+ provider: 'batch-provider',
55
+ model: 'batch-model',
56
+ config: { prompt: 'batch prompt' },
57
+ } as any,
58
+ ],
59
+ },
60
+ });
61
+ });
62
+
63
+ afterEach(() => {
64
+ vi.restoreAllMocks();
65
+ });
66
+
67
+ describe('createImage', () => {
68
+ it('should create image with existing topic', async () => {
69
+ const { result } = renderHook(() => useImageStore());
70
+ const mockRefreshGenerationBatches = vi.fn().mockResolvedValue(undefined);
71
+
72
+ // Set up store state
73
+ act(() => {
74
+ useImageStore.setState({
75
+ refreshGenerationBatches: mockRefreshGenerationBatches,
76
+ });
77
+ });
78
+
79
+ await act(async () => {
80
+ await result.current.createImage();
81
+ });
82
+
83
+ // Verify state changes
84
+ expect(result.current.isCreating).toBe(false);
85
+ expect(result.current.isCreatingWithNewTopic).toBe(false);
86
+
87
+ // Verify service calls
88
+ expect(mockImageService.createImage).toHaveBeenCalledWith({
89
+ generationTopicId: 'active-topic-id',
90
+ provider: 'test-provider',
91
+ model: 'test-model',
92
+ imageNum: 4,
93
+ params: { prompt: 'test prompt', width: 1024, height: 1024 },
94
+ });
95
+
96
+ // Verify refresh was called
97
+ expect(mockRefreshGenerationBatches).toHaveBeenCalled();
98
+ });
99
+
100
+ it('should create new topic when no active topic exists', async () => {
101
+ const mockCreateGenerationTopic = vi.fn().mockResolvedValue('new-topic-id');
102
+ const mockSwitchGenerationTopic = vi.fn();
103
+ const mockSetTopicBatchLoaded = vi.fn();
104
+
105
+ const { result } = renderHook(() => useImageStore());
106
+
107
+ act(() => {
108
+ useImageStore.setState({
109
+ activeGenerationTopicId: '', // No active topic
110
+ createGenerationTopic: mockCreateGenerationTopic,
111
+ switchGenerationTopic: mockSwitchGenerationTopic,
112
+ setTopicBatchLoaded: mockSetTopicBatchLoaded,
113
+ });
114
+ });
115
+
116
+ await act(async () => {
117
+ await result.current.createImage();
118
+ });
119
+
120
+ // Verify state changes
121
+ expect(result.current.isCreating).toBe(false);
122
+ expect(result.current.isCreatingWithNewTopic).toBe(false);
123
+
124
+ // Verify topic creation
125
+ expect(mockCreateGenerationTopic).toHaveBeenCalledWith(['test prompt']);
126
+ expect(mockSetTopicBatchLoaded).toHaveBeenCalledWith('new-topic-id');
127
+ expect(mockSwitchGenerationTopic).toHaveBeenCalledWith('new-topic-id');
128
+
129
+ // Verify service call with new topic id
130
+ expect(mockImageService.createImage).toHaveBeenCalledWith({
131
+ generationTopicId: 'new-topic-id',
132
+ provider: 'test-provider',
133
+ model: 'test-model',
134
+ imageNum: 4,
135
+ params: { prompt: 'test prompt', width: 1024, height: 1024 },
136
+ });
137
+ });
138
+
139
+ it('should throw error when parameters is not initialized', async () => {
140
+ const { result } = renderHook(() => useImageStore());
141
+
142
+ act(() => {
143
+ useImageStore.setState({
144
+ parameters: undefined, // Set parameters to undefined
145
+ });
146
+ });
147
+
148
+ await expect(
149
+ act(async () => {
150
+ await result.current.createImage();
151
+ }),
152
+ ).rejects.toThrow('parameters is not initialized');
153
+ });
154
+
155
+ it('should throw error when prompt is empty', async () => {
156
+ const { result } = renderHook(() => useImageStore());
157
+
158
+ act(() => {
159
+ useImageStore.setState({
160
+ parameters: {
161
+ prompt: '', // Empty prompt
162
+ width: 1024,
163
+ height: 1024,
164
+ },
165
+ });
166
+ });
167
+
168
+ await expect(
169
+ act(async () => {
170
+ await result.current.createImage();
171
+ }),
172
+ ).rejects.toThrow('prompt is empty');
173
+ });
174
+
175
+ it('should handle service error', async () => {
176
+ const error = new Error('Service error');
177
+ mockImageService.createImage.mockRejectedValueOnce(error);
178
+
179
+ const mockRefreshGenerationBatches = vi.fn();
180
+ const { result } = renderHook(() => useImageStore());
181
+
182
+ act(() => {
183
+ useImageStore.setState({
184
+ refreshGenerationBatches: mockRefreshGenerationBatches,
185
+ });
186
+ });
187
+
188
+ await expect(
189
+ act(async () => {
190
+ await result.current.createImage();
191
+ }),
192
+ ).rejects.toThrow('Service error');
193
+
194
+ // The service should have been called before the error
195
+ expect(mockImageService.createImage).toHaveBeenCalled();
196
+ });
197
+
198
+ it('should handle service error with new topic', async () => {
199
+ const error = new Error('Service error');
200
+ mockImageService.createImage.mockRejectedValueOnce(error);
201
+
202
+ const mockCreateGenerationTopic = vi.fn().mockResolvedValue('new-topic-id');
203
+ const mockSwitchGenerationTopic = vi.fn();
204
+ const mockSetTopicBatchLoaded = vi.fn();
205
+
206
+ const { result } = renderHook(() => useImageStore());
207
+
208
+ act(() => {
209
+ useImageStore.setState({
210
+ activeGenerationTopicId: '', // No active topic
211
+ createGenerationTopic: mockCreateGenerationTopic,
212
+ switchGenerationTopic: mockSwitchGenerationTopic,
213
+ setTopicBatchLoaded: mockSetTopicBatchLoaded,
214
+ });
215
+ });
216
+
217
+ await expect(
218
+ act(async () => {
219
+ await result.current.createImage();
220
+ }),
221
+ ).rejects.toThrow('Service error');
222
+
223
+ // Verify topic was created before the error
224
+ expect(mockCreateGenerationTopic).toHaveBeenCalled();
225
+ expect(mockSwitchGenerationTopic).toHaveBeenCalled();
226
+ });
227
+ });
228
+
229
+ describe('recreateImage', () => {
230
+ it('should recreate image successfully', async () => {
231
+ const mockRefreshGenerationBatches = vi.fn().mockResolvedValue(undefined);
232
+ const mockRemoveGenerationBatch = vi.fn().mockResolvedValue(undefined);
233
+
234
+ const { result } = renderHook(() => useImageStore());
235
+
236
+ act(() => {
237
+ useImageStore.setState({
238
+ refreshGenerationBatches: mockRefreshGenerationBatches,
239
+ removeGenerationBatch: mockRemoveGenerationBatch,
240
+ });
241
+ });
242
+
243
+ await act(async () => {
244
+ await result.current.recreateImage('batch-id');
245
+ });
246
+
247
+ // Verify state changes
248
+ expect(result.current.isCreating).toBe(false);
249
+
250
+ // Verify batch removal
251
+ expect(mockRemoveGenerationBatch).toHaveBeenCalledWith('batch-id', 'active-topic-id');
252
+
253
+ // Verify service call
254
+ expect(mockImageService.createImage).toHaveBeenCalledWith({
255
+ generationTopicId: 'active-topic-id',
256
+ provider: 'batch-provider',
257
+ model: 'batch-model',
258
+ imageNum: 4,
259
+ params: { prompt: 'batch prompt' },
260
+ });
261
+
262
+ // Verify refresh was called
263
+ expect(mockRefreshGenerationBatches).toHaveBeenCalled();
264
+ });
265
+
266
+ it('should throw error when no active topic', async () => {
267
+ const { result } = renderHook(() => useImageStore());
268
+
269
+ act(() => {
270
+ useImageStore.setState({
271
+ activeGenerationTopicId: '', // No active topic
272
+ });
273
+ });
274
+
275
+ await expect(
276
+ act(async () => {
277
+ await result.current.recreateImage('batch-id');
278
+ }),
279
+ ).rejects.toThrow('No active generation topic');
280
+ });
281
+
282
+ it('should handle service error', async () => {
283
+ const error = new Error('Service error');
284
+ mockImageService.createImage.mockRejectedValueOnce(error);
285
+
286
+ const mockRefreshGenerationBatches = vi.fn();
287
+ const mockRemoveGenerationBatch = vi.fn().mockResolvedValue(undefined);
288
+
289
+ const { result } = renderHook(() => useImageStore());
290
+
291
+ act(() => {
292
+ useImageStore.setState({
293
+ refreshGenerationBatches: mockRefreshGenerationBatches,
294
+ removeGenerationBatch: mockRemoveGenerationBatch,
295
+ });
296
+ });
297
+
298
+ await expect(
299
+ act(async () => {
300
+ await result.current.recreateImage('batch-id');
301
+ }),
302
+ ).rejects.toThrow('Service error');
303
+
304
+ // Verify batch was removed before the error
305
+ expect(mockRemoveGenerationBatch).toHaveBeenCalledWith('batch-id', 'active-topic-id');
306
+ });
307
+
308
+ it('should handle batch removal error', async () => {
309
+ const error = new Error('Removal error');
310
+ const mockRemoveGenerationBatch = vi.fn().mockRejectedValueOnce(error);
311
+
312
+ const { result } = renderHook(() => useImageStore());
313
+
314
+ act(() => {
315
+ useImageStore.setState({
316
+ removeGenerationBatch: mockRemoveGenerationBatch,
317
+ });
318
+ });
319
+
320
+ await expect(
321
+ act(async () => {
322
+ await result.current.recreateImage('batch-id');
323
+ }),
324
+ ).rejects.toThrow('Removal error');
325
+
326
+ // Verify image service was not called after removal error
327
+ expect(mockImageService.createImage).not.toHaveBeenCalled();
328
+ });
329
+ });
330
+ });