mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-10 03:16:51 +08:00
feat: multiple model configuration (#2196)
Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
58
web/app/components/base/chat/chat/answer/agent-content.tsx
Normal file
58
web/app/components/base/chat/chat/answer/agent-content.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { FC } from 'react'
|
||||
import type {
|
||||
ChatItem,
|
||||
VisionFile,
|
||||
} from '../../types'
|
||||
import { useChatContext } from '../context'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import Thought from '@/app/components/app/chat/thought'
|
||||
import ImageGallery from '@/app/components/base/image-gallery'
|
||||
|
||||
type AgentContentProps = {
|
||||
item: ChatItem
|
||||
}
|
||||
const AgentContent: FC<AgentContentProps> = ({
|
||||
item,
|
||||
}) => {
|
||||
const { allToolIcons } = useChatContext()
|
||||
const {
|
||||
annotation,
|
||||
agent_thoughts,
|
||||
} = item
|
||||
|
||||
const getImgs = (list?: VisionFile[]) => {
|
||||
if (!list)
|
||||
return []
|
||||
return list.filter(file => file.type === 'image' && file.belongs_to === 'assistant')
|
||||
}
|
||||
|
||||
if (annotation?.logAnnotation)
|
||||
return <Markdown content={annotation?.logAnnotation.content || ''} />
|
||||
|
||||
return (
|
||||
<div>
|
||||
{agent_thoughts?.map((thought, index) => (
|
||||
<div key={index}>
|
||||
{thought.thought && (
|
||||
<Markdown content={thought.thought} />
|
||||
)}
|
||||
{/* {item.tool} */}
|
||||
{/* perhaps not use tool */}
|
||||
{!!thought.tool && (
|
||||
<Thought
|
||||
thought={thought}
|
||||
allToolIcons={allToolIcons || {}}
|
||||
isFinished={!!thought.observation}
|
||||
/>
|
||||
)}
|
||||
|
||||
{getImgs(thought.message_files).length > 0 && (
|
||||
<ImageGallery srcs={getImgs(thought.message_files).map(file => file.url)} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentContent
|
||||
22
web/app/components/base/chat/chat/answer/basic-content.tsx
Normal file
22
web/app/components/base/chat/chat/answer/basic-content.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
|
||||
type BasicContentProps = {
|
||||
item: ChatItem
|
||||
}
|
||||
const BasicContent: FC<BasicContentProps> = ({
|
||||
item,
|
||||
}) => {
|
||||
const {
|
||||
annotation,
|
||||
content,
|
||||
} = item
|
||||
|
||||
if (annotation?.logAnnotation)
|
||||
return <Markdown content={annotation?.logAnnotation.content || ''} />
|
||||
|
||||
return <Markdown content={content} />
|
||||
}
|
||||
|
||||
export default BasicContent
|
||||
99
web/app/components/base/chat/chat/answer/index.tsx
Normal file
99
web/app/components/base/chat/chat/answer/index.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { useChatContext } from '../context'
|
||||
import { useCurrentAnswerIsResponsing } from '../hooks'
|
||||
import Operation from './operation'
|
||||
import AgentContent from './agent-content'
|
||||
import BasicContent from './basic-content'
|
||||
import SuggestedQuestions from './suggested-questions'
|
||||
import More from './more'
|
||||
import { AnswerTriangle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import LoadingAnim from '@/app/components/app/chat/loading-anim'
|
||||
import Citation from '@/app/components/app/chat/citation'
|
||||
import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
|
||||
|
||||
type AnswerProps = {
|
||||
item: ChatItem
|
||||
}
|
||||
const Answer: FC<AnswerProps> = ({
|
||||
item,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
config,
|
||||
answerIcon,
|
||||
} = useChatContext()
|
||||
const responsing = useCurrentAnswerIsResponsing(item.id)
|
||||
const {
|
||||
content,
|
||||
citation,
|
||||
agent_thoughts,
|
||||
more,
|
||||
annotation,
|
||||
} = item
|
||||
const hasAgentThoughts = !!agent_thoughts?.length
|
||||
|
||||
return (
|
||||
<div className='flex mb-2 last:mb-0'>
|
||||
<div className='shrink-0 relative w-10 h-10'>
|
||||
{
|
||||
answerIcon || (
|
||||
<div className='flex items-center justify-center w-full h-full rounded-full bg-[#d5f5f6] border-[0.5px] border-black/5 text-xl'>
|
||||
🤖
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
responsing && (
|
||||
<div className='absolute -top-[3px] -left-[3px] pl-[6px] flex items-center w-4 h-4 bg-white rounded-full shadow-xs border-[0.5px] border-gray-50'>
|
||||
<LoadingAnim type='avatar' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='chat-answer-container grow w-0 group ml-4'>
|
||||
<div className='relative pr-10'>
|
||||
<AnswerTriangle className='absolute -left-2 top-0 w-2 h-3 text-gray-100' />
|
||||
<div className='group relative inline-block px-4 py-3 max-w-full bg-gray-100 rounded-b-2xl rounded-tr-2xl text-sm text-gray-900'>
|
||||
<Operation item={item} />
|
||||
{
|
||||
responsing && !content && !hasAgentThoughts && (
|
||||
<div className='flex items-center justify-center w-6 h-5'>
|
||||
<LoadingAnim type='text' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
content && !hasAgentThoughts && (
|
||||
<BasicContent item={item} />
|
||||
)
|
||||
}
|
||||
{
|
||||
hasAgentThoughts && !content && (
|
||||
<AgentContent item={item} />
|
||||
)
|
||||
}
|
||||
{
|
||||
annotation?.id && !annotation?.logAnnotation && (
|
||||
<EditTitle
|
||||
className='mt-1'
|
||||
title={t('appAnnotation.editBy', { author: annotation.authorName })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<SuggestedQuestions item={item} />
|
||||
{
|
||||
!!citation?.length && config?.retriever_resource?.enabled && !responsing && (
|
||||
<Citation data={citation} showHitInfo />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<More more={more} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Answer
|
||||
45
web/app/components/base/chat/chat/answer/more.tsx
Normal file
45
web/app/components/base/chat/chat/answer/more.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
|
||||
type MoreProps = {
|
||||
more: ChatItem['more']
|
||||
}
|
||||
const More: FC<MoreProps> = ({
|
||||
more,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex items-center mt-1 h-[18px] text-xs text-gray-400 opacity-0 group-hover:opacity-100'>
|
||||
{
|
||||
more && (
|
||||
<>
|
||||
<div
|
||||
className='mr-2 shrink-0 truncate max-w-[33.3%]'
|
||||
title={`${t('appLog.detail.timeConsuming')} ${more.latency}${t('appLog.detail.second')}`}
|
||||
>
|
||||
{`${t('appLog.detail.timeConsuming')} ${more.latency}${t('appLog.detail.second')}`}
|
||||
</div>
|
||||
<div
|
||||
className='shrink-0 truncate max-w-[33.3%]'
|
||||
title={`${t('appLog.detail.tokenCost')} ${formatNumber(more.tokens)}`}
|
||||
>
|
||||
{`${t('appLog.detail.tokenCost')} ${formatNumber(more.tokens)}`}
|
||||
</div>
|
||||
<div className='shrink-0 mx-2'>·</div>
|
||||
<div
|
||||
className='shrink-0 truncate max-w-[33.3%]'
|
||||
title={more.time}
|
||||
>
|
||||
{more.time}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default More
|
||||
54
web/app/components/base/chat/chat/answer/operation.tsx
Normal file
54
web/app/components/base/chat/chat/answer/operation.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { useCurrentAnswerIsResponsing } from '../hooks'
|
||||
import { useChatContext } from '../context'
|
||||
import CopyBtn from '@/app/components/app/chat/copy-btn'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import AudioBtn from '@/app/components/base/audio-btn'
|
||||
|
||||
type OperationProps = {
|
||||
item: ChatItem
|
||||
}
|
||||
const Operation: FC<OperationProps> = ({
|
||||
item,
|
||||
}) => {
|
||||
const { config } = useChatContext()
|
||||
const responsing = useCurrentAnswerIsResponsing(item.id)
|
||||
const {
|
||||
isOpeningStatement,
|
||||
content,
|
||||
annotation,
|
||||
} = item
|
||||
|
||||
return (
|
||||
<div className='absolute top-[-14px] right-[-14px] flex justify-end gap-1'>
|
||||
{
|
||||
!isOpeningStatement && !responsing && (
|
||||
<CopyBtn
|
||||
value={content}
|
||||
className='hidden group-hover:block'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{!isOpeningStatement && config?.text_to_speech && (
|
||||
<AudioBtn
|
||||
value={content}
|
||||
className='hidden group-hover:block'
|
||||
/>
|
||||
)}
|
||||
{
|
||||
annotation?.id && (
|
||||
<div
|
||||
className='relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7] shadow-md'
|
||||
>
|
||||
<div className='p-1 rounded-lg bg-[#EEF4FF] '>
|
||||
<MessageFast className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Operation
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { useChatContext } from '../context'
|
||||
|
||||
type SuggestedQuestionsProps = {
|
||||
item: ChatItem
|
||||
}
|
||||
const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
|
||||
item,
|
||||
}) => {
|
||||
const { onSend } = useChatContext()
|
||||
const {
|
||||
isOpeningStatement,
|
||||
suggestedQuestions,
|
||||
} = item
|
||||
|
||||
if (!isOpeningStatement || !suggestedQuestions?.length)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap'>
|
||||
{suggestedQuestions.map((question, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='mt-1 mr-1 max-w-full last:mr-0 shrink-0 py-[5px] leading-[18px] items-center px-4 rounded-lg border border-gray-200 shadow-xs bg-white text-xs font-medium text-primary-600 cursor-pointer'
|
||||
onClick={() => onSend?.(question)}
|
||||
>
|
||||
{question}
|
||||
</div>),
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SuggestedQuestions
|
||||
220
web/app/components/base/chat/chat/chat-input.tsx
Normal file
220
web/app/components/base/chat/chat/chat-input.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Recorder from 'js-audio-recorder'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Textarea from 'rc-textarea'
|
||||
import type {
|
||||
EnableType,
|
||||
OnSend,
|
||||
VisionConfig,
|
||||
} from '../types'
|
||||
import { TransferMethod } from '../types'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import VoiceInput from '@/app/components/base/voice-input'
|
||||
import { Microphone01 } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { Microphone01 as Microphone01Solid } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { Send03 } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import ChatImageUploader from '@/app/components/base/image-uploader/chat-image-uploader'
|
||||
import ImageList from '@/app/components/base/image-uploader/image-list'
|
||||
import {
|
||||
useClipboardUploader,
|
||||
useDraggableUploader,
|
||||
useImageFiles,
|
||||
} from '@/app/components/base/image-uploader/hooks'
|
||||
|
||||
type ChatInputProps = {
|
||||
visionConfig?: VisionConfig
|
||||
speechToTextConfig?: EnableType
|
||||
onSend?: OnSend
|
||||
}
|
||||
const ChatInput: FC<ChatInputProps> = ({
|
||||
visionConfig,
|
||||
speechToTextConfig,
|
||||
onSend,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [voiceInputShow, setVoiceInputShow] = useState(false)
|
||||
const {
|
||||
files,
|
||||
onUpload,
|
||||
onRemove,
|
||||
onReUpload,
|
||||
onImageLinkLoadError,
|
||||
onImageLinkLoadSuccess,
|
||||
onClear,
|
||||
} = useImageFiles()
|
||||
const { onPaste } = useClipboardUploader({ onUpload, visionConfig, files })
|
||||
const { onDragEnter, onDragLeave, onDragOver, onDrop, isDragActive } = useDraggableUploader<HTMLTextAreaElement>({ onUpload, files, visionConfig })
|
||||
const isUseInputMethod = useRef(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value
|
||||
setQuery(value)
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
if (onSend) {
|
||||
onSend(query, files.filter(file => file.progress !== -1).map(fileItem => ({
|
||||
type: 'image',
|
||||
transfer_method: fileItem.type,
|
||||
url: fileItem.url,
|
||||
upload_file_id: fileItem.fileId,
|
||||
})))
|
||||
setQuery('')
|
||||
}
|
||||
if (!files.find(item => item.type === TransferMethod.local_file && !item.fileId)) {
|
||||
if (files.length)
|
||||
onClear()
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.code === 'Enter') {
|
||||
e.preventDefault()
|
||||
// prevent send message when using input method enter
|
||||
if (!e.shiftKey && !isUseInputMethod.current)
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
isUseInputMethod.current = e.nativeEvent.isComposing
|
||||
if (e.code === 'Enter' && !e.shiftKey) {
|
||||
setQuery(query.replace(/\n$/, ''))
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
const logError = (message: string) => {
|
||||
notify({ type: 'error', message, duration: 3000 })
|
||||
}
|
||||
const handleVoiceInputShow = () => {
|
||||
(Recorder as any).getPermission().then(() => {
|
||||
setVoiceInputShow(true)
|
||||
}, () => {
|
||||
logError(t('common.voiceInput.notAllow'))
|
||||
})
|
||||
}
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const sendBtn = (
|
||||
<div
|
||||
className='group flex items-center justify-center w-8 h-8 rounded-lg hover:bg-[#EBF5FF] cursor-pointer'
|
||||
onClick={handleSend}
|
||||
>
|
||||
<Send03
|
||||
className={`
|
||||
w-5 h-5 text-gray-300 group-hover:text-primary-600
|
||||
${!!query.trim() && 'text-primary-600'}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
relative p-[5.5px] max-h-[150px] bg-white border-[1.5px] border-gray-200 rounded-xl overflow-y-auto
|
||||
${isDragActive && 'border-primary-600'}
|
||||
`}
|
||||
>
|
||||
{
|
||||
visionConfig?.enabled && (
|
||||
<>
|
||||
<div className='absolute bottom-2 left-2 flex items-center'>
|
||||
<ChatImageUploader
|
||||
settings={visionConfig}
|
||||
onUpload={onUpload}
|
||||
disabled={files.length >= visionConfig.number_limits}
|
||||
/>
|
||||
<div className='mx-1 w-[1px] h-4 bg-black/5' />
|
||||
</div>
|
||||
<div className='pl-[52px]'>
|
||||
<ImageList
|
||||
list={files}
|
||||
onRemove={onRemove}
|
||||
onReUpload={onReUpload}
|
||||
onImageLinkLoadSuccess={onImageLinkLoadSuccess}
|
||||
onImageLinkLoadError={onImageLinkLoadError}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Textarea
|
||||
className={`
|
||||
block w-full px-2 pr-[118px] py-[7px] leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none
|
||||
${visionConfig?.enabled && 'pl-12'}
|
||||
`}
|
||||
value={query}
|
||||
onChange={handleContentChange}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={onPaste}
|
||||
onDragEnter={onDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
autoSize
|
||||
/>
|
||||
<div className='absolute bottom-[7px] right-2 flex items-center h-8'>
|
||||
<div className='flex items-center px-1 h-5 rounded-md bg-gray-100 text-xs font-medium text-gray-500'>
|
||||
{query.trim().length}
|
||||
</div>
|
||||
{
|
||||
query
|
||||
? (
|
||||
<div className='flex justify-center items-center ml-2 w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg' onClick={() => setQuery('')}>
|
||||
<XCircle className='w-4 h-4 text-[#98A2B3]' />
|
||||
</div>
|
||||
)
|
||||
: speechToTextConfig?.enabled
|
||||
? (
|
||||
<div
|
||||
className='group flex justify-center items-center ml-2 w-8 h-8 hover:bg-primary-50 rounded-lg cursor-pointer'
|
||||
onClick={handleVoiceInputShow}
|
||||
>
|
||||
<Microphone01 className='block w-4 h-4 text-gray-500 group-hover:hidden' />
|
||||
<Microphone01Solid className='hidden w-4 h-4 text-primary-600 group-hover:block' />
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
}
|
||||
<div className='mx-2 w-[1px] h-4 bg-black opacity-5' />
|
||||
{isMobile
|
||||
? sendBtn
|
||||
: (
|
||||
<TooltipPlus
|
||||
popupContent={
|
||||
<div>
|
||||
<div>{t('common.operation.send')} Enter</div>
|
||||
<div>{t('common.operation.lineBreak')} Shift Enter</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{sendBtn}
|
||||
</TooltipPlus>
|
||||
)}
|
||||
</div>
|
||||
{
|
||||
voiceInputShow && (
|
||||
<VoiceInput
|
||||
onCancel={() => setVoiceInputShow(false)}
|
||||
onConverted={text => setQuery(text)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatInput
|
||||
60
web/app/components/base/chat/chat/context.tsx
Normal file
60
web/app/components/base/chat/chat/context.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
|
||||
export type ChatContextValue = {
|
||||
config?: ChatConfig
|
||||
isResponsing?: boolean
|
||||
chatList: ChatItem[]
|
||||
showPromptLog?: boolean
|
||||
questionIcon?: ReactNode
|
||||
answerIcon?: ReactNode
|
||||
allToolIcons?: Record<string, string | Emoji>
|
||||
onSend?: OnSend
|
||||
}
|
||||
|
||||
const ChatContext = createContext<ChatContextValue>({
|
||||
chatList: [],
|
||||
})
|
||||
|
||||
type ChatContextProviderProps = {
|
||||
children: ReactNode
|
||||
} & ChatContextValue
|
||||
|
||||
export const ChatContextProvider = ({
|
||||
children,
|
||||
config,
|
||||
isResponsing,
|
||||
chatList,
|
||||
showPromptLog,
|
||||
questionIcon,
|
||||
answerIcon,
|
||||
allToolIcons,
|
||||
onSend,
|
||||
}: ChatContextProviderProps) => {
|
||||
return (
|
||||
<ChatContext.Provider value={{
|
||||
config,
|
||||
isResponsing,
|
||||
chatList: chatList || [],
|
||||
showPromptLog,
|
||||
questionIcon,
|
||||
answerIcon,
|
||||
allToolIcons,
|
||||
onSend,
|
||||
}}>
|
||||
{children}
|
||||
</ChatContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useChatContext = () => useContext(ChatContext)
|
||||
|
||||
export default ChatContext
|
||||
395
web/app/components/base/chat/chat/hooks.ts
Normal file
395
web/app/components/base/chat/chat/hooks.ts
Normal file
@@ -0,0 +1,395 @@
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { produce } from 'immer'
|
||||
import { useGetState } from 'ahooks'
|
||||
import dayjs from 'dayjs'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
Inputs,
|
||||
PromptVariable,
|
||||
VisionFile,
|
||||
} from '../types'
|
||||
import { useChatContext } from './context'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { ssePost } from '@/service/base'
|
||||
import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
|
||||
|
||||
type GetAbortController = (abortController: AbortController) => void
|
||||
type SendCallback = {
|
||||
onGetConvesationMessages: (conversationId: string, getAbortController: GetAbortController) => Promise<any>
|
||||
onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise<any>
|
||||
}
|
||||
export const useChat = (
|
||||
config: ChatConfig,
|
||||
promptVariablesConfig?: {
|
||||
inputs: Inputs
|
||||
promptVariables: PromptVariable[]
|
||||
},
|
||||
prevChatList?: ChatItem[],
|
||||
stopChat?: (taskId: string) => void,
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const connversationId = useRef('')
|
||||
const hasStopResponded = useRef(false)
|
||||
const [isResponsing, setIsResponsing] = useState(false)
|
||||
const [chatList, setChatList, getChatList] = useGetState<ChatItem[]>(prevChatList || [])
|
||||
const [taskId, setTaskId] = useState('')
|
||||
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
|
||||
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||
const [conversationMessagesAbortController, setConversationMessagesAbortController] = useState<AbortController | null>(null)
|
||||
const [suggestedQuestionsAbortController, setSuggestedQuestionsAbortController] = useState<AbortController | null>(null)
|
||||
|
||||
const getIntroduction = (str: string) => {
|
||||
return replaceStringWithValues(str, promptVariablesConfig?.promptVariables || [], promptVariablesConfig?.inputs || {})
|
||||
}
|
||||
useEffect(() => {
|
||||
if (config.opening_statement && !chatList.some(item => !item.isAnswer)) {
|
||||
setChatList([{
|
||||
id: `${Date.now()}`,
|
||||
content: getIntroduction(config.opening_statement),
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
suggestedQuestions: config.suggested_questions,
|
||||
}])
|
||||
}
|
||||
}, [config.opening_statement, config.suggested_questions, promptVariablesConfig?.inputs])
|
||||
|
||||
const handleStop = () => {
|
||||
if (stopChat && taskId)
|
||||
stopChat(taskId)
|
||||
if (abortController)
|
||||
abortController.abort()
|
||||
if (conversationMessagesAbortController)
|
||||
conversationMessagesAbortController.abort()
|
||||
if (suggestedQuestionsAbortController)
|
||||
suggestedQuestionsAbortController.abort()
|
||||
}
|
||||
|
||||
const handleRestart = () => {
|
||||
handleStop()
|
||||
hasStopResponded.current = true
|
||||
connversationId.current = ''
|
||||
setIsResponsing(false)
|
||||
setChatList(config.opening_statement
|
||||
? [{
|
||||
id: `${Date.now()}`,
|
||||
content: config.opening_statement,
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
suggestedQuestions: config.suggested_questions,
|
||||
}]
|
||||
: [])
|
||||
setSuggestQuestions([])
|
||||
}
|
||||
const handleSend = async (
|
||||
url: string,
|
||||
data: any,
|
||||
{
|
||||
onGetConvesationMessages,
|
||||
onGetSuggestedQuestions,
|
||||
}: SendCallback,
|
||||
) => {
|
||||
setSuggestQuestions([])
|
||||
if (isResponsing) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return false
|
||||
}
|
||||
|
||||
if (promptVariablesConfig?.inputs && promptVariablesConfig?.promptVariables) {
|
||||
const {
|
||||
promptVariables,
|
||||
inputs,
|
||||
} = promptVariablesConfig
|
||||
let hasEmptyInput = ''
|
||||
const requiredVars = promptVariables.filter(({ key, name, required, type }) => {
|
||||
if (type === 'api')
|
||||
return false
|
||||
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
|
||||
return res
|
||||
})
|
||||
|
||||
if (requiredVars?.length) {
|
||||
requiredVars.forEach(({ key, name }) => {
|
||||
if (hasEmptyInput)
|
||||
return
|
||||
|
||||
if (!inputs[key])
|
||||
hasEmptyInput = name
|
||||
})
|
||||
}
|
||||
|
||||
if (hasEmptyInput) {
|
||||
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const updateCurrentQA = ({
|
||||
responseItem,
|
||||
questionId,
|
||||
placeholderAnswerId,
|
||||
questionItem,
|
||||
}: {
|
||||
responseItem: ChatItem
|
||||
questionId: string
|
||||
placeholderAnswerId: string
|
||||
questionItem: ChatItem
|
||||
}) => {
|
||||
// closesure new list is outdated.
|
||||
const newListWithAnswer = produce(
|
||||
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem })
|
||||
|
||||
draft.push({ ...responseItem })
|
||||
})
|
||||
setChatList(newListWithAnswer)
|
||||
}
|
||||
|
||||
const questionId = `question-${Date.now()}`
|
||||
const questionItem = {
|
||||
id: questionId,
|
||||
content: data.query,
|
||||
isAnswer: false,
|
||||
message_files: data.files,
|
||||
}
|
||||
|
||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||
const placeholderAnswerItem = {
|
||||
id: placeholderAnswerId,
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
}
|
||||
|
||||
const newList = [...getChatList(), questionItem, placeholderAnswerItem]
|
||||
setChatList(newList)
|
||||
|
||||
// answer
|
||||
const responseItem: ChatItem = {
|
||||
id: `${Date.now()}`,
|
||||
content: '',
|
||||
agent_thoughts: [],
|
||||
message_files: [],
|
||||
isAnswer: true,
|
||||
}
|
||||
|
||||
setIsResponsing(true)
|
||||
hasStopResponded.current = false
|
||||
|
||||
const bodyParams = {
|
||||
response_mode: 'streaming',
|
||||
conversation_id: connversationId.current,
|
||||
...data,
|
||||
}
|
||||
if (bodyParams?.files?.length) {
|
||||
bodyParams.files = bodyParams.files.map((item: VisionFile) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
...item,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
let isAgentMode = false
|
||||
let hasSetResponseId = false
|
||||
|
||||
ssePost(
|
||||
url,
|
||||
{
|
||||
body: bodyParams,
|
||||
},
|
||||
{
|
||||
getAbortController: (abortController) => {
|
||||
setAbortController(abortController)
|
||||
},
|
||||
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
|
||||
if (!isAgentMode) {
|
||||
responseItem.content = responseItem.content + message
|
||||
}
|
||||
else {
|
||||
const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
|
||||
if (lastThought)
|
||||
lastThought.thought = lastThought.thought + message // need immer setAutoFreeze
|
||||
}
|
||||
|
||||
if (messageId && !hasSetResponseId) {
|
||||
responseItem.id = messageId
|
||||
hasSetResponseId = true
|
||||
}
|
||||
|
||||
if (isFirstMessage && newConversationId)
|
||||
connversationId.current = newConversationId
|
||||
|
||||
setTaskId(taskId)
|
||||
if (messageId)
|
||||
responseItem.id = messageId
|
||||
|
||||
updateCurrentQA({
|
||||
responseItem,
|
||||
questionId,
|
||||
placeholderAnswerId,
|
||||
questionItem,
|
||||
})
|
||||
},
|
||||
async onCompleted(hasError?: boolean) {
|
||||
setIsResponsing(false)
|
||||
|
||||
if (hasError)
|
||||
return
|
||||
|
||||
if (connversationId.current) {
|
||||
const { data }: any = await onGetConvesationMessages(
|
||||
connversationId.current,
|
||||
newAbortController => setConversationMessagesAbortController(newAbortController),
|
||||
)
|
||||
const newResponseItem = data.find((item: any) => item.id === responseItem.id)
|
||||
if (!newResponseItem)
|
||||
return
|
||||
|
||||
setChatList(produce(getChatList(), (draft) => {
|
||||
const index = draft.findIndex(item => item.id === responseItem.id)
|
||||
if (index !== -1) {
|
||||
const requestion = draft[index - 1]
|
||||
draft[index - 1] = {
|
||||
...requestion,
|
||||
log: newResponseItem.message,
|
||||
}
|
||||
draft[index] = {
|
||||
...draft[index],
|
||||
more: {
|
||||
time: dayjs.unix(newResponseItem.created_at).format('hh:mm A'),
|
||||
tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
|
||||
latency: newResponseItem.provider_response_latency.toFixed(2),
|
||||
},
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
if (config.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
|
||||
const { data }: any = await onGetSuggestedQuestions(
|
||||
responseItem.id,
|
||||
newAbortController => setSuggestedQuestionsAbortController(newAbortController),
|
||||
)
|
||||
setSuggestQuestions(data)
|
||||
}
|
||||
},
|
||||
onFile(file) {
|
||||
const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
|
||||
if (lastThought)
|
||||
responseItem.agent_thoughts![responseItem.agent_thoughts!.length - 1].message_files = [...(lastThought as any).message_files, file]
|
||||
|
||||
updateCurrentQA({
|
||||
responseItem,
|
||||
questionId,
|
||||
placeholderAnswerId,
|
||||
questionItem,
|
||||
})
|
||||
},
|
||||
onThought(thought) {
|
||||
isAgentMode = true
|
||||
const response = responseItem as any
|
||||
if (thought.message_id && !hasSetResponseId)
|
||||
response.id = thought.message_id
|
||||
if (response.agent_thoughts.length === 0) {
|
||||
response.agent_thoughts.push(thought)
|
||||
}
|
||||
else {
|
||||
const lastThought = response.agent_thoughts[response.agent_thoughts.length - 1]
|
||||
// thought changed but still the same thought, so update.
|
||||
if (lastThought.id === thought.id) {
|
||||
thought.thought = lastThought.thought
|
||||
thought.message_files = lastThought.message_files
|
||||
responseItem.agent_thoughts![response.agent_thoughts.length - 1] = thought
|
||||
}
|
||||
else {
|
||||
responseItem.agent_thoughts!.push(thought)
|
||||
}
|
||||
}
|
||||
updateCurrentQA({
|
||||
responseItem,
|
||||
questionId,
|
||||
placeholderAnswerId,
|
||||
questionItem,
|
||||
})
|
||||
},
|
||||
onMessageEnd: (messageEnd) => {
|
||||
if (messageEnd.metadata?.annotation_reply) {
|
||||
responseItem.id = messageEnd.id
|
||||
responseItem.annotation = ({
|
||||
id: messageEnd.metadata.annotation_reply.id,
|
||||
authorName: messageEnd.metadata.annotation_reply.account.name,
|
||||
})
|
||||
const newListWithAnswer = produce(
|
||||
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem })
|
||||
|
||||
draft.push({
|
||||
...responseItem,
|
||||
})
|
||||
})
|
||||
setChatList(newListWithAnswer)
|
||||
return
|
||||
}
|
||||
responseItem.citation = messageEnd.metadata?.retriever_resources || []
|
||||
|
||||
const newListWithAnswer = produce(
|
||||
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem })
|
||||
|
||||
draft.push({ ...responseItem })
|
||||
})
|
||||
setChatList(newListWithAnswer)
|
||||
},
|
||||
onMessageReplace: (messageReplace) => {
|
||||
responseItem.content = messageReplace.answer
|
||||
},
|
||||
onError() {
|
||||
setIsResponsing(false)
|
||||
// role back placeholder answer
|
||||
setChatList(produce(getChatList(), (draft) => {
|
||||
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
|
||||
}))
|
||||
},
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
chatList,
|
||||
getChatList,
|
||||
setChatList,
|
||||
conversationId: connversationId.current,
|
||||
isResponsing,
|
||||
setIsResponsing,
|
||||
handleSend,
|
||||
suggestedQuestions,
|
||||
handleRestart,
|
||||
handleStop,
|
||||
}
|
||||
}
|
||||
|
||||
export const useCurrentAnswerIsResponsing = (answerId: string) => {
|
||||
const {
|
||||
isResponsing,
|
||||
chatList,
|
||||
} = useChatContext()
|
||||
|
||||
const isLast = answerId === chatList[chatList.length - 1]?.id
|
||||
|
||||
return isLast && isResponsing
|
||||
}
|
||||
129
web/app/components/base/chat/chat/index.tsx
Normal file
129
web/app/components/base/chat/chat/index.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import type {
|
||||
FC,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import {
|
||||
memo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useThrottleEffect } from 'ahooks'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import Question from './question'
|
||||
import Answer from './answer'
|
||||
import ChatInput from './chat-input'
|
||||
import TryToAsk from './try-to-ask'
|
||||
import { ChatContextProvider } from './context'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
|
||||
export type ChatProps = {
|
||||
config: ChatConfig
|
||||
onSend?: OnSend
|
||||
chatList: ChatItem[]
|
||||
isResponsing: boolean
|
||||
noChatInput?: boolean
|
||||
chatContainerclassName?: string
|
||||
chatFooterClassName?: string
|
||||
suggestedQuestions?: string[]
|
||||
showPromptLog?: boolean
|
||||
questionIcon?: ReactNode
|
||||
answerIcon?: ReactNode
|
||||
allToolIcons?: Record<string, string | Emoji>
|
||||
}
|
||||
const Chat: FC<ChatProps> = ({
|
||||
config,
|
||||
onSend,
|
||||
chatList,
|
||||
isResponsing,
|
||||
noChatInput,
|
||||
chatContainerclassName,
|
||||
chatFooterClassName,
|
||||
suggestedQuestions,
|
||||
showPromptLog,
|
||||
questionIcon,
|
||||
answerIcon,
|
||||
allToolIcons,
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const chatFooterRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useThrottleEffect(() => {
|
||||
if (ref.current)
|
||||
ref.current.scrollTop = ref.current.scrollHeight
|
||||
}, [chatList], { wait: 500 })
|
||||
|
||||
const hasTryToAsk = config.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend
|
||||
|
||||
return (
|
||||
<ChatContextProvider
|
||||
config={config}
|
||||
chatList={chatList}
|
||||
isResponsing={isResponsing}
|
||||
showPromptLog={showPromptLog}
|
||||
questionIcon={questionIcon}
|
||||
answerIcon={answerIcon}
|
||||
allToolIcons={allToolIcons}
|
||||
onSend={onSend}
|
||||
>
|
||||
<div className='relative h-full'>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`relative h-full overflow-y-auto ${chatContainerclassName}`}
|
||||
>
|
||||
{
|
||||
chatList.map((item) => {
|
||||
if (item.isAnswer) {
|
||||
return (
|
||||
<Answer
|
||||
key={item.id}
|
||||
item={item}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Question
|
||||
key={item.id}
|
||||
item={item}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
{
|
||||
(hasTryToAsk || !noChatInput) && (
|
||||
<div
|
||||
className={`sticky bottom-0 w-full backdrop-blur-[20px] ${chatFooterClassName}`}
|
||||
ref={chatFooterRef}
|
||||
style={{
|
||||
background: 'linear-gradient(0deg, #FFF 0%, rgba(255, 255, 255, 0.40) 100%)',
|
||||
}}
|
||||
>
|
||||
{
|
||||
hasTryToAsk && (
|
||||
<TryToAsk
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
onSend={onSend}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!noChatInput && (
|
||||
<ChatInput
|
||||
visionConfig={config?.file_upload?.image}
|
||||
speechToTextConfig={config.speech_to_text}
|
||||
onSend={onSend}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ChatContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Chat)
|
||||
62
web/app/components/base/chat/chat/question.tsx
Normal file
62
web/app/components/base/chat/chat/question.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { FC } from 'react'
|
||||
import { useRef } from 'react'
|
||||
import type { ChatItem } from '../types'
|
||||
import { useChatContext } from './context'
|
||||
import { QuestionTriangle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { User } from '@/app/components/base/icons/src/public/avatar'
|
||||
import Log from '@/app/components/app/chat/log'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import ImageGallery from '@/app/components/base/image-gallery'
|
||||
|
||||
type QuestionProps = {
|
||||
item: ChatItem
|
||||
}
|
||||
const Question: FC<QuestionProps> = ({
|
||||
item,
|
||||
}) => {
|
||||
const ref = useRef(null)
|
||||
const {
|
||||
showPromptLog,
|
||||
isResponsing,
|
||||
questionIcon,
|
||||
} = useChatContext()
|
||||
const {
|
||||
content,
|
||||
message_files,
|
||||
} = item
|
||||
|
||||
const imgSrcs = message_files?.length ? message_files.map(item => item.url) : []
|
||||
|
||||
return (
|
||||
<div className='flex justify-end mb-2 last:mb-0 pl-10' ref={ref}>
|
||||
<div className='group relative mr-4'>
|
||||
<QuestionTriangle className='absolute -right-2 top-0 w-2 h-3 text-[#D1E9FF]/50' />
|
||||
{
|
||||
showPromptLog && !isResponsing && (
|
||||
<Log log={item.log!} containerRef={ref} />
|
||||
)
|
||||
}
|
||||
<div className='px-4 py-3 bg-[#D1E9FF]/50 rounded-b-2xl rounded-tl-2xl text-sm text-gray-900'>
|
||||
{
|
||||
!!imgSrcs.length && (
|
||||
<ImageGallery srcs={imgSrcs} />
|
||||
)
|
||||
}
|
||||
<Markdown content={content} />
|
||||
</div>
|
||||
<div className='mt-1 h-[18px]' />
|
||||
</div>
|
||||
<div className='shrink-0 w-10 h-10'>
|
||||
{
|
||||
questionIcon || (
|
||||
<div className='w-full h-full rounded-full border-[0.5px] border-black/5'>
|
||||
<User className='w-full h-full' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Question
|
||||
54
web/app/components/base/chat/chat/try-to-ask.tsx
Normal file
54
web/app/components/base/chat/chat/try-to-ask.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { OnSend } from '../types'
|
||||
import { Star04 } from '@/app/components/base/icons/src/vender/solid/shapes'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type TryToAskProps = {
|
||||
suggestedQuestions: string[]
|
||||
onSend: OnSend
|
||||
}
|
||||
const TryToAsk: FC<TryToAskProps> = ({
|
||||
suggestedQuestions,
|
||||
onSend,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='flex items-center mb-2.5 py-2'>
|
||||
<div
|
||||
className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)',
|
||||
}}
|
||||
/>
|
||||
<div className='shrink-0 flex items-center px-3 text-gray-500'>
|
||||
<Star04 className='mr-1 w-2.5 h-2.5' />
|
||||
<span className='text-xs text-gray-500 font-medium'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.tryToAsk')}</span>
|
||||
</div>
|
||||
<div
|
||||
className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-wrap'>
|
||||
{
|
||||
suggestedQuestions.map((suggestQuestion, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
className='mb-2 mr-2 last:mr-0 px-3 py-[5px] bg-white text-primary-600 text-xs font-medium'
|
||||
onClick={() => onSend(suggestQuestion)}
|
||||
>
|
||||
{suggestQuestion}
|
||||
</Button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TryToAsk
|
||||
48
web/app/components/base/chat/types.ts
Normal file
48
web/app/components/base/chat/types.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type {
|
||||
ModelConfig,
|
||||
VisionFile,
|
||||
VisionSettings,
|
||||
} from '@/types/app'
|
||||
import type { IChatItem } from '@/app/components/app/chat/type'
|
||||
|
||||
export type { VisionFile } from '@/types/app'
|
||||
export { TransferMethod } from '@/types/app'
|
||||
export type {
|
||||
Inputs,
|
||||
PromptVariable,
|
||||
} from '@/models/debug'
|
||||
|
||||
export type UserInputForm = {
|
||||
default: string
|
||||
label: string
|
||||
required: boolean
|
||||
variable: string
|
||||
}
|
||||
|
||||
export type UserInputFormTextInput = {
|
||||
'text-inpput': UserInputForm & {
|
||||
max_length: number
|
||||
}
|
||||
}
|
||||
|
||||
export type UserInputFormSelect = {
|
||||
'select': UserInputForm & {
|
||||
options: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export type UserInputFormParagraph = {
|
||||
'paragraph': UserInputForm
|
||||
}
|
||||
|
||||
export type VisionConfig = VisionSettings
|
||||
|
||||
export type EnableType = {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type ChatConfig = Omit<ModelConfig, 'model'>
|
||||
|
||||
export type ChatItem = IChatItem
|
||||
|
||||
export type OnSend = (message: string, files?: VisionFile[]) => void
|
||||
Reference in New Issue
Block a user