mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-30 18:48:55 +00:00
4.6.4-alpha (#582)
This commit is contained in:
@@ -69,78 +69,85 @@ const MessageInput = ({
|
||||
maxCount: 10
|
||||
});
|
||||
|
||||
const uploadFile = async (file: FileItemType) => {
|
||||
if (file.type === FileTypeEnum.image) {
|
||||
try {
|
||||
const src = await compressImgFileAndUpload({
|
||||
file: file.rawFile,
|
||||
maxW: 4329,
|
||||
maxH: 4329,
|
||||
maxSize: 1024 * 1024 * 5,
|
||||
// 30 day expired.
|
||||
expiredTime: addDays(new Date(), 30)
|
||||
});
|
||||
setFileList((state) =>
|
||||
state.map((item) =>
|
||||
item.id === file.id
|
||||
? {
|
||||
...item,
|
||||
src: `${location.origin}${src}`
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
setFileList((state) => state.filter((item) => item.id !== file.id));
|
||||
console.log(error);
|
||||
const uploadFile = useCallback(
|
||||
async (file: FileItemType) => {
|
||||
if (file.type === FileTypeEnum.image) {
|
||||
try {
|
||||
const src = await compressImgFileAndUpload({
|
||||
file: file.rawFile,
|
||||
maxW: 4329,
|
||||
maxH: 4329,
|
||||
maxSize: 1024 * 1024 * 5,
|
||||
// 30 day expired.
|
||||
expiredTime: addDays(new Date(), 30),
|
||||
shareId
|
||||
});
|
||||
setFileList((state) =>
|
||||
state.map((item) =>
|
||||
item.id === file.id
|
||||
? {
|
||||
...item,
|
||||
src: `${location.origin}${src}`
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
setFileList((state) => state.filter((item) => item.id !== file.id));
|
||||
console.log(error);
|
||||
|
||||
toast({
|
||||
status: 'error',
|
||||
title: t('common.Upload File Failed')
|
||||
});
|
||||
toast({
|
||||
status: 'error',
|
||||
title: t('common.Upload File Failed')
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const onSelectFile = useCallback(async (files: File[]) => {
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
const loadFiles = await Promise.all(
|
||||
files.map(
|
||||
(file) =>
|
||||
new Promise<FileItemType>((resolve, reject) => {
|
||||
if (file.type.includes('image')) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
const item = {
|
||||
},
|
||||
[shareId, t, toast]
|
||||
);
|
||||
const onSelectFile = useCallback(
|
||||
async (files: File[]) => {
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
const loadFiles = await Promise.all(
|
||||
files.map(
|
||||
(file) =>
|
||||
new Promise<FileItemType>((resolve, reject) => {
|
||||
if (file.type.includes('image')) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
const item = {
|
||||
id: nanoid(),
|
||||
rawFile: file,
|
||||
type: FileTypeEnum.image,
|
||||
name: file.name,
|
||||
icon: reader.result as string
|
||||
};
|
||||
uploadFile(item);
|
||||
resolve(item);
|
||||
};
|
||||
reader.onerror = () => {
|
||||
reject(reader.error);
|
||||
};
|
||||
} else {
|
||||
resolve({
|
||||
id: nanoid(),
|
||||
rawFile: file,
|
||||
type: FileTypeEnum.image,
|
||||
type: FileTypeEnum.file,
|
||||
name: file.name,
|
||||
icon: reader.result as string
|
||||
};
|
||||
uploadFile(item);
|
||||
resolve(item);
|
||||
};
|
||||
reader.onerror = () => {
|
||||
reject(reader.error);
|
||||
};
|
||||
} else {
|
||||
resolve({
|
||||
id: nanoid(),
|
||||
rawFile: file,
|
||||
type: FileTypeEnum.file,
|
||||
name: file.name,
|
||||
icon: 'pdf'
|
||||
});
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
icon: 'pdf'
|
||||
});
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
setFileList((state) => [...state, ...loadFiles]);
|
||||
}, []);
|
||||
setFileList((state) => [...state, ...loadFiles]);
|
||||
},
|
||||
[uploadFile]
|
||||
);
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
const textareaValue = TextareaDom.current?.value || '';
|
||||
|
@@ -12,12 +12,21 @@ import { formatPrice } from '@fastgpt/global/support/wallet/bill/tools';
|
||||
import Markdown from '../Markdown';
|
||||
import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constant';
|
||||
|
||||
function Row({ label, value }: { label: string; value?: string | number }) {
|
||||
function Row({
|
||||
label,
|
||||
value,
|
||||
rawDom
|
||||
}: {
|
||||
label: string;
|
||||
value?: string | number;
|
||||
rawDom?: React.ReactNode;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const val = value || rawDom;
|
||||
const strValue = `${value}`;
|
||||
const isCodeBlock = strValue.startsWith('~~~json');
|
||||
|
||||
return value !== undefined && value !== '' && value !== 'undefined' ? (
|
||||
return val !== undefined && val !== '' && val !== 'undefined' ? (
|
||||
<Box mb={3}>
|
||||
<Box fontSize={['sm', 'md']} mb={isCodeBlock ? 0 : 1} flex={'0 0 90px'}>
|
||||
{label}:
|
||||
@@ -29,7 +38,8 @@ function Row({ label, value }: { label: string; value?: string | number }) {
|
||||
? { transform: 'translateY(-3px)' }
|
||||
: { px: 3, py: 1, border: theme.borders.base })}
|
||||
>
|
||||
<Markdown source={strValue} />
|
||||
{value && <Markdown source={strValue} />}
|
||||
{rawDom}
|
||||
</Box>
|
||||
</Box>
|
||||
) : null;
|
||||
@@ -113,12 +123,28 @@ const WholeResponseModal = ({
|
||||
<Row label={t('chat.response.module maxToken')} value={activeModule?.maxToken} />
|
||||
<Row
|
||||
label={t('chat.response.module historyPreview')}
|
||||
value={(() => {
|
||||
if (!activeModule?.historyPreview) return '';
|
||||
return activeModule.historyPreview
|
||||
.map((item, i) => `**${item.obj}**\n${item.value}`)
|
||||
.join('\n\n---\n\n');
|
||||
})()}
|
||||
rawDom={
|
||||
activeModule.historyPreview ? (
|
||||
<>
|
||||
{activeModule.historyPreview?.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
_notLast={{
|
||||
borderBottom: '1px solid',
|
||||
borderBottomColor: 'myWhite.700',
|
||||
mb: 2
|
||||
}}
|
||||
pb={2}
|
||||
>
|
||||
<Box fontWeight={'bold'}>{item.obj}</Box>
|
||||
<Box whiteSpace={'pre-wrap'}>{item.value}</Box>
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
/>
|
||||
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
|
||||
<Row
|
||||
|
@@ -30,7 +30,7 @@ import {
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { feConfigs } from '@/web/common/system/staticData';
|
||||
import { eventBus } from '@/web/common/utils/eventbus';
|
||||
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
|
||||
import { adaptChat2GptMessages } from '@fastgpt/global/core/chat/adapt';
|
||||
import { useMarkdown } from '@/web/common/hooks/useMarkdown';
|
||||
import { ModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
@@ -48,7 +48,7 @@ import type { AdminMarkType } from './SelectMarkCollection';
|
||||
|
||||
import MyIcon from '@/components/Icon';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import Markdown, { CodeClassName } from '@/components/Markdown';
|
||||
import MySelect from '@/components/Select';
|
||||
import MyTooltip from '../MyTooltip';
|
||||
import ChatBoxDivider from '@/components/core/chat/Divider';
|
||||
@@ -64,6 +64,7 @@ import { splitGuideModule } from '@fastgpt/global/core/module/utils';
|
||||
import type { AppTTSConfigType } from '@fastgpt/global/core/module/type.d';
|
||||
import MessageInput from './MessageInput';
|
||||
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
|
||||
|
||||
@@ -132,6 +133,7 @@ const ChatBox = (
|
||||
const ChatBoxRef = useRef<HTMLDivElement>(null);
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { shareId } = router.query as { shareId?: string };
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { isPc, setLoading } = useSystemStore();
|
||||
@@ -258,7 +260,7 @@ const ChatBox = (
|
||||
const result = await postQuestionGuide(
|
||||
{
|
||||
messages: adaptChat2GptMessages({ messages: history, reserveId: false }).slice(-6),
|
||||
shareId: router.query.shareId as string
|
||||
shareId
|
||||
},
|
||||
abortSignal
|
||||
);
|
||||
@@ -270,7 +272,7 @@ const ChatBox = (
|
||||
}
|
||||
} catch (error) {}
|
||||
},
|
||||
[questionGuide, scrollToBottom, router.query.shareId]
|
||||
[questionGuide, scrollToBottom, shareId]
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -323,7 +325,6 @@ const ChatBox = (
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
|
||||
try {
|
||||
// create abort obj
|
||||
const abortSignal = new AbortController();
|
||||
@@ -518,16 +519,22 @@ const ChatBox = (
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', windowMessage);
|
||||
eventBus.on('guideClick', ({ text }: { text: string }) => {
|
||||
|
||||
eventBus.on(EventNameEnum.sendQuestion, ({ text }: { text: string }) => {
|
||||
if (!text) return;
|
||||
handleSubmit((data) => sendPrompt(data, text))();
|
||||
});
|
||||
eventBus.on(EventNameEnum.editQuestion, ({ text }: { text: string }) => {
|
||||
if (!text) return;
|
||||
resetInputVal(text);
|
||||
});
|
||||
|
||||
return () => {
|
||||
eventBus.off('guideClick');
|
||||
eventBus.off(EventNameEnum.sendQuestion);
|
||||
eventBus.off(EventNameEnum.editQuestion);
|
||||
window.removeEventListener('message', windowMessage);
|
||||
};
|
||||
}, [handleSubmit, sendPrompt]);
|
||||
}, [handleSubmit, resetInputVal, sendPrompt]);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'}>
|
||||
@@ -757,40 +764,30 @@ const ChatBox = (
|
||||
<Box textAlign={'left'} mt={['6px', 2]}>
|
||||
<Card bg={'white'} {...MessageCardStyle}>
|
||||
<Markdown
|
||||
source={item.value}
|
||||
source={(() => {
|
||||
const text = item.value as string;
|
||||
|
||||
// replace quote tag: [source1] 标识第一个来源,需要提取数字1,从而去数组里查找来源
|
||||
const quoteReg = /\[source:(.+)\]/g;
|
||||
const replaceText = text.replace(quoteReg, `[QUOTE SIGN]($1)`);
|
||||
|
||||
// question guide
|
||||
if (
|
||||
index === chatHistory.length - 1 &&
|
||||
!isChatting &&
|
||||
questionGuides.length > 0
|
||||
) {
|
||||
return `${replaceText}\n\`\`\`${
|
||||
CodeClassName.questionGuide
|
||||
}\n${JSON.stringify(questionGuides)}`;
|
||||
}
|
||||
return replaceText;
|
||||
})()}
|
||||
isChatting={index === chatHistory.length - 1 && isChatting}
|
||||
/>
|
||||
|
||||
<ResponseTags responseData={item.responseData} />
|
||||
{/* question guide */}
|
||||
{index === chatHistory.length - 1 &&
|
||||
!isChatting &&
|
||||
questionGuides.length > 0 && (
|
||||
<Box mt={2}>
|
||||
<ChatBoxDivider
|
||||
icon="core/chat/QGFill"
|
||||
text={t('chat.Question Guide Tips')}
|
||||
/>
|
||||
<Flex alignItems={'center'} flexWrap={'wrap'} gap={2}>
|
||||
{questionGuides.map((item) => (
|
||||
<Button
|
||||
key={item}
|
||||
borderRadius={'md'}
|
||||
variant={'outline'}
|
||||
colorScheme={'gray'}
|
||||
size={'xs'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
h={'auto'}
|
||||
py={1}
|
||||
onClick={() => {
|
||||
resetInputVal(item);
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* admin mark content */}
|
||||
{showMarkIcon && item.adminFeedback && (
|
||||
<Box>
|
||||
|
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1701930523211"
|
||||
class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9398"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128">
|
||||
<path
|
||||
d="M742.4 179.2H281.6a128 128 0 0 0-128 128v596.7104L298.496 768H742.4a128 128 0 0 0 128-128V307.2a128 128 0 0 0-128-128zM281.6 230.4h460.8a76.8 76.8 0 0 1 76.8 76.8v332.8a76.8 76.8 0 0 1-76.8 76.8H278.272L204.8 785.664V307.2a76.8 76.8 0 0 1 76.8-76.8z"
|
||||
p-id="9399"></path>
|
||||
<path
|
||||
d="M534.016 525.2608v-0.1792c0-66.9952 36.3008-129.1008 95.6416-163.6352a22.3744 22.3744 0 0 1 30.1312 7.2192 20.8384 20.8384 0 0 1-7.424 29.1584 150.7584 150.7584 0 0 0-59.8528 64.0768c27.0848-2.9184 53.248 10.624 65.7664 34.048a62.0288 62.0288 0 0 1-9.3952 71.6032 67.328 67.328 0 0 1-72.4992 17.0752c-25.472-9.3696-42.3424-32.9984-42.368-59.392z m-175.616 0v-0.1792c0-66.9952 36.3008-129.1008 95.6416-163.6352a22.3744 22.3744 0 0 1 30.1568 7.2192 20.8384 20.8384 0 0 1-7.4496 29.1584 150.7584 150.7584 0 0 0-59.8528 64.0768c27.1104-2.9184 53.248 10.624 65.792 34.048a62.0288 62.0288 0 0 1-9.4208 71.6032 67.328 67.328 0 0 1-72.4736 17.0752c-25.4976-9.3696-42.3424-32.9984-42.3936-59.392z"
|
||||
p-id="9400"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1701927696489"
|
||||
class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5076"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128">
|
||||
<path
|
||||
d="M1015.485274 476.463913c-7.599113-15.198226-20.197642-27.796755-35.395868-35.395868L114.790411 8.418547c-18.997782-9.498891-40.495273-10.998716-60.592927-4.299498S17.901721 25.01661 8.40283 43.914404C-1.496015 63.812081-2.695875 87.009374 5.303191 107.806946l155.381863 404.252812L5.303191 916.212582c-7.599113 19.797689-6.999183 41.395168 1.599814 60.692915 8.598996 19.397736 24.297164 34.196008 43.994864 41.795122 9.198926 3.499591 18.797806 5.299381 28.496674 5.299381 12.198576 0 24.397152-2.799673 35.495856-8.299031L980.089406 582.951483c39.095436-19.597712 54.993581-67.392133 35.395868-106.48757zM79.094578 944.509279l151.182353-392.954131h310.363771c21.797456 0 39.49539-17.697934 39.49539-39.49539s-17.697934-39.49539-39.49539-39.49539H230.276931L79.494531 79.210284l865.199007 432.649497L79.094578 944.509279z"
|
||||
p-id="5077"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@@ -103,6 +103,8 @@ const iconPaths = {
|
||||
'core/app/tts': () => import('./icons/core/app/tts.svg'),
|
||||
'core/app/headphones': () => import('./icons/core/app/headphones.svg'),
|
||||
'common/playLight': () => import('./icons/common/playLight.svg'),
|
||||
'core/chat/quoteSign': () => import('./icons/core/chat/quoteSign.svg'),
|
||||
'core/chat/sendLight': () => import('./icons/core/chat/sendLight.svg'),
|
||||
'core/chat/sendFill': () => import('./icons/core/chat/sendFill.svg'),
|
||||
'core/chat/recordFill': () => import('./icons/core/chat/recordFill.svg'),
|
||||
'core/chat/stopSpeechFill': () => import('./icons/core/chat/stopSpeechFill.svg'),
|
||||
|
@@ -315,7 +315,7 @@ const CodeLight = ({
|
||||
</Flex>
|
||||
</Flex>
|
||||
<SyntaxHighlighter style={codeLight as any} language={match?.[1]} PreTag="pre">
|
||||
{String(children)}
|
||||
{String(children).replace(/ /g, ' ')}
|
||||
</SyntaxHighlighter>
|
||||
</Box>
|
||||
);
|
||||
|
@@ -5,7 +5,7 @@ import RemarkGfm from 'remark-gfm';
|
||||
import RemarkMath from 'remark-math';
|
||||
import RehypeKatex from 'rehype-katex';
|
||||
import RemarkBreaks from 'remark-breaks';
|
||||
import { eventBus } from '@/web/common/utils/eventbus';
|
||||
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
import styles from '../index.module.scss';
|
||||
@@ -27,7 +27,7 @@ function MyLink(e: any) {
|
||||
textDecoration={'underline'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
eventBus.emit('guideClick', { text });
|
||||
eventBus.emit(EventNameEnum.sendQuestion, { text });
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
|
92
projects/app/src/components/Markdown/chat/QuestionGuide.tsx
Normal file
92
projects/app/src/components/Markdown/chat/QuestionGuide.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Flex, useTheme } from '@chakra-ui/react';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import ChatBoxDivider from '@/components/core/chat/Divider';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import MyIcon from '@/components/Icon';
|
||||
|
||||
const QuestionGuide = ({ text }: { text: string }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const questionGuides = useMemo(() => {
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
if (Array.isArray(json) && !json.find((item) => typeof item !== 'string')) {
|
||||
return json as string[];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
return questionGuides.length > 0 ? (
|
||||
<Box mt={2}>
|
||||
<ChatBoxDivider icon="core/chat/QGFill" text={t('chat.Question Guide Tips')} />
|
||||
<Flex alignItems={'center'} flexWrap={'wrap'} gap={2}>
|
||||
{questionGuides.map((text) => (
|
||||
<Flex
|
||||
key={text}
|
||||
alignItems={'center'}
|
||||
flexWrap={'wrap'}
|
||||
fontSize={'sm'}
|
||||
border={theme.borders.sm}
|
||||
py={'1px'}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
_hover={{
|
||||
'.controller': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
overflow={'hidden'}
|
||||
position={'relative'}
|
||||
>
|
||||
<Box className="textEllipsis" flex={'1 0 0'}>
|
||||
{text}
|
||||
</Box>
|
||||
<Box
|
||||
className="controller"
|
||||
display={['flex', 'none']}
|
||||
pr={2}
|
||||
position={'absolute'}
|
||||
right={0}
|
||||
left={0}
|
||||
justifyContent={'flex-end'}
|
||||
alignItems={'center'}
|
||||
h={'100%'}
|
||||
lineHeight={0}
|
||||
bg={`linear-gradient(to left, white,white min(60px,100%),rgba(255,255,255,0) 80%)`}
|
||||
>
|
||||
<MyTooltip label={t('core.chat.markdown.Edit Question')}>
|
||||
<MyIcon
|
||||
name={'edit'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'green.600'
|
||||
}}
|
||||
onClick={() => eventBus.emit(EventNameEnum.editQuestion, { text })}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={t('core.chat.markdown.Send Question')}>
|
||||
<MyIcon
|
||||
ml={4}
|
||||
name={'core/chat/sendLight'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'myBlue.600' }}
|
||||
onClick={() => eventBus.emit(EventNameEnum.sendQuestion, { text })}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</Box>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default React.memo(QuestionGuide);
|
@@ -1,64 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, useTheme } from '@chakra-ui/react';
|
||||
import { getFileAndOpen } from '@/web/core/dataset/utils';
|
||||
import { useToast } from '@/web/common/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
|
||||
type QuoteItemType = {
|
||||
file_id?: string;
|
||||
filename: string;
|
||||
};
|
||||
|
||||
const QuoteBlock = ({ code }: { code: string }) => {
|
||||
const theme = useTheme();
|
||||
const { toast } = useToast();
|
||||
const quoteList = useMemo(() => {
|
||||
try {
|
||||
return JSON.parse(code) as QuoteItemType[];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}, [code]);
|
||||
|
||||
return (
|
||||
<Box mt={3} pt={2} borderTop={theme.borders.base}>
|
||||
{quoteList.length > 0 ? (
|
||||
<>
|
||||
<Box>本次回答的引用:</Box>
|
||||
<Box as={'ol'}>
|
||||
{quoteList.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
as={'li'}
|
||||
{...(item.file_id
|
||||
? {
|
||||
textDecoration: 'underline',
|
||||
color: 'myBlue.800',
|
||||
cursor: 'pointer'
|
||||
}
|
||||
: {})}
|
||||
onClick={async () => {
|
||||
if (!item.file_id) return;
|
||||
try {
|
||||
await getFileAndOpen(item.file_id);
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: getErrText(error, '打开文件失败')
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.filename}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Box>正在生成引用……</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuoteBlock;
|
@@ -46,7 +46,7 @@ const MdImage = ({ src }: { src?: string }) => {
|
||||
/>
|
||||
<Modal isOpen={isOpen} onClose={onClose} isCentered>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW={'auto'} w="auto" bg={'transparent'}>
|
||||
<ModalContent boxShadow={'none'} maxW={'auto'} w="auto" bg={'transparent'}>
|
||||
<Image
|
||||
borderRadius={'md'}
|
||||
src={src}
|
||||
|
@@ -9,17 +9,26 @@ import 'katex/dist/katex.min.css';
|
||||
import styles from './index.module.scss';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import CodeLight from './CodeLight';
|
||||
import { Link, Button } from '@chakra-ui/react';
|
||||
import MyTooltip from '../MyTooltip';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
|
||||
import MyIcon from '../Icon';
|
||||
import { getFileAndOpen } from '@/web/core/dataset/utils';
|
||||
import { MARKDOWN_QUOTE_SIGN } from '@fastgpt/global/core/chat/constants';
|
||||
|
||||
const CodeLight = dynamic(() => import('./CodeLight'));
|
||||
const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'));
|
||||
const MdImage = dynamic(() => import('./img/Image'));
|
||||
const ChatGuide = dynamic(() => import('./chat/Guide'));
|
||||
const EChartsCodeBlock = dynamic(() => import('./img/EChartsCodeBlock'));
|
||||
const QuoteBlock = dynamic(() => import('./chat/Quote'));
|
||||
|
||||
const ChatGuide = dynamic(() => import('./chat/Guide'));
|
||||
const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'));
|
||||
const ImageBlock = dynamic(() => import('./chat/Image'));
|
||||
|
||||
export enum CodeClassName {
|
||||
guide = 'guide',
|
||||
questionGuide = 'questionGuide',
|
||||
mermaid = 'mermaid',
|
||||
echarts = 'echarts',
|
||||
quote = 'quote',
|
||||
@@ -37,12 +46,12 @@ function Code({ inline, className, children }: any) {
|
||||
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.quote) {
|
||||
return <QuoteBlock code={String(children)} />;
|
||||
}
|
||||
if (codeType === CodeClassName.img) {
|
||||
return <ImageBlock images={String(children)} />;
|
||||
}
|
||||
@@ -55,6 +64,52 @@ function Code({ inline, className, children }: any) {
|
||||
function Image({ src }: { src?: string }) {
|
||||
return <MdImage src={src} />;
|
||||
}
|
||||
function A({ children, ...props }: any) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// empty href link
|
||||
if (!props.href && typeof children?.[0] === 'string') {
|
||||
const text = useMemo(() => String(children), [children]);
|
||||
|
||||
return (
|
||||
<MyTooltip label={t('core.chat.markdown.Quick Question')}>
|
||||
<Button
|
||||
variant={'base'}
|
||||
size={'xs'}
|
||||
borderRadius={'md'}
|
||||
my={1}
|
||||
onClick={() => eventBus.emit(EventNameEnum.sendQuestion, { text })}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
</MyTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// quote link
|
||||
if (children?.length === 1 && typeof children?.[0] === 'string') {
|
||||
const text = String(children);
|
||||
if (text === MARKDOWN_QUOTE_SIGN && props.href) {
|
||||
return (
|
||||
<MyTooltip label={props.href}>
|
||||
<MyIcon
|
||||
name={'core/chat/quoteSign'}
|
||||
transform={'translateY(-2px)'}
|
||||
w={'18px'}
|
||||
color={'myBlue.600'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'myBlue.800'
|
||||
}}
|
||||
onClick={() => getFileAndOpen(props.href)}
|
||||
/>
|
||||
</MyTooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <Link {...props}>{children}</Link>;
|
||||
}
|
||||
|
||||
const Markdown = ({ source, isChatting = false }: { source: string; isChatting?: boolean }) => {
|
||||
const components = useMemo(
|
||||
@@ -62,14 +117,16 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
|
||||
img: Image,
|
||||
pre: 'div',
|
||||
p: 'div',
|
||||
code: Code
|
||||
code: Code,
|
||||
a: A
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const formatSource = source
|
||||
.replace(/\\n/g, '\n ')
|
||||
.replace(/(http[s]?:\/\/[^\s,。]+)([。,])/g, '$1 $2');
|
||||
.replace(/(http[s]?:\/\/[^\s,。]+)([。,])/g, '$1 $2')
|
||||
.replace(/\n*(\[QUOTE SIGN\]\(.*\))/g, '$1');
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
|
@@ -82,7 +82,7 @@ const MyRadio = ({
|
||||
<Box pr={2}>
|
||||
<Box>{t(item.title)}</Box>
|
||||
{!!item.desc && (
|
||||
<Box fontSize={'sm'} color={'myGray.500'}>
|
||||
<Box fontSize={['xs', 'sm']} color={'myGray.500'}>
|
||||
{t(item.desc)}
|
||||
</Box>
|
||||
)}
|
||||
|
@@ -18,6 +18,7 @@ type DatasetParamsProps = {
|
||||
limit?: number;
|
||||
searchMode: `${DatasetSearchModeEnum}`;
|
||||
searchEmptyText?: string;
|
||||
maxTokens?: number;
|
||||
};
|
||||
|
||||
const DatasetParamsModal = ({
|
||||
@@ -25,6 +26,7 @@ const DatasetParamsModal = ({
|
||||
limit,
|
||||
similarity,
|
||||
searchMode = DatasetSearchModeEnum.embedding,
|
||||
maxTokens = 3000,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: DatasetParamsProps & { onClose: () => void; onSuccess: (e: DatasetParamsProps) => void }) => {
|
||||
@@ -52,8 +54,8 @@ const DatasetParamsModal = ({
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
iconSrc="/imgs/modal/params.svg"
|
||||
title={'搜索参数调整'}
|
||||
minW={['90vw', '500px']}
|
||||
title={t('core.dataset.search.Dataset Search Params')}
|
||||
w={['90vw', '550px']}
|
||||
h={['90vh', 'auto']}
|
||||
overflow={'unset'}
|
||||
isCentered={searchEmptyText !== undefined}
|
||||
@@ -78,36 +80,42 @@ const DatasetParamsModal = ({
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '0', value: 0 },
|
||||
{ label: '1', value: 1 }
|
||||
]}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={getValues(ModuleInputKeyEnum.datasetSimilarity) ?? 0.5}
|
||||
onChange={(val) => {
|
||||
setValue(ModuleInputKeyEnum.datasetSimilarity, val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
<Box flex={1} mx={4}>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '0', value: 0 },
|
||||
{ label: '1', value: 1 }
|
||||
]}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={getValues(ModuleInputKeyEnum.datasetSimilarity) ?? 0.5}
|
||||
onChange={(val) => {
|
||||
setValue(ModuleInputKeyEnum.datasetSimilarity, val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{limit !== undefined && (
|
||||
<Box display={['block', 'flex']} py={8}>
|
||||
<Box flex={'0 0 100px'} mb={[8, 0]}>
|
||||
{t('core.dataset.search.Top K')}
|
||||
{t('core.dataset.search.Max Tokens')}
|
||||
<MyTooltip label={t('core.dataset.search.Max Tokens Tips')} forceShow>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
<Box flex={1} mx={4}>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '1', value: 1 },
|
||||
{ label: '30', value: 30 }
|
||||
{ label: '300', value: 300 },
|
||||
{ label: maxTokens, value: maxTokens }
|
||||
]}
|
||||
min={1}
|
||||
max={30}
|
||||
value={getValues(ModuleInputKeyEnum.datasetLimit) ?? 5}
|
||||
min={300}
|
||||
max={maxTokens}
|
||||
step={10}
|
||||
value={getValues(ModuleInputKeyEnum.datasetLimit) ?? 1000}
|
||||
onChange={(val) => {
|
||||
setValue(ModuleInputKeyEnum.datasetLimit, val);
|
||||
setRefresh(!refresh);
|
||||
|
@@ -17,7 +17,7 @@ import {
|
||||
Grid,
|
||||
Switch
|
||||
} from '@chakra-ui/react';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { onChangeNode, useFlowProviderStore } from '../../FlowProvider';
|
||||
@@ -37,6 +37,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import type { EditFieldModeType, EditFieldType } from '../modules/FieldEditModal';
|
||||
import { feConfigs } from '@/web/common/system/staticData';
|
||||
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constant';
|
||||
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
|
||||
|
||||
const FieldEditModal = dynamic(() => import('../modules/FieldEditModal'));
|
||||
const SelectAppModal = dynamic(() => import('../../SelectAppModal'));
|
||||
@@ -635,9 +636,12 @@ const SelectAppRender = React.memo(function SelectAppRender({ item, moduleId }:
|
||||
});
|
||||
|
||||
const SelectDatasetParamsRender = React.memo(function SelectDatasetParamsRender({
|
||||
item,
|
||||
inputs = [],
|
||||
moduleId
|
||||
}: RenderProps) {
|
||||
const { nodes } = useFlowProviderStore();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [data, setData] = useState({
|
||||
searchMode: DatasetSearchModeEnum.embedding,
|
||||
@@ -645,6 +649,23 @@ const SelectDatasetParamsRender = React.memo(function SelectDatasetParamsRender(
|
||||
similarity: 0.5
|
||||
});
|
||||
|
||||
const tokenLimit = useMemo(() => {
|
||||
let maxTokens = 3000;
|
||||
|
||||
nodes.forEach((item) => {
|
||||
if (item.type === FlowNodeTypeEnum.chatNode) {
|
||||
const model =
|
||||
item.data.inputs.find((item) => item.key === ModuleInputKeyEnum.aiModel)?.value || '';
|
||||
const quoteMaxToken =
|
||||
chatModelList.find((item) => item.model === model)?.quoteMaxToken || 3000;
|
||||
|
||||
maxTokens = Math.max(maxTokens, quoteMaxToken);
|
||||
}
|
||||
});
|
||||
|
||||
return maxTokens;
|
||||
}, [nodes]);
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -671,6 +692,7 @@ const SelectDatasetParamsRender = React.memo(function SelectDatasetParamsRender(
|
||||
{isOpen && (
|
||||
<DatasetParamsModal
|
||||
{...data}
|
||||
maxTokens={tokenLimit}
|
||||
onClose={onClose}
|
||||
onSuccess={(e) => {
|
||||
for (let key in e) {
|
||||
|
Reference in New Issue
Block a user