From c5664c7e901e6f70386443e462629893210dfa13 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Sat, 18 Nov 2023 15:42:35 +0800 Subject: [PATCH] feat: vision model (#489) * mongo init * perf: mongo connect * perf: tts perf: whisper and tts peref: tts whisper permission log reabase (#488) * perf: modal * i18n * perf: schema lean * feat: vision model format * perf: tts loading * perf: static data * perf: tts * feat: image * perf: image * perf: upload image and title * perf: image size * doc * perf: color * doc * speaking can not select file * doc --- .../content/docs/development/configuration.md | 15 + .../docs/installation/upgrading/461.md | 16 + packages/global/common/string/tools.ts | 2 +- packages/global/core/ai/model.d.ts | 1 + packages/global/core/ai/model.ts | 15 + packages/global/core/ai/type.d.ts | 6 +- packages/global/core/chat/constants.ts | 3 + packages/global/core/chat/utils.ts | 6 + packages/service/common/buffer/tts/schema.ts | 1 + .../service/common/file/image/controller.ts | 20 +- packages/service/common/file/image/schema.ts | 19 +- packages/service/core/app/schema.ts | 2 + packages/service/core/chat/chatItemSchema.ts | 2 + packages/service/core/chat/chatSchema.ts | 3 +- packages/service/core/chat/utils.ts | 101 ++++- packages/service/core/dataset/controller.ts | 6 +- packages/service/core/dataset/data/schema.ts | 1 + packages/service/core/dataset/schema.ts | 1 + .../service/core/dataset/training/schema.ts | 2 + packages/service/core/plugin/schema.ts | 3 +- .../support/activity/promotion/schema.ts | 1 + packages/service/support/openapi/schema.ts | 1 + packages/service/support/outLink/schema.ts | 2 + .../service/support/permission/auth/app.ts | 4 +- .../service/support/permission/auth/chat.ts | 6 +- .../support/permission/auth/dataset.ts | 2 +- packages/service/support/user/schema.ts | 1 + .../service/support/wallet/bill/schema.ts | 1 + projects/app/data/config.json | 15 + projects/app/public/icon/speaking.gif | Bin 0 -> 47666 bytes projects/app/public/locales/en/common.json | 1 + projects/app/public/locales/zh/common.json | 1 + .../src/components/ChatBox/MessageInput.tsx | 403 +++++++++++------- projects/app/src/components/ChatBox/index.tsx | 19 +- .../src/components/Markdown/chat/Image.tsx | 19 +- .../app/src/components/Markdown/img/Image.tsx | 3 +- .../components/core/module/Flow/ChatTest.tsx | 2 + .../user/team/UpdateInviteModal/index.tsx | 6 +- projects/app/src/pages/_app.tsx | 2 +- .../src/pages/api/common/file/uploadImage.ts | 11 +- .../src/pages/api/core/dataset/allDataset.ts | 4 +- .../app/src/pages/api/core/dataset/list.ts | 6 +- .../app/detail/components/BasicEdit/index.tsx | 2 + .../pages/app/detail/components/TTSSelect.tsx | 6 +- .../src/pages/chat/components/ChatHeader.tsx | 6 +- projects/app/src/pages/chat/index.tsx | 8 +- projects/app/src/pages/chat/share.tsx | 4 + projects/app/src/service/events/generateQA.ts | 22 +- .../app/src/service/events/generateVector.ts | 22 +- .../src/service/moduleDispatch/chat/oneapi.ts | 28 +- .../app/src/service/utils/chat/saveChat.ts | 10 +- projects/app/src/web/common/file/api.ts | 4 +- .../app/src/web/common/file/controller.ts | 6 +- .../web/common/file/hooks/useSelectFile.tsx | 12 +- projects/app/src/web/common/utils/voice.ts | 7 +- .../app/src/web/core/chat/storeShareChat.ts | 9 +- projects/app/src/web/core/chat/utils.ts | 21 + projects/app/src/web/styles/theme.ts | 2 +- 58 files changed, 650 insertions(+), 254 deletions(-) create mode 100644 docSite/content/docs/installation/upgrading/461.md create mode 100644 packages/global/core/chat/utils.ts create mode 100644 projects/app/public/icon/speaking.gif create mode 100644 projects/app/src/web/core/chat/utils.ts diff --git a/docSite/content/docs/development/configuration.md b/docSite/content/docs/development/configuration.md index c6766157d..1f8a8acd9 100644 --- a/docSite/content/docs/development/configuration.md +++ b/docSite/content/docs/development/configuration.md @@ -36,6 +36,7 @@ weight: 520 "quoteMaxToken": 2000, // 最大引用内容长度 "maxTemperature": 1.2, // 最大温度值 "censor": false, // 是否开启敏感词过滤(商业版) + "vision": false, // 支持图片输入 "defaultSystemChatPrompt": "" }, { @@ -47,6 +48,7 @@ weight: 520 "quoteMaxToken": 8000, "maxTemperature": 1.2, "censor": false, + "vision": false, "defaultSystemChatPrompt": "" }, { @@ -58,6 +60,19 @@ weight: 520 "quoteMaxToken": 4000, "maxTemperature": 1.2, "censor": false, + "vision": false, + "defaultSystemChatPrompt": "" + }, + { + "model": "gpt-4-vision-preview", + "name": "GPT4-Vision", + "maxContext": 128000, + "maxResponse": 4000, + "price": 0, + "quoteMaxToken": 100000, + "maxTemperature": 1.2, + "censor": false, + "vision": true, "defaultSystemChatPrompt": "" } ], diff --git a/docSite/content/docs/installation/upgrading/461.md b/docSite/content/docs/installation/upgrading/461.md new file mode 100644 index 000000000..44515e34b --- /dev/null +++ b/docSite/content/docs/installation/upgrading/461.md @@ -0,0 +1,16 @@ +--- +title: 'V4.6.1' +description: 'FastGPT V4.6 .1' +icon: 'upgrade' +draft: false +toc: true +weight: 835 +--- + + +## V4.6.1 功能介绍 + +1. 新增 - GPT4-v 模型支持 +2. 新增 - whisper 语音输入 +3. 优化 - TTS 流传输 +4. 优化 - TTS 缓存 diff --git a/packages/global/common/string/tools.ts b/packages/global/common/string/tools.ts index 61cdf44eb..5e4e7487c 100644 --- a/packages/global/common/string/tools.ts +++ b/packages/global/common/string/tools.ts @@ -24,7 +24,7 @@ export const simpleText = (text: string) => { }; /* - replace {{variable}} to value + replace {{variable}} to value */ export function replaceVariable(text: string, obj: Record) { for (const key in obj) { diff --git a/packages/global/core/ai/model.d.ts b/packages/global/core/ai/model.d.ts index 2d40c32a9..fd316c035 100644 --- a/packages/global/core/ai/model.d.ts +++ b/packages/global/core/ai/model.d.ts @@ -9,6 +9,7 @@ export type ChatModelItemType = LLMModelItemType & { quoteMaxToken: number; maxTemperature: number; censor?: boolean; + vision?: boolean; defaultSystemChatPrompt?: string; }; diff --git a/packages/global/core/ai/model.ts b/packages/global/core/ai/model.ts index 021ee1a7f..f074be10c 100644 --- a/packages/global/core/ai/model.ts +++ b/packages/global/core/ai/model.ts @@ -17,6 +17,7 @@ export const defaultChatModels: ChatModelItemType[] = [ quoteMaxToken: 2000, maxTemperature: 1.2, censor: false, + vision: false, defaultSystemChatPrompt: '' }, { @@ -28,6 +29,7 @@ export const defaultChatModels: ChatModelItemType[] = [ quoteMaxToken: 8000, maxTemperature: 1.2, censor: false, + vision: false, defaultSystemChatPrompt: '' }, { @@ -39,6 +41,19 @@ export const defaultChatModels: ChatModelItemType[] = [ quoteMaxToken: 4000, maxTemperature: 1.2, censor: false, + vision: false, + defaultSystemChatPrompt: '' + }, + { + model: 'gpt-4-vision-preview', + name: 'GPT4-Vision', + maxContext: 128000, + maxResponse: 4000, + price: 0, + quoteMaxToken: 100000, + maxTemperature: 1.2, + censor: false, + vision: true, defaultSystemChatPrompt: '' } ]; diff --git a/packages/global/core/ai/type.d.ts b/packages/global/core/ai/type.d.ts index 1689ee044..1aaefb121 100644 --- a/packages/global/core/ai/type.d.ts +++ b/packages/global/core/ai/type.d.ts @@ -5,12 +5,14 @@ import type { ChatCompletionMessageParam, ChatCompletionContentPart } from 'openai/resources'; + export type ChatCompletionContentPart = ChatCompletionContentPart; export type ChatCompletionCreateParams = ChatCompletionCreateParams; -export type ChatMessageItemType = Omit & { +export type ChatMessageItemType = Omit & { + name?: any; dataId?: string; content: any; -}; +} & any; export type ChatCompletion = ChatCompletion; export type StreamChatType = Stream; diff --git a/packages/global/core/chat/constants.ts b/packages/global/core/chat/constants.ts index 2313fb445..26e2ead32 100644 --- a/packages/global/core/chat/constants.ts +++ b/packages/global/core/chat/constants.ts @@ -54,3 +54,6 @@ export const ChatSourceMap = { export const HUMAN_ICON = `/icon/human.svg`; export const LOGO_ICON = `/icon/logo.svg`; + +export const IMG_BLOCK_KEY = 'img-block'; +export const FILE_BLOCK_KEY = 'file-block'; diff --git a/packages/global/core/chat/utils.ts b/packages/global/core/chat/utils.ts new file mode 100644 index 000000000..19b6759a9 --- /dev/null +++ b/packages/global/core/chat/utils.ts @@ -0,0 +1,6 @@ +import { IMG_BLOCK_KEY, FILE_BLOCK_KEY } from './constants'; + +export function chatContentReplaceBlock(content: string = '') { + const regex = new RegExp(`\`\`\`(${IMG_BLOCK_KEY})\\n([\\s\\S]*?)\`\`\``, 'g'); + return content.replace(regex, '').trim(); +} diff --git a/packages/service/common/buffer/tts/schema.ts b/packages/service/common/buffer/tts/schema.ts index 9e7cf6b83..3004325d2 100644 --- a/packages/service/common/buffer/tts/schema.ts +++ b/packages/service/common/buffer/tts/schema.ts @@ -33,3 +33,4 @@ try { export const MongoTTSBuffer: Model = models[collectionName] || model(collectionName, TTSBufferSchema); +MongoTTSBuffer.syncIndexes(); diff --git a/packages/service/common/file/image/controller.ts b/packages/service/common/file/image/controller.ts index fc51430d0..e8c1c4b97 100644 --- a/packages/service/common/file/image/controller.ts +++ b/packages/service/common/file/image/controller.ts @@ -5,12 +5,26 @@ export function getMongoImgUrl(id: string) { return `${imageBaseUrl}${id}`; } -export async function uploadMongoImg({ base64Img, userId }: { base64Img: string; userId: string }) { +export const maxImgSize = 1024 * 1024 * 12; +export async function uploadMongoImg({ + base64Img, + teamId, + expiredTime +}: { + base64Img: string; + teamId: string; + expiredTime?: Date; +}) { + if (base64Img.length > maxImgSize) { + return Promise.reject('Image too large'); + } + const base64Data = base64Img.split(',')[1]; const { _id } = await MongoImage.create({ - userId, - binary: Buffer.from(base64Data, 'base64') + teamId, + binary: Buffer.from(base64Data, 'base64'), + expiredTime }); return getMongoImgUrl(String(_id)); diff --git a/packages/service/common/file/image/schema.ts b/packages/service/common/file/image/schema.ts index fb7ea7e69..fbb484c00 100644 --- a/packages/service/common/file/image/schema.ts +++ b/packages/service/common/file/image/schema.ts @@ -1,16 +1,27 @@ +import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant'; import { connectionMongo, type Model } from '../../mongo'; const { Schema, model, models } = connectionMongo; const ImageSchema = new Schema({ - userId: { + teamId: { type: Schema.Types.ObjectId, - ref: 'user', - required: true + ref: TeamCollectionName }, binary: { type: Buffer + }, + expiredTime: { + type: Date } }); -export const MongoImage: Model<{ userId: string; binary: Buffer }> = +try { + ImageSchema.index({ expiredTime: 1 }, { expireAfterSeconds: 60 }); +} catch (error) { + console.log(error); +} + +export const MongoImage: Model<{ teamId: string; binary: Buffer }> = models['image'] || model('image', ImageSchema); + +MongoImage.syncIndexes(); diff --git a/packages/service/core/app/schema.ts b/packages/service/core/app/schema.ts index 356f1412c..5240a9b70 100644 --- a/packages/service/core/app/schema.ts +++ b/packages/service/core/app/schema.ts @@ -67,3 +67,5 @@ try { export const MongoApp: Model = models[appCollectionName] || model(appCollectionName, AppSchema); + +MongoApp.syncIndexes(); diff --git a/packages/service/core/chat/chatItemSchema.ts b/packages/service/core/chat/chatItemSchema.ts index 6161f975c..ea7a4f5fd 100644 --- a/packages/service/core/chat/chatItemSchema.ts +++ b/packages/service/core/chat/chatItemSchema.ts @@ -83,3 +83,5 @@ try { export const MongoChatItem: Model = models['chatItem'] || model('chatItem', ChatItemSchema); + +MongoChatItem.syncIndexes(); diff --git a/packages/service/core/chat/chatSchema.ts b/packages/service/core/chat/chatSchema.ts index 71cd37729..66bacf8e7 100644 --- a/packages/service/core/chat/chatSchema.ts +++ b/packages/service/core/chat/chatSchema.ts @@ -92,7 +92,7 @@ const ChatSchema = new Schema({ }); try { - ChatSchema.index({ userId: 1 }); + ChatSchema.index({ tmbId: 1 }); ChatSchema.index({ updateTime: -1 }); ChatSchema.index({ appId: 1 }); } catch (error) { @@ -101,3 +101,4 @@ try { export const MongoChat: Model = models[chatCollectionName] || model(chatCollectionName, ChatSchema); +MongoChat.syncIndexes(); diff --git a/packages/service/core/chat/utils.ts b/packages/service/core/chat/utils.ts index 6272b7c56..3a5056dc7 100644 --- a/packages/service/core/chat/utils.ts +++ b/packages/service/core/chat/utils.ts @@ -1,7 +1,8 @@ import type { ChatItemType } from '@fastgpt/global/core/chat/type.d'; -import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; +import { ChatRoleEnum, IMG_BLOCK_KEY } from '@fastgpt/global/core/chat/constants'; import { countMessagesTokens, countPromptTokens } from '@fastgpt/global/common/string/tiktoken'; import { adaptRole_Chat2Message } from '@fastgpt/global/core/chat/adapt'; +import type { ChatCompletionContentPart } from '@fastgpt/global/core/ai/type.d'; /* slice chat context by tokens */ export function ChatContextFilter({ @@ -51,3 +52,101 @@ export function ChatContextFilter({ return [...systemPrompts, ...chats]; } + +/** + string to vision model. Follow the markdown code block rule for interception: + + @rule: + ```img-block + {src:""} + {src:""} + ``` + ```file-block + {name:"",src:""}, + {name:"",src:""} + ``` + @example: + What’s in this image? + ```img-block + {src:"https://1.png"} + ``` + @return + [ + { type: 'text', text: 'What’s in this image?' }, + { + type: 'image_url', + image_url: { + url: 'https://1.png' + } + } + ] + */ +export function formatStr2ChatContent(str: string) { + const content: ChatCompletionContentPart[] = []; + let lastIndex = 0; + const regex = new RegExp(`\`\`\`(${IMG_BLOCK_KEY})\\n([\\s\\S]*?)\`\`\``, 'g'); + + let match; + + while ((match = regex.exec(str)) !== null) { + // add previous text + if (match.index > lastIndex) { + const text = str.substring(lastIndex, match.index).trim(); + if (text) { + content.push({ type: 'text', text }); + } + } + + const blockType = match[1].trim(); + + if (blockType === IMG_BLOCK_KEY) { + const blockContentLines = match[2].trim().split('\n'); + const jsonLines = blockContentLines.map((item) => { + try { + return JSON.parse(item) as { src: string }; + } catch (error) { + return { src: '' }; + } + }); + + for (const item of jsonLines) { + if (!item.src) throw new Error("image block's content error"); + } + + content.push( + ...jsonLines.map((item) => ({ + type: 'image_url' as any, + image_url: { + url: item.src + } + })) + ); + } + + lastIndex = regex.lastIndex; + } + + // add remaining text + if (lastIndex < str.length) { + const remainingText = str.substring(lastIndex).trim(); + if (remainingText) { + content.push({ type: 'text', text: remainingText }); + } + } + + // Continuous text type content, if type=text, merge them + for (let i = 0; i < content.length - 1; i++) { + const currentContent = content[i]; + const nextContent = content[i + 1]; + if (currentContent.type === 'text' && nextContent.type === 'text') { + currentContent.text += nextContent.text; + content.splice(i + 1, 1); + i--; + } + } + + if (content.length === 1 && content[0].type === 'text') { + return content[0].text; + } + return content ? content : null; +} diff --git a/packages/service/core/dataset/controller.ts b/packages/service/core/dataset/controller.ts index baf81aa4a..20faa55ae 100644 --- a/packages/service/core/dataset/controller.ts +++ b/packages/service/core/dataset/controller.ts @@ -22,9 +22,9 @@ export async function findDatasetIdTreeByTopDatasetId( } export async function getCollectionWithDataset(collectionId: string) { - const data = ( - await MongoDatasetCollection.findById(collectionId).populate('datasetId') - )?.toJSON() as CollectionWithDatasetType; + const data = (await MongoDatasetCollection.findById(collectionId) + .populate('datasetId') + .lean()) as CollectionWithDatasetType; if (!data) { return Promise.reject('Collection is not exist'); } diff --git a/packages/service/core/dataset/data/schema.ts b/packages/service/core/dataset/data/schema.ts index 17bfe01ae..e1993c1a3 100644 --- a/packages/service/core/dataset/data/schema.ts +++ b/packages/service/core/dataset/data/schema.ts @@ -76,3 +76,4 @@ try { export const MongoDatasetData: Model = models[DatasetDataCollectionName] || model(DatasetDataCollectionName, DatasetDataSchema); +MongoDatasetData.syncIndexes(); diff --git a/packages/service/core/dataset/schema.ts b/packages/service/core/dataset/schema.ts index 6f61eb90a..5e024c800 100644 --- a/packages/service/core/dataset/schema.ts +++ b/packages/service/core/dataset/schema.ts @@ -82,3 +82,4 @@ try { export const MongoDataset: Model = models[DatasetCollectionName] || model(DatasetCollectionName, DatasetSchema); +MongoDataset.syncIndexes(); diff --git a/packages/service/core/dataset/training/schema.ts b/packages/service/core/dataset/training/schema.ts index b7ce90969..d68f570da 100644 --- a/packages/service/core/dataset/training/schema.ts +++ b/packages/service/core/dataset/training/schema.ts @@ -104,3 +104,5 @@ try { export const MongoDatasetTraining: Model = models[DatasetTrainingCollectionName] || model(DatasetTrainingCollectionName, TrainingDataSchema); + +MongoDatasetTraining.syncIndexes(); diff --git a/packages/service/core/plugin/schema.ts b/packages/service/core/plugin/schema.ts index 2aff509b8..ea933ff07 100644 --- a/packages/service/core/plugin/schema.ts +++ b/packages/service/core/plugin/schema.ts @@ -46,10 +46,11 @@ const PluginSchema = new Schema({ }); try { - PluginSchema.index({ userId: 1 }); + PluginSchema.index({ tmbId: 1 }); } catch (error) { console.log(error); } export const MongoPlugin: Model = models[ModuleCollectionName] || model(ModuleCollectionName, PluginSchema); +MongoPlugin.syncIndexes(); diff --git a/packages/service/support/activity/promotion/schema.ts b/packages/service/support/activity/promotion/schema.ts index 5f9a52a5b..263202d8d 100644 --- a/packages/service/support/activity/promotion/schema.ts +++ b/packages/service/support/activity/promotion/schema.ts @@ -31,3 +31,4 @@ const PromotionRecordSchema = new Schema({ export const MongoPromotionRecord: Model = models['promotionRecord'] || model('promotionRecord', PromotionRecordSchema); +MongoPromotionRecord.syncIndexes(); diff --git a/packages/service/support/openapi/schema.ts b/packages/service/support/openapi/schema.ts index ebedc52dd..d8f51f5a9 100644 --- a/packages/service/support/openapi/schema.ts +++ b/packages/service/support/openapi/schema.ts @@ -70,3 +70,4 @@ const OpenApiSchema = new Schema( export const MongoOpenApi: Model = models['openapi'] || model('openapi', OpenApiSchema); +MongoOpenApi.syncIndexes(); diff --git a/packages/service/support/outLink/schema.ts b/packages/service/support/outLink/schema.ts index 85b15fece..598a82902 100644 --- a/packages/service/support/outLink/schema.ts +++ b/packages/service/support/outLink/schema.ts @@ -71,3 +71,5 @@ const OutLinkSchema = new Schema({ export const MongoOutLink: Model = models['outlinks'] || model('outlinks', OutLinkSchema); + +MongoOutLink.syncIndexes(); diff --git a/packages/service/support/permission/auth/app.ts b/packages/service/support/permission/auth/app.ts index a15211f47..13cd86cd3 100644 --- a/packages/service/support/permission/auth/app.ts +++ b/packages/service/support/permission/auth/app.ts @@ -22,12 +22,12 @@ export async function authApp({ } > { const result = await parseHeaderCert(props); - const { userId, teamId, tmbId } = result; + const { teamId, tmbId } = result; const { role } = await getTeamInfoByTmbId({ tmbId }); const { app, isOwner, canWrite } = await (async () => { // get app - const app = (await MongoApp.findOne({ _id: appId, teamId }))?.toJSON(); + const app = await MongoApp.findOne({ _id: appId, teamId }).lean(); if (!app) { return Promise.reject(AppErrEnum.unAuthApp); } diff --git a/packages/service/support/permission/auth/chat.ts b/packages/service/support/permission/auth/chat.ts index ddad76e61..c0a89042e 100644 --- a/packages/service/support/permission/auth/chat.ts +++ b/packages/service/support/permission/auth/chat.ts @@ -24,9 +24,9 @@ export async function authChat({ const { chat, isOwner, canWrite } = await (async () => { // get chat - const chat = ( - await MongoChat.findOne({ chatId, teamId }).populate('appId') - )?.toJSON() as ChatWithAppSchema; + const chat = (await MongoChat.findOne({ chatId, teamId }) + .populate('appId') + .lean()) as ChatWithAppSchema; if (!chat) { return Promise.reject('Chat is not exists'); diff --git a/packages/service/support/permission/auth/dataset.ts b/packages/service/support/permission/auth/dataset.ts index 86ac29b60..84bca9474 100644 --- a/packages/service/support/permission/auth/dataset.ts +++ b/packages/service/support/permission/auth/dataset.ts @@ -31,7 +31,7 @@ export async function authDataset({ const { role } = await getTeamInfoByTmbId({ tmbId }); const { dataset, isOwner, canWrite } = await (async () => { - const dataset = (await MongoDataset.findOne({ _id: datasetId, teamId }))?.toObject(); + const dataset = await MongoDataset.findOne({ _id: datasetId, teamId }).lean(); if (!dataset) { return Promise.reject(DatasetErrEnum.unAuthDataset); diff --git a/packages/service/support/user/schema.ts b/packages/service/support/user/schema.ts index 995ff0953..6ad84a803 100644 --- a/packages/service/support/user/schema.ts +++ b/packages/service/support/user/schema.ts @@ -64,3 +64,4 @@ const UserSchema = new Schema({ export const MongoUser: Model = models[userCollectionName] || model(userCollectionName, UserSchema); +MongoUser.syncIndexes(); diff --git a/packages/service/support/wallet/bill/schema.ts b/packages/service/support/wallet/bill/schema.ts index 275577624..bd2e4138e 100644 --- a/packages/service/support/wallet/bill/schema.ts +++ b/packages/service/support/wallet/bill/schema.ts @@ -59,3 +59,4 @@ try { } export const MongoBill: Model = models['bill'] || model('bill', BillSchema); +MongoBill.syncIndexes(); diff --git a/projects/app/data/config.json b/projects/app/data/config.json index f675dc51e..556863d56 100644 --- a/projects/app/data/config.json +++ b/projects/app/data/config.json @@ -15,6 +15,7 @@ "quoteMaxToken": 2000, "maxTemperature": 1.2, "censor": false, + "vision": false, "defaultSystemChatPrompt": "" }, { @@ -26,6 +27,7 @@ "quoteMaxToken": 8000, "maxTemperature": 1.2, "censor": false, + "vision": false, "defaultSystemChatPrompt": "" }, { @@ -37,6 +39,19 @@ "quoteMaxToken": 4000, "maxTemperature": 1.2, "censor": false, + "vision": false, + "defaultSystemChatPrompt": "" + }, + { + "model": "gpt-4-vision-preview", + "name": "GPT4-Vision", + "maxContext": 128000, + "maxResponse": 4000, + "price": 0, + "quoteMaxToken": 100000, + "maxTemperature": 1.2, + "censor": false, + "vision": true, "defaultSystemChatPrompt": "" } ], diff --git a/projects/app/public/icon/speaking.gif b/projects/app/public/icon/speaking.gif new file mode 100644 index 0000000000000000000000000000000000000000..bfc5838419dbe2172d597a38b983e8ca6219929e GIT binary patch literal 47666 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 MlepeF-quw8H#Bp;n*aa+ literal 0 HcmV?d00001 diff --git a/projects/app/public/locales/en/common.json b/projects/app/public/locales/en/common.json index 36b754852..4f9d7ccae 100644 --- a/projects/app/public/locales/en/common.json +++ b/projects/app/public/locales/en/common.json @@ -191,6 +191,7 @@ "Update Success": "Update Success", "Update Successful": "Update Successful", "Update Time": "Update Time", + "Upload File Failed": "Upload File Failed", "Username": "UserName", "error": { "unKnow": "There was an accident" diff --git a/projects/app/public/locales/zh/common.json b/projects/app/public/locales/zh/common.json index 5340c1523..27007ef59 100644 --- a/projects/app/public/locales/zh/common.json +++ b/projects/app/public/locales/zh/common.json @@ -191,6 +191,7 @@ "Update Success": "更新成功", "Update Successful": "更新成功", "Update Time": "更新时间", + "Upload File Failed": "上传文件失败", "Username": "用户名", "error": { "unKnow": "出现了点意外~" diff --git a/projects/app/src/components/ChatBox/MessageInput.tsx b/projects/app/src/components/ChatBox/MessageInput.tsx index 98c5c5ec1..a4053d47c 100644 --- a/projects/app/src/components/ChatBox/MessageInput.tsx +++ b/projects/app/src/components/ChatBox/MessageInput.tsx @@ -1,7 +1,7 @@ import { useSpeech } from '@/web/common/hooks/useSpeech'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { Box, Flex, Image, Spinner, Textarea } from '@chakra-ui/react'; -import React, { useRef, useEffect, useCallback, useState, useMemo } from 'react'; +import React, { useRef, useEffect, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import MyTooltip from '../MyTooltip'; import MyIcon from '../Icon'; @@ -10,6 +10,23 @@ import { useRouter } from 'next/router'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; import { compressImgAndUpload } from '@/web/common/file/controller'; import { useToast } from '@/web/common/hooks/useToast'; +import { customAlphabet } from 'nanoid'; +import { IMG_BLOCK_KEY } from '@fastgpt/global/core/chat/constants'; +import { addDays } from 'date-fns'; +const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6); + +enum FileTypeEnum { + image = 'image', + file = 'file' +} +type FileItemType = { + id: string; + rawFile: File; + type: `${FileTypeEnum}`; + name: string; + icon: string; // img is base64 + src?: string; +}; const MessageInput = ({ onChange, @@ -17,16 +34,19 @@ const MessageInput = ({ onStop, isChatting, TextareaDom, + showFileSelector = false, resetInputVal }: { onChange: (e: string) => void; onSendMessage: (e: string) => void; onStop: () => void; isChatting: boolean; + showFileSelector?: boolean; TextareaDom: React.MutableRefObject; resetInputVal: (val: string) => void; }) => { const { shareId } = useRouter().query as { shareId?: string }; + const { toast } = useToast(); const { isSpeaking, isTransCription, @@ -37,64 +57,106 @@ const MessageInput = ({ stream } = useSpeech({ shareId }); const { isPc } = useSystemStore(); - const canvasRef = useRef(); + const canvasRef = useRef(null); const { t } = useTranslation(); const textareaMinH = '22px'; - const havInput = !!TextareaDom.current?.value; - const { toast } = useToast(); - const [imgBase64Array, setImgBase64Array] = useState([]); - const [fileList, setFileList] = useState([]); - const [imgSrcArray, setImgSrcArray] = useState([]); + const [fileList, setFileList] = useState([]); + const havInput = !!TextareaDom.current?.value || fileList.length > 0; const { File, onOpen: onOpenSelectFile } = useSelectFile({ - fileType: '.jpg,.png', - multiple: true + fileType: 'image/*', + multiple: true, + maxCount: 10 }); - useEffect(() => { - fileList.forEach((file) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = async () => { - setImgBase64Array((prev) => [...prev, reader.result as string]); - }; - }); - }, [fileList]); - - const onSelectFile = useCallback((e: File[]) => { - if (!e || e.length === 0) { + const uploadFile = async (file: FileItemType) => { + if (file.type === FileTypeEnum.image) { + try { + const src = await compressImgAndUpload({ + file: file.rawFile, + maxW: 1000, + maxH: 1000, + maxSize: 1024 * 1024 * 5, + // 30 day expired. + expiredTime: addDays(new Date(), 30) + }); + setFileList((state) => + state.map((item) => + item.id === file.id + ? { + ...item, + src: `${location.origin}${src}` + } + : item + ) + ); + } catch (error) { + setFileList((state) => state.filter((item) => item.id !== file.id)); + toast({ + status: 'error', + title: t('common.Upload File Failed') + }); + } + } + }; + const onSelectFile = useCallback(async (files: File[]) => { + if (!files || files.length === 0) { return; } - setFileList(e); + const loadFiles = await Promise.all( + files.map( + (file) => + new Promise((resolve, reject) => { + if (file.type.includes('image')) { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + const item = { + id: nanoid(), + rawFile: file, + type: FileTypeEnum.image, + name: file.name, + icon: reader.result as string + }; + uploadFile(item); + resolve(item); + }; + reader.onerror = () => { + reject(reader.error); + }; + } else { + resolve({ + id: nanoid(), + rawFile: file, + type: FileTypeEnum.file, + name: file.name, + icon: 'pdf' + }); + } + }) + ) + ); + + setFileList((state) => [...state, ...loadFiles]); }, []); const handleSend = useCallback(async () => { - try { - for (const file of fileList) { - const src = await compressImgAndUpload({ - file, - maxW: 1000, - maxH: 1000, - maxSize: 1024 * 1024 * 2 - }); - imgSrcArray.push(src); - } - } catch (err: any) { - toast({ - title: typeof err === 'string' ? err : '文件上传异常', - status: 'warning' - }); - } - const textareaValue = TextareaDom.current?.value || ''; - const inputMessage = - imgSrcArray.length === 0 - ? textareaValue - : `\`\`\`img-block\n${JSON.stringify(imgSrcArray)}\n\`\`\`\n${textareaValue}`; + + const images = fileList.filter((item) => item.type === FileTypeEnum.image); + const imagesText = + images.length === 0 + ? '' + : `\`\`\`${IMG_BLOCK_KEY} +${images.map((img) => JSON.stringify({ src: img.src })).join('\n')} +\`\`\` +`; + + const inputMessage = `${imagesText}${textareaValue}`; + onSendMessage(inputMessage); - setImgBase64Array([]); - setImgSrcArray([]); - }, [TextareaDom, fileList, imgSrcArray, onSendMessage, toast]); + setFileList([]); + }, [TextareaDom, fileList, onSendMessage]); useEffect(() => { if (!stream) { @@ -107,117 +169,139 @@ const MessageInput = ({ const source = audioContext.createMediaStreamSource(stream); source.connect(analyser); const renderCurve = () => { - renderAudioGraph(analyser, canvasRef.current as HTMLCanvasElement); + if (!canvasRef.current) return; + renderAudioGraph(analyser, canvasRef.current); window.requestAnimationFrame(renderCurve); }; renderCurve(); }, [renderAudioGraph, stream]); return ( - <> - - 0 ? '8px' : '18px'} - position={'relative'} - boxShadow={isSpeaking ? `0 0 10px rgba(54,111,255,0.4)` : `0 0 10px rgba(0,0,0,0.2)`} - {...(isPc - ? { - border: '1px solid', - borderColor: 'rgba(0,0,0,0.12)' - } - : { - borderTop: '1px solid', - borderTopColor: 'rgba(0,0,0,0.15)' - })} - borderRadius={['none', 'md']} - backgroundColor={'white'} + + 0 ? '10px' : ['14px', '18px']} + pb={['14px', '18px']} + position={'relative'} + boxShadow={isSpeaking ? `0 0 10px rgba(54,111,255,0.4)` : `0 0 10px rgba(0,0,0,0.2)`} + borderRadius={['none', 'md']} + bg={'white'} + {...(isPc + ? { + border: '1px solid', + borderColor: 'rgba(0,0,0,0.12)' + } + : { + borderTop: '1px solid', + borderTopColor: 'rgba(0,0,0,0.15)' + })} + > + {/* translate loading */} + - {/* translate loading */} - - - {t('chat.Converting to text')} - - {/* file uploader */} - - - - - - - {/* file preview */} - - {imgBase64Array.length > 0 && - imgBase64Array.map((src, index) => ( - + {t('chat.Converting to text')} + + + {/* file preview */} + + {fileList.map((item) => ( + + {/* uploading */} + {!item.src && ( + - { - setImgBase64Array((prev) => { - prev.splice(index, 1); - return [...prev]; - }); - }} - className="close-icon" - display={['', 'none']} - /> - {'img'} - - ))} - + + + )} + { + setFileList((state) => state.filter((file) => file.id !== item.id)); + }} + className="close-icon" + display={['', 'none']} + /> + {item.type === FileTypeEnum.image && ( + {'img'} + )} + + ))} + + + 0 ? 1 : 0} pl={[2, 4]}> + {/* file selector */} + {showFileSelector && ( + { + if (isSpeaking) return; + onOpenSelectFile; + }} + > + + + + + + )} + {/* input area */}