diff --git a/package.json b/package.json index 36c1bc305567..028e57030fbf 100644 --- a/package.json +++ b/package.json @@ -99,33 +99,33 @@ ] }, "dependencies": { - "@ant-design/icons": "^5.3.7", + "@ant-design/icons": "^5.4.0", "@anthropic-ai/sdk": "^0.24.3", "@auth/core": "0.28.0", - "@aws-sdk/client-bedrock-runtime": "^3.609.0", - "@aws-sdk/client-s3": "^3.609.0", - "@aws-sdk/s3-request-presigner": "^3.609.0", + "@aws-sdk/client-bedrock-runtime": "^3.637.0", + "@aws-sdk/client-s3": "^3.637.0", + "@aws-sdk/s3-request-presigner": "^3.637.0", "@azure/core-rest-pipeline": "1.16.0", "@azure/openai": "1.0.0-beta.12", "@cfworker/json-schema": "^1.12.8", "@clerk/localizations": "2.0.0", - "@clerk/nextjs": "^5.2.14", - "@clerk/themes": "^2.1.10", + "@clerk/nextjs": "^5.3.3", + "@clerk/themes": "^2.1.21", "@cyntler/react-doc-viewer": "^1.16.6", "@google/generative-ai": "^0.16.0", "@icons-pack/react-simple-icons": "9.6.0", "@khmyznikov/pwa-install": "^0.3.9", - "@langchain/community": "^0.2.27", + "@langchain/community": "^0.2.31", "@lobehub/chat-plugin-sdk": "^1.32.4", "@lobehub/chat-plugins-gateway": "^1.9.0", "@lobehub/icons": "^1.28.0", "@lobehub/tts": "^1.24.3", "@lobehub/ui": "^1.149.2", "@neondatabase/serverless": "^0.9.4", - "@next/third-parties": "^14.2.4", - "@sentry/nextjs": "^7.118.0", + "@next/third-parties": "^14.2.6", + "@sentry/nextjs": "^7.119.0", "@t3-oss/env-nextjs": "^0.10.1", - "@tanstack/react-query": "^5.51.11", + "@tanstack/react-query": "^5.52.1", "@trpc/client": "next", "@trpc/next": "next", "@trpc/react-query": "next", @@ -133,32 +133,32 @@ "@vercel/analytics": "^1.3.1", "@vercel/edge-config": "^1.2.1", "@vercel/speed-insights": "^1.0.12", - "ahooks": "^3.8.0", - "ai": "^3.2.16", + "ahooks": "^3.8.1", + "ai": "^3.3.16", "antd": "^5.20.2", "antd-style": "^3.6.2", "brotli-wasm": "^3.0.1", - "chroma-js": "^2.4.2", - "dayjs": "^1.11.11", - "debug": "^4.3.5", + "chroma-js": "^2.6.0", + "dayjs": "^1.11.13", + "debug": "^4.3.6", "dexie": "^3.2.7", "diff": "^5.2.0", - "drizzle-orm": "^0.32.0", + "drizzle-orm": "^0.32.2", "drizzle-zod": "^0.5.1", "fast-deep-equal": "^3.1.3", "file-type": "^19.4.1", - "gpt-tokenizer": "^2.1.2", - "i18next": "^23.11.5", + "gpt-tokenizer": "^2.2.1", + "i18next": "^23.14.0", "i18next-browser-languagedetector": "^7.2.1", "i18next-resources-to-backend": "^1.2.1", "idb-keyval": "^6.2.1", "immer": "^10.1.1", "ip": "^2.0.1", - "jose": "^5.6.3", + "jose": "^5.7.0", "js-sha256": "^0.11.0", - "langchain": "^0.2.16", - "langfuse": "^3.14.0", - "langfuse-core": "^3.14.0", + "langchain": "^0.2.17", + "langfuse": "^3.19.0", + "langfuse-core": "^3.19.0", "lodash-es": "^4.17.21", "lucide-react": "latest", "mammoth": "^1.8.0", @@ -168,7 +168,7 @@ "next-auth": "5.0.0-beta.15", "next-sitemap": "^4.2.3", "numeral": "^2.0.6", - "nuqs": "^1.17.4", + "nuqs": "^1.17.8", "officeparser": "^4.1.1", "ollama": "^0.5.8", "openai": "~4.54.0", @@ -176,11 +176,11 @@ "pdf-parse": "^1.1.1", "pdfjs-dist": "4.4.168", "pg": "^8.12.0", - "pino": "^9.2.0", + "pino": "^9.3.2", "polished": "^4.3.1", - "posthog-js": "^1.144.2", + "posthog-js": "^1.157.2", "pwa-install-handler": "^2.6.0", - "query-string": "^9.0.0", + "query-string": "^9.1.0", "random-words": "^2.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -190,22 +190,22 @@ "react-layout-kit": "^1.9.0", "react-lazy-load": "^4.0.1", "react-pdf": "^9.1.0", - "react-virtuoso": "^4.7.11", + "react-virtuoso": "^4.10.1", "react-wrap-balancer": "^1.1.1", "remark": "^14.0.3", "remark-gfm": "^3.0.1", "remark-html": "^15.0.2", - "resolve-accept-language": "^3.1.4", + "resolve-accept-language": "^3.1.5", "rtl-detect": "^1.1.2", - "semver": "^7.6.2", - "sharp": "^0.33.4", + "semver": "^7.6.3", + "sharp": "^0.33.5", "superjson": "^2.2.1", - "svix": "^1.24.0", + "svix": "^1.30.0", "swr": "^2.2.5", "systemjs": "^6.15.1", "ts-md5": "^1.3.1", "ua-parser-js": "^1.0.38", - "unstructured-client": "^0.15.0", + "unstructured-client": "^0.15.1", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "utility-types": "^3.11.0", @@ -213,74 +213,74 @@ "ws": "^8.18.0", "y-protocols": "^1.0.6", "y-webrtc": "^10.3.0", - "yaml": "^2.4.5", + "yaml": "^2.5.0", "yjs": "^13.6.18", "zod": "^3.23.8", - "zustand": "^4.5.4", + "zustand": "^4.5.5", "zustand-utils": "^1.3.2" }, "devDependencies": { - "@commitlint/cli": "^19.3.0", - "@ducanh2912/next-pwa": "^10.2.7", - "@edge-runtime/vm": "^4.0.0", + "@commitlint/cli": "^19.4.0", + "@ducanh2912/next-pwa": "^10.2.8", + "@edge-runtime/vm": "^4.0.2", "@lobehub/i18n-cli": "^1.19.1", - "@lobehub/lint": "^1.24.3", - "@lobehub/seo-cli": "^1.4.1", - "@next/bundle-analyzer": "^14.2.4", - "@next/eslint-plugin-next": "^14.2.4", + "@lobehub/lint": "^1.24.4", + "@lobehub/seo-cli": "^1.4.2", + "@next/bundle-analyzer": "^14.2.6", + "@next/eslint-plugin-next": "^14.2.6", "@peculiar/webcrypto": "^1.5.0", - "@testing-library/jest-dom": "^6.4.6", + "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", "@types/chroma-js": "^2.4.4", "@types/debug": "^4.1.12", "@types/diff": "^5.2.1", "@types/ip": "^1.1.3", "@types/json-schema": "^7.0.15", - "@types/lodash": "^4.17.6", + "@types/lodash": "^4.17.7", "@types/lodash-es": "^4.17.12", - "@types/node": "^20.14.10", + "@types/node": "^20.16.1", "@types/numeral": "^2.0.5", "@types/pg": "^8.11.6", - "@types/react": "^18.3.3", + "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", "@types/rtl-detect": "^1.0.3", "@types/semver": "^7.5.8", "@types/systemjs": "^6.13.5", "@types/ua-parser-js": "^0.7.39", "@types/uuid": "^10.0.0", - "@types/ws": "^8.5.10", + "@types/ws": "^8.5.12", "@vitest/coverage-v8": "~1.2.2", "ajv-keywords": "^5.1.0", - "commitlint": "^19.3.0", + "commitlint": "^19.4.0", "consola": "^3.2.3", "dotenv": "^16.4.5", "dpdm": "^3.14.0", - "drizzle-kit": "^0.23.0", + "drizzle-kit": "^0.23.2", "eslint": "^8.57.0", "eslint-plugin-mdx": "^2.3.4", "eslint-plugin-unused-imports": "4.0.1", "fake-indexeddb": "^6.0.0", - "glob": "^10.4.3", + "glob": "^10.4.5", "gray-matter": "^4.0.3", "happy-dom": "^14.12.3", - "husky": "^9.0.11", + "husky": "^9.1.5", "just-diff": "^6.0.2", - "lint-staged": "^15.2.7", + "lint-staged": "^15.2.9", "lodash": "^4.17.21", "markdown-table": "^3.0.3", "node-fetch": "^3.3.2", - "node-gyp": "^10.1.0", + "node-gyp": "^10.2.0", "p-map": "^7.0.2", - "prettier": "^3.3.2", + "prettier": "^3.3.3", "remark-cli": "^11.0.0", "remark-parse": "^10.0.2", "semantic-release": "^21.1.2", "stylelint": "^15.11.0", - "tsx": "^4.16.2", - "typescript": "^5.5.3", + "tsx": "^4.17.0", + "typescript": "^5.5.4", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", - "vite": "^5.3.3", + "vite": "^5.4.2", "vitest": "~1.2.2", "vitest-canvas-mock": "^0.3.3" }, diff --git a/src/app/(main)/files/[id]/Header.tsx b/src/app/(main)/files/[id]/Header.tsx index 840a6acc72a5..c52b58fb8972 100644 --- a/src/app/(main)/files/[id]/Header.tsx +++ b/src/app/(main)/files/[id]/Header.tsx @@ -9,7 +9,7 @@ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; -import { downloadFile } from '@/utils/downloadFile'; +import { downloadFile } from '@/utils/client/downloadFile'; interface HeaderProps { filename: string; diff --git a/src/features/FileManager/FileList/FileListItem/DropdownMenu.tsx b/src/features/FileManager/FileList/FileListItem/DropdownMenu.tsx index 22465bf714f3..43b09a684e8e 100644 --- a/src/features/FileManager/FileList/FileListItem/DropdownMenu.tsx +++ b/src/features/FileManager/FileList/FileListItem/DropdownMenu.tsx @@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next'; import { useAddFilesToKnowledgeBaseModal } from '@/features/KnowledgeBaseModal'; import { useFileStore } from '@/store/file'; import { useKnowledgeBaseStore } from '@/store/knowledgeBase'; -import { downloadFile } from '@/utils/downloadFile'; +import { downloadFile } from '@/utils/client/downloadFile'; interface DropdownMenuProps { filename: string; diff --git a/src/features/FileViewer/NotSupport/index.tsx b/src/features/FileViewer/NotSupport/index.tsx index efa5eaca1166..a5b90a2a9985 100644 --- a/src/features/FileViewer/NotSupport/index.tsx +++ b/src/features/FileViewer/NotSupport/index.tsx @@ -5,7 +5,7 @@ import React, { ComponentType, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Center, Flexbox } from 'react-layout-kit'; -import { downloadFile } from '@/utils/downloadFile'; +import { downloadFile } from '@/utils/client/downloadFile'; const useStyles = createStyles(({ css, token }) => ({ page: css` diff --git a/src/server/routers/lambda/file.ts b/src/server/routers/lambda/file.ts index d9134640329d..0725fda6a4e9 100644 --- a/src/server/routers/lambda/file.ts +++ b/src/server/routers/lambda/file.ts @@ -66,7 +66,10 @@ export const fileRouter = router({ }), ) .query(async ({ ctx, input }) => { - return ctx.fileModel.findById(input.id); + const item = await ctx.fileModel.findById(input.id); + if (!item) throw new TRPCError({ code: 'BAD_REQUEST', message: 'File not found' }); + + return { ...item, url: getFullFileUrl(item?.url) }; }), getFileItemById: fileProcedure diff --git a/src/services/__tests__/chat.test.ts b/src/services/__tests__/chat.test.ts index 99071c431c78..efb9a204118f 100644 --- a/src/services/__tests__/chat.test.ts +++ b/src/services/__tests__/chat.test.ts @@ -180,48 +180,6 @@ describe('ChatService', () => { ); }); - it('should not include image content when default model', async () => { - const messages = [ - { content: 'Hello', role: 'user', files: ['file1'] }, // Message with files - { content: 'Hi', role: 'tool', plugin: { identifier: 'plugin1', apiName: 'api1' } }, // Message with function role - { content: 'Hey', role: 'assistant' }, // Regular user message - ] as ChatMessage[]; - - // Mock file store state to return a specific image URL or Base64 for the given files - act(() => { - useFileStore.setState({ - imagesMap: { - file1: { - id: 'file1', - name: 'abc.png', - saveMode: 'url', - fileType: 'image/png', - url: 'http://example.com/image.jpg', - }, - }, - }); - }); - - const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion'); - await chatService.createAssistantMessage({ - messages, - plugins: [], - model: 'gpt-3.5-turbo', - }); - - expect(getChatCompletionSpy).toHaveBeenCalledWith( - { - messages: [ - { content: 'Hello', role: 'user' }, - { content: 'Hi', name: 'plugin1____api1', role: 'tool' }, - { content: 'Hey', role: 'assistant' }, - ], - model: 'gpt-3.5-turbo', - }, - undefined, - ); - }); - it('should not include image with vision models when can not find the image', async () => { const messages = [ { content: 'Hello', role: 'user', files: ['file2'] }, // Message with files @@ -229,21 +187,6 @@ describe('ChatService', () => { { content: 'Hey', role: 'assistant' }, // Regular user message ] as ChatMessage[]; - // Mock file store state to return a specific image URL or Base64 for the given files - act(() => { - useFileStore.setState({ - imagesMap: { - file1: { - id: 'file1', - name: 'abc.png', - saveMode: 'url', - fileType: 'image/png', - url: 'http://example.com/image.jpg', - }, - }, - }); - }); - const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion'); await chatService.createAssistantMessage({ messages, plugins: [] }); diff --git a/src/services/file/client.test.ts b/src/services/file/client.test.ts index da5b6ad72bc6..b7282fb4de4a 100644 --- a/src/services/file/client.test.ts +++ b/src/services/file/client.test.ts @@ -74,13 +74,15 @@ describe('FileService', () => { describe('getFile', () => { it('should retrieve and convert local file info to FilePreview', async () => { const fileId = '1'; - const fileData: DB_File = { + const fileData = { name: 'test', data: new ArrayBuffer(1), fileType: 'image/png', saveMode: 'local', size: 1, - }; + createdAt: 1, + updatedAt: 2, + } as DB_File; (FileModel.findById as Mock).mockResolvedValue(fileData); (global.URL.createObjectURL as Mock).mockReturnValue('blob:test'); @@ -90,12 +92,13 @@ describe('FileService', () => { expect(FileModel.findById).toHaveBeenCalledWith(fileId); expect(result).toEqual({ + createdAt: new Date(1), id: '1', - base64Url: 'data:image/png;base64,AA==', - fileType: 'image/png', + size: 1, + type: 'image/png', name: 'test', - saveMode: 'local', url: 'blob:test', + updatedAt: new Date(2), }); }); diff --git a/src/services/file/client.ts b/src/services/file/client.ts index 21521f062271..f9361238a2fa 100644 --- a/src/services/file/client.ts +++ b/src/services/file/client.ts @@ -1,6 +1,6 @@ import { FileModel } from '@/database/client/models/file'; import { DB_File } from '@/database/client/schemas/files'; -import { FilePreview } from '@/types/files'; +import { FileItem } from '@/types/files'; import { IFileService } from './type'; @@ -18,7 +18,7 @@ export class ClientService implements IFileService { }; } - async getFile(id: string): Promise { + async getFile(id: string): Promise { const item = await FileModel.findById(id); if (!item) { throw new Error('file not found'); @@ -26,14 +26,14 @@ export class ClientService implements IFileService { // arrayBuffer to url const url = URL.createObjectURL(new Blob([item.data!], { type: item.fileType })); - const base64 = Buffer.from(item.data!).toString('base64'); return { - base64Url: `data:${item.fileType};base64,${base64}`, - fileType: item.fileType, + createdAt: new Date(item.createdAt), id, name: item.name, - saveMode: 'local', + size: item.size, + type: item.fileType, + updatedAt: new Date(item.updatedAt), url, }; } diff --git a/src/services/file/server.ts b/src/services/file/server.ts index 08b86fdcae55..77023576544c 100644 --- a/src/services/file/server.ts +++ b/src/services/file/server.ts @@ -1,9 +1,6 @@ -import urlJoin from 'url-join'; - -import { fileEnv } from '@/config/file'; import { lambdaClient } from '@/libs/trpc/client'; import { - FilePreview, + FileItem, QueryFileListParams, QueryFileListSchemaType, UploadFileParams, @@ -21,28 +18,14 @@ export class ServerService implements IFileService { return lambdaClient.file.createFile.mutate({ ...params, knowledgeBaseId } as CreateFileParams); } - /** - * @deprecated - * @param id - */ - async getFile(id: string): Promise { - if (!fileEnv.NEXT_PUBLIC_S3_DOMAIN) { - throw new Error('fileEnv.NEXT_PUBLIC_S3_DOMAIN is not set while enable server upload'); - } - + async getFile(id: string): Promise { const item = await lambdaClient.file.findById.query({ id }); if (!item) { throw new Error('file not found'); } - return { - fileType: item.fileType, - id: item.id, - name: item.name, - saveMode: 'url', - url: urlJoin(fileEnv.NEXT_PUBLIC_S3_DOMAIN!, item.url!), - }; + return { ...item, type: item.fileType }; } async removeFile(id: string) { diff --git a/src/services/file/type.ts b/src/services/file/type.ts index 489b5bd4b0c9..6ed6a090d257 100644 --- a/src/services/file/type.ts +++ b/src/services/file/type.ts @@ -1,11 +1,11 @@ -import { FilePreview, UploadFileParams } from '@/types/files'; +import { FileItem, UploadFileParams } from '@/types/files'; export interface IFileService { createFile( file: UploadFileParams, knowledgeBaseId?: string, ): Promise<{ id: string; url: string }>; - getFile(id: string): Promise; + getFile(id: string): Promise; removeAllFiles(): Promise; removeFile(id: string): Promise; removeFiles(ids: string[]): Promise; diff --git a/src/services/upload.ts b/src/services/upload.ts index 0b38e16c21e7..a4f110627d85 100644 --- a/src/services/upload.ts +++ b/src/services/upload.ts @@ -1,6 +1,7 @@ import { fileEnv } from '@/config/file'; import { FileModel } from '@/database/client/models/file'; import { edgeClient } from '@/libs/trpc/client'; +import { API_ENDPOINTS } from '@/services/_url'; import { FileMetadata, UploadFileParams } from '@/types/files'; import { FileUploadState, FileUploadStatus } from '@/types/files/upload'; import { uuid } from '@/utils/uuid'; @@ -79,6 +80,19 @@ class UploadService { }; }; + /** + * get image File item with cors image URL + * @param url + * @param filename + * @param fileType + */ + getImageFileByUrlWithCORS = async (url: string, filename: string, fileType = 'image/png') => { + const res = await fetch(API_ENDPOINTS.proxy, { body: url, method: 'POST' }); + const data = await res.arrayBuffer(); + + return new File([data], filename, { lastModified: Date.now(), type: fileType }); + }; + private getSignedUploadUrl = async ( file: File, ): Promise< diff --git a/src/store/chat/slices/builtinTool/action.test.ts b/src/store/chat/slices/builtinTool/action.test.ts index 343aee751ecd..795806b44195 100644 --- a/src/store/chat/slices/builtinTool/action.test.ts +++ b/src/store/chat/slices/builtinTool/action.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest'; import { fileService } from '@/services/file'; import { imageGenerationService } from '@/services/textToImage'; -import { legacyUploadService as uploadService } from '@/services/upload_legacy'; +import { uploadService } from '@/services/upload'; import { chatSelectors } from '@/store/chat/selectors'; import { ChatMessage } from '@/types/message'; import { DallEImageItem } from '@/types/tool/dalle'; @@ -36,7 +36,10 @@ describe('chatToolSlice', () => { const mockId = 'image-id'; vi.spyOn(imageGenerationService, 'generateImage').mockResolvedValue(mockUrl); - vi.spyOn(uploadService, 'uploadImageByUrl').mockResolvedValue({} as any); + vi.spyOn(uploadService, 'getImageFileByUrlWithCORS').mockResolvedValue( + new File(['1'], 'file.png', { type: 'image/png' }), + ); + vi.spyOn(uploadService, 'uploadToClientDB').mockResolvedValue({} as any); vi.spyOn(fileService, 'createFile').mockResolvedValue({ id: mockId, url: '' }); vi.spyOn(result.current, 'toggleDallEImageLoading'); @@ -45,7 +48,7 @@ describe('chatToolSlice', () => { }); // For each prompt, loading is toggled on and then off expect(imageGenerationService.generateImage).toHaveBeenCalledTimes(prompts.length); - expect(uploadService.uploadImageByUrl).toHaveBeenCalledTimes(prompts.length); + expect(uploadService.uploadToClientDB).toHaveBeenCalledTimes(prompts.length); expect(result.current.toggleDallEImageLoading).toHaveBeenCalledTimes(prompts.length * 2); }); diff --git a/src/store/chat/slices/builtinTool/action.ts b/src/store/chat/slices/builtinTool/action.ts index 03972a9d34b7..d6e4ee3e7b75 100644 --- a/src/store/chat/slices/builtinTool/action.ts +++ b/src/store/chat/slices/builtinTool/action.ts @@ -1,17 +1,21 @@ import { produce } from 'immer'; import pMap from 'p-map'; +import { SWRResponse } from 'swr'; import { StateCreator } from 'zustand/vanilla'; +import { useClientDataSWR } from '@/libs/swr'; import { fileService } from '@/services/file'; import { imageGenerationService } from '@/services/textToImage'; -import { legacyUploadService } from '@/services/upload_legacy'; +import { uploadService } from '@/services/upload'; import { chatSelectors } from '@/store/chat/selectors'; import { ChatStore } from '@/store/chat/store'; +import { useFileStore } from '@/store/file'; import { DallEImageItem } from '@/types/tool/dalle'; import { setNamespace } from '@/utils/storeDebug'; const n = setNamespace('tool'); +const SWR_FETCH_KEY = 'FetchImageItem'; /** * builtin tool action */ @@ -20,6 +24,7 @@ export interface ChatBuiltinToolAction { text2image: (id: string, data: DallEImageItem[]) => Promise; toggleDallEImageLoading: (key: string, value: boolean) => void; updateImageItem: (id: string, updater: (data: DallEImageItem[]) => void) => Promise; + useFetchDalleImageItem: (id: string) => SWRResponse; } export const chatToolSlice: StateCreator< @@ -60,20 +65,22 @@ export const chatToolSlice: StateCreator< }); toggleDallEImageLoading(messageId + params.prompt, false); + const imageFile = await uploadService.getImageFileByUrlWithCORS( + url, + `${originPrompt || params.prompt}_${index}.png`, + ); + + const data = await useFileStore.getState().uploadWithProgress({ + file: imageFile, + onStatusUpdate: () => {}, + }); + + if (!data) return; - legacyUploadService - .uploadImageByUrl(url, { - metadata: { ...params, originPrompt: originPrompt }, - name: `${originPrompt || params.prompt}_${index}.png`, - }) - .then(async (res) => { - const data = await fileService.createFile(res); - - updateImageItem(messageId, (draft) => { - draft[index].imageId = data.id; - draft[index].previewUrl = undefined; - }); - }); + await updateImageItem(messageId, (draft) => { + draft[index].imageId = data.id; + draft[index].previewUrl = undefined; + }); }); }, text2image: async (id, data) => { @@ -98,4 +105,20 @@ export const chatToolSlice: StateCreator< const nextContent = produce(data, updater); await get().internal_updateMessageContent(id, JSON.stringify(nextContent)); }, + useFetchDalleImageItem: (id) => + useClientDataSWR([SWR_FETCH_KEY, id], async () => { + const item = await fileService.getFile(id); + + set( + produce((draft) => { + if (draft.dalleImageMap[id]) return; + + draft.dalleImageMap[id] = item; + }), + false, + n('useFetchFile'), + ); + + return item; + }), }); diff --git a/src/store/chat/slices/builtinTool/initialState.ts b/src/store/chat/slices/builtinTool/initialState.ts index 055a752a60b9..e74bcf6605ac 100644 --- a/src/store/chat/slices/builtinTool/initialState.ts +++ b/src/store/chat/slices/builtinTool/initialState.ts @@ -1,7 +1,11 @@ +import { FileItem } from '@/types/files'; + export interface ChatToolState { dalleImageLoading: Record; + dalleImageMap: Record; } export const initialToolState: ChatToolState = { dalleImageLoading: {}, + dalleImageMap: {}, }; diff --git a/src/store/file/slices/chat/action.test.ts b/src/store/file/slices/chat/action.test.ts index 3d5a2337defe..0050168d81ee 100644 --- a/src/store/file/slices/chat/action.test.ts +++ b/src/store/file/slices/chat/action.test.ts @@ -84,40 +84,6 @@ describe('useFileStore:chat', () => { // expect(result.current.inputFilesList).toEqual([]); // }); - // Test for useFetchFile - it('useFetchFile should call useSWR and update the store', async () => { - const fileId = 'test-id'; - const fileData = { - id: fileId, - name: 'test', - url: 'blob:test', - fileType: 'image/png', - base64Url: '', - saveMode: 'local', - }; - - // Mock the fileService.getFile to resolve with fileData - vi.spyOn(fileService, 'getFile').mockResolvedValue(fileData as any); - - // Mock useSWR to call the fetcher function immediately - const useSWRMock = vi.mocked(useSWR); - useSWRMock.mockImplementation(((key: string, fetcher: any) => { - const data = fetcher(key); - return { data, error: undefined, isValidating: false, mutate: vi.fn() }; - }) as any); - - const { result } = renderHook(() => useStore().useFetchFile(fileId)); - - await act(async () => { - await result.current.data; - }); - - expect(fileService.getFile).toHaveBeenCalledWith(fileId); - - // Since we are not rendering a component with the hook, we cannot test the state update here - // Instead, we would need to use a test renderer that can work with hooks, like @testing-library/react - }); - // describe('uploadFile', () => { // it('uploadFile should handle errors', async () => { // const { result } = renderHook(() => useStore()); diff --git a/src/store/file/slices/chat/action.ts b/src/store/file/slices/chat/action.ts index dfebcc453e57..5582befc2672 100644 --- a/src/store/file/slices/chat/action.ts +++ b/src/store/file/slices/chat/action.ts @@ -1,6 +1,4 @@ import { t } from 'i18next'; -import { produce } from 'immer'; -import useSWR, { SWRResponse } from 'swr'; import { StateCreator } from 'zustand/vanilla'; import { notification } from '@/components/AntdStaticMethods'; @@ -17,7 +15,7 @@ import { } from '@/store/file/reducers/uploadFileList'; import { useUserStore } from '@/store/user'; import { preferenceSelectors } from '@/store/user/selectors'; -import { FileListItem, FilePreview } from '@/types/files'; +import { FileListItem } from '@/types/files'; import { UploadFileItem } from '@/types/files/upload'; import { sleep } from '@/utils/sleep'; import { setNamespace } from '@/utils/storeDebug'; @@ -40,12 +38,6 @@ export interface FileAction { ) => Promise; uploadChatFiles: (files: File[]) => Promise; - - /** - * en: delete it after refactoring the Dalle plugin - * @deprecated - */ - useFetchFile: (id: string) => SWRResponse; } export const createFileSlice: StateCreator< @@ -211,21 +203,4 @@ export const createFileSlice: StateCreator< await Promise.all(pools); }, - - useFetchFile: (id) => - useSWR(id, async (id) => { - const item = await fileService.getFile(id); - - set( - produce((draft) => { - if (draft.imagesMap[id]) return; - - draft.imagesMap[id] = item; - }), - false, - n('useFetchFile'), - ); - - return item; - }), }); diff --git a/src/store/file/slices/chat/initialState.ts b/src/store/file/slices/chat/initialState.ts index 50d7d4a58a12..307b7be94277 100644 --- a/src/store/file/slices/chat/initialState.ts +++ b/src/store/file/slices/chat/initialState.ts @@ -1,18 +1,11 @@ -import { FilePreview } from '@/types/files'; import { UploadFileItem } from '@/types/files/upload'; export interface ImageFileState { chatUploadFileList: UploadFileItem[]; - /** - * it should be removed after dalle plugin refactor - * @deprecated - */ - imagesMap: Record; uploadingIds: string[]; } export const initialImageFileState: ImageFileState = { chatUploadFileList: [], - imagesMap: {}, uploadingIds: [], }; diff --git a/src/store/file/slices/tts/action.ts b/src/store/file/slices/tts/action.ts index 8780a068bd99..57492070ec3a 100644 --- a/src/store/file/slices/tts/action.ts +++ b/src/store/file/slices/tts/action.ts @@ -3,7 +3,7 @@ import { StateCreator } from 'zustand/vanilla'; import { fileService } from '@/services/file'; import { legacyUploadService } from '@/services/upload_legacy'; -import { FilePreview } from '@/types/files'; +import { FileItem } from '@/types/files'; import { FileStore } from '../../store'; @@ -17,7 +17,7 @@ export interface TTSFileAction { uploadTTSFile: (file: File) => Promise; - useFetchTTSFile: (id: string | null) => SWRResponse; + useFetchTTSFile: (id: string | null) => SWRResponse; } export const createTTSFileSlice: StateCreator< diff --git a/src/tools/dalle/Render/Item/ImageFileItem.tsx b/src/tools/dalle/Render/Item/ImageFileItem.tsx index e564b2635905..26cf80a21ef3 100644 --- a/src/tools/dalle/Render/Item/ImageFileItem.tsx +++ b/src/tools/dalle/Render/Item/ImageFileItem.tsx @@ -3,7 +3,7 @@ import { createStyles } from 'antd-style'; import { CSSProperties, memo } from 'react'; import { usePlatform } from '@/hooks/usePlatform'; -import { useFileStore } from '@/store/file'; +import { useChatStore } from '@/store/chat'; const MIN_IMAGE_SIZE = 64; @@ -34,9 +34,9 @@ interface FileItemProps { style?: CSSProperties; } const ImageFileItem = memo(({ editable, id, alwaysShowClose }) => { - const [useFetchFile] = useFileStore((s) => [s.useFetchFile]); + const [useFetchDalleImageItem] = useChatStore((s) => [s.useFetchDalleImageItem]); const IMAGE_SIZE = editable ? MIN_IMAGE_SIZE : '100%'; - const { data, isLoading } = useFetchFile(id); + const { data, isLoading } = useFetchDalleImageItem(id); const { styles, cx } = useStyles(); const { isSafari } = usePlatform(); diff --git a/src/tools/dalle/Render/Item/index.tsx b/src/tools/dalle/Render/Item/index.tsx index f3fe0b302b26..31c3b416eda0 100644 --- a/src/tools/dalle/Render/Item/index.tsx +++ b/src/tools/dalle/Render/Item/index.tsx @@ -56,7 +56,7 @@ const ImageItem = memo( ); if (imageId || previewUrl) - return ; + return ; return ( diff --git a/src/tools/renders.ts b/src/tools/renders.ts index d6493a620104..122a7d412d81 100644 --- a/src/tools/renders.ts +++ b/src/tools/renders.ts @@ -5,10 +5,4 @@ import DalleRender from './dalle/Render'; export const BuiltinToolsRenders: Record = { [DalleManifest.identifier]: DalleRender as BuiltinRender, - /** - * 兼容旧版本 dalle3 的 identifier - * TODO: 后续数据库版本迁移时记得迁移 dalle3 对应的 identifier - * @deprecated - */ - dalle3: DalleRender as BuiltinRender, }; diff --git a/src/types/files/index.ts b/src/types/files/index.ts index 4ef74833bbd3..280866cea0fc 100644 --- a/src/types/files/index.ts +++ b/src/types/files/index.ts @@ -7,18 +7,6 @@ export enum FilesTabs { Websites = 'websites', } -/** - * @deprecated - */ -export interface FilePreview { - base64Url?: string; - data?: ArrayBuffer; - fileType: string; - id: string; - name: string; - saveMode: 'local' | 'url'; - url: string; -} export interface FileItem { createdAt: Date; diff --git a/src/utils/downloadFile.ts b/src/utils/client/downloadFile.ts similarity index 87% rename from src/utils/downloadFile.ts rename to src/utils/client/downloadFile.ts index 617ca9e7cff4..bf3d229e3143 100644 --- a/src/utils/downloadFile.ts +++ b/src/utils/client/downloadFile.ts @@ -13,6 +13,7 @@ export const downloadFile = async (url: string, fileName: string) => { link.remove(); window.URL.revokeObjectURL(blobUrl); } catch (error) { - console.error('Download failed:', error); + console.log('Download failed:', error); + window.open(url); } };