Files
DB-GPT/web/new-components/chat/input/ChatInputPanel.tsx

250 lines
7.4 KiB
TypeScript

import { ChatContentContext } from '@/pages/chat';
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Input, Spin } from 'antd';
import classNames from 'classnames';
import { useSearchParams } from 'next/navigation';
import React, { forwardRef, useContext, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UserChatContent } from '@/types/chat';
import { parseResourceValue } from '@/utils';
import { SlashCommand } from './CommandPopover';
import EnhancedChatInput, { ContentPart, EnhancedChatInputRef } from './EnhancedChatInput';
import ToolsBar from './ToolsBar';
const USE_ENHANCED_INPUT = false;
const defaultCommands: SlashCommand[] = [
{ id: 'clear', trigger: 'clear', title: 'Clear chat history', type: 'builtin' },
{ id: 'help', trigger: 'help', title: 'Show available commands', type: 'builtin' },
{ id: 'model', trigger: 'model', title: 'Switch AI model', type: 'builtin' },
];
const ChatInputPanel: React.ForwardRefRenderFunction<any, { ctrl: AbortController }> = ({ ctrl }, ref) => {
const { t } = useTranslation();
const {
replyLoading,
handleChat,
appInfo,
currentDialogue,
temperatureValue,
maxNewTokensValue,
resourceValue,
knowledgeValue,
setResourceValue,
refreshDialogList,
} = useContext(ChatContentContext);
const searchParams = useSearchParams();
const scene = searchParams?.get('scene') ?? '';
const select_param = searchParams?.get('select_param') ?? '';
const [userInput, setUserInput] = useState<string>('');
const [isFocus, setIsFocus] = useState<boolean>(false);
const [isZhInput, setIsZhInput] = useState<boolean>(false);
const enhancedInputRef = useRef<EnhancedChatInputRef>(null);
const submitCountRef = useRef(0);
const paramKey: string[] = useMemo(() => {
return appInfo.param_need?.map(i => i.type) || [];
}, [appInfo.param_need]);
const buildChatParams = () => ({
app_code: appInfo.app_code || '',
...(paramKey.includes('temperature') && { temperature: temperatureValue }),
...(paramKey.includes('max_new_tokens') && { max_new_tokens: maxNewTokensValue }),
select_param,
...(paramKey.includes('resource') && {
select_param:
typeof resourceValue === 'string'
? resourceValue
: JSON.stringify(resourceValue) || currentDialogue.select_param,
}),
// Include knowledge space in ext_info for RAG
...(knowledgeValue && {
ext_info: { knowledge_space: knowledgeValue },
}),
});
const handleEnhancedSubmit = async (text: string, parts: ContentPart[]) => {
submitCountRef.current++;
const resources = parseResourceValue(resourceValue);
let newUserInput: UserChatContent;
const imageParts = parts.filter(p => p.type === 'image');
const hasImages = imageParts.length > 0;
const hasResources = resources.length > 0;
if (hasResources || hasImages) {
if (scene !== 'chat_excel') {
setResourceValue(null);
}
const messages = [...resources];
imageParts.forEach(img => {
if (img.dataUrl) {
messages.push({
type: 'image_url',
image_url: {
url: img.dataUrl,
fileName: img.filename || 'image',
},
});
}
});
messages.push({
type: 'text',
text: text,
});
newUserInput = {
role: 'user',
content: messages,
};
} else {
newUserInput = text;
}
const params = buildChatParams();
await handleChat(newUserInput, params);
if (submitCountRef.current === 1) {
await refreshDialogList();
}
};
const onLegacySubmit = async () => {
submitCountRef.current++;
setUserInput('');
const resources = parseResourceValue(resourceValue);
let newUserInput: UserChatContent;
if (resources.length > 0) {
if (scene !== 'chat_excel') {
setResourceValue(null);
}
const messages = [...resources];
messages.push({
type: 'text',
text: userInput,
});
newUserInput = {
role: 'user',
content: messages,
};
} else {
newUserInput = userInput;
}
const params = buildChatParams();
await handleChat(newUserInput, params);
if (submitCountRef.current === 1) {
await refreshDialogList();
}
};
const handleCommandSelect = (command: SlashCommand) => {
console.log('Command selected:', command);
};
useImperativeHandle(ref, () => ({
setUserInput: (value: string) => {
if (USE_ENHANCED_INPUT) {
enhancedInputRef.current?.setValue(value);
} else {
setUserInput(value);
}
},
}));
if (USE_ENHANCED_INPUT) {
return (
<div className='flex flex-col w-5/6 mx-auto pt-4 pb-6 bg-transparent'>
<div className='flex flex-col bg-white dark:bg-[rgba(255,255,255,0.16)] px-5 py-4 pt-2 rounded-xl relative'>
<ToolsBar ctrl={ctrl} />
<EnhancedChatInput
ref={enhancedInputRef}
onSubmit={handleEnhancedSubmit}
disabled={replyLoading}
loading={replyLoading}
placeholder={t('input_tips')}
commands={defaultCommands}
onCommandSelect={handleCommandSelect}
className='border-0 bg-transparent'
maxHeight={150}
/>
</div>
</div>
);
}
return (
<div className='flex flex-col w-5/6 mx-auto pt-4 pb-6 bg-transparent'>
<div
className={`flex flex-1 flex-col bg-white dark:bg-[rgba(255,255,255,0.16)] px-5 py-4 pt-2 rounded-xl relative border-t border-b border-l border-r dark:border-[rgba(255,255,255,0.6)] ${
isFocus ? 'border-[#0c75fc]' : ''
}`}
id='input-panel'
>
<ToolsBar ctrl={ctrl} />
<Input.TextArea
placeholder={t('input_tips')}
className='w-full h-20 resize-none border-0 p-0 focus:shadow-none dark:bg-transparent'
value={userInput}
onKeyDown={e => {
if (e.key === 'Enter') {
if (e.shiftKey) {
return;
}
if (isZhInput) {
return;
}
e.preventDefault();
if (!userInput.trim() || replyLoading) {
return;
}
onLegacySubmit();
}
}}
onChange={e => {
setUserInput(e.target.value);
}}
onFocus={() => {
setIsFocus(true);
}}
onBlur={() => setIsFocus(false)}
onCompositionStart={() => setIsZhInput(true)}
onCompositionEnd={() => setIsZhInput(false)}
/>
<Button
type='primary'
className={classNames(
'flex items-center justify-center w-14 h-8 rounded-lg text-sm absolute right-4 bottom-3 bg-button-gradient border-0',
{
'cursor-not-allowed': !userInput.trim(),
},
)}
onClick={() => {
if (replyLoading || !userInput.trim()) {
return;
}
onLegacySubmit();
}}
>
{replyLoading ? (
<Spin spinning={replyLoading} indicator={<LoadingOutlined className='text-white' />} />
) : (
t('sent')
)}
</Button>
</div>
</div>
);
};
export default forwardRef(ChatInputPanel);