feat: [frontend] support vision (#1518)
Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
fetchChatList,
|
||||
fetchConversations,
|
||||
fetchSuggestedQuestions,
|
||||
generationConversationName,
|
||||
pinConversation,
|
||||
sendChatMessage,
|
||||
stopChatMessageResponding,
|
||||
@@ -39,6 +41,9 @@ import { replaceStringWithValues } from '@/app/components/app/configuration/prom
|
||||
import { userInputsFormToPromptVariables } from '@/utils/model-config'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import { fetchFileUploadConfig } from '@/service/common'
|
||||
|
||||
export type IMainProps = {
|
||||
isInstalledApp?: boolean
|
||||
@@ -244,6 +249,7 @@ const Main: FC<IMainProps> = ({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: item.message_files,
|
||||
})
|
||||
newChatList.push({
|
||||
id: item.id,
|
||||
@@ -351,6 +357,8 @@ const Main: FC<IMainProps> = ({
|
||||
: fetchAppInfo(), fetchAllConversations(), fetchAppParams(isInstalledApp, installedAppInfo?.id)])
|
||||
}
|
||||
|
||||
const { data: fileUploadConfigResponse } = useSWR(isInstalledApp ? { url: '/files/upload' } : null, fetchFileUploadConfig)
|
||||
|
||||
// init
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -368,7 +376,11 @@ const Main: FC<IMainProps> = ({
|
||||
const isNotNewConversation = allConversations.some(item => item.id === _conversationId)
|
||||
setAllConversationList(allConversations)
|
||||
// fetch new conversation info
|
||||
const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, retriever_resource, sensitive_word_avoidance }: any = appParams
|
||||
const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, retriever_resource, file_upload, sensitive_word_avoidance }: any = appParams
|
||||
setVisionConfig({
|
||||
...file_upload.image,
|
||||
image_file_size_limit: appParams?.system_parameters?.image_file_size_limit,
|
||||
})
|
||||
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
|
||||
if (siteInfo.default_language)
|
||||
changeLanguage(siteInfo.default_language)
|
||||
@@ -448,24 +460,48 @@ const Main: FC<IMainProps> = ({
|
||||
const [messageTaskId, setMessageTaskId] = useState('')
|
||||
const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false)
|
||||
const [isResponsingConIsCurrCon, setIsResponsingConCurrCon, getIsResponsingConIsCurrCon] = useGetState(true)
|
||||
|
||||
const handleSend = async (message: string) => {
|
||||
const [visionConfig, setVisionConfig] = useState<VisionSettings>({
|
||||
enabled: false,
|
||||
number_limits: 2,
|
||||
detail: Resolution.low,
|
||||
transfer_methods: [TransferMethod.local_file],
|
||||
})
|
||||
const handleSend = async (message: string, files?: VisionFile[]) => {
|
||||
if (isResponsing) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return
|
||||
}
|
||||
const data = {
|
||||
|
||||
if (files?.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
|
||||
return false
|
||||
}
|
||||
|
||||
const data: Record<string, any> = {
|
||||
inputs: currInputs,
|
||||
query: message,
|
||||
conversation_id: isNewConversation ? null : currConversationId,
|
||||
}
|
||||
|
||||
if (visionConfig.enabled && files && files?.length > 0) {
|
||||
data.files = files.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
...item,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
// qustion
|
||||
const questionId = `question-${Date.now()}`
|
||||
const questionItem = {
|
||||
id: questionId,
|
||||
content: message,
|
||||
isAnswer: false,
|
||||
message_files: files,
|
||||
}
|
||||
|
||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||
@@ -519,13 +555,17 @@ const Main: FC<IMainProps> = ({
|
||||
setChatList(newListWithAnswer)
|
||||
},
|
||||
async onCompleted(hasError?: boolean) {
|
||||
setResponsingFalse()
|
||||
if (hasError)
|
||||
return
|
||||
|
||||
if (getConversationIdChangeBecauseOfNew()) {
|
||||
const { data: allConversations }: any = await fetchAllConversations()
|
||||
setAllConversationList(allConversations)
|
||||
const newItem: any = await generationConversationName(isInstalledApp, installedAppInfo?.id, allConversations[0].id)
|
||||
|
||||
const newAllConversations = produce(allConversations, (draft: any) => {
|
||||
draft[0].name = newItem.name
|
||||
})
|
||||
setAllConversationList(newAllConversations as any)
|
||||
noticeUpdateList()
|
||||
}
|
||||
setConversationIdChangeBecauseOfNew(false)
|
||||
@@ -537,6 +577,7 @@ const Main: FC<IMainProps> = ({
|
||||
setSuggestQuestions(data)
|
||||
setIsShowSuggestion(true)
|
||||
}
|
||||
setResponsingFalse()
|
||||
},
|
||||
onMessageEnd: isInstalledApp
|
||||
? (messageEnd) => {
|
||||
@@ -717,6 +758,10 @@ const Main: FC<IMainProps> = ({
|
||||
suggestionList={suggestQuestions}
|
||||
isShowSpeechToText={speechToTextConfig?.enabled}
|
||||
isShowCitation={citationConfig?.enabled && isInstalledApp}
|
||||
visionConfig={{
|
||||
...visionConfig,
|
||||
image_file_size_limit: fileUploadConfigResponse ? fileUploadConfigResponse.image_file_size_limit : visionConfig.image_file_size_limit,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>)
|
||||
|
@@ -14,7 +14,7 @@ import s from './style.module.css'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import ConfigScene from '@/app/components/share/chatbot/config-scence'
|
||||
import Header from '@/app/components/share/header'
|
||||
import { fetchAppInfo, fetchAppParams, fetchChatList, fetchConversations, fetchSuggestedQuestions, sendChatMessage, stopChatMessageResponding, updateFeedback } from '@/service/share'
|
||||
import { fetchAppInfo, fetchAppParams, fetchChatList, fetchConversations, fetchSuggestedQuestions, generationConversationName, sendChatMessage, stopChatMessageResponding, updateFeedback } from '@/service/share'
|
||||
import type { ConversationItem, SiteInfo } from '@/models/share'
|
||||
import type { PromptConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
|
||||
import type { Feedbacktype, IChatItem } from '@/app/components/app/chat/type'
|
||||
@@ -28,6 +28,8 @@ import type { InstalledApp } from '@/models/explore'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import LogoHeader from '@/app/components/base/logo/logo-embeded-chat-header'
|
||||
import LogoAvatar from '@/app/components/base/logo/logo-embeded-chat-avatar'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
|
||||
export type IMainProps = {
|
||||
isInstalledApp?: boolean
|
||||
@@ -184,6 +186,7 @@ const Main: FC<IMainProps> = ({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: item.message_files,
|
||||
})
|
||||
newChatList.push({
|
||||
id: item.id,
|
||||
@@ -292,7 +295,11 @@ const Main: FC<IMainProps> = ({
|
||||
const isNotNewConversation = allConversations.some(item => item.id === _conversationId)
|
||||
setAllConversationList(allConversations)
|
||||
// fetch new conversation info
|
||||
const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, sensitive_word_avoidance }: any = appParams
|
||||
const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, file_upload, sensitive_word_avoidance }: any = appParams
|
||||
setVisionConfig({
|
||||
...file_upload.image,
|
||||
image_file_size_limit: appParams?.system_parameters?.image_file_size_limit,
|
||||
})
|
||||
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
|
||||
if (siteInfo.default_language)
|
||||
changeLanguage(siteInfo.default_language)
|
||||
@@ -371,24 +378,48 @@ const Main: FC<IMainProps> = ({
|
||||
const [messageTaskId, setMessageTaskId] = useState('')
|
||||
const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false)
|
||||
const [shouldReload, setShouldReload] = useState(false)
|
||||
const [visionConfig, setVisionConfig] = useState<VisionSettings>({
|
||||
enabled: false,
|
||||
number_limits: 2,
|
||||
detail: Resolution.low,
|
||||
transfer_methods: [TransferMethod.local_file],
|
||||
})
|
||||
|
||||
const handleSend = async (message: string) => {
|
||||
const handleSend = async (message: string, files?: VisionFile[]) => {
|
||||
if (isResponsing) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return
|
||||
}
|
||||
const data = {
|
||||
|
||||
if (files?.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
|
||||
return false
|
||||
}
|
||||
const data: Record<string, any> = {
|
||||
inputs: currInputs,
|
||||
query: message,
|
||||
conversation_id: isNewConversation ? null : currConversationId,
|
||||
}
|
||||
|
||||
if (visionConfig.enabled && files && files?.length > 0) {
|
||||
data.files = files.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
...item,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
// qustion
|
||||
const questionId = `question-${Date.now()}`
|
||||
const questionItem = {
|
||||
id: questionId,
|
||||
content: message,
|
||||
isAnswer: false,
|
||||
message_files: files,
|
||||
}
|
||||
|
||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||
@@ -436,13 +467,16 @@ const Main: FC<IMainProps> = ({
|
||||
setChatList(newListWithAnswer)
|
||||
},
|
||||
async onCompleted(hasError?: boolean) {
|
||||
setResponsingFalse()
|
||||
if (hasError)
|
||||
return
|
||||
|
||||
if (getConversationIdChangeBecauseOfNew()) {
|
||||
const { data: allConversations }: any = await fetchAllConversations()
|
||||
setAllConversationList(allConversations)
|
||||
const newItem: any = await generationConversationName(isInstalledApp, installedAppInfo?.id, allConversations[0].id)
|
||||
const newAllConversations = produce(allConversations, (draft: any) => {
|
||||
draft[0].name = newItem.name
|
||||
})
|
||||
setAllConversationList(newAllConversations as any)
|
||||
noticeUpdateList()
|
||||
}
|
||||
setConversationIdChangeBecauseOfNew(false)
|
||||
@@ -454,6 +488,7 @@ const Main: FC<IMainProps> = ({
|
||||
setSuggestQuestions(data)
|
||||
setIsShowSuggestion(true)
|
||||
}
|
||||
setResponsingFalse()
|
||||
},
|
||||
onMessageReplace: (messageReplace) => {
|
||||
setChatList(produce(
|
||||
@@ -581,6 +616,7 @@ const Main: FC<IMainProps> = ({
|
||||
displayScene='web'
|
||||
isShowSpeechToText={speechToTextConfig?.enabled}
|
||||
answerIcon={<LogoAvatar className='relative shrink-0' />}
|
||||
visionConfig={visionConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>)
|
||||
|
@@ -26,6 +26,8 @@ import SavedItems from '@/app/components/app/text-generate/saved-items'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import { DEFAULT_VALUE_MAX_LEN, appDefaultIconBackground } from '@/config'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
|
||||
const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group.
|
||||
enum TaskStatus {
|
||||
@@ -92,6 +94,14 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
// send message task
|
||||
const [controlSend, setControlSend] = useState(0)
|
||||
const [controlStopResponding, setControlStopResponding] = useState(0)
|
||||
const [visionConfig, setVisionConfig] = useState<VisionSettings>({
|
||||
enabled: false,
|
||||
number_limits: 2,
|
||||
detail: Resolution.low,
|
||||
transfer_methods: [TransferMethod.local_file],
|
||||
})
|
||||
const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
|
||||
|
||||
const handleSend = () => {
|
||||
setIsCallBatchAPI(false)
|
||||
setControlSend(Date.now())
|
||||
@@ -338,7 +348,11 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
setSiteInfo(siteInfo as SiteInfo)
|
||||
changeLanguage(siteInfo.default_language)
|
||||
|
||||
const { user_input_form, more_like_this, sensitive_word_avoidance }: any = appParams
|
||||
const { user_input_form, more_like_this, file_upload, sensitive_word_avoidance }: any = appParams
|
||||
setVisionConfig({
|
||||
...file_upload.image,
|
||||
image_file_size_limit: appParams?.system_parameters?.image_file_size_limit,
|
||||
})
|
||||
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
|
||||
setPromptConfig({
|
||||
prompt_template: '', // placeholder for feture
|
||||
@@ -378,6 +392,8 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
handleSaveMessage={handleSaveMessage}
|
||||
taskId={task?.id}
|
||||
onCompleted={handleCompleted}
|
||||
visionConfig={visionConfig}
|
||||
completionFiles={completionFiles}
|
||||
/>)
|
||||
|
||||
const renderBatchRes = () => {
|
||||
@@ -512,6 +528,8 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
onInputsChange={setInputs}
|
||||
promptConfig={promptConfig}
|
||||
onSend={handleSend}
|
||||
visionConfig={visionConfig}
|
||||
onVisionFilesChange={setCompletionFiles}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(isInBatchTab ? 'block' : 'hidden')}>
|
||||
|
@@ -13,6 +13,7 @@ import Loading from '@/app/components/base/loading'
|
||||
import type { PromptConfig } from '@/models/debug'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import type { ModerationService } from '@/models/common'
|
||||
import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
|
||||
export type IResultProps = {
|
||||
isCallBatchAPI: boolean
|
||||
isPC: boolean
|
||||
@@ -32,6 +33,8 @@ export type IResultProps = {
|
||||
onCompleted: (completionRes: string, taskId?: number, success?: boolean) => void
|
||||
enableModeration?: boolean
|
||||
moderationService?: (text: string) => ReturnType<ModerationService>
|
||||
visionConfig: VisionSettings
|
||||
completionFiles: VisionFile[]
|
||||
}
|
||||
|
||||
const Result: FC<IResultProps> = ({
|
||||
@@ -51,6 +54,8 @@ const Result: FC<IResultProps> = ({
|
||||
handleSaveMessage,
|
||||
taskId,
|
||||
onCompleted,
|
||||
visionConfig,
|
||||
completionFiles,
|
||||
}) => {
|
||||
const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false)
|
||||
useEffect(() => {
|
||||
@@ -108,6 +113,11 @@ const Result: FC<IResultProps> = ({
|
||||
logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }))
|
||||
return false
|
||||
}
|
||||
|
||||
if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
|
||||
return false
|
||||
}
|
||||
return !hasEmptyInput
|
||||
}
|
||||
|
||||
@@ -120,9 +130,20 @@ const Result: FC<IResultProps> = ({
|
||||
if (!checkCanSend())
|
||||
return
|
||||
|
||||
const data = {
|
||||
const data: Record<string, any> = {
|
||||
inputs,
|
||||
}
|
||||
if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
|
||||
data.files = completionFiles.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
...item,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
setMessageId(null)
|
||||
setFeedback({
|
||||
@@ -145,7 +166,6 @@ const Result: FC<IResultProps> = ({
|
||||
setResponsingFalse()
|
||||
onCompleted(getCompletionRes(), taskId, false)
|
||||
isTimeout = true
|
||||
console.log(`[#${taskId}]: timeout`)
|
||||
}
|
||||
}, 1000)
|
||||
sendCompletionMessage(data, {
|
||||
|
@@ -9,6 +9,8 @@ import type { SiteInfo } from '@/models/share'
|
||||
import type { PromptConfig } from '@/models/debug'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
|
||||
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
|
||||
export type IRunOnceProps = {
|
||||
siteInfo: SiteInfo
|
||||
@@ -16,12 +18,16 @@ export type IRunOnceProps = {
|
||||
inputs: Record<string, any>
|
||||
onInputsChange: (inputs: Record<string, any>) => void
|
||||
onSend: () => void
|
||||
visionConfig: VisionSettings
|
||||
onVisionFilesChange: (files: VisionFile[]) => void
|
||||
}
|
||||
const RunOnce: FC<IRunOnceProps> = ({
|
||||
promptConfig,
|
||||
inputs,
|
||||
onInputsChange,
|
||||
onSend,
|
||||
visionConfig,
|
||||
onVisionFilesChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -73,6 +79,24 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{
|
||||
visionConfig?.enabled && (
|
||||
<div className="w-full mt-4">
|
||||
<div className="text-gray-900 text-sm font-medium">Image Upload</div>
|
||||
<div className='mt-2'>
|
||||
<TextGenerationImageUploader
|
||||
settings={visionConfig}
|
||||
onFilesChange={files => onVisionFilesChange(files.filter(file => file.progress !== -1).map(fileItem => ({
|
||||
type: 'image',
|
||||
transfer_method: fileItem.type,
|
||||
url: fileItem.url,
|
||||
upload_file_id: fileItem.fileId,
|
||||
})))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{promptConfig.prompt_variables.length > 0 && (
|
||||
<div className='mt-4 h-[1px] bg-gray-100'></div>
|
||||
)}
|
||||
|
Reference in New Issue
Block a user