@lobehub/lobehub 2.0.0-next.53 → 2.0.0-next.55

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 (165) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/common.json +1 -0
  4. package/locales/ar/file.json +85 -2
  5. package/locales/bg-BG/common.json +1 -0
  6. package/locales/bg-BG/file.json +85 -2
  7. package/locales/de-DE/common.json +1 -0
  8. package/locales/de-DE/file.json +85 -2
  9. package/locales/en-US/common.json +1 -0
  10. package/locales/en-US/file.json +85 -2
  11. package/locales/es-ES/common.json +1 -0
  12. package/locales/es-ES/file.json +85 -2
  13. package/locales/fa-IR/common.json +1 -0
  14. package/locales/fa-IR/file.json +85 -2
  15. package/locales/fr-FR/common.json +1 -0
  16. package/locales/fr-FR/file.json +85 -2
  17. package/locales/it-IT/common.json +1 -0
  18. package/locales/it-IT/file.json +85 -2
  19. package/locales/ja-JP/common.json +1 -0
  20. package/locales/ja-JP/file.json +85 -2
  21. package/locales/ko-KR/common.json +1 -0
  22. package/locales/ko-KR/file.json +85 -2
  23. package/locales/nl-NL/common.json +1 -0
  24. package/locales/nl-NL/file.json +85 -2
  25. package/locales/pl-PL/common.json +1 -0
  26. package/locales/pl-PL/file.json +85 -2
  27. package/locales/pt-BR/common.json +1 -0
  28. package/locales/pt-BR/file.json +85 -2
  29. package/locales/ru-RU/common.json +1 -0
  30. package/locales/ru-RU/file.json +85 -2
  31. package/locales/tr-TR/common.json +1 -0
  32. package/locales/tr-TR/file.json +85 -2
  33. package/locales/vi-VN/common.json +1 -0
  34. package/locales/vi-VN/file.json +85 -2
  35. package/locales/zh-CN/common.json +1 -0
  36. package/locales/zh-CN/file.json +85 -2
  37. package/locales/zh-TW/common.json +1 -0
  38. package/locales/zh-TW/file.json +85 -2
  39. package/package.json +1 -1
  40. package/packages/database/src/models/__tests__/file.test.ts +94 -29
  41. package/packages/database/src/models/file.ts +15 -4
  42. package/packages/database/src/repositories/knowledge/index.test.ts +300 -0
  43. package/packages/database/src/repositories/knowledge/index.ts +420 -0
  44. package/packages/model-bank/src/aiModels/aihubmix.ts +1 -0
  45. package/packages/model-bank/src/aiModels/google.ts +9 -5
  46. package/packages/model-bank/src/aiModels/openai.ts +2 -35
  47. package/packages/model-bank/src/aiModels/openrouter.ts +1 -0
  48. package/packages/model-bank/src/aiModels/vertexai.ts +2 -0
  49. package/packages/model-bank/src/types/aiModel.ts +15 -2
  50. package/packages/model-runtime/src/core/usageConverters/index.ts +1 -0
  51. package/packages/model-runtime/src/core/usageConverters/utils/resolveImageSinglePrice.ts +34 -0
  52. package/packages/types/src/document/index.ts +14 -2
  53. package/packages/types/src/files/index.ts +2 -0
  54. package/packages/types/src/files/list.ts +10 -0
  55. package/packages/types/src/llm.ts +1 -1
  56. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect/ImageModelItem.tsx +93 -0
  57. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/{ModelSelect.tsx → ModelSelect/index.tsx} +17 -2
  58. package/src/app/[variants]/(main)/knowledge/KnowledgeRouter.tsx +2 -1
  59. package/src/app/[variants]/(main)/knowledge/components/KnowledgeBaseItem/index.tsx +0 -2
  60. package/src/app/[variants]/(main)/knowledge/hooks/useFileCategory.ts +6 -3
  61. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/index.tsx +2 -2
  62. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/{MenuItems.tsx → CategoryMenu.tsx} +3 -3
  63. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/Menu.tsx +2 -2
  64. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/index.tsx +40 -18
  65. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/layout/Container.tsx +1 -1
  66. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/CategoryMenu.tsx +148 -0
  67. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/KnowledgeBase.tsx +20 -7
  68. package/src/components/FileIcon/index.tsx +3 -1
  69. package/src/features/ChatInput/ActionBar/Knowledge/index.tsx +2 -2
  70. package/src/features/Conversation/Messages/Assistant/index.tsx +7 -1
  71. package/src/features/FileSidePanel/index.tsx +1 -1
  72. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItem.tsx +80 -0
  73. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItemWrapper.tsx +27 -0
  74. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/List.tsx +104 -23
  75. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/MasonrySkeleton.tsx +62 -0
  76. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/index.tsx +3 -2
  77. package/src/features/KnowledgeBaseModal/CreateNew/CreateForm.tsx +1 -1
  78. package/src/features/KnowledgeManager/DocumentExplorer/DocumentActions.tsx +111 -0
  79. package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditor.tsx +723 -0
  80. package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditorPlaceholder.tsx +169 -0
  81. package/src/features/KnowledgeManager/DocumentExplorer/DocumentListItem.tsx +148 -0
  82. package/src/features/KnowledgeManager/DocumentExplorer/DocumentListSkeleton.tsx +39 -0
  83. package/src/features/KnowledgeManager/DocumentExplorer/NoteEditorModal.tsx +348 -0
  84. package/src/features/KnowledgeManager/DocumentExplorer/RenamePopover.tsx +163 -0
  85. package/src/features/KnowledgeManager/DocumentExplorer/index.tsx +318 -0
  86. package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/index.tsx +48 -9
  87. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/DefaultFileItem.tsx +149 -0
  88. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/ImageFileItem.tsx +245 -0
  89. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/MarkdownFileItem.tsx +232 -0
  90. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/NoteFileItem.tsx +230 -0
  91. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/index.tsx +398 -0
  92. package/src/features/KnowledgeManager/FileExplorer/ToolBar/ViewSwitcher.tsx +45 -0
  93. package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/index.tsx +68 -16
  94. package/src/features/KnowledgeManager/Header/AddButton.tsx +107 -0
  95. package/src/features/KnowledgeManager/Header/NewNoteButton.tsx +33 -0
  96. package/src/features/{FileManager → KnowledgeManager}/Header/index.tsx +3 -9
  97. package/src/features/KnowledgeManager/Home/RecentDocumentCard.tsx +116 -0
  98. package/src/features/KnowledgeManager/Home/RecentDocuments.tsx +77 -0
  99. package/src/features/KnowledgeManager/Home/RecentFileCard.tsx +121 -0
  100. package/src/features/KnowledgeManager/Home/RecentFiles.tsx +73 -0
  101. package/src/features/KnowledgeManager/Home/RecentFilesSkeleton.tsx +83 -0
  102. package/src/features/KnowledgeManager/Home/UploadEntries.tsx +208 -0
  103. package/src/features/KnowledgeManager/Home/index.tsx +221 -0
  104. package/src/features/KnowledgeManager/index.tsx +75 -0
  105. package/src/features/Portal/FilePreview/Body/index.tsx +1 -1
  106. package/src/features/Portal/FilePreview/Header.tsx +1 -1
  107. package/src/locales/default/common.ts +1 -0
  108. package/src/locales/default/file.ts +85 -2
  109. package/src/locales/default/tool.ts +8 -0
  110. package/src/server/routers/lambda/__tests__/file.test.ts +85 -6
  111. package/src/server/routers/lambda/document.ts +57 -0
  112. package/src/server/routers/lambda/file.ts +72 -0
  113. package/src/server/routers/lambda/knowledge.ts +94 -0
  114. package/src/server/services/document/index.ts +103 -0
  115. package/src/services/document/index.ts +44 -0
  116. package/src/services/file/index.ts +5 -3
  117. package/src/store/aiInfra/slices/aiProvider/__tests__/action.test.ts +125 -229
  118. package/src/store/aiInfra/slices/aiProvider/action.ts +113 -33
  119. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +1 -1
  120. package/src/store/file/initialState.ts +6 -1
  121. package/src/store/file/slices/chat/action.ts +3 -3
  122. package/src/store/file/slices/document/action.ts +359 -0
  123. package/src/store/file/slices/document/index.ts +3 -0
  124. package/src/store/file/slices/document/initialState.ts +22 -0
  125. package/src/store/file/slices/document/selectors.ts +25 -0
  126. package/src/store/file/slices/fileManager/action.test.ts +16 -9
  127. package/src/store/file/slices/fileManager/action.ts +11 -11
  128. package/src/store/file/store.ts +3 -0
  129. package/src/store/global/initialState.ts +3 -1
  130. package/src/tools/interventions.ts +3 -5
  131. package/src/tools/local-system/Intervention/MoveLocalFiles/MoveFileItem.tsx +56 -0
  132. package/src/tools/local-system/Intervention/MoveLocalFiles/index.tsx +26 -0
  133. package/src/tools/local-system/Intervention/RunCommand/index.tsx +1 -2
  134. package/src/tools/local-system/Intervention/index.ts +11 -0
  135. package/src/tools/local-system/Render/MoveLocalFiles/MoveFileItem.tsx +56 -0
  136. package/src/tools/local-system/Render/MoveLocalFiles/index.tsx +26 -0
  137. package/src/tools/local-system/Render/index.ts +21 -0
  138. package/src/tools/renders.ts +6 -24
  139. package/src/tools/web-browsing/Render/index.ts +13 -0
  140. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/FileMenu.tsx +0 -75
  141. package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +0 -582
  142. package/src/features/FileManager/index.tsx +0 -36
  143. /package/src/features/{FileManager/FileList/ToolBar → KnowledgeBaseModal/AssignKnowledgeBase}/ViewSwitcher.tsx +0 -0
  144. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/ChunkItem.tsx +0 -0
  145. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/index.tsx +0 -0
  146. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Content.tsx +0 -0
  147. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Loading/index.tsx +0 -0
  148. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/Item.tsx +0 -0
  149. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/index.tsx +0 -0
  150. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/index.tsx +0 -0
  151. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/EmptyStatus.tsx +0 -0
  152. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/ChunkTag.tsx +0 -0
  153. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/DropdownMenu.tsx +0 -0
  154. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileSkeleton.tsx +0 -0
  155. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonryFileItem/MasonryItemWrapper.tsx +0 -0
  156. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonrySkeleton.tsx +0 -0
  157. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/Config.tsx +0 -0
  158. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/MultiSelectActions.tsx +0 -0
  159. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/index.tsx +0 -0
  160. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/useCheckTaskStatus.ts +0 -0
  161. /package/src/features/{FileManager → KnowledgeManager}/Header/FilesSearchBar.tsx +0 -0
  162. /package/src/features/{FileManager → KnowledgeManager}/Header/TogglePanelButton.tsx +0 -0
  163. /package/src/features/{FileManager → KnowledgeManager}/Header/UploadFileButton.tsx +0 -0
  164. /package/src/features/{FileManager → KnowledgeManager}/UploadDock/Item.tsx +0 -0
  165. /package/src/features/{FileManager → KnowledgeManager}/UploadDock/index.tsx +0 -0
