mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-30 18:48:55 +00:00
V4.6.7-production (#759)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useSpeech } from '@/web/common/hooks/useSpeech';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { Box, Flex, Image, Spinner, Textarea } from '@chakra-ui/react';
|
||||
import React, { useRef, useEffect, useCallback, useState } from 'react';
|
||||
import React, { useRef, useEffect, useCallback, useState, useTransition } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyTooltip from '../MyTooltip';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
@@ -37,7 +37,7 @@ const MessageInput = ({
|
||||
showFileSelector = false,
|
||||
resetInputVal
|
||||
}: {
|
||||
onChange: (e: string) => void;
|
||||
onChange?: (e: string) => void;
|
||||
onSendMessage: (e: string) => void;
|
||||
onStop: () => void;
|
||||
isChatting: boolean;
|
||||
@@ -45,6 +45,8 @@ const MessageInput = ({
|
||||
TextareaDom: React.MutableRefObject<HTMLTextAreaElement | null>;
|
||||
resetInputVal: (val: string) => void;
|
||||
}) => {
|
||||
const [, startSts] = useTransition();
|
||||
|
||||
const { shareId } = useRouter().query as { shareId?: string };
|
||||
const {
|
||||
isSpeaking,
|
||||
@@ -330,17 +332,29 @@ ${images.map((img) => JSON.stringify({ src: img.src })).join('\n')}
|
||||
const textarea = e.target;
|
||||
textarea.style.height = textareaMinH;
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
onChange(textarea.value);
|
||||
|
||||
startSts(() => {
|
||||
onChange?.(textarea.value);
|
||||
});
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// enter send.(pc or iframe && enter and unPress shift)
|
||||
const isEnter = e.keyCode === 13;
|
||||
if (isEnter && TextareaDom.current && (e.ctrlKey || e.altKey)) {
|
||||
TextareaDom.current.value += '\n';
|
||||
TextareaDom.current.style.height = textareaMinH;
|
||||
TextareaDom.current.style.height = `${TextareaDom.current.scrollHeight}px`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 全选内容
|
||||
// @ts-ignore
|
||||
e.key === 'a' && e.ctrlKey && e.target?.select();
|
||||
|
||||
if ((isPc || window !== parent) && e.keyCode === 13 && !e.shiftKey) {
|
||||
handleSend();
|
||||
e.preventDefault();
|
||||
}
|
||||
// 全选内容
|
||||
// @ts-ignore
|
||||
e.key === 'a' && e.ctrlKey && e.target?.select();
|
||||
}}
|
||||
onPaste={(e) => {
|
||||
const clipboardData = e.clipboardData;
|
||||
|
@@ -36,7 +36,7 @@ import { adaptChat2GptMessages } from '@fastgpt/global/core/chat/adapt';
|
||||
import { useMarkdown } from '@/web/common/hooks/useMarkdown';
|
||||
import { ModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import { VariableInputEnum } from '@fastgpt/global/core/module/constants';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { UseFormReturn, useForm } from 'react-hook-form';
|
||||
import type { ChatMessageItemType } from '@fastgpt/global/core/ai/type.d';
|
||||
import { fileDownload } from '@/web/common/file/utils';
|
||||
import { htmlTemplate } from '@/constants/common';
|
||||
@@ -65,7 +65,7 @@ const SelectMarkCollection = dynamic(() => import('./SelectMarkCollection'));
|
||||
import styles from './index.module.scss';
|
||||
import { postQuestionGuide } from '@/web/core/ai/api';
|
||||
import { splitGuideModule } from '@fastgpt/global/core/module/utils';
|
||||
import type { AppTTSConfigType } from '@fastgpt/global/core/module/type.d';
|
||||
import type { AppTTSConfigType, VariableItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import MessageInput from './MessageInput';
|
||||
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
|
||||
import ChatBoxDivider from '../core/chat/Divider';
|
||||
@@ -98,6 +98,15 @@ enum FeedbackTypeEnum {
|
||||
hidden = 'hidden'
|
||||
}
|
||||
|
||||
const MessageCardStyle: BoxProps = {
|
||||
px: 4,
|
||||
py: 3,
|
||||
borderRadius: '0 8px 8px 8px',
|
||||
boxShadow: '0 0 8px rgba(0,0,0,0.15)',
|
||||
display: 'inline-block',
|
||||
maxW: ['calc(100% - 25px)', 'calc(100% - 40px)']
|
||||
};
|
||||
|
||||
type Props = {
|
||||
feedbackType?: `${FeedbackTypeEnum}`;
|
||||
showMarkIcon?: boolean; // admin mark dataset
|
||||
@@ -157,7 +166,6 @@ const ChatBox = (
|
||||
const isNewChatReplace = useRef(false);
|
||||
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const [variables, setVariables] = useState<Record<string, any>>({}); // settings variable
|
||||
const [chatHistory, setChatHistory] = useState<ChatSiteItemType[]>([]);
|
||||
const [feedbackId, setFeedbackId] = useState<string>();
|
||||
const [readFeedbackData, setReadFeedbackData] = useState<{
|
||||
@@ -180,7 +188,17 @@ const ChatBox = (
|
||||
);
|
||||
|
||||
// compute variable input is finish.
|
||||
const [variableInputFinish, setVariableInputFinish] = useState(false);
|
||||
const chatForm = useForm<{
|
||||
variables: Record<string, any>;
|
||||
}>({
|
||||
defaultValues: {
|
||||
variables: {}
|
||||
}
|
||||
});
|
||||
const { setValue, watch, handleSubmit } = chatForm;
|
||||
const variables = watch('variables');
|
||||
|
||||
const [variableInputFinish, setVariableInputFinish] = useState(false); // clicked start chat button
|
||||
const variableIsFinish = useMemo(() => {
|
||||
if (!variableModules || variableModules.length === 0 || chatHistory.length > 0) return true;
|
||||
|
||||
@@ -194,21 +212,15 @@ const ChatBox = (
|
||||
return variableInputFinish;
|
||||
}, [chatHistory.length, variableInputFinish, variableModules, variables]);
|
||||
|
||||
const { register, reset, getValues, setValue, handleSubmit } = useForm<Record<string, any>>({
|
||||
defaultValues: variables
|
||||
});
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = useCallback(
|
||||
(behavior: 'smooth' | 'auto' = 'smooth') => {
|
||||
if (!ChatBoxRef.current) return;
|
||||
ChatBoxRef.current.scrollTo({
|
||||
top: ChatBoxRef.current.scrollHeight,
|
||||
behavior
|
||||
});
|
||||
},
|
||||
[ChatBoxRef]
|
||||
);
|
||||
const scrollToBottom = (behavior: 'smooth' | 'auto' = 'smooth') => {
|
||||
if (!ChatBoxRef.current) return;
|
||||
ChatBoxRef.current.scrollTo({
|
||||
top: ChatBoxRef.current.scrollHeight,
|
||||
behavior
|
||||
});
|
||||
};
|
||||
|
||||
// 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
|
||||
const generatingScroll = useCallback(
|
||||
throttle(() => {
|
||||
@@ -222,28 +234,31 @@ const ChatBox = (
|
||||
[]
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const generatingMessage = ({ text = '', status, name }: generatingMessageProps) => {
|
||||
setChatHistory((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
...(text
|
||||
? {
|
||||
value: item.value + text
|
||||
}
|
||||
: {}),
|
||||
...(status && name
|
||||
? {
|
||||
status,
|
||||
moduleName: name
|
||||
}
|
||||
: {})
|
||||
};
|
||||
})
|
||||
);
|
||||
generatingScroll();
|
||||
};
|
||||
const generatingMessage = useCallback(
|
||||
({ text = '', status, name }: generatingMessageProps) => {
|
||||
setChatHistory((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
...(text
|
||||
? {
|
||||
value: item.value + text
|
||||
}
|
||||
: {}),
|
||||
...(status && name
|
||||
? {
|
||||
status,
|
||||
moduleName: name
|
||||
}
|
||||
: {})
|
||||
};
|
||||
})
|
||||
);
|
||||
generatingScroll();
|
||||
},
|
||||
[generatingScroll]
|
||||
);
|
||||
|
||||
// 重置输入内容
|
||||
const resetInputVal = useCallback((val: string) => {
|
||||
@@ -284,149 +299,157 @@ const ChatBox = (
|
||||
}
|
||||
} catch (error) {}
|
||||
},
|
||||
[questionGuide, scrollToBottom, shareId]
|
||||
[questionGuide, shareId]
|
||||
);
|
||||
|
||||
/**
|
||||
* user confirm send prompt
|
||||
*/
|
||||
const sendPrompt = useCallback(
|
||||
async (variables: Record<string, any> = {}, inputVal = '', history = chatHistory) => {
|
||||
if (!onStartChat) return;
|
||||
if (isChatting) {
|
||||
toast({
|
||||
title: '正在聊天中...请等待结束',
|
||||
status: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
questionGuideController.current?.abort('stop');
|
||||
// get input value
|
||||
const val = inputVal.trim();
|
||||
|
||||
if (!val) {
|
||||
toast({
|
||||
title: '内容为空',
|
||||
status: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newChatList: ChatSiteItemType[] = [
|
||||
...history,
|
||||
{
|
||||
dataId: nanoid(),
|
||||
obj: 'Human',
|
||||
value: val,
|
||||
status: 'finish'
|
||||
},
|
||||
{
|
||||
dataId: nanoid(),
|
||||
obj: 'AI',
|
||||
value: '',
|
||||
status: 'loading'
|
||||
}
|
||||
];
|
||||
|
||||
// 插入内容
|
||||
setChatHistory(newChatList);
|
||||
|
||||
// 清空输入内容
|
||||
resetInputVal('');
|
||||
setQuestionGuide([]);
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
try {
|
||||
// create abort obj
|
||||
const abortSignal = new AbortController();
|
||||
chatController.current = abortSignal;
|
||||
|
||||
const messages = adaptChat2GptMessages({ messages: newChatList, reserveId: true });
|
||||
|
||||
const {
|
||||
responseData,
|
||||
responseText,
|
||||
isNewChat = false
|
||||
} = await onStartChat({
|
||||
chatList: newChatList.map((item) => ({
|
||||
dataId: item.dataId,
|
||||
obj: item.obj,
|
||||
value: item.value,
|
||||
status: item.status,
|
||||
moduleName: item.moduleName
|
||||
})),
|
||||
messages,
|
||||
controller: abortSignal,
|
||||
generatingMessage,
|
||||
variables
|
||||
});
|
||||
|
||||
isNewChatReplace.current = isNewChat;
|
||||
|
||||
// set finish status
|
||||
setChatHistory((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish',
|
||||
responseData
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
createQuestionGuide({
|
||||
history: newChatList.map((item, i) =>
|
||||
i === newChatList.length - 1
|
||||
? {
|
||||
...item,
|
||||
value: responseText
|
||||
}
|
||||
: item
|
||||
)
|
||||
({
|
||||
inputVal = '',
|
||||
history = chatHistory
|
||||
}: {
|
||||
inputVal?: string;
|
||||
history?: ChatSiteItemType[];
|
||||
}) => {
|
||||
handleSubmit(async ({ variables }) => {
|
||||
if (!onStartChat) return;
|
||||
if (isChatting) {
|
||||
toast({
|
||||
title: '正在聊天中...请等待结束',
|
||||
status: 'warning'
|
||||
});
|
||||
generatingScroll();
|
||||
isPc && TextareaDom.current?.focus();
|
||||
}, 100);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: t(getErrText(err, 'core.chat.error.Chat error')),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
questionGuideController.current?.abort('stop');
|
||||
// get input value
|
||||
const val = inputVal.trim();
|
||||
|
||||
if (!err?.responseText) {
|
||||
resetInputVal(inputVal);
|
||||
setChatHistory(newChatList.slice(0, newChatList.length - 2));
|
||||
if (!val) {
|
||||
toast({
|
||||
title: '内容为空',
|
||||
status: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// set finish status
|
||||
setChatHistory((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish'
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
const newChatList: ChatSiteItemType[] = [
|
||||
...history,
|
||||
{
|
||||
dataId: nanoid(),
|
||||
obj: 'Human',
|
||||
value: val,
|
||||
status: 'finish'
|
||||
},
|
||||
{
|
||||
dataId: nanoid(),
|
||||
obj: 'AI',
|
||||
value: '',
|
||||
status: 'loading'
|
||||
}
|
||||
];
|
||||
|
||||
// 插入内容
|
||||
setChatHistory(newChatList);
|
||||
|
||||
// 清空输入内容
|
||||
resetInputVal('');
|
||||
setQuestionGuide([]);
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
try {
|
||||
// create abort obj
|
||||
const abortSignal = new AbortController();
|
||||
chatController.current = abortSignal;
|
||||
|
||||
const messages = adaptChat2GptMessages({ messages: newChatList, reserveId: true });
|
||||
|
||||
const {
|
||||
responseData,
|
||||
responseText,
|
||||
isNewChat = false
|
||||
} = await onStartChat({
|
||||
chatList: newChatList.map((item) => ({
|
||||
dataId: item.dataId,
|
||||
obj: item.obj,
|
||||
value: item.value,
|
||||
status: item.status,
|
||||
moduleName: item.moduleName
|
||||
})),
|
||||
messages,
|
||||
controller: abortSignal,
|
||||
generatingMessage,
|
||||
variables
|
||||
});
|
||||
|
||||
isNewChatReplace.current = isNewChat;
|
||||
|
||||
// set finish status
|
||||
setChatHistory((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish',
|
||||
responseData
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
createQuestionGuide({
|
||||
history: newChatList.map((item, i) =>
|
||||
i === newChatList.length - 1
|
||||
? {
|
||||
...item,
|
||||
value: responseText
|
||||
}
|
||||
: item
|
||||
)
|
||||
});
|
||||
generatingScroll();
|
||||
isPc && TextareaDom.current?.focus();
|
||||
}, 100);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: t(getErrText(err, 'core.chat.error.Chat error')),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true
|
||||
});
|
||||
|
||||
if (!err?.responseText) {
|
||||
resetInputVal(inputVal);
|
||||
setChatHistory(newChatList.slice(0, newChatList.length - 2));
|
||||
}
|
||||
|
||||
// set finish status
|
||||
setChatHistory((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish'
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
})();
|
||||
},
|
||||
[
|
||||
chatHistory,
|
||||
onStartChat,
|
||||
isChatting,
|
||||
resetInputVal,
|
||||
toast,
|
||||
scrollToBottom,
|
||||
generatingMessage,
|
||||
createQuestionGuide,
|
||||
generatingMessage,
|
||||
generatingScroll,
|
||||
handleSubmit,
|
||||
isChatting,
|
||||
isPc,
|
||||
t
|
||||
onStartChat,
|
||||
resetInputVal,
|
||||
t,
|
||||
toast
|
||||
]
|
||||
);
|
||||
|
||||
@@ -444,11 +467,14 @@ const ChatBox = (
|
||||
);
|
||||
setChatHistory((state) => (index === 0 ? [] : state.slice(0, index)));
|
||||
|
||||
sendPrompt(variables, delHistory[0].value, chatHistory.slice(0, index));
|
||||
sendPrompt({
|
||||
inputVal: delHistory[0].value,
|
||||
history: chatHistory.slice(0, index)
|
||||
});
|
||||
} catch (error) {}
|
||||
setLoading(false);
|
||||
},
|
||||
[chatHistory, onDelMessage, sendPrompt, setLoading, variables]
|
||||
[chatHistory, onDelMessage, sendPrompt, setLoading]
|
||||
);
|
||||
// delete one message
|
||||
const delOneMessage = useCallback(
|
||||
@@ -471,27 +497,21 @@ const ChatBox = (
|
||||
defaultVal[item.key] = '';
|
||||
});
|
||||
|
||||
reset(e || defaultVal);
|
||||
setVariables(e || defaultVal);
|
||||
setValue('variables', e || defaultVal);
|
||||
},
|
||||
resetHistory(e) {
|
||||
setVariableInputFinish(!!e.length);
|
||||
setChatHistory(e);
|
||||
},
|
||||
scrollToBottom,
|
||||
sendPrompt: (question: string) => handleSubmit((item) => sendPrompt(item, question))()
|
||||
sendPrompt: (question: string) => {
|
||||
sendPrompt({
|
||||
inputVal: question
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
/* style start */
|
||||
const MessageCardStyle: BoxProps = {
|
||||
px: 4,
|
||||
py: 3,
|
||||
borderRadius: '0 8px 8px 8px',
|
||||
boxShadow: '0 0 8px rgba(0,0,0,0.15)',
|
||||
display: 'inline-block',
|
||||
maxW: ['calc(100% - 25px)', 'calc(100% - 40px)']
|
||||
};
|
||||
|
||||
const showEmpty = useMemo(
|
||||
() =>
|
||||
feConfigs?.show_emptyChat &&
|
||||
@@ -534,14 +554,18 @@ const ChatBox = (
|
||||
useEffect(() => {
|
||||
const windowMessage = ({ data }: MessageEvent<{ type: 'sendPrompt'; text: string }>) => {
|
||||
if (data?.type === 'sendPrompt' && data?.text) {
|
||||
handleSubmit((item) => sendPrompt(item, data.text))();
|
||||
sendPrompt({
|
||||
inputVal: data.text
|
||||
});
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', windowMessage);
|
||||
|
||||
eventBus.on(EventNameEnum.sendQuestion, ({ text }: { text: string }) => {
|
||||
if (!text) return;
|
||||
handleSubmit((data) => sendPrompt(data, text))();
|
||||
sendPrompt({
|
||||
inputVal: text
|
||||
});
|
||||
});
|
||||
eventBus.on(EventNameEnum.editQuestion, ({ text }: { text: string }) => {
|
||||
if (!text) return;
|
||||
@@ -553,140 +577,81 @@ const ChatBox = (
|
||||
eventBus.off(EventNameEnum.sendQuestion);
|
||||
eventBus.off(EventNameEnum.editQuestion);
|
||||
};
|
||||
}, [handleSubmit, resetInputVal, sendPrompt]);
|
||||
}, [resetInputVal, sendPrompt]);
|
||||
|
||||
const onSubmitVariables = useCallback(
|
||||
(data: Record<string, any>) => {
|
||||
setVariableInputFinish(true);
|
||||
onUpdateVariable?.(data);
|
||||
},
|
||||
[onUpdateVariable]
|
||||
);
|
||||
const HumanChatCard = useCallback(
|
||||
({ item, index }: { item: ChatSiteItemType; index: number }) => {
|
||||
return (
|
||||
<>
|
||||
{/* control icon */}
|
||||
<Flex w={'100%'} alignItems={'center'} justifyContent={'flex-end'}>
|
||||
<ChatControllerComponent
|
||||
chat={item}
|
||||
onDelete={
|
||||
onDelMessage
|
||||
? () => {
|
||||
delOneMessage({ dataId: item.dataId, index });
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onRetry={useCallback(() => retryInput(index), [index])}
|
||||
/>
|
||||
<ChatAvatar src={userAvatar} type={'Human'} />
|
||||
</Flex>
|
||||
{/* content */}
|
||||
<Box mt={['6px', 2]} textAlign={'right'}>
|
||||
<Card
|
||||
className="markdown"
|
||||
{...MessageCardStyle}
|
||||
bg={'primary.200'}
|
||||
borderRadius={'8px 0 8px 8px'}
|
||||
textAlign={'left'}
|
||||
>
|
||||
<Markdown source={item.value} isChatting={false} />
|
||||
</Card>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'}>
|
||||
<Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script>
|
||||
|
||||
{/* chat box container */}
|
||||
<Box ref={ChatBoxRef} flex={'1 0 0'} h={0} w={'100%'} overflow={'overlay'} px={[4, 0]} pb={3}>
|
||||
<Box id="chat-container" maxW={['100%', '92%']} h={'100%'} mx={'auto'}>
|
||||
{showEmpty && <Empty />}
|
||||
|
||||
{!!welcomeText && (
|
||||
<Box py={3}>
|
||||
{/* avatar */}
|
||||
<ChatAvatar src={appAvatar} type={'AI'} />
|
||||
{/* message */}
|
||||
<Box textAlign={'left'}>
|
||||
<Card order={2} mt={2} {...MessageCardStyle} bg={'white'}>
|
||||
<Markdown source={`~~~guide \n${welcomeText}`} isChatting={false} />
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{!!welcomeText && <WelcomeText appAvatar={appAvatar} welcomeText={welcomeText} />}
|
||||
{/* variable input */}
|
||||
{!!variableModules?.length && (
|
||||
<Box py={3}>
|
||||
{/* avatar */}
|
||||
<ChatAvatar src={appAvatar} type={'AI'} />
|
||||
{/* message */}
|
||||
<Box textAlign={'left'}>
|
||||
<Card order={2} mt={2} bg={'white'} w={'400px'} {...MessageCardStyle}>
|
||||
{variableModules.map((item) => (
|
||||
<Box key={item.id} mb={4}>
|
||||
<VariableLabel required={item.required}>{item.label}</VariableLabel>
|
||||
{item.type === VariableInputEnum.input && (
|
||||
<Input
|
||||
isDisabled={variableIsFinish}
|
||||
bg={'myWhite.400'}
|
||||
{...register(item.key, {
|
||||
required: item.required
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.textarea && (
|
||||
<Textarea
|
||||
isDisabled={variableIsFinish}
|
||||
bg={'myWhite.400'}
|
||||
{...register(item.key, {
|
||||
required: item.required
|
||||
})}
|
||||
rows={5}
|
||||
maxLength={4000}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.select && (
|
||||
<MySelect
|
||||
width={'100%'}
|
||||
isDisabled={variableIsFinish}
|
||||
list={(item.enums || []).map((item) => ({
|
||||
label: item.value,
|
||||
value: item.value
|
||||
}))}
|
||||
{...register(item.key, {
|
||||
required: item.required
|
||||
})}
|
||||
value={getValues(item.key)}
|
||||
onchange={(e) => {
|
||||
setValue(item.key, e);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
{!variableIsFinish && (
|
||||
<Button
|
||||
leftIcon={<MyIcon name={'core/chat/chatFill'} w={'16px'} />}
|
||||
size={'sm'}
|
||||
maxW={'100px'}
|
||||
onClick={handleSubmit((data) => {
|
||||
onUpdateVariable?.(data);
|
||||
setVariables(data);
|
||||
setVariableInputFinish(true);
|
||||
})}
|
||||
>
|
||||
{t('core.chat.Start Chat')}
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
<VariableInput
|
||||
appAvatar={appAvatar}
|
||||
variableModules={variableModules}
|
||||
variableIsFinish={variableIsFinish}
|
||||
chatForm={chatForm}
|
||||
onSubmitVariables={onSubmitVariables}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* chat history */}
|
||||
<Box id={'history'}>
|
||||
{chatHistory.map((item, index) => (
|
||||
<Box key={item.dataId} py={5}>
|
||||
{item.obj === 'Human' && (
|
||||
<>
|
||||
{/* control icon */}
|
||||
<Flex w={'100%'} alignItems={'center'} justifyContent={'flex-end'}>
|
||||
<ChatController
|
||||
chat={item}
|
||||
onDelete={
|
||||
onDelMessage
|
||||
? () => {
|
||||
delOneMessage({ dataId: item.dataId, index });
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onRetry={() => retryInput(index)}
|
||||
/>
|
||||
<ChatAvatar src={userAvatar} type={'Human'} />
|
||||
</Flex>
|
||||
{/* content */}
|
||||
<Box mt={['6px', 2]} textAlign={'right'}>
|
||||
<Card
|
||||
className="markdown"
|
||||
{...MessageCardStyle}
|
||||
bg={'primary.200'}
|
||||
borderRadius={'8px 0 8px 8px'}
|
||||
textAlign={'left'}
|
||||
>
|
||||
<Markdown source={item.value} isChatting={false} />
|
||||
</Card>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{item.obj === 'Human' && <HumanChatCard item={item} index={index} />}
|
||||
{item.obj === 'AI' && (
|
||||
<>
|
||||
{/* control icon */}
|
||||
<Flex w={'100%'} alignItems={'center'}>
|
||||
<ChatAvatar src={appAvatar} type={'AI'} />
|
||||
<ChatController
|
||||
{/* control icon */}
|
||||
<ChatControllerComponent
|
||||
ml={2}
|
||||
chat={item}
|
||||
setChatHistory={setChatHistory}
|
||||
@@ -723,36 +688,35 @@ const ChatBox = (
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onAddUserLike={(() => {
|
||||
if (feedbackType !== FeedbackTypeEnum.user || item.userBadFeedback) {
|
||||
return;
|
||||
}
|
||||
return () => {
|
||||
if (!item.dataId || !chatId || !appId) return;
|
||||
onAddUserLike={
|
||||
feedbackType !== FeedbackTypeEnum.user || item.userBadFeedback
|
||||
? undefined
|
||||
: () => {
|
||||
if (!item.dataId || !chatId || !appId) return;
|
||||
|
||||
const isGoodFeedback = !!item.userGoodFeedback;
|
||||
setChatHistory((state) =>
|
||||
state.map((chatItem) =>
|
||||
chatItem.dataId === item.dataId
|
||||
? {
|
||||
...chatItem,
|
||||
userGoodFeedback: isGoodFeedback ? undefined : 'yes'
|
||||
}
|
||||
: chatItem
|
||||
)
|
||||
);
|
||||
try {
|
||||
updateChatUserFeedback({
|
||||
appId,
|
||||
chatId,
|
||||
chatItemId: item.dataId,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
userGoodFeedback: isGoodFeedback ? undefined : 'yes'
|
||||
});
|
||||
} catch (error) {}
|
||||
};
|
||||
})()}
|
||||
const isGoodFeedback = !!item.userGoodFeedback;
|
||||
setChatHistory((state) =>
|
||||
state.map((chatItem) =>
|
||||
chatItem.dataId === item.dataId
|
||||
? {
|
||||
...chatItem,
|
||||
userGoodFeedback: isGoodFeedback ? undefined : 'yes'
|
||||
}
|
||||
: chatItem
|
||||
)
|
||||
);
|
||||
try {
|
||||
updateChatUserFeedback({
|
||||
appId,
|
||||
chatId,
|
||||
chatItemId: item.dataId,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
userGoodFeedback: isGoodFeedback ? undefined : 'yes'
|
||||
});
|
||||
} catch (error) {}
|
||||
}
|
||||
}
|
||||
onCloseUserLike={
|
||||
feedbackType === FeedbackTypeEnum.admin
|
||||
? () => {
|
||||
@@ -931,13 +895,12 @@ const ChatBox = (
|
||||
</Box>
|
||||
</Box>
|
||||
{/* message input */}
|
||||
{onStartChat && variableIsFinish && active ? (
|
||||
{onStartChat && variableIsFinish && active && (
|
||||
<MessageInput
|
||||
onChange={(e) => {
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
onSendMessage={(e) => {
|
||||
handleSubmit((data) => sendPrompt(data, e))();
|
||||
onSendMessage={(inputVal) => {
|
||||
sendPrompt({
|
||||
inputVal
|
||||
});
|
||||
}}
|
||||
onStop={() => chatController.current?.abort('stop')}
|
||||
isChatting={isChatting}
|
||||
@@ -945,7 +908,7 @@ const ChatBox = (
|
||||
resetInputVal={resetInputVal}
|
||||
showFileSelector={showFileSelector}
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
{/* user feedback modal */}
|
||||
{!!feedbackId && chatId && appId && (
|
||||
<FeedbackModal
|
||||
@@ -1115,30 +1078,125 @@ export const useChatBox = () => {
|
||||
};
|
||||
};
|
||||
|
||||
function VariableLabel({
|
||||
required = false,
|
||||
children
|
||||
const WelcomeText = React.memo(function Welcome({
|
||||
appAvatar,
|
||||
welcomeText
|
||||
}: {
|
||||
required?: boolean;
|
||||
children: React.ReactNode | string;
|
||||
appAvatar?: string;
|
||||
welcomeText: string;
|
||||
}) {
|
||||
return (
|
||||
<Box as={'label'} display={'inline-block'} position={'relative'} mb={1}>
|
||||
{children}
|
||||
{required && (
|
||||
<Box
|
||||
position={'absolute'}
|
||||
top={'-2px'}
|
||||
right={'-10px'}
|
||||
color={'red.500'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
<Box py={3}>
|
||||
{/* avatar */}
|
||||
<ChatAvatar src={appAvatar} type={'AI'} />
|
||||
{/* message */}
|
||||
<Box textAlign={'left'}>
|
||||
<Card order={2} mt={2} {...MessageCardStyle} bg={'white'}>
|
||||
<Markdown source={`~~~guide \n${welcomeText}`} isChatting={false} />
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
});
|
||||
const VariableInput = React.memo(function VariableInput({
|
||||
appAvatar,
|
||||
variableModules,
|
||||
variableIsFinish,
|
||||
chatForm,
|
||||
onSubmitVariables
|
||||
}: {
|
||||
appAvatar?: string;
|
||||
variableModules: VariableItemType[];
|
||||
variableIsFinish: boolean;
|
||||
onSubmitVariables: (e: Record<string, any>) => void;
|
||||
chatForm: UseFormReturn<{
|
||||
variables: Record<string, any>;
|
||||
}>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { register, setValue, handleSubmit: handleSubmitChat, watch } = chatForm;
|
||||
const variables = watch('variables');
|
||||
|
||||
return (
|
||||
<Box py={3}>
|
||||
{/* avatar */}
|
||||
<ChatAvatar src={appAvatar} type={'AI'} />
|
||||
{/* message */}
|
||||
<Box textAlign={'left'}>
|
||||
<Card order={2} mt={2} bg={'white'} w={'400px'} {...MessageCardStyle}>
|
||||
{variableModules.map((item) => (
|
||||
<Box key={item.id} mb={4}>
|
||||
<Box as={'label'} display={'inline-block'} position={'relative'} mb={1}>
|
||||
{item.label}
|
||||
{item.required && (
|
||||
<Box
|
||||
position={'absolute'}
|
||||
top={'-2px'}
|
||||
right={'-10px'}
|
||||
color={'red.500'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{item.type === VariableInputEnum.input && (
|
||||
<Input
|
||||
isDisabled={variableIsFinish}
|
||||
bg={'myWhite.400'}
|
||||
{...register(`variables.${item.key}`, {
|
||||
required: item.required
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.textarea && (
|
||||
<Textarea
|
||||
isDisabled={variableIsFinish}
|
||||
bg={'myWhite.400'}
|
||||
{...register(`variables.${item.key}`, {
|
||||
required: item.required
|
||||
})}
|
||||
rows={5}
|
||||
maxLength={4000}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.select && (
|
||||
<MySelect
|
||||
width={'100%'}
|
||||
isDisabled={variableIsFinish}
|
||||
list={(item.enums || []).map((item) => ({
|
||||
label: item.value,
|
||||
value: item.value
|
||||
}))}
|
||||
{...register(`variables.${item.key}`, {
|
||||
required: item.required
|
||||
})}
|
||||
value={variables[item.key]}
|
||||
onchange={(e) => {
|
||||
setValue(`variables.${item.key}`, e);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
{!variableIsFinish && (
|
||||
<Button
|
||||
leftIcon={<MyIcon name={'core/chat/chatFill'} w={'16px'} />}
|
||||
size={'sm'}
|
||||
maxW={'100px'}
|
||||
onClick={handleSubmitChat((data) => {
|
||||
onSubmitVariables(data);
|
||||
})}
|
||||
>
|
||||
{t('core.chat.Start Chat')}
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
function ChatAvatar({ src, type }: { src?: string; type: 'Human' | 'AI' }) {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
@@ -1173,7 +1231,7 @@ function Empty() {
|
||||
);
|
||||
}
|
||||
|
||||
function ChatController({
|
||||
const ChatControllerComponent = React.memo(function ChatControllerComponent({
|
||||
chat,
|
||||
setChatHistory,
|
||||
display,
|
||||
@@ -1226,7 +1284,7 @@ function ChatController({
|
||||
|
||||
return (
|
||||
<Flex {...controlContainerStyle} ml={ml} mr={mr} display={display}>
|
||||
<MyTooltip label={'复制'}>
|
||||
<MyTooltip label={t('common.Copy')}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'copy'}
|
||||
@@ -1246,7 +1304,7 @@ function ChatController({
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
<MyTooltip label={'删除'}>
|
||||
<MyTooltip label={t('common.Delete')}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'delete'}
|
||||
@@ -1259,7 +1317,7 @@ function ChatController({
|
||||
{showVoiceIcon &&
|
||||
hasAudio &&
|
||||
(audioLoading ? (
|
||||
<MyTooltip label={'加载中...'}>
|
||||
<MyTooltip label={t('common.Loading')}>
|
||||
<MyIcon {...controlIconStyle} name={'common/loading'} />
|
||||
</MyTooltip>
|
||||
) : audioPlaying ? (
|
||||
@@ -1372,4 +1430,4 @@ function ChatController({
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@@ -35,36 +35,79 @@ export enum CodeClassName {
|
||||
img = 'img'
|
||||
}
|
||||
|
||||
function Code({ inline, className, children }: any) {
|
||||
const Markdown = ({ source, isChatting = false }: { source: string; isChatting?: boolean }) => {
|
||||
const components = useMemo<any>(
|
||||
() => ({
|
||||
img: Image,
|
||||
pre: 'div',
|
||||
p: (pProps: any) => <p {...pProps} dir="auto" />,
|
||||
code: Code,
|
||||
a: A
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const formatSource = source
|
||||
.replace(/\\n/g, '\n ')
|
||||
.replace(/(http[s]?:\/\/[^\s,。]+)([。,])/g, '$1 $2')
|
||||
.replace(/\n*(\[QUOTE SIGN\]\(.*\))/g, '$1');
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className={`markdown ${styles.markdown}
|
||||
${isChatting ? `${formatSource ? styles.waitingAnimation : styles.animation}` : ''}
|
||||
`}
|
||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||
rehypePlugins={[RehypeKatex]}
|
||||
components={components}
|
||||
linkTarget={'_blank'}
|
||||
>
|
||||
{formatSource}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Markdown);
|
||||
|
||||
const Code = React.memo(function Code(e: any) {
|
||||
const { inline, className, children } = e;
|
||||
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const codeType = match?.[1];
|
||||
|
||||
if (codeType === CodeClassName.mermaid) {
|
||||
return <MermaidCodeBlock code={String(children)} />;
|
||||
}
|
||||
const strChildren = String(children);
|
||||
|
||||
if (codeType === CodeClassName.guide) {
|
||||
return <ChatGuide text={String(children)} />;
|
||||
}
|
||||
if (codeType === CodeClassName.questionGuide) {
|
||||
return <QuestionGuide text={String(children)} />;
|
||||
}
|
||||
if (codeType === CodeClassName.echarts) {
|
||||
return <EChartsCodeBlock code={String(children)} />;
|
||||
}
|
||||
if (codeType === CodeClassName.img) {
|
||||
return <ImageBlock images={String(children)} />;
|
||||
}
|
||||
return (
|
||||
<CodeLight className={className} inline={inline} match={match}>
|
||||
{children}
|
||||
</CodeLight>
|
||||
);
|
||||
}
|
||||
function Image({ src }: { src?: string }) {
|
||||
const Component = useMemo(() => {
|
||||
if (codeType === CodeClassName.mermaid) {
|
||||
return <MermaidCodeBlock code={strChildren} />;
|
||||
}
|
||||
|
||||
if (codeType === CodeClassName.guide) {
|
||||
return <ChatGuide text={strChildren} />;
|
||||
}
|
||||
if (codeType === CodeClassName.questionGuide) {
|
||||
return <QuestionGuide text={strChildren} />;
|
||||
}
|
||||
if (codeType === CodeClassName.echarts) {
|
||||
return <EChartsCodeBlock code={strChildren} />;
|
||||
}
|
||||
if (codeType === CodeClassName.img) {
|
||||
return <ImageBlock images={strChildren} />;
|
||||
}
|
||||
return (
|
||||
<CodeLight className={className} inline={inline} match={match}>
|
||||
{children}
|
||||
</CodeLight>
|
||||
);
|
||||
}, [codeType, className, inline, match, strChildren]);
|
||||
|
||||
return Component;
|
||||
});
|
||||
|
||||
const Image = React.memo(function Image({ src }: { src?: string }) {
|
||||
return <MdImage src={src} />;
|
||||
}
|
||||
function A({ children, ...props }: any) {
|
||||
});
|
||||
const A = React.memo(function A({ children, ...props }: any) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// empty href link
|
||||
@@ -109,38 +152,4 @@ function A({ children, ...props }: any) {
|
||||
}
|
||||
|
||||
return <Link {...props}>{children}</Link>;
|
||||
}
|
||||
|
||||
const Markdown = ({ source, isChatting = false }: { source: string; isChatting?: boolean }) => {
|
||||
const components = useMemo<any>(
|
||||
() => ({
|
||||
img: Image,
|
||||
pre: 'div',
|
||||
p: (pProps: any) => <p {...pProps} dir="auto" />,
|
||||
code: Code,
|
||||
a: A
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const formatSource = source
|
||||
.replace(/\\n/g, '\n ')
|
||||
.replace(/(http[s]?:\/\/[^\s,。]+)([。,])/g, '$1 $2')
|
||||
.replace(/\n*(\[QUOTE SIGN\]\(.*\))/g, '$1');
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className={`markdown ${styles.markdown}
|
||||
${isChatting ? `${formatSource ? styles.waitingAnimation : styles.animation}` : ''}
|
||||
`}
|
||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||
rehypePlugins={[RehypeKatex]}
|
||||
components={components}
|
||||
linkTarget={'_blank'}
|
||||
>
|
||||
{formatSource}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Markdown);
|
||||
});
|
||||
|
@@ -79,6 +79,8 @@ const TagTextarea = ({ defaultValues, onUpdate, ...props }: Props) => {
|
||||
ref={InputRef}
|
||||
variant={'unstyled'}
|
||||
display={'inline-block'}
|
||||
h={'24px'}
|
||||
borderRadius={'none'}
|
||||
w="auto"
|
||||
onBlur={(e) => {
|
||||
const value = e.target.value;
|
||||
|
@@ -66,7 +66,6 @@ const AIChatSettingsModal = ({
|
||||
}, [getValues]);
|
||||
|
||||
const quoteTemplateVariables = (() => [
|
||||
...pickerMenu,
|
||||
{
|
||||
key: 'q',
|
||||
label: 'q',
|
||||
@@ -91,15 +90,21 @@ const AIChatSettingsModal = ({
|
||||
key: 'index',
|
||||
label: t('core.dataset.search.Quote index'),
|
||||
icon: 'core/app/simpleMode/variable'
|
||||
}
|
||||
},
|
||||
...pickerMenu
|
||||
])();
|
||||
const quotePromptVariables = (() => [
|
||||
...pickerMenu,
|
||||
{
|
||||
key: 'quote',
|
||||
label: t('core.app.Quote templates'),
|
||||
icon: 'core/app/simpleMode/variable'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'question',
|
||||
label: t('core.module.input.label.user question'),
|
||||
icon: 'core/app/simpleMode/variable'
|
||||
},
|
||||
...pickerMenu
|
||||
])();
|
||||
|
||||
const LabelStyles: BoxProps = {
|
||||
|
@@ -55,11 +55,13 @@ const InviteModal = ({
|
||||
openConfirm(
|
||||
() => onClose(),
|
||||
undefined,
|
||||
t('user.team.Invite Member Success Tip', {
|
||||
success: res.invite.length,
|
||||
inValid: res.inValid.map((item) => item.username).join(', '),
|
||||
inTeam: res.inTeam.map((item) => item.username).join(', ')
|
||||
})
|
||||
<Box whiteSpace={'pre-wrap'}>
|
||||
{t('user.team.Invite Member Success Tip', {
|
||||
success: res.invite.length,
|
||||
inValid: res.inValid.map((item) => item.username).join(', '),
|
||||
inTeam: res.inTeam.map((item) => item.username).join(', ')
|
||||
})}
|
||||
</Box>
|
||||
)();
|
||||
},
|
||||
errorToast: t('user.team.Invite Member Failed Tip')
|
||||
|
@@ -75,7 +75,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { mutate: onSwitchTeam, isLoading: isSwitchTeam } = useRequest({
|
||||
mutationFn: async (teamId: string) => {
|
||||
const token = await putSwitchTeam(teamId);
|
||||
setToken(token);
|
||||
token && setToken(token);
|
||||
return initUserInfo();
|
||||
},
|
||||
errorToast: t('user.team.Switch Team Failed')
|
||||
@@ -286,13 +286,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
|
||||
size="sm"
|
||||
borderRadius={'md'}
|
||||
ml={3}
|
||||
leftIcon={
|
||||
<MyIcon
|
||||
name={'support/account/loginoutLight'}
|
||||
w={'14px'}
|
||||
color={'primary.500'}
|
||||
/>
|
||||
}
|
||||
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
|
||||
onClick={() => {
|
||||
openLeaveConfirm(() => onLeaveTeam(userInfo?.team?.teamId))();
|
||||
}}
|
||||
|
Reference in New Issue
Block a user