mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 11:42:29 +08:00
feat: multiple model configuration (#2196)
Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
parent
6bfdfab6f3
commit
d5361b8d09
|
@ -7,6 +7,8 @@ import {
|
|||
AlignLeft01,
|
||||
AlignRight01,
|
||||
} from '@/app/components/base/icons/src/vender/line/layout'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { APP_SIDEBAR_SHOULD_COLLAPSE } from '@/app/components/app/configuration/debug/types'
|
||||
|
||||
export type IAppDetailNavProps = {
|
||||
iconType?: 'app' | 'dataset' | 'notion'
|
||||
|
@ -39,6 +41,14 @@ const AppDetailNav = ({ title, desc, icon, icon_background, navigation, extraInf
|
|||
})
|
||||
}, [])
|
||||
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === APP_SIDEBAR_SHOULD_COLLAPSE) {
|
||||
setModeState('collapse')
|
||||
localStorage.setItem('app-detail-collapse-or-expand', 'collapse')
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
|
|
|
@ -15,10 +15,12 @@ export type Resources = {
|
|||
type CitationProps = {
|
||||
data: CitationItem[]
|
||||
showHitInfo?: boolean
|
||||
containerClassName?: string
|
||||
}
|
||||
const Citation: FC<CitationProps> = ({
|
||||
data,
|
||||
showHitInfo,
|
||||
containerClassName = 'chat-answer-container',
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const elesRef = useRef<HTMLDivElement[]>([])
|
||||
|
@ -46,7 +48,7 @@ const Citation: FC<CitationProps> = ({
|
|||
}, []), [data])
|
||||
|
||||
const handleAdjustResourcesLayout = () => {
|
||||
const containerWidth = document.querySelector('.chat-answer-container')!.clientWidth - 40
|
||||
const containerWidth = document.querySelector(`.${containerClassName}`)!.clientWidth - 40
|
||||
let totalWidth = 0
|
||||
for (let i = 0; i < resources.length; i++) {
|
||||
totalWidth += elesRef.current[i].clientWidth
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { Dispatch, FC, ReactNode, RefObject, SetStateAction } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { uniqueId } from 'lodash-es'
|
||||
import { File02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import PromptLogModal from '@/app/components/base/prompt-log-modal'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
@ -39,7 +40,7 @@ const Log: FC<LogProps> = ({
|
|||
children
|
||||
? children(setShowModal)
|
||||
: (
|
||||
<Tooltip selector='prompt-log-modal-trigger' content={t('common.operation.log') || ''}>
|
||||
<Tooltip selector={`prompt-log-modal-trigger-${uniqueId()}`} content={t('common.operation.log') || ''}>
|
||||
<div className={`
|
||||
hidden absolute -left-[14px] -top-[14px] group-hover:block w-7 h-7
|
||||
p-0.5 rounded-lg border-[0.5px] border-gray-100 bg-white shadow-md cursor-pointer
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import type { ModelAndParameter } from '../types'
|
||||
import {
|
||||
APP_CHAT_WITH_MULTIPLE_MODEL,
|
||||
APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
|
||||
} from '../types'
|
||||
import {
|
||||
AgentStrategy,
|
||||
ModelModeType,
|
||||
} from '@/types/app'
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
import { useChat } from '@/app/components/base/chat/chat/hooks'
|
||||
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||
import type {
|
||||
ChatConfig,
|
||||
OnSend,
|
||||
} from '@/app/components/base/chat/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import {
|
||||
fetchConvesationMessages,
|
||||
fetchSuggestedQuestions,
|
||||
stopChatMessageResponding,
|
||||
} from '@/service/debug'
|
||||
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
type ChatItemProps = {
|
||||
modelAndParameter: ModelAndParameter
|
||||
}
|
||||
const ChatItem: FC<ChatItemProps> = ({
|
||||
modelAndParameter,
|
||||
}) => {
|
||||
const { userProfile } = useAppContext()
|
||||
const {
|
||||
isAdvancedMode,
|
||||
modelConfig,
|
||||
appId,
|
||||
inputs,
|
||||
promptMode,
|
||||
speechToTextConfig,
|
||||
introduction,
|
||||
suggestedQuestions: openingSuggestedQuestions,
|
||||
suggestedQuestionsAfterAnswerConfig,
|
||||
citationConfig,
|
||||
moderationConfig,
|
||||
chatPromptConfig,
|
||||
completionPromptConfig,
|
||||
dataSets,
|
||||
datasetConfigs,
|
||||
visionConfig,
|
||||
annotationConfig,
|
||||
collectionList,
|
||||
textToSpeechConfig,
|
||||
} = useDebugConfigurationContext()
|
||||
const { textGenerationModelList } = useProviderContext()
|
||||
const postDatasets = dataSets.map(({ id }) => ({
|
||||
dataset: {
|
||||
enabled: true,
|
||||
id,
|
||||
},
|
||||
}))
|
||||
const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
|
||||
const config: ChatConfig = {
|
||||
pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
|
||||
prompt_type: promptMode,
|
||||
chat_prompt_config: isAdvancedMode ? chatPromptConfig : {},
|
||||
completion_prompt_config: isAdvancedMode ? completionPromptConfig : {},
|
||||
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
|
||||
dataset_query_variable: contextVar || '',
|
||||
opening_statement: introduction,
|
||||
more_like_this: {
|
||||
enabled: false,
|
||||
},
|
||||
suggested_questions: openingSuggestedQuestions,
|
||||
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
|
||||
text_to_speech: textToSpeechConfig,
|
||||
speech_to_text: speechToTextConfig,
|
||||
retriever_resource: citationConfig,
|
||||
sensitive_word_avoidance: moderationConfig,
|
||||
agent_mode: {
|
||||
...modelConfig.agentConfig,
|
||||
strategy: (modelAndParameter.provider === 'openai' && modelConfig.mode === ModelModeType.chat) ? AgentStrategy.functionCall : AgentStrategy.react,
|
||||
},
|
||||
dataset_configs: {
|
||||
...datasetConfigs,
|
||||
datasets: {
|
||||
datasets: [...postDatasets],
|
||||
} as any,
|
||||
},
|
||||
file_upload: {
|
||||
image: visionConfig,
|
||||
},
|
||||
annotation_reply: annotationConfig,
|
||||
}
|
||||
const {
|
||||
chatList,
|
||||
isResponsing,
|
||||
handleSend,
|
||||
suggestedQuestions,
|
||||
handleRestart,
|
||||
} = useChat(
|
||||
config,
|
||||
{
|
||||
inputs,
|
||||
promptVariables: modelConfig.configs.prompt_variables,
|
||||
},
|
||||
[],
|
||||
taskId => stopChatMessageResponding(appId, taskId),
|
||||
)
|
||||
|
||||
const doSend: OnSend = (message, files) => {
|
||||
const currentProvider = textGenerationModelList.find(item => item.provider === modelAndParameter.provider)
|
||||
const currentModel = currentProvider?.models.find(model => model.model === modelAndParameter.model)
|
||||
const supportVision = currentModel?.features?.includes(ModelFeatureEnum.vision)
|
||||
|
||||
const configData = {
|
||||
...config,
|
||||
model: {
|
||||
provider: modelAndParameter.provider,
|
||||
name: modelAndParameter.model,
|
||||
mode: currentModel?.model_properties.mode,
|
||||
completion_params: modelAndParameter.parameters,
|
||||
},
|
||||
}
|
||||
|
||||
const data: any = {
|
||||
query: message,
|
||||
inputs,
|
||||
model_config: configData,
|
||||
}
|
||||
|
||||
if (visionConfig.enabled && files?.length && supportVision)
|
||||
data.files = files
|
||||
|
||||
handleSend(
|
||||
`apps/${appId}/chat-messages`,
|
||||
data,
|
||||
{
|
||||
onGetConvesationMessages: (conversationId, getAbortController) => fetchConvesationMessages(appId, conversationId, getAbortController),
|
||||
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === APP_CHAT_WITH_MULTIPLE_MODEL)
|
||||
doSend(v.payload.message, v.payload.files)
|
||||
if (v.type === APP_CHAT_WITH_MULTIPLE_MODEL_RESTART)
|
||||
handleRestart()
|
||||
})
|
||||
|
||||
const allToolIcons = useMemo(() => {
|
||||
const icons: Record<string, any> = {}
|
||||
modelConfig.agentConfig.tools?.forEach((item: any) => {
|
||||
icons[item.tool_name] = collectionList.find((collection: any) => collection.id === item.provider_id)?.icon
|
||||
})
|
||||
return icons
|
||||
}, [collectionList, modelConfig.agentConfig.tools])
|
||||
|
||||
if (!chatList.length)
|
||||
return null
|
||||
|
||||
return (
|
||||
<Chat
|
||||
config={config}
|
||||
chatList={chatList}
|
||||
isResponsing={isResponsing}
|
||||
noChatInput
|
||||
chatContainerclassName='p-4'
|
||||
chatFooterClassName='!-bottom-4'
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
onSend={doSend}
|
||||
showPromptLog
|
||||
questionIcon={<Avatar name={userProfile.name} size={40} />}
|
||||
allToolIcons={allToolIcons}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ChatItem)
|
|
@ -0,0 +1,39 @@
|
|||
'use client'
|
||||
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
import type { ModelAndParameter } from '../types'
|
||||
|
||||
export type DebugWithMultipleModelContextType = {
|
||||
multipleModelConfigs: ModelAndParameter[]
|
||||
onMultipleModelConfigsChange: (multiple: boolean, modelConfigs: ModelAndParameter[]) => void
|
||||
onDebugWithMultipleModelChange: (singleModelConfig: ModelAndParameter) => void
|
||||
}
|
||||
const DebugWithMultipleModelContext = createContext<DebugWithMultipleModelContextType>({
|
||||
multipleModelConfigs: [],
|
||||
onMultipleModelConfigsChange: () => {},
|
||||
onDebugWithMultipleModelChange: () => {},
|
||||
})
|
||||
|
||||
export const useDebugWithMultipleModelContext = () => useContext(DebugWithMultipleModelContext)
|
||||
|
||||
type DebugWithMultipleModelContextProviderProps = {
|
||||
children: React.ReactNode
|
||||
} & DebugWithMultipleModelContextType
|
||||
export const DebugWithMultipleModelContextProvider = ({
|
||||
children,
|
||||
onMultipleModelConfigsChange,
|
||||
multipleModelConfigs,
|
||||
onDebugWithMultipleModelChange,
|
||||
}: DebugWithMultipleModelContextProviderProps) => {
|
||||
return (
|
||||
<DebugWithMultipleModelContext.Provider value={{
|
||||
onMultipleModelConfigsChange,
|
||||
multipleModelConfigs,
|
||||
onDebugWithMultipleModelChange,
|
||||
}}>
|
||||
{children}
|
||||
</DebugWithMultipleModelContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default DebugWithMultipleModelContext
|
|
@ -0,0 +1,124 @@
|
|||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { memo } from 'react'
|
||||
import type { ModelAndParameter } from '../types'
|
||||
import ModelParameterTrigger from './model-parameter-trigger'
|
||||
import ChatItem from './chat-item'
|
||||
import TextGenerationItem from './text-generation-item'
|
||||
import { useDebugWithMultipleModelContext } from './context'
|
||||
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||
import Dropdown from '@/app/components/base/dropdown'
|
||||
import type { Item } from '@/app/components/base/dropdown'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { ModelStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
type DebugItemProps = {
|
||||
modelAndParameter: ModelAndParameter
|
||||
className?: string
|
||||
}
|
||||
const DebugItem: FC<DebugItemProps> = ({
|
||||
modelAndParameter,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { mode } = useDebugConfigurationContext()
|
||||
const {
|
||||
multipleModelConfigs,
|
||||
onMultipleModelConfigsChange,
|
||||
onDebugWithMultipleModelChange,
|
||||
} = useDebugWithMultipleModelContext()
|
||||
const { textGenerationModelList } = useProviderContext()
|
||||
|
||||
const index = multipleModelConfigs.findIndex(v => v.id === modelAndParameter.id)
|
||||
const currentProvider = textGenerationModelList.find(item => item.provider === modelAndParameter.provider)
|
||||
const currentModel = currentProvider?.models.find(item => item.model === modelAndParameter.model)
|
||||
|
||||
const handleSelect = (item: Item) => {
|
||||
if (item.value === 'duplicate') {
|
||||
if (multipleModelConfigs.length >= 4)
|
||||
return
|
||||
|
||||
onMultipleModelConfigsChange(
|
||||
true,
|
||||
[
|
||||
...multipleModelConfigs.slice(0, index + 1),
|
||||
{
|
||||
...modelAndParameter,
|
||||
id: `${Date.now()}`,
|
||||
},
|
||||
...multipleModelConfigs.slice(index + 1),
|
||||
],
|
||||
)
|
||||
}
|
||||
if (item.value === 'debug-as-single-model')
|
||||
onDebugWithMultipleModelChange(modelAndParameter)
|
||||
if (item.value === 'remove') {
|
||||
onMultipleModelConfigsChange(
|
||||
true,
|
||||
multipleModelConfigs.filter(item => item.id !== modelAndParameter.id),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col min-w-[320px] rounded-xl bg-white border-[0.5px] border-black/5 ${className}`}>
|
||||
<div className='shrink-0 flex items-center justify-between h-10 px-3 border-b-[0.5px] border-b-black/5'>
|
||||
<div className='flex items-center justify-center w-6 h-5 font-medium italic text-gray-500'>
|
||||
#{index + 1}
|
||||
</div>
|
||||
<ModelParameterTrigger
|
||||
modelAndParameter={modelAndParameter}
|
||||
/>
|
||||
<Dropdown
|
||||
onSelect={handleSelect}
|
||||
items={[
|
||||
...(
|
||||
multipleModelConfigs.length <= 3
|
||||
? [
|
||||
{
|
||||
value: 'duplicate',
|
||||
text: t('appDebug.duplicateModel'),
|
||||
},
|
||||
]
|
||||
: []
|
||||
),
|
||||
...(
|
||||
(modelAndParameter.provider && modelAndParameter.model)
|
||||
? [
|
||||
{
|
||||
value: 'debug-as-single-model',
|
||||
text: t('appDebug.debugAsSingleModel'),
|
||||
},
|
||||
]
|
||||
: []
|
||||
),
|
||||
]}
|
||||
secondItems={
|
||||
multipleModelConfigs.length > 2
|
||||
? [
|
||||
{
|
||||
value: 'remove',
|
||||
text: t('common.operation.remove'),
|
||||
},
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ height: 'calc(100% - 40px)' }}>
|
||||
{
|
||||
mode === 'chat' && currentProvider && currentModel && currentModel.status === ModelStatusEnum.active && (
|
||||
<ChatItem modelAndParameter={modelAndParameter} />
|
||||
)
|
||||
}
|
||||
{
|
||||
mode === 'completion' && currentProvider && currentModel && currentModel.status === ModelStatusEnum.active && (
|
||||
<TextGenerationItem modelAndParameter={modelAndParameter}/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(DebugItem)
|
|
@ -0,0 +1,131 @@
|
|||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../types'
|
||||
import DebugItem from './debug-item'
|
||||
import {
|
||||
DebugWithMultipleModelContextProvider,
|
||||
useDebugWithMultipleModelContext,
|
||||
} from './context'
|
||||
import type { DebugWithMultipleModelContextType } from './context'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import ChatInput from '@/app/components/base/chat/chat/chat-input'
|
||||
import type { VisionFile } from '@/app/components/base/chat/types'
|
||||
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||
|
||||
const DebugWithMultipleModel = () => {
|
||||
const {
|
||||
mode,
|
||||
speechToTextConfig,
|
||||
visionConfig,
|
||||
} = useDebugConfigurationContext()
|
||||
const { multipleModelConfigs } = useDebugWithMultipleModelContext()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
const handleSend = useCallback((message: string, files?: VisionFile[]) => {
|
||||
eventEmitter?.emit({
|
||||
type: APP_CHAT_WITH_MULTIPLE_MODEL,
|
||||
payload: {
|
||||
message,
|
||||
files,
|
||||
},
|
||||
} as any)
|
||||
}, [eventEmitter])
|
||||
|
||||
const twoLine = multipleModelConfigs.length === 2
|
||||
const threeLine = multipleModelConfigs.length === 3
|
||||
const fourLine = multipleModelConfigs.length === 4
|
||||
|
||||
return (
|
||||
<div className='flex flex-col h-full'>
|
||||
<div
|
||||
className={`
|
||||
mb-3 overflow-auto
|
||||
${(twoLine || threeLine) && 'flex gap-2'}
|
||||
`}
|
||||
style={{ height: mode === 'chat' ? 'calc(100% - 60px)' : '100%' }}
|
||||
>
|
||||
{
|
||||
(twoLine || threeLine) && multipleModelConfigs.map(modelConfig => (
|
||||
<DebugItem
|
||||
key={modelConfig.id}
|
||||
modelAndParameter={modelConfig}
|
||||
className={`
|
||||
h-full min-h-[200px]
|
||||
${twoLine && 'w-1/2'}
|
||||
${threeLine && 'w-1/3'}
|
||||
`}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{
|
||||
fourLine && (
|
||||
<>
|
||||
<div
|
||||
className='flex space-x-2 mb-2 min-h-[200px]'
|
||||
style={{ height: 'calc(50% - 4px)' }}
|
||||
>
|
||||
{
|
||||
multipleModelConfigs.slice(0, 2).map(modelConfig => (
|
||||
<DebugItem
|
||||
key={modelConfig.id}
|
||||
modelAndParameter={modelConfig}
|
||||
className='w-1/2 h-full'
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
className='flex space-x-2 min-h-[200px]'
|
||||
style={{ height: 'calc(50% - 4px)' }}
|
||||
>
|
||||
{
|
||||
multipleModelConfigs.slice(2, 4).map(modelConfig => (
|
||||
<DebugItem
|
||||
key={modelConfig.id}
|
||||
modelAndParameter={modelConfig}
|
||||
className='w-1/2 h-full'
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
mode === 'chat' && (
|
||||
<div className='shrink-0'>
|
||||
<ChatInput
|
||||
onSend={handleSend}
|
||||
speechToTextConfig={speechToTextConfig}
|
||||
visionConfig={visionConfig}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DebugWithMultipleModelMemoed = memo(DebugWithMultipleModel)
|
||||
|
||||
const DebugWithMultipleModelWrapper: FC<DebugWithMultipleModelContextType> = ({
|
||||
onMultipleModelConfigsChange,
|
||||
multipleModelConfigs,
|
||||
onDebugWithMultipleModelChange,
|
||||
}) => {
|
||||
return (
|
||||
<DebugWithMultipleModelContextProvider
|
||||
onMultipleModelConfigsChange={onMultipleModelConfigsChange}
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onDebugWithMultipleModelChange={onDebugWithMultipleModelChange}
|
||||
>
|
||||
<DebugWithMultipleModelMemoed />
|
||||
</DebugWithMultipleModelContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(DebugWithMultipleModelWrapper)
|
|
@ -0,0 +1,125 @@
|
|||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ModelAndParameter } from '../types'
|
||||
import { useDebugWithMultipleModelContext } from './context'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
|
||||
import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name'
|
||||
import {
|
||||
MODEL_STATUS_TEXT,
|
||||
ModelStatusEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
|
||||
type ModelParameterTriggerProps = {
|
||||
modelAndParameter: ModelAndParameter
|
||||
}
|
||||
const ModelParameterTrigger: FC<ModelParameterTriggerProps> = ({
|
||||
modelAndParameter,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
mode,
|
||||
isAdvancedMode,
|
||||
} = useDebugConfigurationContext()
|
||||
const {
|
||||
multipleModelConfigs,
|
||||
onMultipleModelConfigsChange,
|
||||
onDebugWithMultipleModelChange,
|
||||
} = useDebugWithMultipleModelContext()
|
||||
const language = useLanguage()
|
||||
const index = multipleModelConfigs.findIndex(v => v.id === modelAndParameter.id)
|
||||
|
||||
const handleSelectModel = ({ modelId, provider }: { modelId: string; provider: string }) => {
|
||||
const newModelConfigs = [...multipleModelConfigs]
|
||||
newModelConfigs[index] = {
|
||||
...newModelConfigs[index],
|
||||
model: modelId,
|
||||
provider,
|
||||
}
|
||||
onMultipleModelConfigsChange(true, newModelConfigs)
|
||||
}
|
||||
const handleParamsChange = (params: any) => {
|
||||
const newModelConfigs = [...multipleModelConfigs]
|
||||
newModelConfigs[index] = {
|
||||
...newModelConfigs[index],
|
||||
parameters: params,
|
||||
}
|
||||
onMultipleModelConfigsChange(true, newModelConfigs)
|
||||
}
|
||||
|
||||
return (
|
||||
<ModelParameterModal
|
||||
mode={mode}
|
||||
isAdvancedMode={isAdvancedMode}
|
||||
provider={modelAndParameter.provider}
|
||||
modelId={modelAndParameter.model}
|
||||
completionParams={modelAndParameter.parameters}
|
||||
onCompletionParamsChange={handleParamsChange}
|
||||
setModel={handleSelectModel}
|
||||
debugWithMultipleModel
|
||||
onDebugWithMultipleModelChange={() => onDebugWithMultipleModelChange(modelAndParameter)}
|
||||
renderTrigger={({
|
||||
open,
|
||||
currentProvider,
|
||||
currentModel,
|
||||
}) => (
|
||||
<div
|
||||
className={`
|
||||
flex items-center max-w-[200px] h-8 px-2 rounded-lg cursor-pointer
|
||||
${open && 'bg-gray-100'}
|
||||
${currentModel && currentModel.status !== ModelStatusEnum.active && '!bg-[#FFFAEB]'}
|
||||
`}
|
||||
>
|
||||
{
|
||||
currentProvider && (
|
||||
<ModelIcon
|
||||
className='mr-1 !w-4 !h-4'
|
||||
provider={currentProvider}
|
||||
modelName={currentModel?.model}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!currentProvider && (
|
||||
<div className='flex items-center justify-center mr-1 w-4 h-4 rounded border border-dashed border-primary-100'>
|
||||
<CubeOutline className='w-[11px] h-[11px] text-primary-600' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
currentModel && (
|
||||
<ModelName
|
||||
className='mr-0.5 text-gray-800'
|
||||
modelItem={currentModel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!currentModel && (
|
||||
<div className='mr-0.5 text-[13px] font-medium text-primary-600 truncate'>
|
||||
{t('common.modelProvider.selectModel')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<ChevronDown className={`w-3 h-3 ${(currentModel && currentProvider) ? 'text-gray-800' : 'text-primary-600'}`} />
|
||||
{
|
||||
currentModel && currentModel.status !== ModelStatusEnum.active && (
|
||||
<TooltipPlus popupContent={MODEL_STATUS_TEXT[currentModel.status][language]}>
|
||||
<AlertTriangle className='w-4 h-4 text-[#F79009]' />
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ModelParameterTrigger)
|
|
@ -0,0 +1,103 @@
|
|||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ModelAndParameter } from '../types'
|
||||
import Button from '@/app/components/base/button'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import type { ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
|
||||
type PublishWithMultipleModelProps = {
|
||||
multipleModelConfigs: ModelAndParameter[]
|
||||
onSelect: (v: ModelAndParameter) => void
|
||||
}
|
||||
const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
|
||||
multipleModelConfigs,
|
||||
onSelect,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const { textGenerationModelList } = useProviderContext()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const validModelConfigs: (ModelAndParameter & { modelItem: ModelItem })[] = []
|
||||
|
||||
multipleModelConfigs.forEach((item) => {
|
||||
const provider = textGenerationModelList.find(model => model.provider === item.provider)
|
||||
|
||||
if (provider) {
|
||||
const model = provider.models.find(model => model.model === item.model)
|
||||
|
||||
if (model) {
|
||||
validModelConfigs.push({
|
||||
id: item.id,
|
||||
model: item.model,
|
||||
provider: item.provider,
|
||||
modelItem: model,
|
||||
parameters: item.parameters,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleToggle = () => {
|
||||
if (validModelConfigs.length)
|
||||
setOpen(v => !v)
|
||||
}
|
||||
|
||||
const handleSelect = (item: ModelAndParameter) => {
|
||||
onSelect(item)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={handleToggle}>
|
||||
<Button
|
||||
type='primary'
|
||||
disabled={!validModelConfigs.length}
|
||||
className='pl-3 pr-2 h-8 text-[13px]'
|
||||
>
|
||||
{t('appDebug.operation.applyConfig')}
|
||||
<ChevronDown className='ml-0.5 w-3 h-3' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent>
|
||||
<div className='p-1 w-[168px] rounded-lg border-[0.5px] border-gray-200 shadow-lg bg-white'>
|
||||
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-gray-500'>
|
||||
{t('appDebug.publishAs')}
|
||||
</div>
|
||||
{
|
||||
validModelConfigs.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className='flex items-center px-3 h-8 rounded-lg hover:bg-gray-100 cursor-pointer text-sm text-gray-500'
|
||||
onClick={() => handleSelect(item)}
|
||||
>
|
||||
#{index + 1}
|
||||
<div
|
||||
className='ml-1 text-gray-700 truncate'
|
||||
title={item.modelItem.label[language]}
|
||||
>
|
||||
{item.modelItem.label[language]}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default PublishWithMultipleModel
|
|
@ -0,0 +1,153 @@
|
|||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import type { ModelAndParameter } from '../types'
|
||||
import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../types'
|
||||
import type {
|
||||
OnSend,
|
||||
TextGenerationConfig,
|
||||
} from '@/app/components/base/text-generation/types'
|
||||
import { useTextGeneration } from '@/app/components/base/text-generation/hooks'
|
||||
import TextGeneration from '@/app/components/app/text-generate/item'
|
||||
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
|
||||
import { TransferMethod } from '@/app/components/base/chat/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
type TextGenerationItemProps = {
|
||||
modelAndParameter: ModelAndParameter
|
||||
}
|
||||
const TextGenerationItem: FC<TextGenerationItemProps> = ({
|
||||
modelAndParameter,
|
||||
}) => {
|
||||
const {
|
||||
isAdvancedMode,
|
||||
modelConfig,
|
||||
appId,
|
||||
inputs,
|
||||
promptMode,
|
||||
speechToTextConfig,
|
||||
introduction,
|
||||
suggestedQuestionsAfterAnswerConfig,
|
||||
citationConfig,
|
||||
moderationConfig,
|
||||
externalDataToolsConfig,
|
||||
chatPromptConfig,
|
||||
completionPromptConfig,
|
||||
dataSets,
|
||||
datasetConfigs,
|
||||
visionConfig,
|
||||
moreLikeThisConfig,
|
||||
} = useDebugConfigurationContext()
|
||||
const { textGenerationModelList } = useProviderContext()
|
||||
const postDatasets = dataSets.map(({ id }) => ({
|
||||
dataset: {
|
||||
enabled: true,
|
||||
id,
|
||||
},
|
||||
}))
|
||||
const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
|
||||
const config: TextGenerationConfig = {
|
||||
pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
|
||||
prompt_type: promptMode,
|
||||
chat_prompt_config: isAdvancedMode ? chatPromptConfig : {},
|
||||
completion_prompt_config: isAdvancedMode ? completionPromptConfig : {},
|
||||
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
|
||||
dataset_query_variable: contextVar || '',
|
||||
opening_statement: introduction,
|
||||
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
|
||||
speech_to_text: speechToTextConfig,
|
||||
retriever_resource: citationConfig,
|
||||
sensitive_word_avoidance: moderationConfig,
|
||||
external_data_tools: externalDataToolsConfig,
|
||||
more_like_this: moreLikeThisConfig,
|
||||
agent_mode: {
|
||||
enabled: false,
|
||||
tools: [],
|
||||
},
|
||||
dataset_configs: {
|
||||
...datasetConfigs,
|
||||
datasets: {
|
||||
datasets: [...postDatasets],
|
||||
} as any,
|
||||
},
|
||||
file_upload: {
|
||||
image: visionConfig,
|
||||
},
|
||||
}
|
||||
const {
|
||||
completion,
|
||||
handleSend,
|
||||
isResponsing,
|
||||
messageId,
|
||||
} = useTextGeneration()
|
||||
|
||||
const doSend: OnSend = (message, files) => {
|
||||
const currentProvider = textGenerationModelList.find(item => item.provider === modelAndParameter.provider)
|
||||
const currentModel = currentProvider?.models.find(model => model.model === modelAndParameter.model)
|
||||
|
||||
const configData = {
|
||||
...config,
|
||||
model: {
|
||||
provider: modelAndParameter.provider,
|
||||
name: modelAndParameter.model,
|
||||
mode: currentModel?.model_properties.mode,
|
||||
completion_params: modelAndParameter.parameters,
|
||||
},
|
||||
}
|
||||
|
||||
const data: any = {
|
||||
inputs,
|
||||
model_config: configData,
|
||||
}
|
||||
|
||||
if (visionConfig.enabled && files && files?.length > 0) {
|
||||
data.files = files.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
...item,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
handleSend(
|
||||
`apps/${appId}/completion-messages`,
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === APP_CHAT_WITH_MULTIPLE_MODEL)
|
||||
doSend(v.payload.message, v.payload.files)
|
||||
})
|
||||
|
||||
const varList = modelConfig.configs.prompt_variables.map((item: any) => {
|
||||
return {
|
||||
label: item.key,
|
||||
value: inputs[item.key],
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<TextGeneration
|
||||
className='flex flex-col h-full overflow-y-auto border-none'
|
||||
innerClassName='grow flex flex-col'
|
||||
contentClassName='grow'
|
||||
content={completion}
|
||||
isLoading={!completion && isResponsing}
|
||||
isResponsing={isResponsing}
|
||||
isInstalledApp={false}
|
||||
messageId={messageId}
|
||||
isError={false}
|
||||
onRetry={() => { }}
|
||||
appId={appId}
|
||||
varList={varList}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(TextGenerationItem)
|
54
web/app/components/app/configuration/debug/hooks.tsx
Normal file
54
web/app/components/app/configuration/debug/hooks.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type {
|
||||
DebugWithSingleOrMultipleModelConfigs,
|
||||
ModelAndParameter,
|
||||
} from './types'
|
||||
|
||||
export const useDebugWithSingleOrMultipleModel = (appId: string) => {
|
||||
const localeDebugWithSingleOrMultipleModelConfigs = localStorage.getItem('app-debug-with-single-or-multiple-models')
|
||||
|
||||
const debugWithSingleOrMultipleModelConfigs = useRef<DebugWithSingleOrMultipleModelConfigs>({})
|
||||
|
||||
if (localeDebugWithSingleOrMultipleModelConfigs) {
|
||||
try {
|
||||
debugWithSingleOrMultipleModelConfigs.current = JSON.parse(localeDebugWithSingleOrMultipleModelConfigs) || {}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const [
|
||||
debugWithMultipleModel,
|
||||
setDebugWithMultipleModel,
|
||||
] = useState(debugWithSingleOrMultipleModelConfigs.current[appId]?.multiple || false)
|
||||
|
||||
const [
|
||||
multipleModelConfigs,
|
||||
setMultipleModelConfigs,
|
||||
] = useState(debugWithSingleOrMultipleModelConfigs.current[appId]?.configs || [])
|
||||
|
||||
const handleMultipleModelConfigsChange = useCallback((
|
||||
multiple: boolean,
|
||||
modelConfigs: ModelAndParameter[],
|
||||
) => {
|
||||
const value = {
|
||||
multiple,
|
||||
configs: modelConfigs,
|
||||
}
|
||||
debugWithSingleOrMultipleModelConfigs.current[appId] = value
|
||||
localStorage.setItem('app-debug-with-single-or-multiple-models', JSON.stringify(debugWithSingleOrMultipleModelConfigs.current))
|
||||
setDebugWithMultipleModel(value.multiple)
|
||||
setMultipleModelConfigs(value.configs)
|
||||
}, [appId])
|
||||
|
||||
return {
|
||||
debugWithMultipleModel,
|
||||
multipleModelConfigs,
|
||||
handleMultipleModelConfigsChange,
|
||||
}
|
||||
}
|
|
@ -12,6 +12,12 @@ import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
|
|||
import FormattingChanged from '../base/warning-mask/formatting-changed'
|
||||
import GroupName from '../base/group-name'
|
||||
import CannotQueryDataset from '../base/warning-mask/cannot-query-dataset'
|
||||
import DebugWithMultipleModel from './debug-with-multiple-model'
|
||||
import type { ModelAndParameter } from './types'
|
||||
import {
|
||||
APP_CHAT_WITH_MULTIPLE_MODEL,
|
||||
APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
|
||||
} from './types'
|
||||
import { AgentStrategy, AppType, ModelModeType, TransferMethod } from '@/types/app'
|
||||
import PromptValuePanel, { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
|
||||
import type { IChatItem } from '@/app/components/app/chat/type'
|
||||
|
@ -28,17 +34,30 @@ import type { Inputs } from '@/models/debug'
|
|||
import { fetchFileUploadConfig } from '@/service/common'
|
||||
import type { Annotation as AnnotationType } from '@/models/log'
|
||||
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { ModelParameterModalProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
type IDebug = {
|
||||
hasSetAPIKEY: boolean
|
||||
onSetting: () => void
|
||||
inputs: Inputs
|
||||
modelParameterParams: Pick<ModelParameterModalProps, 'setModel' | 'onCompletionParamsChange'>
|
||||
debugWithMultipleModel: boolean
|
||||
multipleModelConfigs: ModelAndParameter[]
|
||||
onMultipleModelConfigsChange: (multiple: boolean, modelConfigs: ModelAndParameter[]) => void
|
||||
}
|
||||
|
||||
const Debug: FC<IDebug> = ({
|
||||
hasSetAPIKEY = true,
|
||||
onSetting,
|
||||
inputs,
|
||||
modelParameterParams,
|
||||
debugWithMultipleModel,
|
||||
multipleModelConfigs,
|
||||
onMultipleModelConfigsChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
|
@ -72,7 +91,9 @@ const Debug: FC<IDebug> = ({
|
|||
datasetConfigs,
|
||||
visionConfig,
|
||||
annotationConfig,
|
||||
setVisionConfig,
|
||||
} = useContext(ConfigContext)
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const { data: speech2textDefaultModel } = useDefaultModel(4)
|
||||
const { data: text2speechDefaultModel } = useDefaultModel(5)
|
||||
const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
|
||||
|
@ -119,7 +140,7 @@ const Debug: FC<IDebug> = ({
|
|||
setFormattingChanged(false)
|
||||
}, [formattingChanged])
|
||||
|
||||
const clearConversation = async () => {
|
||||
const handleClearConversation = () => {
|
||||
setConversationId(null)
|
||||
abortController?.abort()
|
||||
setResponsingFalse()
|
||||
|
@ -134,6 +155,16 @@ const Debug: FC<IDebug> = ({
|
|||
: [])
|
||||
setIsShowSuggestion(false)
|
||||
}
|
||||
const clearConversation = async () => {
|
||||
if (debugWithMultipleModel) {
|
||||
eventEmitter?.emit({
|
||||
type: APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
|
||||
} as any)
|
||||
return
|
||||
}
|
||||
|
||||
handleClearConversation()
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
clearConversation()
|
||||
|
@ -601,6 +632,21 @@ const Debug: FC<IDebug> = ({
|
|||
})
|
||||
}
|
||||
|
||||
const handleSendTextCompletion = () => {
|
||||
if (debugWithMultipleModel) {
|
||||
eventEmitter?.emit({
|
||||
type: APP_CHAT_WITH_MULTIPLE_MODEL,
|
||||
payload: {
|
||||
message: '',
|
||||
files: completionFiles,
|
||||
},
|
||||
} as any)
|
||||
return
|
||||
}
|
||||
|
||||
sendTextCompletion()
|
||||
}
|
||||
|
||||
const varList = modelConfig.configs.prompt_variables.map((item: any) => {
|
||||
return {
|
||||
label: item.key,
|
||||
|
@ -608,6 +654,51 @@ const Debug: FC<IDebug> = ({
|
|||
}
|
||||
})
|
||||
|
||||
const { textGenerationModelList } = useProviderContext()
|
||||
const handleChangeToSingleModel = (item: ModelAndParameter) => {
|
||||
const currentProvider = textGenerationModelList.find(modelItem => modelItem.provider === item.provider)
|
||||
const currentModel = currentProvider?.models.find(model => model.model === item.model)
|
||||
|
||||
modelParameterParams.setModel({
|
||||
modelId: item.model,
|
||||
provider: item.provider,
|
||||
mode: currentModel?.model_properties.mode as string,
|
||||
features: currentModel?.features,
|
||||
})
|
||||
modelParameterParams.onCompletionParamsChange(item.parameters)
|
||||
onMultipleModelConfigsChange(
|
||||
false,
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
const handleVisionConfigInMultipleModel = () => {
|
||||
if (debugWithMultipleModel && !visionConfig.enabled) {
|
||||
const supportedVision = multipleModelConfigs.some((modelConfig) => {
|
||||
const currentProvider = textGenerationModelList.find(modelItem => modelItem.provider === modelConfig.provider)
|
||||
const currentModel = currentProvider?.models.find(model => model.model === modelConfig.model)
|
||||
|
||||
return currentModel?.features?.includes(ModelFeatureEnum.vision)
|
||||
})
|
||||
|
||||
if (supportedVision) {
|
||||
setVisionConfig({
|
||||
...visionConfig,
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
else {
|
||||
setVisionConfig({
|
||||
...visionConfig,
|
||||
enabled: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleVisionConfigInMultipleModel()
|
||||
}, [multipleModelConfigs])
|
||||
const allToolIcons = (() => {
|
||||
const icons: Record<string, any> = {}
|
||||
modelConfig.agentConfig.tools?.forEach((item: any) => {
|
||||
|
@ -621,18 +712,40 @@ const Debug: FC<IDebug> = ({
|
|||
<div className="shrink-0">
|
||||
<div className='flex items-center justify-between mb-2'>
|
||||
<div className='h2 '>{t('appDebug.inputs.title')}</div>
|
||||
{mode === 'chat' && (
|
||||
<Button className='flex items-center gap-1 !h-8 !bg-white' onClick={clearConversation}>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.66663 2.66629V5.99963H3.05463M3.05463 5.99963C3.49719 4.90505 4.29041 3.98823 5.30998 3.39287C6.32954 2.7975 7.51783 2.55724 8.68861 2.70972C9.85938 2.8622 10.9465 3.39882 11.7795 4.23548C12.6126 5.07213 13.1445 6.16154 13.292 7.33296M3.05463 5.99963H5.99996M13.3333 13.333V9.99963H12.946M12.946 9.99963C12.5028 11.0936 11.7093 12.0097 10.6898 12.6045C9.67038 13.1993 8.48245 13.4393 7.31203 13.2869C6.1416 13.1344 5.05476 12.5982 4.22165 11.7621C3.38854 10.926 2.8562 9.83726 2.70796 8.66629M12.946 9.99963H9.99996" stroke="#1C64F2" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<span className='text-primary-600 text-[13px] font-semibold'>{t('common.operation.refresh')}</span>
|
||||
</Button>
|
||||
)}
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
debugWithMultipleModel
|
||||
? (
|
||||
<>
|
||||
<Button
|
||||
className={`
|
||||
h-8 px-2.5 text-[13px] font-medium text-primary-600 bg-white
|
||||
${multipleModelConfigs.length >= 4 && 'opacity-30'}
|
||||
`}
|
||||
onClick={() => onMultipleModelConfigsChange(true, [...multipleModelConfigs, { id: `${Date.now()}`, model: '', provider: '', parameters: {} }])}
|
||||
disabled={multipleModelConfigs.length >= 4}
|
||||
>
|
||||
<Plus className='mr-1 w-3.5 h-3.5' />
|
||||
{t('common.modelProvider.addModel')}({multipleModelConfigs.length}/4)
|
||||
</Button>
|
||||
<div className='mx-2 w-[1px] h-[14px] bg-gray-200' />
|
||||
</>
|
||||
)
|
||||
: null
|
||||
}
|
||||
{mode === 'chat' && (
|
||||
<Button className='flex items-center gap-1 !h-8 !bg-white' onClick={clearConversation}>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.66663 2.66629V5.99963H3.05463M3.05463 5.99963C3.49719 4.90505 4.29041 3.98823 5.30998 3.39287C6.32954 2.7975 7.51783 2.55724 8.68861 2.70972C9.85938 2.8622 10.9465 3.39882 11.7795 4.23548C12.6126 5.07213 13.1445 6.16154 13.292 7.33296M3.05463 5.99963H5.99996M13.3333 13.333V9.99963H12.946M12.946 9.99963C12.5028 11.0936 11.7093 12.0097 10.6898 12.6045C9.67038 13.1993 8.48245 13.4393 7.31203 13.2869C6.1416 13.1344 5.05476 12.5982 4.22165 11.7621C3.38854 10.926 2.8562 9.83726 2.70796 8.66629M12.946 9.99963H9.99996" stroke="#1C64F2" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<span className='text-primary-600 text-[13px] font-semibold'>{t('common.operation.refresh')}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<PromptValuePanel
|
||||
appType={mode as AppType}
|
||||
onSend={sendTextCompletion}
|
||||
onSend={handleSendTextCompletion}
|
||||
inputs={inputs}
|
||||
visionConfig={{
|
||||
...visionConfig,
|
||||
|
@ -641,81 +754,96 @@ const Debug: FC<IDebug> = ({
|
|||
onVisionFilesChange={setCompletionFiles}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col grow">
|
||||
{/* Chat */}
|
||||
{mode === AppType.chat && (
|
||||
<div className="mt-[34px] h-full flex flex-col">
|
||||
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[76px]'), 'relative mt-1.5 grow h-[200px] overflow-hidden')}>
|
||||
<div className="h-full overflow-y-auto overflow-x-hidden" ref={chatListDomRef}>
|
||||
<Chat
|
||||
chatList={chatList}
|
||||
query={userQuery}
|
||||
onQueryChange={setUserQuery}
|
||||
onSend={onSend}
|
||||
checkCanSend={checkCanSend}
|
||||
feedbackDisabled
|
||||
useCurrentUserAvatar
|
||||
isResponsing={isResponsing}
|
||||
canStopResponsing={!!messageTaskId}
|
||||
abortResponsing={async () => {
|
||||
await stopChatMessageResponding(appId, messageTaskId)
|
||||
setHasStopResponded(true)
|
||||
setResponsingFalse()
|
||||
}}
|
||||
isShowSuggestion={doShowSuggestion}
|
||||
suggestionList={suggestQuestions}
|
||||
isShowSpeechToText={speechToTextConfig.enabled && !!speech2textDefaultModel}
|
||||
isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
|
||||
isShowCitation={citationConfig.enabled}
|
||||
isShowCitationHitInfo
|
||||
isShowPromptLog
|
||||
visionConfig={{
|
||||
...visionConfig,
|
||||
image_file_size_limit: fileUploadConfigResponse?.image_file_size_limit,
|
||||
}}
|
||||
supportAnnotation
|
||||
appId={appId}
|
||||
onChatListChange={setChatList}
|
||||
allToolIcons={allToolIcons}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
debugWithMultipleModel && (
|
||||
<div className='grow mt-3 overflow-hidden'>
|
||||
<DebugWithMultipleModel
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onMultipleModelConfigsChange={onMultipleModelConfigsChange}
|
||||
onDebugWithMultipleModelChange={handleChangeToSingleModel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Text Generation */}
|
||||
{mode === AppType.completion && (
|
||||
<div className="mt-6">
|
||||
<GroupName name={t('appDebug.result')} />
|
||||
{(completionRes || isResponsing) && (
|
||||
<TextGeneration
|
||||
className="mt-2"
|
||||
content={completionRes}
|
||||
isLoading={!completionRes && isResponsing}
|
||||
isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
|
||||
isResponsing={isResponsing}
|
||||
isInstalledApp={false}
|
||||
messageId={messageId}
|
||||
isError={false}
|
||||
onRetry={() => { }}
|
||||
supportAnnotation
|
||||
appId={appId}
|
||||
varList={varList}
|
||||
)
|
||||
}
|
||||
{
|
||||
!debugWithMultipleModel && (
|
||||
<div className="flex flex-col grow">
|
||||
{/* Chat */}
|
||||
{mode === AppType.chat && (
|
||||
<div className="mt-[34px] h-full flex flex-col">
|
||||
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[76px]'), 'relative mt-1.5 grow h-[200px] overflow-hidden')}>
|
||||
<div className="h-full overflow-y-auto overflow-x-hidden" ref={chatListDomRef}>
|
||||
<Chat
|
||||
chatList={chatList}
|
||||
query={userQuery}
|
||||
onQueryChange={setUserQuery}
|
||||
onSend={onSend}
|
||||
checkCanSend={checkCanSend}
|
||||
feedbackDisabled
|
||||
useCurrentUserAvatar
|
||||
isResponsing={isResponsing}
|
||||
canStopResponsing={!!messageTaskId}
|
||||
abortResponsing={async () => {
|
||||
await stopChatMessageResponding(appId, messageTaskId)
|
||||
setHasStopResponded(true)
|
||||
setResponsingFalse()
|
||||
}}
|
||||
isShowSuggestion={doShowSuggestion}
|
||||
suggestionList={suggestQuestions}
|
||||
isShowSpeechToText={speechToTextConfig.enabled && !!speech2textDefaultModel}
|
||||
isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
|
||||
isShowCitation={citationConfig.enabled}
|
||||
isShowCitationHitInfo
|
||||
isShowPromptLog
|
||||
visionConfig={{
|
||||
...visionConfig,
|
||||
image_file_size_limit: fileUploadConfigResponse?.image_file_size_limit,
|
||||
}}
|
||||
supportAnnotation
|
||||
appId={appId}
|
||||
onChatListChange={setChatList}
|
||||
allToolIcons={allToolIcons}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Text Generation */}
|
||||
{mode === AppType.completion && (
|
||||
<div className="mt-6">
|
||||
<GroupName name={t('appDebug.result')} />
|
||||
{(completionRes || isResponsing) && (
|
||||
<TextGeneration
|
||||
className="mt-2"
|
||||
content={completionRes}
|
||||
isLoading={!completionRes && isResponsing}
|
||||
isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
|
||||
isResponsing={isResponsing}
|
||||
isInstalledApp={false}
|
||||
messageId={messageId}
|
||||
isError={false}
|
||||
onRetry={() => { }}
|
||||
supportAnnotation
|
||||
appId={appId}
|
||||
varList={varList}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isShowFormattingChangeConfirm && (
|
||||
<FormattingChanged
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
{isShowCannotQueryDataset && (
|
||||
<CannotQueryDataset
|
||||
onConfirm={() => setShowCannotQueryDataset(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isShowFormattingChangeConfirm && (
|
||||
<FormattingChanged
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
{isShowCannotQueryDataset && (
|
||||
<CannotQueryDataset
|
||||
onConfirm={() => setShowCannotQueryDataset(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{!hasSetAPIKEY && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
|
||||
</>
|
||||
)
|
||||
|
|
18
web/app/components/app/configuration/debug/types.ts
Normal file
18
web/app/components/app/configuration/debug/types.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
export type ModelAndParameter = {
|
||||
id: string
|
||||
model: string
|
||||
provider: string
|
||||
parameters: Record<string, any>
|
||||
}
|
||||
|
||||
export type MultipleAndConfigs = {
|
||||
multiple: boolean
|
||||
configs: ModelAndParameter[]
|
||||
}
|
||||
|
||||
export type DebugWithSingleOrMultipleModelConfigs = {
|
||||
[k: string]: MultipleAndConfigs
|
||||
}
|
||||
export const APP_CHAT_WITH_MULTIPLE_MODEL = 'APP_CHAT_WITH_MULTIPLE_MODEL'
|
||||
export const APP_CHAT_WITH_MULTIPLE_MODEL_RESTART = 'APP_CHAT_WITH_MULTIPLE_MODEL_RESTART'
|
||||
export const APP_SIDEBAR_SHOULD_COLLAPSE = 'APP_SIDEBAR_SHOULD_COLLAPSE'
|
|
@ -13,6 +13,10 @@ import Button from '../../base/button'
|
|||
import Loading from '../../base/loading'
|
||||
import useAdvancedPromptConfig from './hooks/use-advanced-prompt-config'
|
||||
import EditHistoryModal from './config-prompt/conversation-histroy/edit-modal'
|
||||
import { useDebugWithSingleOrMultipleModel } from './debug/hooks'
|
||||
import type { ModelAndParameter } from './debug/types'
|
||||
import { APP_SIDEBAR_SHOULD_COLLAPSE } from './debug/types'
|
||||
import PublishWithMultipleModel from './debug/debug-with-multiple-model/publish-with-multiple-model'
|
||||
import AssistantTypePicker from './config/assistant-type-picker'
|
||||
import type {
|
||||
AnnotationReplyConfig,
|
||||
|
@ -50,6 +54,7 @@ import type { FormValue } from '@/app/components/header/account-setting/model-pr
|
|||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { fetchCollectionList } from '@/service/tools'
|
||||
import { type Collection } from '@/app/components/tools/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
type PublichConfig = {
|
||||
modelConfig: ModelConfig
|
||||
|
@ -524,8 +529,8 @@ const Configuration: FC = () => {
|
|||
else { return promptEmpty }
|
||||
})()
|
||||
const contextVarEmpty = mode === AppType.completion && dataSets.length > 0 && !hasSetContextVar
|
||||
const handlePublish = async (isSilence?: boolean) => {
|
||||
const modelId = modelConfig.model_id
|
||||
const handlePublish = async (isSilence?: boolean, modelAndParameter?: ModelAndParameter) => {
|
||||
const modelId = modelAndParameter?.model || modelConfig.model_id
|
||||
const promptTemplate = modelConfig.configs.prompt_template
|
||||
const promptVariables = modelConfig.configs.prompt_variables
|
||||
|
||||
|
@ -578,10 +583,10 @@ const Configuration: FC = () => {
|
|||
strategy: isFunctionCall ? AgentStrategy.functionCall : AgentStrategy.react,
|
||||
},
|
||||
model: {
|
||||
provider: modelConfig.provider,
|
||||
provider: modelAndParameter?.provider || modelConfig.provider,
|
||||
name: modelId,
|
||||
mode: modelConfig.mode,
|
||||
completion_params: completionParams as any,
|
||||
completion_params: modelAndParameter?.parameters || completionParams as any,
|
||||
},
|
||||
dataset_configs: {
|
||||
...datasetConfigs,
|
||||
|
@ -629,6 +634,26 @@ const Configuration: FC = () => {
|
|||
const [showUseGPT4Confirm, setShowUseGPT4Confirm] = useState(false)
|
||||
const { locale } = useContext(I18n)
|
||||
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const {
|
||||
debugWithMultipleModel,
|
||||
multipleModelConfigs,
|
||||
handleMultipleModelConfigsChange,
|
||||
} = useDebugWithSingleOrMultipleModel(appId)
|
||||
|
||||
const handleDebugWithMultipleModelChange = () => {
|
||||
handleMultipleModelConfigsChange(
|
||||
true,
|
||||
[
|
||||
{ id: `${Date.now()}`, model: modelConfig.model_id, provider: modelConfig.provider, parameters: completionParams },
|
||||
{ id: `${Date.now()}-no-repeat`, model: '', provider: '', parameters: {} },
|
||||
],
|
||||
)
|
||||
eventEmitter?.emit({
|
||||
type: APP_SIDEBAR_SHOULD_COLLAPSE,
|
||||
} as any)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <div className='flex h-full items-center justify-center'>
|
||||
<Loading type='area' />
|
||||
|
@ -709,7 +734,7 @@ const Configuration: FC = () => {
|
|||
<>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className='flex grow h-[200px]'>
|
||||
<div className="w-full sm:w-1/2 shrink-0 flex flex-col h-full">
|
||||
<div className={`w-full sm:w-1/2 shrink-0 flex flex-col h-full ${debugWithMultipleModel && 'max-w-[560px]'}`}>
|
||||
{/* Header Left */}
|
||||
<div className='flex justify-between items-center px-6 h-14'>
|
||||
<div className='flex items-center'>
|
||||
|
@ -743,22 +768,30 @@ const Configuration: FC = () => {
|
|||
</div>
|
||||
<Config />
|
||||
</div>
|
||||
{!isMobile && <div className="relative w-1/2 h-full overflow-y-auto flex flex-col " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
|
||||
{!isMobile && <div className="grow relative w-1/2 h-full overflow-y-auto flex flex-col " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
|
||||
{/* Header Right */}
|
||||
<div className='flex justify-end items-center flex-wrap px-6 h-14 space-x-2'>
|
||||
{/* Model and Parameters */}
|
||||
<ModelParameterModal
|
||||
isAdvancedMode={isAdvancedMode}
|
||||
mode={mode}
|
||||
provider={modelConfig.provider}
|
||||
completionParams={completionParams}
|
||||
modelId={modelConfig.model_id}
|
||||
setModel={setModel as any}
|
||||
onCompletionParamsChange={(newParams: FormValue) => {
|
||||
setCompletionParams(newParams)
|
||||
}}
|
||||
/>
|
||||
<div className='w-[1px] h-[14px] bg-gray-200'></div>
|
||||
{
|
||||
!debugWithMultipleModel && (
|
||||
<>
|
||||
<ModelParameterModal
|
||||
isAdvancedMode={isAdvancedMode}
|
||||
mode={mode}
|
||||
provider={modelConfig.provider}
|
||||
completionParams={completionParams}
|
||||
modelId={modelConfig.model_id}
|
||||
setModel={setModel as any}
|
||||
onCompletionParamsChange={(newParams: FormValue) => {
|
||||
setCompletionParams(newParams)
|
||||
}}
|
||||
debugWithMultipleModel={debugWithMultipleModel}
|
||||
onDebugWithMultipleModelChange={handleDebugWithMultipleModelChange}
|
||||
/>
|
||||
<div className='w-[1px] h-[14px] bg-gray-200'></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Button onClick={() => setShowConfirm(true)} className='shrink-0 mr-2 w-[70px] !h-8 !text-[13px] font-medium'>{t('appDebug.operation.resetConfig')}</Button>
|
||||
{isMobile && (
|
||||
<Button className='!h-8 !text-[13px] font-medium' onClick={showDebugPanel}>
|
||||
|
@ -766,13 +799,37 @@ const Configuration: FC = () => {
|
|||
<CodeBracketIcon className="h-4 w-4 text-gray-500" />
|
||||
</Button>
|
||||
)}
|
||||
<Button type='primary' onClick={() => handlePublish(false)} className={cn(cannotPublish && '!bg-primary-200 !cursor-not-allowed', 'shrink-0 w-[70px] !h-8 !text-[13px] font-medium')}>{t('appDebug.operation.applyConfig')}</Button>
|
||||
{
|
||||
debugWithMultipleModel
|
||||
? (
|
||||
<PublishWithMultipleModel
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onSelect={item => handlePublish(false, item)}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={() => handlePublish(false)}
|
||||
className={cn(cannotPublish && '!bg-primary-200 !cursor-not-allowed', 'shrink-0 w-[70px] !h-8 !text-[13px] font-medium')}
|
||||
>
|
||||
{t('appDebug.operation.applyConfig')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='flex flex-col grow h-0 px-6 py-4 rounded-tl-2xl border-t border-l bg-gray-50 '>
|
||||
<Debug
|
||||
hasSetAPIKEY={hasSettedApiKey}
|
||||
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
|
||||
inputs={inputs}
|
||||
modelParameterParams={{
|
||||
setModel: setModel as any,
|
||||
onCompletionParamsChange: setCompletionParams,
|
||||
}}
|
||||
debugWithMultipleModel={debugWithMultipleModel}
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onMultipleModelConfigsChange={handleMultipleModelConfigsChange}
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
|
@ -829,6 +886,13 @@ const Configuration: FC = () => {
|
|||
hasSetAPIKEY={hasSettedApiKey}
|
||||
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
|
||||
inputs={inputs}
|
||||
modelParameterParams={{
|
||||
setModel: setModel as any,
|
||||
onCompletionParamsChange: setCompletionParams,
|
||||
}}
|
||||
debugWithMultipleModel={debugWithMultipleModel}
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onMultipleModelConfigsChange={handleMultipleModelConfigsChange}
|
||||
/>
|
||||
</Drawer>
|
||||
)}
|
||||
|
|
|
@ -49,6 +49,9 @@ export type IGenerationItemProps = {
|
|||
isShowTextToSpeech?: boolean
|
||||
appId?: string
|
||||
varList?: { label: string; value: string | number | object }[]
|
||||
innerClassName?: string
|
||||
contentClassName?: string
|
||||
footerClassName?: string
|
||||
}
|
||||
|
||||
export const SimpleBtn = ({ className, isDisabled, onClick, children }: {
|
||||
|
@ -95,6 +98,8 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||
isShowTextToSpeech,
|
||||
appId,
|
||||
varList,
|
||||
innerClassName,
|
||||
contentClassName,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const params = useParams()
|
||||
|
@ -177,7 +182,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||
|
||||
const handleOpenLogModal = async (setModal: Dispatch<SetStateAction<boolean>>) => {
|
||||
const data = await fetchTextGenerationMessge({
|
||||
appId: params.appId,
|
||||
appId: params.appId as string,
|
||||
messageId: messageId!,
|
||||
})
|
||||
setPromptLog(data.message as any || [])
|
||||
|
@ -249,7 +254,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||
)
|
||||
: (
|
||||
<div
|
||||
className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4')}
|
||||
className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4', innerClassName)}
|
||||
style={mainStyle}
|
||||
>
|
||||
{(isTop && taskId) && (
|
||||
|
@ -258,7 +263,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||
{taskId}
|
||||
</div>)
|
||||
}
|
||||
<div className='flex'>
|
||||
<div className={`flex ${contentClassName}`}>
|
||||
<div className='grow w-0'>
|
||||
{isError
|
||||
? <div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
|
||||
|
|
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
|
97
web/app/components/base/dropdown/index.tsx
Normal file
97
web/app/components/base/dropdown/index.tsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
|
||||
export type Item = {
|
||||
value: string
|
||||
text: string
|
||||
}
|
||||
type DropdownProps = {
|
||||
items: Item[]
|
||||
secondItems?: Item[]
|
||||
onSelect: (item: Item) => void
|
||||
renderTrigger?: (open: boolean) => React.ReactNode
|
||||
}
|
||||
const Dropdown: FC<DropdownProps> = ({
|
||||
items,
|
||||
onSelect,
|
||||
secondItems,
|
||||
renderTrigger,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
{
|
||||
renderTrigger
|
||||
? renderTrigger(open)
|
||||
: (
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center w-6 h-6 cursor-pointer rounded-md
|
||||
${open && 'bg-black/5'}
|
||||
`}
|
||||
>
|
||||
<DotsHorizontal className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent>
|
||||
<div className='rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg text-sm text-gray-700'>
|
||||
{
|
||||
!!items.length && (
|
||||
<div className='p-1'>
|
||||
{
|
||||
items.map(item => (
|
||||
<div
|
||||
key={item.value}
|
||||
className='flex items-center px-3 h-8 rounded-lg cursor-pointer hover:bg-gray-100'
|
||||
onClick={() => onSelect(item)}
|
||||
>
|
||||
{item.text}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
(!!items.length && !!secondItems?.length) && (
|
||||
<div className='h-[1px] bg-gray-100' />
|
||||
)
|
||||
}
|
||||
{
|
||||
!!secondItems?.length && (
|
||||
<div className='p-1'>
|
||||
{
|
||||
secondItems.map(item => (
|
||||
<div
|
||||
key={item.value}
|
||||
className='flex items-center px-3 h-8 rounded-lg cursor-pointer hover:bg-gray-100'
|
||||
onClick={() => onSelect(item)}
|
||||
>
|
||||
{item.text}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default Dropdown
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="message-heart-circle">
|
||||
<path id="Solid" fill-rule="evenodd" clip-rule="evenodd" d="M8.33334 1.3335C4.83554 1.3335 2.00001 4.16903 2.00001 7.66683C2.00001 8.3735 2.116 9.05444 2.33051 9.69084C2.36824 9.80278 2.39045 9.86902 2.40488 9.91786L2.40961 9.93431L2.40711 9.93952C2.38997 9.97486 2.36451 10.0223 2.31687 10.1105L1.21562 12.1489C1.14736 12.2751 1.07614 12.4069 1.02717 12.5214C0.978485 12.6353 0.89963 12.8442 0.93843 13.0919C0.983911 13.3822 1.15477 13.6378 1.40562 13.7908C1.61963 13.9213 1.84282 13.9283 1.96665 13.9269C2.09123 13.9254 2.24018 13.91 2.38296 13.8952L5.8196 13.54C5.87464 13.5343 5.90342 13.5314 5.92449 13.5297L5.92721 13.5295L5.93545 13.5325C5.96135 13.5418 5.99648 13.5553 6.05711 13.5786C6.76441 13.8511 7.53226 14.0002 8.33334 14.0002C11.8311 14.0002 14.6667 11.1646 14.6667 7.66683C14.6667 4.16903 11.8311 1.3335 8.33334 1.3335ZM5.97972 5.72165C6.73124 5.08746 7.73145 5.27376 8.33126 5.96633C8.93106 5.27376 9.91836 5.09414 10.6828 5.72165C11.4472 6.34916 11.5401 7.41616 10.9499 8.16621C10.5843 8.63089 9.66661 9.4796 9.02123 10.0581C8.78417 10.2706 8.66564 10.3769 8.52339 10.4197C8.40136 10.4564 8.26116 10.4564 8.13913 10.4197C7.99688 10.3769 7.87835 10.2706 7.64128 10.0581C6.9959 9.4796 6.0782 8.63089 5.71257 8.16621C5.1224 7.41616 5.22821 6.35583 5.97972 5.72165Z" fill="#DD2590"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,5 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="send-03">
|
||||
<path id="Solid" d="M18.4385 10.5535C18.6111 10.2043 18.6111 9.79465 18.4385 9.44548C18.2865 9.13803 18.0197 8.97682 17.8815 8.89905C17.7327 8.81532 17.542 8.72955 17.3519 8.64403L3.36539 2.35014C3.17087 2.26257 2.97694 2.17526 2.81335 2.11859C2.66315 2.06656 2.36076 1.97151 2.02596 2.06467C1.64761 2.16994 1.34073 2.4469 1.19734 2.81251C1.07045 3.13604 1.13411 3.44656 1.17051 3.60129C1.21017 3.76983 1.27721 3.9717 1.34445 4.17418L2.69818 8.25278C2.80718 8.58118 2.86168 8.74537 2.96302 8.86678C3.05252 8.97399 3.16752 9.05699 3.29746 9.10816C3.44462 9.1661 3.61762 9.1661 3.96363 9.1661H10.0001C10.4603 9.1661 10.8334 9.53919 10.8334 9.99943C10.8334 10.4597 10.4603 10.8328 10.0001 10.8328H3.97939C3.63425 10.8328 3.46168 10.8328 3.3148 10.8905C3.18508 10.9414 3.07022 11.0241 2.98072 11.1309C2.87937 11.2519 2.82459 11.4155 2.71502 11.7428L1.3504 15.8191C1.28243 16.0221 1.21472 16.2242 1.17455 16.3929C1.13773 16.5476 1.07301 16.8587 1.19956 17.1831C1.34245 17.5493 1.64936 17.827 2.02806 17.9327C2.36342 18.0263 2.6665 17.9309 2.81674 17.8789C2.98066 17.8221 3.17507 17.7346 3.37023 17.6467L17.3518 11.355C17.542 11.2695 17.7327 11.1837 17.8815 11.0999C18.0197 11.0222 18.2865 10.861 18.4385 10.5535Z" fill="#D0D5DD"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Rectangle 1" d="M1.03647 1.5547C0.59343 0.890144 1.06982 0 1.86852 0H8V12L1.03647 1.5547Z" fill="#F2F4F7"/>
|
||||
</svg>
|
After Width: | Height: | Size: 219 B |
|
@ -0,0 +1,6 @@
|
|||
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Rectangle 2">
|
||||
<path d="M6.96353 1.5547C7.40657 0.890144 6.93018 0 6.13148 0H0V12L6.96353 1.5547Z" fill="white"/>
|
||||
<path d="M6.96353 1.5547C7.40657 0.890144 6.93018 0 6.13148 0H0V12L6.96353 1.5547Z" fill="#D1E9FF" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 346 B |
|
@ -0,0 +1,5 @@
|
|||
<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="star-04">
|
||||
<path id="Solid" d="M5.88897 0.683596C5.82708 0.522683 5.67249 0.416504 5.50008 0.416504C5.32768 0.416504 5.17308 0.522683 5.11119 0.683596L4.27287 2.86321C4.1477 3.18865 4.10837 3.28243 4.05457 3.35809C4.00059 3.43401 3.93426 3.50034 3.85834 3.55433C3.78267 3.60813 3.68889 3.64746 3.36346 3.77263L1.18384 4.61094C1.02293 4.67283 0.916748 4.82743 0.916748 4.99984C0.916748 5.17224 1.02293 5.32684 1.18384 5.38873L3.36346 6.22705C3.68889 6.35221 3.78267 6.39155 3.85834 6.44535C3.93426 6.49933 4.00059 6.56566 4.05457 6.64158C4.10837 6.71724 4.1477 6.81102 4.27287 7.13646L5.11119 9.31608C5.17308 9.47699 5.32768 9.58317 5.50008 9.58317C5.67249 9.58317 5.82709 9.47699 5.88898 9.31608L6.72729 7.13646C6.85246 6.81102 6.89179 6.71724 6.94559 6.64158C6.99957 6.56566 7.06591 6.49933 7.14183 6.44535C7.21749 6.39155 7.31127 6.35221 7.6367 6.22705L9.81632 5.38873C9.97723 5.32684 10.0834 5.17224 10.0834 4.99984C10.0834 4.82743 9.97723 4.67283 9.81632 4.61094L7.6367 3.77263C7.31127 3.64746 7.21749 3.60813 7.14183 3.55433C7.06591 3.50034 6.99957 3.43401 6.94559 3.35809C6.89179 3.28243 6.85246 3.18865 6.72729 2.86321L5.88897 0.683596Z" fill="#667085"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "message-heart-circle"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Solid",
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M8.33334 1.3335C4.83554 1.3335 2.00001 4.16903 2.00001 7.66683C2.00001 8.3735 2.116 9.05444 2.33051 9.69084C2.36824 9.80278 2.39045 9.86902 2.40488 9.91786L2.40961 9.93431L2.40711 9.93952C2.38997 9.97486 2.36451 10.0223 2.31687 10.1105L1.21562 12.1489C1.14736 12.2751 1.07614 12.4069 1.02717 12.5214C0.978485 12.6353 0.89963 12.8442 0.93843 13.0919C0.983911 13.3822 1.15477 13.6378 1.40562 13.7908C1.61963 13.9213 1.84282 13.9283 1.96665 13.9269C2.09123 13.9254 2.24018 13.91 2.38296 13.8952L5.8196 13.54C5.87464 13.5343 5.90342 13.5314 5.92449 13.5297L5.92721 13.5295L5.93545 13.5325C5.96135 13.5418 5.99648 13.5553 6.05711 13.5786C6.76441 13.8511 7.53226 14.0002 8.33334 14.0002C11.8311 14.0002 14.6667 11.1646 14.6667 7.66683C14.6667 4.16903 11.8311 1.3335 8.33334 1.3335ZM5.97972 5.72165C6.73124 5.08746 7.73145 5.27376 8.33126 5.96633C8.93106 5.27376 9.91836 5.09414 10.6828 5.72165C11.4472 6.34916 11.5401 7.41616 10.9499 8.16621C10.5843 8.63089 9.66661 9.4796 9.02123 10.0581C8.78417 10.2706 8.66564 10.3769 8.52339 10.4197C8.40136 10.4564 8.26116 10.4564 8.13913 10.4197C7.99688 10.3769 7.87835 10.2706 7.64128 10.0581C6.9959 9.4796 6.0782 8.63089 5.71257 8.16621C5.1224 7.41616 5.22821 6.35583 5.97972 5.72165Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "MessageHeartCircle"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './MessageHeartCircle.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'MessageHeartCircle'
|
||||
|
||||
export default Icon
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "20",
|
||||
"height": "20",
|
||||
"viewBox": "0 0 20 20",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "send-03"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Solid",
|
||||
"d": "M18.4385 10.5535C18.6111 10.2043 18.6111 9.79465 18.4385 9.44548C18.2865 9.13803 18.0197 8.97682 17.8815 8.89905C17.7327 8.81532 17.542 8.72955 17.3519 8.64403L3.36539 2.35014C3.17087 2.26257 2.97694 2.17526 2.81335 2.11859C2.66315 2.06656 2.36076 1.97151 2.02596 2.06467C1.64761 2.16994 1.34073 2.4469 1.19734 2.81251C1.07045 3.13604 1.13411 3.44656 1.17051 3.60129C1.21017 3.76983 1.27721 3.9717 1.34445 4.17418L2.69818 8.25278C2.80718 8.58118 2.86168 8.74537 2.96302 8.86678C3.05252 8.97399 3.16752 9.05699 3.29746 9.10816C3.44462 9.1661 3.61762 9.1661 3.96363 9.1661H10.0001C10.4603 9.1661 10.8334 9.53919 10.8334 9.99943C10.8334 10.4597 10.4603 10.8328 10.0001 10.8328H3.97939C3.63425 10.8328 3.46168 10.8328 3.3148 10.8905C3.18508 10.9414 3.07022 11.0241 2.98072 11.1309C2.87937 11.2519 2.82459 11.4155 2.71502 11.7428L1.3504 15.8191C1.28243 16.0221 1.21472 16.2242 1.17455 16.3929C1.13773 16.5476 1.07301 16.8587 1.19956 17.1831C1.34245 17.5493 1.64936 17.827 2.02806 17.9327C2.36342 18.0263 2.6665 17.9309 2.81674 17.8789C2.98066 17.8221 3.17507 17.7346 3.37023 17.6467L17.3518 11.355C17.542 11.2695 17.7327 11.1837 17.8815 11.0999C18.0197 11.0222 18.2865 10.861 18.4385 10.5535Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Send03"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Send03.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Send03'
|
||||
|
||||
export default Icon
|
|
@ -2,3 +2,5 @@ export { default as AiText } from './AiText'
|
|||
export { default as CuteRobote } from './CuteRobote'
|
||||
export { default as MessageDotsCircle } from './MessageDotsCircle'
|
||||
export { default as MessageFast } from './MessageFast'
|
||||
export { default as MessageHeartCircle } from './MessageHeartCircle'
|
||||
export { default as Send03 } from './Send03'
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "8",
|
||||
"height": "12",
|
||||
"viewBox": "0 0 8 12",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Rectangle 1",
|
||||
"d": "M1.03647 1.5547C0.59343 0.890144 1.06982 0 1.86852 0H8V12L1.03647 1.5547Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "AnswerTriangle"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './AnswerTriangle.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'AnswerTriangle'
|
||||
|
||||
export default Icon
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "8",
|
||||
"height": "12",
|
||||
"viewBox": "0 0 8 12",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "Rectangle 2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M6.96353 1.5547C7.40657 0.890144 6.93018 0 6.13148 0H0V12L6.96353 1.5547Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M6.96353 1.5547C7.40657 0.890144 6.93018 0 6.13148 0H0V12L6.96353 1.5547Z",
|
||||
"fill": "currentColor",
|
||||
"fill-opacity": "0.5"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "QuestionTriangle"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './QuestionTriangle.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'QuestionTriangle'
|
||||
|
||||
export default Icon
|
|
@ -1,3 +1,4 @@
|
|||
export { default as AnswerTriangle } from './AnswerTriangle'
|
||||
export { default as CheckCircle } from './CheckCircle'
|
||||
export { default as CheckDone01 } from './CheckDone01'
|
||||
export { default as Download02 } from './Download02'
|
||||
|
@ -5,6 +6,7 @@ export { default as Edit04 } from './Edit04'
|
|||
export { default as Eye } from './Eye'
|
||||
export { default as MessageClockCircle } from './MessageClockCircle'
|
||||
export { default as PlusCircle } from './PlusCircle'
|
||||
export { default as QuestionTriangle } from './QuestionTriangle'
|
||||
export { default as SearchMd } from './SearchMd'
|
||||
export { default as Target04 } from './Target04'
|
||||
export { default as Tool03 } from './Tool03'
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "11",
|
||||
"height": "10",
|
||||
"viewBox": "0 0 11 10",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "star-04"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Solid",
|
||||
"d": "M5.88897 0.683596C5.82708 0.522683 5.67249 0.416504 5.50008 0.416504C5.32768 0.416504 5.17308 0.522683 5.11119 0.683596L4.27287 2.86321C4.1477 3.18865 4.10837 3.28243 4.05457 3.35809C4.00059 3.43401 3.93426 3.50034 3.85834 3.55433C3.78267 3.60813 3.68889 3.64746 3.36346 3.77263L1.18384 4.61094C1.02293 4.67283 0.916748 4.82743 0.916748 4.99984C0.916748 5.17224 1.02293 5.32684 1.18384 5.38873L3.36346 6.22705C3.68889 6.35221 3.78267 6.39155 3.85834 6.44535C3.93426 6.49933 4.00059 6.56566 4.05457 6.64158C4.10837 6.71724 4.1477 6.81102 4.27287 7.13646L5.11119 9.31608C5.17308 9.47699 5.32768 9.58317 5.50008 9.58317C5.67249 9.58317 5.82709 9.47699 5.88898 9.31608L6.72729 7.13646C6.85246 6.81102 6.89179 6.71724 6.94559 6.64158C6.99957 6.56566 7.06591 6.49933 7.14183 6.44535C7.21749 6.39155 7.31127 6.35221 7.6367 6.22705L9.81632 5.38873C9.97723 5.32684 10.0834 5.17224 10.0834 4.99984C10.0834 4.82743 9.97723 4.67283 9.81632 4.61094L7.6367 3.77263C7.31127 3.64746 7.21749 3.60813 7.14183 3.55433C7.06591 3.50034 6.99957 3.43401 6.94559 3.35809C6.89179 3.28243 6.85246 3.18865 6.72729 2.86321L5.88897 0.683596Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Star04"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Star04.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Star04'
|
||||
|
||||
export default Icon
|
|
@ -0,0 +1 @@
|
|||
export { default as Star04 } from './Star04'
|
61
web/app/components/base/text-generation/hooks.ts
Normal file
61
web/app/components/base/text-generation/hooks.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { ssePost } from '@/service/base'
|
||||
|
||||
export const useTextGeneration = () => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const [isResponsing, setIsResponsing] = useState(false)
|
||||
const [completion, setCompletion] = useState('')
|
||||
const [messageId, setMessageId] = useState<string | null>(null)
|
||||
|
||||
const handleSend = async (
|
||||
url: string,
|
||||
data: any,
|
||||
) => {
|
||||
if (isResponsing) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return false
|
||||
}
|
||||
|
||||
setIsResponsing(true)
|
||||
setCompletion('')
|
||||
setMessageId('')
|
||||
let res: string[] = []
|
||||
ssePost(
|
||||
url,
|
||||
{
|
||||
body: {
|
||||
response_mode: 'streaming',
|
||||
...data,
|
||||
},
|
||||
},
|
||||
{
|
||||
onData: (data: string, _isFirstMessage: boolean, { messageId }) => {
|
||||
res.push(data)
|
||||
setCompletion(res.join(''))
|
||||
setMessageId(messageId)
|
||||
},
|
||||
onMessageReplace: (messageReplace) => {
|
||||
res = [messageReplace.answer]
|
||||
setCompletion(res.join(''))
|
||||
},
|
||||
onCompleted() {
|
||||
setIsResponsing(false)
|
||||
},
|
||||
onError() {
|
||||
setIsResponsing(false)
|
||||
},
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
completion,
|
||||
isResponsing,
|
||||
setIsResponsing,
|
||||
handleSend,
|
||||
messageId,
|
||||
}
|
||||
}
|
41
web/app/components/base/text-generation/types.ts
Normal file
41
web/app/components/base/text-generation/types.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import type {
|
||||
ModelConfig,
|
||||
VisionFile,
|
||||
VisionSettings,
|
||||
} from '@/types/app'
|
||||
|
||||
export type { VisionFile } from '@/types/app'
|
||||
export { TransferMethod } from '@/types/app'
|
||||
|
||||
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 TextGenerationConfig = Omit<ModelConfig, 'model'>
|
||||
|
||||
export type OnSend = (message: string, files?: VisionFile[]) => void
|
|
@ -1,4 +1,7 @@
|
|||
import type { FC } from 'react'
|
||||
import type {
|
||||
FC,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import cn from 'classnames'
|
||||
|
@ -8,32 +11,25 @@ import type {
|
|||
FormValue,
|
||||
ModelParameterRule,
|
||||
} from '../declarations'
|
||||
import {
|
||||
MODEL_STATUS_TEXT,
|
||||
ModelStatusEnum,
|
||||
} from '../declarations'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
import { ModelStatusEnum } from '../declarations'
|
||||
import ModelSelector from '../model-selector'
|
||||
import {
|
||||
useLanguage,
|
||||
useTextGenerationCurrentProviderAndModelAndModelList,
|
||||
} from '../hooks'
|
||||
import { isNullOrUndefined } from '../utils'
|
||||
import ParameterItem from './parameter-item'
|
||||
import type { ParameterValue } from './parameter-item'
|
||||
import Trigger from './trigger'
|
||||
import type { TriggerProps } from './trigger'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { SlidersH } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
|
||||
import { fetchModelParameterRules } from '@/service/common'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import Radio from '@/app/components/base/radio'
|
||||
import { TONE_LIST } from '@/config'
|
||||
import { Brush01 } from '@/app/components/base/icons/src/vender/solid/editor'
|
||||
|
@ -41,15 +37,19 @@ import { Scales02 } from '@/app/components/base/icons/src/vender/solid/FinanceAn
|
|||
import { Target04 } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { Sliders02 } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
type ModelParameterModalProps = {
|
||||
export type ModelParameterModalProps = {
|
||||
isAdvancedMode: boolean
|
||||
mode: string
|
||||
modelId: string
|
||||
provider: string
|
||||
setModel: (model: { modelId: string; provider: string; mode?: string; features: string[] }) => void
|
||||
setModel: (model: { modelId: string; provider: string; mode?: string; features?: string[] }) => void
|
||||
completionParams: FormValue
|
||||
onCompletionParamsChange: (newParams: FormValue) => void
|
||||
debugWithMultipleModel: boolean
|
||||
onDebugWithMultipleModelChange: () => void
|
||||
renderTrigger?: (v: TriggerProps) => ReactNode
|
||||
}
|
||||
const stopParameerRule: ModelParameterRule = {
|
||||
default: [],
|
||||
|
@ -78,14 +78,16 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
|||
setModel,
|
||||
completionParams,
|
||||
onCompletionParamsChange,
|
||||
debugWithMultipleModel,
|
||||
onDebugWithMultipleModelChange,
|
||||
renderTrigger,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const { hasSettedApiKey, modelProviders } = useProviderContext()
|
||||
const { hasSettedApiKey } = useProviderContext()
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const [open, setOpen] = useState(false)
|
||||
const { data: parameterRulesData, isLoading } = useSWR(`/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}`, fetchModelParameterRules)
|
||||
const { data: parameterRulesData, isLoading } = useSWR((provider && modelId) ? `/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}` : null, fetchModelParameterRules)
|
||||
const {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
|
@ -220,71 +222,32 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
|||
onClick={() => setOpen(v => !v)}
|
||||
className='block'
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-2 h-8 rounded-lg border cursor-pointer hover:border-[1.5px]
|
||||
${disabled ? 'border-[#F79009] bg-[#FFFAEB]' : 'border-[#444CE7] bg-primary-50'}
|
||||
`}
|
||||
>
|
||||
{
|
||||
currentProvider && (
|
||||
<ModelIcon
|
||||
className='mr-1.5 !w-5 !h-5'
|
||||
provider={currentProvider}
|
||||
modelName={currentModel?.model}
|
||||
{
|
||||
renderTrigger
|
||||
? renderTrigger({
|
||||
open,
|
||||
disabled,
|
||||
modelDisabled,
|
||||
hasDeprecated,
|
||||
currentProvider,
|
||||
currentModel,
|
||||
providerName: provider,
|
||||
modelId,
|
||||
})
|
||||
: (
|
||||
<Trigger
|
||||
disabled={disabled}
|
||||
modelDisabled={modelDisabled}
|
||||
hasDeprecated={hasDeprecated}
|
||||
currentProvider={currentProvider}
|
||||
currentModel={currentModel}
|
||||
providerName={provider}
|
||||
modelId={modelId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!currentProvider && (
|
||||
<ModelIcon
|
||||
className='mr-1.5 !w-5 !h-5'
|
||||
provider={modelProviders.find(item => item.provider === provider)}
|
||||
modelName={modelId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
currentModel && (
|
||||
<ModelName
|
||||
className='mr-1.5 text-gray-900'
|
||||
modelItem={currentModel}
|
||||
showMode
|
||||
modeClassName='!text-[#444CE7] !border-[#A4BCFD]'
|
||||
showFeatures
|
||||
featuresClassName='!text-[#444CE7] !border-[#A4BCFD]'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!currentModel && (
|
||||
<div className='mr-1 text-[13px] font-medium text-gray-900 truncate'>
|
||||
{modelId}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
disabled
|
||||
? (
|
||||
<TooltipPlus
|
||||
popupContent={
|
||||
hasDeprecated
|
||||
? t('common.modelProvider.deprecated')
|
||||
: (modelDisabled && currentModel)
|
||||
? MODEL_STATUS_TEXT[currentModel.status as string][language]
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<AlertTriangle className='w-4 h-4 text-[#F79009]' />
|
||||
</TooltipPlus>
|
||||
)
|
||||
: (
|
||||
<SlidersH className='w-4 h-4 text-indigo-600' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent>
|
||||
<PortalToFollowElemContent className='z-[60]'>
|
||||
<div className='w-[496px] rounded-xl border border-gray-100 bg-white shadow-xl'>
|
||||
<div className='flex items-center px-4 h-12 rounded-t-xl border-b border-gray-100 bg-gray-50 text-md font-medium text-gray-900'>
|
||||
<CubeOutline className='mr-2 w-4 h-4 text-primary-600' />
|
||||
|
@ -296,7 +259,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
|||
{t('common.modelProvider.model')}
|
||||
</div>
|
||||
<ModelSelector
|
||||
defaultModel={{ provider, model: modelId }}
|
||||
defaultModel={(provider || modelId) ? { provider, model: modelId } : undefined}
|
||||
modelList={textGenerationModelList}
|
||||
onSelect={handleChangeModel}
|
||||
/>
|
||||
|
@ -370,6 +333,17 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
|||
)
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center justify-between px-6 h-[50px] bg-gray-50 border-t border-t-gray-100 text-xs font-medium text-primary-600 cursor-pointer rounded-b-xl'
|
||||
onClick={() => onDebugWithMultipleModelChange()}
|
||||
>
|
||||
{
|
||||
debugWithMultipleModel
|
||||
? t('appDebug.debugAsSingleModel')
|
||||
: t('appDebug.debugAsMultipleModel')
|
||||
}
|
||||
<ArrowNarrowLeft className='w-3 h-3 rotate-180' />
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
Model,
|
||||
ModelItem,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { MODEL_STATUS_TEXT } from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { SlidersH } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
|
||||
export type TriggerProps = {
|
||||
open?: boolean
|
||||
disabled?: boolean
|
||||
currentProvider?: ModelProvider | Model
|
||||
currentModel?: ModelItem
|
||||
providerName?: string
|
||||
modelId?: string
|
||||
hasDeprecated?: boolean
|
||||
modelDisabled?: boolean
|
||||
}
|
||||
const Trigger: FC<TriggerProps> = ({
|
||||
disabled,
|
||||
currentProvider,
|
||||
currentModel,
|
||||
providerName,
|
||||
modelId,
|
||||
hasDeprecated,
|
||||
modelDisabled,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const { modelProviders } = useProviderContext()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-2 h-8 rounded-lg border cursor-pointer hover:border-[1.5px]
|
||||
${disabled ? 'border-[#F79009] bg-[#FFFAEB]' : 'border-[#444CE7] bg-primary-50'}
|
||||
`}
|
||||
>
|
||||
{
|
||||
currentProvider && (
|
||||
<ModelIcon
|
||||
className='mr-1.5 !w-5 !h-5'
|
||||
provider={currentProvider}
|
||||
modelName={currentModel?.model}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!currentProvider && (
|
||||
<ModelIcon
|
||||
className='mr-1.5 !w-5 !h-5'
|
||||
provider={modelProviders.find(item => item.provider === providerName)}
|
||||
modelName={modelId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
currentModel && (
|
||||
<ModelName
|
||||
className='mr-1.5 text-gray-900'
|
||||
modelItem={currentModel}
|
||||
showMode
|
||||
modeClassName='!text-[#444CE7] !border-[#A4BCFD]'
|
||||
showFeatures
|
||||
featuresClassName='!text-[#444CE7] !border-[#A4BCFD]'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!currentModel && (
|
||||
<div className='mr-1 text-[13px] font-medium text-gray-900 truncate'>
|
||||
{modelId}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
disabled
|
||||
? (
|
||||
<TooltipPlus
|
||||
popupContent={
|
||||
hasDeprecated
|
||||
? t('common.modelProvider.deprecated')
|
||||
: (modelDisabled && currentModel)
|
||||
? MODEL_STATUS_TEXT[currentModel.status as string][language]
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<AlertTriangle className='w-4 h-4 text-[#F79009]' />
|
||||
</TooltipPlus>
|
||||
)
|
||||
: (
|
||||
<SlidersH className='w-4 h-4 text-indigo-600' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Trigger
|
|
@ -1,4 +1,4 @@
|
|||
import { createContext } from 'use-context-selector'
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
import { PromptMode } from '@/models/debug'
|
||||
import type {
|
||||
AnnotationReplyConfig,
|
||||
|
@ -239,4 +239,6 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
|
|||
setVisionConfig: () => { },
|
||||
})
|
||||
|
||||
export const useDebugConfigurationContext = () => useContext(DebugConfigurationContext)
|
||||
|
||||
export default DebugConfigurationContext
|
||||
|
|
|
@ -352,6 +352,10 @@ const translation = {
|
|||
score_thresholdTip: 'Used to set the similarity threshold for chunks filtering.',
|
||||
retrieveChangeTip: 'Modifying the index mode and retrieval mode may affect applications associated with this Knowledge.',
|
||||
},
|
||||
debugAsSingleModel: 'Debug as Single Model',
|
||||
debugAsMultipleModel: 'Debug as Multiple Models',
|
||||
duplicateModel: 'Dusplicate',
|
||||
publishAs: 'Publish as',
|
||||
assistantType: {
|
||||
name: 'Assistant Type',
|
||||
chatAssistant: {
|
||||
|
|
|
@ -346,6 +346,10 @@ const translation = {
|
|||
score_thresholdTip: '用于设置文本片段筛选的相似度阈值。',
|
||||
retrieveChangeTip: '修改索引模式和检索模式可能会影响与该知识库关联的应用程序。',
|
||||
},
|
||||
debugAsSingleModel: '单一模型进行调试',
|
||||
debugAsMultipleModel: '多个模型进行调试',
|
||||
duplicateModel: '复制模型',
|
||||
publishAs: '发布为',
|
||||
assistantType: {
|
||||
name: '助手类型',
|
||||
chatAssistant: {
|
||||
|
|
|
@ -175,9 +175,15 @@ const baseFetch = <T>(
|
|||
bodyStringify = true,
|
||||
needAllResponseContent,
|
||||
deleteContentType,
|
||||
getAbortController,
|
||||
}: IOtherOptions,
|
||||
): Promise<T> => {
|
||||
const options: typeof baseOptions & FetchOptionType = Object.assign({}, baseOptions, fetchOptions)
|
||||
if (getAbortController) {
|
||||
const abortController = new AbortController()
|
||||
getAbortController(abortController)
|
||||
options.signal = abortController.signal
|
||||
}
|
||||
if (isPublicAPI) {
|
||||
const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0]
|
||||
const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' })
|
||||
|
|
|
@ -45,15 +45,23 @@ export const sendCompletionMessage = async (appId: string, body: Record<string,
|
|||
}, { onData, onCompleted, onError, onMessageReplace })
|
||||
}
|
||||
|
||||
export const fetchSuggestedQuestions = (appId: string, messageId: string) => {
|
||||
return get(`apps/${appId}/chat-messages/${messageId}/suggested-questions`)
|
||||
export const fetchSuggestedQuestions = (appId: string, messageId: string, getAbortController?: any) => {
|
||||
return get(
|
||||
`apps/${appId}/chat-messages/${messageId}/suggested-questions`,
|
||||
{},
|
||||
{
|
||||
getAbortController,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export const fetchConvesationMessages = (appId: string, conversation_id: string) => {
|
||||
export const fetchConvesationMessages = (appId: string, conversation_id: string, getAbortController?: any) => {
|
||||
return get(`apps/${appId}/chat-messages`, {
|
||||
params: {
|
||||
conversation_id,
|
||||
},
|
||||
}, {
|
||||
getAbortController,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user