@@ -0,0 +1,420 @@
1
+ import { FilesTabs, QueryFileListParams, SortType } from '@lobechat/types';
2
+ import { sql } from 'drizzle-orm';
3
+
4
+ import { DocumentModel } from '../../models/document';
5
+ import { FileModel } from '../../models/file';
6
+ import { documents, files, knowledgeBaseFiles } from '../../schemas';
7
+ import { LobeChatDatabase } from '../../type';
8
+
9
+ export interface KnowledgeItem {
10
+ chunkTaskId?: string | null;
11
+ content?: string | null;
12
+ createdAt: Date;
13
+ editorData?: Record<string, any> | null;
14
+ embeddingTaskId?: string | null;
15
+ fileType: string;
16
+ id: string;
17
+ metadata?: Record<string, any> | null;
18
+ name: string;
19
+ size: number;
20
+ /**
21
+ * Source type to distinguish between files and documents
22
+ * - 'file': from files table
23
+ * - 'document': from documents table
24
+ */
25
+ sourceType: 'file' | 'document';
26
+ updatedAt: Date;
27
+ url?: string;
28
+ }
29
+
30
+ /**
31
+ * Knowledge Repository - combines files and documents into a unified interface
32
+ */
33
+ export class KnowledgeRepo {
34
+ private userId: string;
35
+ private db: LobeChatDatabase;
36
+ private fileModel: FileModel;
37
+ private documentModel: DocumentModel;
38
+
39
+ constructor(db: LobeChatDatabase, userId: string) {
40
+ this.userId = userId;
41
+ this.db = db;
42
+ this.fileModel = new FileModel(db, userId);
43
+ this.documentModel = new DocumentModel(db, userId);
44
+ }
45
+
46
+ /**
47
+ * Query combined results from files and documents tables
48
+ */
49
+ async query({
50
+ category,
51
+ q,
52
+ sortType,
53
+ sorter,
54
+ knowledgeBaseId,
55
+ showFilesInKnowledgeBase,
56
+ }: QueryFileListParams = {}): Promise<KnowledgeItem[]> {
57
+ // Build file query
58
+ const fileQuery = this.buildFileQuery({
59
+ category,
60
+ knowledgeBaseId,
61
+ q,
62
+ showFilesInKnowledgeBase,
63
+ sortType,
64
+ sorter,
65
+ });
66
+
67
+ // Build document query (notes)
68
+ const documentQuery = this.buildDocumentQuery({
69
+ category,
70
+ knowledgeBaseId,
71
+ q,
72
+ sortType,
73
+ sorter,
74
+ });
75
+
76
+ // Combine both queries with UNION ALL
77
+ const combinedQuery = sql`
78
+ (${fileQuery})
79
+ UNION ALL
80
+ (${documentQuery})
81
+ `;
82
+
83
+ // Add final ordering
84
+ const orderClause = this.buildOrderClause(sortType, sorter);
85
+ const finalQuery = sql`
86
+ SELECT * FROM (${combinedQuery}) as combined
87
+ ORDER BY ${orderClause}
88
+ `;
89
+
90
+ const result = await this.db.execute(finalQuery);
91
+
92
+ const mappedResults = result.rows.map((row: any) => {
93
+ // Parse editor_data if it's a string (raw SQL returns JSONB as string)
94
+ let editorData = row.editor_data;
95
+ if (typeof editorData === 'string') {
96
+ try {
97
+ editorData = JSON.parse(editorData);
98
+ } catch (e) {
99
+ console.error('[KnowledgeRepo] Failed to parse editor_data:', e);
100
+ editorData = null;
101
+ }
102
+ }
103
+
104
+ // Parse metadata if it's a string (raw SQL returns JSONB as string)
105
+ let metadata = row.metadata;
106
+ if (typeof metadata === 'string') {
107
+ try {
108
+ metadata = JSON.parse(metadata);
109
+ } catch (e) {
110
+ console.error('[KnowledgeRepo] Failed to parse metadata:', e);
111
+ metadata = null;
112
+ }
113
+ }
114
+
115
+ return {
116
+ chunkTaskId: row.chunk_task_id,
117
+ content: row.content,
118
+ createdAt: new Date(row.created_at),
119
+ editorData,
120
+ embeddingTaskId: row.embedding_task_id,
121
+ fileType: row.file_type,
122
+ id: row.id,
123
+ metadata,
124
+ name: row.name,
125
+ size: Number(row.size),
126
+ sourceType: row.source_type,
127
+ updatedAt: new Date(row.updated_at),
128
+ url: row.url,
129
+ };
130
+ });
131
+
132
+ console.log('[KnowledgeRepo.query] Fetched items:', {
133
+ count: mappedResults.length,
134
+ documents: mappedResults.filter((item) => item.sourceType === 'document'),
135
+ sampleEditorData: mappedResults.find((item) => item.sourceType === 'document')?.editorData,
136
+ });
137
+
138
+ return mappedResults;
139
+ }
140
+
141
+ /**
142
+ * Delete item by id - routes to appropriate model based on sourceType
143
+ */
144
+ async deleteItem(id: string, sourceType: 'file' | 'document'): Promise<void> {
145
+ if (sourceType === 'file') {
146
+ await this.fileModel.delete(id);
147
+ } else {
148
+ await this.documentModel.delete(id);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Batch delete items
154
+ */
155
+ async deleteMany(items: Array<{ id: string; sourceType: 'file' | 'document' }>): Promise<void> {
156
+ const fileIds = items.filter((item) => item.sourceType === 'file').map((item) => item.id);
157
+ const documentIds = items
158
+ .filter((item) => item.sourceType === 'document')
159
+ .map((item) => item.id);
160
+
161
+ await Promise.all([
162
+ fileIds.length > 0 ? this.fileModel.deleteMany(fileIds) : Promise.resolve(),
163
+ documentIds.length > 0
164
+ ? Promise.all(documentIds.map((id) => this.documentModel.delete(id)))
165
+ : Promise.resolve(),
166
+ ]);
167
+ }
168
+
169
+ /**
170
+ * Find item by id
171
+ */
172
+ async findById(id: string, sourceType: 'file' | 'document'): Promise<any> {
173
+ if (sourceType === 'file') {
174
+ return this.fileModel.findById(id);
175
+ } else {
176
+ return this.documentModel.findById(id);
177
+ }
178
+ }
179
+
180
+ private buildFileQuery({
181
+ category,
182
+ q,
183
+ knowledgeBaseId,
184
+ showFilesInKnowledgeBase,
185
+ }: QueryFileListParams = {}): ReturnType<typeof sql> {
186
+ let whereConditions: any[] = [sql`${files.userId} = ${this.userId}`];
187
+
188
+ // Search filter
189
+ if (q) {
190
+ whereConditions.push(sql`${files.name} ILIKE ${`%${q}%`}`);
191
+ }
192
+
193
+ // Category filter
194
+ if (category && category !== FilesTabs.All && category !== FilesTabs.Home) {
195
+ const fileTypePrefix = this.getFileTypePrefix(category as FilesTabs);
196
+ if (Array.isArray(fileTypePrefix)) {
197
+ // For multiple file types (e.g., Documents includes 'application' and 'custom')
198
+ const orConditions = fileTypePrefix.map(
199
+ (prefix) => sql`${files.fileType} ILIKE ${`${prefix}%`}`,
200
+ );
201
+ whereConditions.push(sql`(${sql.join(orConditions, sql` OR `)})`);
202
+ } else {
203
+ whereConditions.push(sql`${files.fileType} ILIKE ${`${fileTypePrefix}%`}`);
204
+ }
205
+ }
206
+
207
+ // Knowledge base filter
208
+ if (knowledgeBaseId) {
209
+ // Build where conditions using proper table references (f.column instead of files.column)
210
+ const kbWhereConditions: any[] = [sql`f.user_id = ${this.userId}`];
211
+
212
+ // Search filter
213
+ if (q) {
214
+ kbWhereConditions.push(sql`f.name ILIKE ${`%${q}%`}`);
215
+ }
216
+
217
+ // Category filter
218
+ if (category && category !== FilesTabs.All && category !== FilesTabs.Home) {
219
+ const fileTypePrefix = this.getFileTypePrefix(category as FilesTabs);
220
+ if (Array.isArray(fileTypePrefix)) {
221
+ const orConditions = fileTypePrefix.map(
222
+ (prefix) => sql`f.file_type ILIKE ${`${prefix}%`}`,
223
+ );
224
+ kbWhereConditions.push(sql`(${sql.join(orConditions, sql` OR `)})`);
225
+ } else {
226
+ kbWhereConditions.push(sql`f.file_type ILIKE ${`${fileTypePrefix}%`}`);
227
+ }
228
+ }
229
+
230
+ return sql`
231
+ SELECT
232
+ f.id,
233
+ f.name,
234
+ f.file_type,
235
+ f.size,
236
+ f.url,
237
+ f.created_at,
238
+ f.updated_at,
239
+ f.chunk_task_id,
240
+ f.embedding_task_id,
241
+ NULL as editor_data,
242
+ NULL as content,
243
+ NULL as metadata,
244
+ 'file' as source_type
245
+ FROM ${files} f
246
+ INNER JOIN ${knowledgeBaseFiles} kbf
247
+ ON f.id = kbf.file_id
248
+ AND kbf.knowledge_base_id = ${knowledgeBaseId}
249
+ WHERE ${sql.join(kbWhereConditions, sql` AND `)}
250
+ `;
251
+ }
252
+
253
+ // Exclude files in knowledge base if needed
254
+ if (!showFilesInKnowledgeBase) {
255
+ whereConditions.push(
256
+ sql`
257
+ NOT EXISTS (
258
+ SELECT 1 FROM ${knowledgeBaseFiles}
259
+ WHERE ${knowledgeBaseFiles.fileId} = ${files.id}
260
+ )
261
+ `,
262
+ );
263
+ }
264
+
265
+ return sql`
266
+ SELECT
267
+ id,
268
+ name,
269
+ file_type,
270
+ size,
271
+ url,
272
+ created_at,
273
+ updated_at,
274
+ chunk_task_id,
275
+ embedding_task_id,
276
+ NULL as editor_data,
277
+ NULL as content,
278
+ NULL as metadata,
279
+ 'file' as source_type
280
+ FROM ${files}
281
+ WHERE ${sql.join(whereConditions, sql` AND `)}
282
+ `;
283
+ }
284
+
285
+ private buildDocumentQuery({
286
+ category,
287
+ q,
288
+ knowledgeBaseId,
289
+ }: QueryFileListParams = {}): ReturnType<typeof sql> {
290
+ let whereConditions: any[] = [sql`${documents.userId} = ${this.userId}`];
291
+
292
+ // Search filter
293
+ if (q) {
294
+ whereConditions.push(
295
+ sql`(${documents.title} ILIKE ${`%${q}%`} OR ${documents.filename} ILIKE ${`%${q}%`})`,
296
+ );
297
+ }
298
+
299
+ // Category filter - match documents by fileType prefix
300
+ if (category && category !== FilesTabs.All && category !== FilesTabs.Home) {
301
+ const fileTypePrefix = this.getFileTypePrefix(category as FilesTabs);
302
+ if (Array.isArray(fileTypePrefix)) {
303
+ // For multiple file types (e.g., Documents includes 'application' and 'custom')
304
+ const orConditions = fileTypePrefix.map(
305
+ (prefix) => sql`${documents.fileType} ILIKE ${`${prefix}%`}`,
306
+ );
307
+ whereConditions.push(sql`(${sql.join(orConditions, sql` OR `)})`);
308
+
309
+ // Exclude custom/document and source_type='file' from Documents category
310
+ if (category === FilesTabs.Documents) {
311
+ whereConditions.push(sql`${documents.fileType} != ${'custom/document'}`, sql`${documents.sourceType} != ${'file'}`);
312
+ }
313
+ } else if (fileTypePrefix) {
314
+ whereConditions.push(sql`${documents.fileType} ILIKE ${`${fileTypePrefix}%`}`);
315
+ } else {
316
+ // Exclude documents from other categories (Images, Videos, Audios, Websites)
317
+ return sql`
318
+ SELECT
319
+ NULL::varchar(30) as id,
320
+ NULL::text as name,
321
+ NULL::varchar(255) as file_type,
322
+ NULL::integer as size,
323
+ NULL::text as url,
324
+ NULL::timestamp with time zone as created_at,
325
+ NULL::timestamp with time zone as updated_at,
326
+ NULL::uuid as chunk_task_id,
327
+ NULL::uuid as embedding_task_id,
328
+ NULL::jsonb as editor_data,
329
+ NULL::text as content,
330
+ NULL::jsonb as metadata,
331
+ NULL::text as source_type
332
+ WHERE false
333
+ `;
334
+ }
335
+ }
336
+
337
+ // Knowledge base filter for documents
338
+ // Documents don't have knowledge base association currently, so skip if knowledgeBaseId is set
339
+ if (knowledgeBaseId) {
340
+ return sql`
341
+ SELECT
342
+ NULL::varchar(30) as id,
343
+ NULL::text as name,
344
+ NULL::varchar(255) as file_type,
345
+ NULL::integer as size,
346
+ NULL::text as url,
347
+ NULL::timestamp with time zone as created_at,
348
+ NULL::timestamp with time zone as updated_at,
349
+ NULL::uuid as chunk_task_id,
350
+ NULL::uuid as embedding_task_id,
351
+ NULL::jsonb as editor_data,
352
+ NULL::text as content,
353
+ NULL::jsonb as metadata,
354
+ NULL::text as source_type
355
+ WHERE false
356
+ `;
357
+ }
358
+
359
+ return sql`
360
+ SELECT
361
+ id,
362
+ COALESCE(title, filename, 'Untitled') as name,
363
+ file_type,
364
+ total_char_count as size,
365
+ source as url,
366
+ created_at,
367
+ updated_at,
368
+ NULL as chunk_task_id,
369
+ NULL as embedding_task_id,
370
+ editor_data,
371
+ content,
372
+ metadata,
373
+ 'document' as source_type
374
+ FROM ${documents}
375
+ WHERE ${sql.join(whereConditions, sql` AND `)}
376
+ `;
377
+ }
378
+
379
+ private buildOrderClause(
380
+ sortType?: string,
381
+ sorter?: string,
382
+ ): ReturnType<typeof sql.raw> | ReturnType<typeof sql> {
383
+ const sortableFields: Record<string, string> = {
384
+ createdAt: 'created_at',
385
+ name: 'name',
386
+ size: 'size',
387
+ updatedAt: 'updated_at',
388
+ };
389
+
390
+ if (sorter && sortType && sorter in sortableFields) {
391
+ const direction = sortType.toLowerCase() === SortType.Asc ? 'ASC' : 'DESC';
392
+ return sql.raw(`${sortableFields[sorter]} ${direction}`);
393
+ }
394
+
395
+ return sql.raw('created_at DESC');
396
+ }
397
+
398
+ private getFileTypePrefix(category: FilesTabs): string | string[] {
399
+ switch (category) {
400
+ case FilesTabs.Audios: {
401
+ return 'audio';
402
+ }
403
+ case FilesTabs.Documents: {
404
+ return ['application', 'custom'];
405
+ }
406
+ case FilesTabs.Images: {
407
+ return 'image';
408
+ }
409
+ case FilesTabs.Videos: {
410
+ return 'video';
411
+ }
412
+ case FilesTabs.Websites: {
413
+ return 'text/html';
414
+ }
415
+ default: {
416
+ return '';
417
+ }
418
+ }
419
+ }
420
+ }
@@ -1056,6 +1056,7 @@ const aihubmixModels: AIChatModelCard[] = [
1056
1056
  id: 'gemini-2.5-flash-image',
1057
1057
  maxOutput: 8192,
1058
1058
  pricing: {
1059
+ approximatePricePerImage: 0.039,
1059
1060
  units: [
1060
1061
  { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
1061
1062
  { name: 'imageInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
@@ -370,6 +370,7 @@ const googleChatModels: AIChatModelCard[] = [
370
370
  id: 'gemini-2.5-flash-image',
371
371
  maxOutput: 8192,
372
372
  pricing: {
373
+ approximatePricePerImage: 0.039,
373
374
  units: [
374
375
  { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
375
376
  { name: 'imageInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
@@ -392,6 +393,7 @@ const googleChatModels: AIChatModelCard[] = [
392
393
  id: 'gemini-2.5-flash-image-preview',
393
394
  maxOutput: 8192,
394
395
  pricing: {
396
+ approximatePricePerImage: 0.039,
395
397
  units: [
396
398
  { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
397
399
  { name: 'imageInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
@@ -864,6 +866,7 @@ const googleImageModels: AIImageModelCard[] = [
864
866
  releasedAt: '2025-08-26',
865
867
  parameters: nanoBananaParameters,
866
868
  pricing: {
869
+ approximatePricePerImage: 0.039,
867
870
  units: [
868
871
  { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
869
872
  { name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
@@ -880,6 +883,7 @@ const googleImageModels: AIImageModelCard[] = [
880
883
  releasedAt: '2025-08-26',
881
884
  parameters: CHAT_MODEL_IMAGE_GENERATION_PARAMS,
882
885
  pricing: {
886
+ approximatePricePerImage: 0.039,
883
887
  units: [
884
888
  { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
885
889
  { name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
@@ -892,7 +896,7 @@ const googleImageModels: AIImageModelCard[] = [
892
896
  id: 'imagen-4.0-generate-001',
893
897
  enabled: true,
894
898
  type: 'image',
895
- description: 'Imagen 4th generation text-to-image model series',
899
+ description: 'Imagen 第四代文生图模型系列',
896
900
  organization: 'Deepmind',
897
901
  releasedAt: '2025-08-15',
898
902
  parameters: imagenGenParameters,
@@ -905,7 +909,7 @@ const googleImageModels: AIImageModelCard[] = [
905
909
  id: 'imagen-4.0-ultra-generate-001',
906
910
  enabled: true,
907
911
  type: 'image',
908
- description: 'Imagen 4th generation text-to-image model series Ultra version',
912
+ description: 'Imagen 第四代文生图模型系列的 Ultra 版本',
909
913
  organization: 'Deepmind',
910
914
  releasedAt: '2025-08-15',
911
915
  parameters: imagenGenParameters,
@@ -918,7 +922,7 @@ const googleImageModels: AIImageModelCard[] = [
918
922
  id: 'imagen-4.0-fast-generate-001',
919
923
  enabled: true,
920
924
  type: 'image',
921
- description: 'Imagen 4th generation text-to-image model series Fast version',
925
+ description: 'Imagen 第四代文生图模型系列的快速版本',
922
926
  organization: 'Deepmind',
923
927
  releasedAt: '2025-08-15',
924
928
  parameters: imagenGenParameters,
@@ -930,7 +934,7 @@ const googleImageModels: AIImageModelCard[] = [
930
934
  displayName: 'Imagen 4 Preview 06-06',
931
935
  id: 'imagen-4.0-generate-preview-06-06',
932
936
  type: 'image',
933
- description: 'Imagen 4th generation text-to-image model series',
937
+ description: 'Imagen 第四代文生图模型系列',
934
938
  organization: 'Deepmind',
935
939
  releasedAt: '2025-06-06',
936
940
  parameters: imagenGenParameters,
@@ -942,7 +946,7 @@ const googleImageModels: AIImageModelCard[] = [
942
946
  displayName: 'Imagen 4 Ultra Preview 06-06',
943
947
  id: 'imagen-4.0-ultra-generate-preview-06-06',
944
948
  type: 'image',
945
- description: 'Imagen 4th generation text-to-image model series Ultra version',
949
+ description: 'Imagen 第四代文生图模型系列的 Ultra 版本',
946
950
  organization: 'Deepmind',
947
951
  releasedAt: '2025-06-11',
948
952
  parameters: imagenGenParameters,
@@ -1171,31 +1171,13 @@ export const openaiImageModels: AIImageModelCard[] = [
1171
1171
  id: 'gpt-image-1',
1172
1172
  parameters: gptImage1ParamsSchema,
1173
1173
  pricing: {
1174
+ approximatePricePerImage: 0.042,
1174
1175
  units: [
1175
1176
  { name: 'textInput', rate: 5, strategy: 'fixed', unit: 'millionTokens' },
1176
1177
  { name: 'textInput_cacheRead', rate: 1.25, strategy: 'fixed', unit: 'millionTokens' },
1177
1178
  { name: 'imageInput', rate: 10, strategy: 'fixed', unit: 'millionTokens' },
1178
1179
  { name: 'imageInput_cacheRead', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
1179
1180
  { name: 'imageOutput', rate: 40, strategy: 'fixed', unit: 'millionTokens' },
1180
- {
1181
- lookup: {
1182
- prices: {
1183
- high_1024x1024: 0.167,
1184
- high_1024x1536: 0.25,
1185
- high_1536x1024: 0.25,
1186
- low_1024x1024: 0.011,
1187
- low_1024x1536: 0.016,
1188
- low_1536x1024: 0.016,
1189
- medium_1024x1024: 0.042,
1190
- medium_1024x1536: 0.063,
1191
- medium_1536x1024: 0.063,
1192
- },
1193
- pricingParams: ['quality', 'size'],
1194
- },
1195
- name: 'imageGeneration',
1196
- strategy: 'lookup',
1197
- unit: 'image',
1198
- },
1199
1181
  ],
1200
1182
  },
1201
1183
  resolutions: ['1024x1024', '1024x1536', '1536x1024'],
@@ -1208,28 +1190,13 @@ export const openaiImageModels: AIImageModelCard[] = [
1208
1190
  id: 'gpt-image-1-mini',
1209
1191
  parameters: gptImage1ParamsSchema,
1210
1192
  pricing: {
1193
+ approximatePricePerImage: 0.011,
1211
1194
  units: [
1212
1195
  { name: 'textInput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
1213
1196
  { name: 'textInput_cacheRead', rate: 0.2, strategy: 'fixed', unit: 'millionTokens' },
1214
1197
  { name: 'imageInput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
1215
1198
  { name: 'imageInput_cacheRead', rate: 0.25, strategy: 'fixed', unit: 'millionTokens' },
1216
1199
  { name: 'imageOutput', rate: 8, strategy: 'fixed', unit: 'millionTokens' },
1217
- {
1218
- lookup: {
1219
- prices: {
1220
- low_1024x1024: 0.005,
1221
- low_1024x1536: 0.006,
1222
- low_1536x1024: 0.006,
1223
- medium_1024x1024: 0.011,
1224
- medium_1024x1536: 0.015,
1225
- medium_1536x1024: 0.015,
1226
- },
1227
- pricingParams: ['quality', 'size'],
1228
- },
1229
- name: 'imageGeneration',
1230
- strategy: 'lookup',
1231
- unit: 'image',
1232
- },
1233
1200
  ],
1234
1201
  },
1235
1202
  releasedAt: '2025-10-06',
@@ -41,6 +41,7 @@ const openrouterChatModels: AIChatModelCard[] = [
41
41
  id: 'google/gemini-2.5-flash-image-preview',
42
42
  maxOutput: 8192,
43
43
  pricing: {
44
+ approximatePricePerImage: 0.039,
44
45
  units: [
45
46
  { name: 'imageOutput', rate: 30, strategy: 'fixed', unit: 'millionTokens' },
46
47
  { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
@@ -135,6 +135,7 @@ const vertexaiChatModels: AIChatModelCard[] = [
135
135
  id: 'gemini-2.5-flash-image-preview',
136
136
  maxOutput: 8192,
137
137
  pricing: {
138
+ approximatePricePerImage: 0.039,
138
139
  units: [
139
140
  { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
140
141
  { name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
@@ -291,6 +292,7 @@ const vertexaiImageModels: AIImageModelCard[] = [
291
292
  releasedAt: '2025-08-26',
292
293
  parameters: nanoBananaParameters,
293
294
  pricing: {
295
+ approximatePricePerImage: 0.039,
294
296
  units: [
295
297
  { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
296
298
  { name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
@@ -185,6 +185,10 @@ export interface LookupPricingUnit extends PricingUnitBase {
185
185
  export type PricingUnit = FixedPricingUnit | TieredPricingUnit | LookupPricingUnit;
186
186
 
187
187
  export interface Pricing {
188
+ /**
189
+ * Fallback approximate per-image price (USD) when detailed pricing table is unavailable
190
+ */
191
+ approximatePricePerImage?: number;
188
192
  currency?: ModelPriceCurrency;
189
193
  units: PricingUnit[];
190
194
  }
@@ -391,13 +395,22 @@ export const ToggleAiModelEnableSchema = z.object({
391
395
 
392
396
  export type ToggleAiModelEnableParams = z.infer<typeof ToggleAiModelEnableSchema>;
393
397
 
394
- //
395
-
396
398
  export interface AiModelForSelect {
397
399
  abilities: ModelAbilities;
400
+ /**
401
+ * Approximate per-image price (USD), used when exact calculation is not possible
402
+ */
403
+ approximatePricePerImage?: number;
398
404
  contextWindowTokens?: number;
405
+ description?: string;
399
406
  displayName?: string;
400
407
  id: string;
408
+ parameters?: ModelParamsSchema;
409
+ /**
410
+ * Exact per-image price (USD) calculated from pricing units
411
+ */
412
+ pricePerImage?: number;
413
+ pricing?: Pricing;
401
414
  }
402
415
 
403
416
  export interface EnabledAiModel {
@@ -2,3 +2,4 @@ export { convertAnthropicUsage } from './anthropic';
2
2
  export { convertGoogleAIUsage } from './google-ai';
3
3
  export { convertOpenAIResponseUsage, convertOpenAIUsage } from './openai';
4
4
  export { computeImageCost } from './utils/computeImageCost';
5
+ export { resolveImageSinglePrice } from './utils/resolveImageSinglePrice';
@@ -0,0 +1,34 @@
1
+ import { Pricing } from 'model-bank';
2
+
3
+ export interface ImageSinglePriceResult {
4
+ approximatePrice?: number;
5
+ price?: number;
6
+ }
7
+
8
+ const DEFAULT_REFERENCE_MP = (1024 * 1024) / 1_000_000;
9
+
10
+ export const resolveImageSinglePrice = (pricing?: Pricing): ImageSinglePriceResult => {
11
+ if (!pricing) return {};
12
+
13
+ // Priority 1: Use approximate price if explicitly provided
14
+ if (typeof pricing.approximatePricePerImage === 'number') {
15
+ return { approximatePrice: pricing.approximatePricePerImage };
16
+ }
17
+
18
+ // Priority 2: Calculate exact price from pricing units
19
+ const imageGenerationUnit = pricing.units.find((unit) => unit.name === 'imageGeneration');
20
+ if (!imageGenerationUnit) return {};
21
+
22
+ if (imageGenerationUnit.strategy === 'fixed') {
23
+ if (imageGenerationUnit.unit === 'image') {
24
+ return { price: imageGenerationUnit.rate };
25
+ }
26
+
27
+ if (imageGenerationUnit.unit === 'megapixel') {
28
+ return { price: imageGenerationUnit.rate * DEFAULT_REFERENCE_MP };
29
+ }
30
+ }
31
+
32
+ // Lookup/tiered pricing typically requires explicit configuration; treat as unavailable here.
33
+ return {};
34
+ };