perf: markdown more wrap (#365)

This commit is contained in:
Archer
2023-10-02 20:19:09 +08:00
committed by GitHub
parent 36f5648cae
commit bf172fab81
31 changed files with 657 additions and 560 deletions

View File

@@ -15,8 +15,9 @@ OPENAI_BASE_URL=https://api.openai.com/v1
# 通用key。可以是 openai 的也可以是 oneapi 的。
# 此处逻辑:优先走 ONEAPI_URL如果填写了 ONEAPI_URLkey 也需要是 ONEAPI 的 key
CHAT_API_KEY=sk-xxxx
# db
# mongo 数据库连接参数
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/fastgpt?authSource=admin
# PG 数据库连接参数
PG_URL=postgresql://username:password@host:port/postgres
# 首页路径
HOME_URL=/

View File

@@ -1,16 +1,4 @@
{
"FeConfig": {
"show_emptyChat": true,
"show_contact": true,
"show_git": true,
"show_doc": true,
"systemTitle": "FastGPT",
"authorText": "Made by FastGPT Team.",
"limit": {
"exportLimitMinutes": 0
},
"scripts": []
},
"SystemParams": {
"vectorMaxProcess": 15,
"qaMaxProcess": 15,

View File

@@ -237,7 +237,7 @@
"key tips": "你可以使用 API 秘钥访问一些特定的接口"
},
"outlink": {
"Copy Iframe": "复制嵌入",
"Copy Iframe": "嵌入网页",
"Copy Link": "复制",
"Create API Key": "创建新 Key",
"Create Link": "创建链接",

View File

@@ -31,7 +31,7 @@ const SelectDataset = ({
setParentId={setParentId}
tips={t('chat.Select Mark Kb Desc')}
>
<ModalBody flex={['1 0 0', '0 0 auto']} maxH={'80vh'} overflowY={'auto'}>
<ModalBody flex={'1 0 0'} overflowY={'auto'}>
<Grid
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
gridGap={3}

View File

@@ -16,10 +16,20 @@ import {
ExportChatType
} from '@/types/chat';
import { useToast } from '@/hooks/useToast';
import { voiceBroadcast, cancelBroadcast, hasVoiceApi } from '@/utils/web/voice';
import { useAudioPlay } from '@/utils/web/voice';
import { getErrText } from '@/utils/tools';
import { useCopyData } from '@/hooks/useCopyData';
import { Box, Card, Flex, Input, Textarea, Button, useTheme, BoxProps } from '@chakra-ui/react';
import {
Box,
Card,
Flex,
Input,
Textarea,
Button,
useTheme,
BoxProps,
FlexProps
} from '@chakra-ui/react';
import { feConfigs } from '@/store/static';
import { event } from '@/utils/plugin/eventbus';
import { adaptChat2GptMessages } from '@/utils/common/adapt/message';
@@ -57,7 +67,9 @@ import { splitGuideModule } from './utils';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
const textareaMinH = '22px';
type generatingMessageProps = { text?: string; name?: string; status?: 'running' | 'finish' };
export type StartChatFnProps = {
chatList: ChatSiteItemType[];
messages: MessageItemType[];
@@ -79,55 +91,23 @@ enum FeedbackTypeEnum {
hidden = 'hidden'
}
const VariableLabel = ({
required = false,
children
}: {
required?: boolean;
children: React.ReactNode | string;
}) => (
<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>
);
const Empty = () => {
const { data: chatProblem } = useMarkdown({ url: '/chatProblem.md' });
const { data: versionIntro } = useMarkdown({ url: '/versionIntro.md' });
return (
<Box pt={6} w={'85%'} maxW={'600px'} m={'auto'} alignItems={'center'} justifyContent={'center'}>
{/* version intro */}
<Card p={4} mb={10} minH={'200px'}>
<Markdown source={versionIntro} />
</Card>
<Card p={4} minH={'600px'}>
<Markdown source={chatProblem} />
</Card>
</Box>
);
};
const ChatAvatar = ({ src, type }: { src?: string; type: 'Human' | 'AI' }) => {
const theme = useTheme();
return (
<Box
w={['28px', '34px']}
h={['28px', '34px']}
p={'2px'}
borderRadius={'lg'}
border={theme.borders.base}
boxShadow={'0 0 5px rgba(0,0,0,0.1)'}
bg={type === 'Human' ? 'white' : 'myBlue.100'}
>
<Avatar src={src} w={'100%'} h={'100%'} />
</Box>
);
type Props = {
feedbackType?: `${FeedbackTypeEnum}`;
showMarkIcon?: boolean; // admin mark dataset
showVoiceIcon?: boolean;
showEmptyIntro?: boolean;
chatId?: string;
appAvatar?: string;
userAvatar?: string;
userGuideModule?: AppModuleItemType;
active?: boolean;
onUpdateVariable?: (e: Record<string, any>) => void;
onStartChat?: (e: StartChatFnProps) => Promise<{
responseText: string;
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType[];
isNewChat?: boolean;
}>;
onDelMessage?: (e: { contentId?: string; index: number }) => void;
};
const ChatBox = (
@@ -144,31 +124,13 @@ const ChatBox = (
onUpdateVariable,
onStartChat,
onDelMessage
}: {
feedbackType?: `${FeedbackTypeEnum}`;
showMarkIcon?: boolean; // admin mark dataset
showVoiceIcon?: boolean;
showEmptyIntro?: boolean;
chatId?: string;
appAvatar?: string;
userAvatar?: string;
userGuideModule?: AppModuleItemType;
active?: boolean;
onUpdateVariable?: (e: Record<string, any>) => void;
onStartChat?: (e: StartChatFnProps) => Promise<{
responseText: string;
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType[];
isNewChat?: boolean;
}>;
onDelMessage?: (e: { contentId?: string; index: number }) => void;
},
}: Props,
ref: ForwardedRef<ComponentRef>
) => {
const ChatBoxRef = useRef<HTMLDivElement>(null);
const theme = useTheme();
const router = useRouter();
const { t } = useTranslation();
const { copyData } = useCopyData();
const { toast } = useToast();
const { isPc } = useGlobalStore();
const TextareaDom = useRef<HTMLTextAreaElement>(null);
@@ -252,6 +214,7 @@ const ChatBox = (
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const generatingMessage = useCallback(
// concat text to end of message
({ text = '', status, name }: generatingMessageProps) => {
setChatHistory((state) =>
state.map((item, index) => {
@@ -277,14 +240,6 @@ const ChatBox = (
[generatingScroll, setChatHistory]
);
// 复制内容
const onclickCopy = useCallback(
(value: string) => {
copyData(value);
},
[copyData]
);
// 重置输入内容
const resetInputVal = useCallback((val: string) => {
if (!TextareaDom.current) return;
@@ -477,6 +432,17 @@ const ChatBox = (
},
[chatHistory, onDelMessage, sendPrompt, variables]
);
// delete one message
const delOneMessage = useCallback(
({ dataId, index }: { dataId?: string; index: number }) => {
setChatHistory((state) => state.filter((chat) => chat.dataId !== dataId));
onDelMessage?.({
contentId: dataId,
index
});
},
[onDelMessage]
);
// output data
useImperativeHandle(ref, () => ({
@@ -497,23 +463,7 @@ const ChatBox = (
scrollToBottom
}));
const controlIconStyle = {
w: '14px',
cursor: 'pointer',
p: 1,
bg: 'white',
borderRadius: 'lg',
boxShadow: '0 0 5px rgba(0,0,0,0.1)',
border: theme.borders.base,
mr: 3
};
const controlContainerStyle = {
className: 'control',
color: 'myGray.400',
display: 'flex',
pl: 1,
mt: 2
};
/* style start */
const MessageCardStyle: BoxProps = {
px: 4,
py: 3,
@@ -547,6 +497,7 @@ const ChatBox = (
name: t(chatContent.moduleName || 'Running')
};
}, [chatHistory, isChatting, t]);
/* style end */
// page change and abort request
useEffect(() => {
@@ -556,23 +507,9 @@ const ChatBox = (
if (!isNewChatReplace.current) {
questionGuideController.current?.abort('leave');
}
// close voice
cancelBroadcast();
};
}, [router.query]);
// page destroy and abort request
useEffect(() => {
const listen = () => {
cancelBroadcast();
};
window.addEventListener('beforeunload', listen);
return () => {
window.removeEventListener('beforeunload', listen);
};
}, []);
// add guide text listener
useEffect(() => {
event.on('guideClick', ({ text }: { text: string }) => {
@@ -678,45 +615,17 @@ const ChatBox = (
<>
{/* control icon */}
<Flex w={'100%'} alignItems={'center'} justifyContent={'flex-end'}>
<Flex {...controlContainerStyle} justifyContent={'flex-end'} mr={3}>
<MyTooltip label={t('common.Copy')}>
<MyIcon
{...controlIconStyle}
name={'copy'}
_hover={{ color: 'myBlue.700' }}
onClick={() => onclickCopy(item.value)}
/>
</MyTooltip>
{!!onDelMessage && (
<MyTooltip label={t('chat.retry')}>
<MyIcon
{...controlIconStyle}
name={'retryLight'}
_hover={{ color: 'green.500' }}
onClick={() => retryInput(index)}
/>
</MyTooltip>
)}
{onDelMessage && (
<MyTooltip label={t('common.Delete')}>
<MyIcon
{...controlIconStyle}
mr={0}
name={'delete'}
_hover={{ color: 'red.600' }}
onClick={() => {
setChatHistory((state) =>
state.filter((chat) => chat.dataId !== item.dataId)
);
onDelMessage({
contentId: item.dataId,
index
});
}}
/>
</MyTooltip>
)}
</Flex>
<ChatController
chat={item}
onDelete={
onDelMessage
? () => {
delOneMessage({ dataId: item.dataId, index });
}
: undefined
}
onRetry={() => retryInput(index)}
/>
<ChatAvatar src={userAvatar} type={'Human'} />
</Flex>
{/* content */}
@@ -737,57 +646,23 @@ const ChatBox = (
{item.obj === 'AI' && (
<>
{/* control icon */}
<Flex w={'100%'} alignItems={'flex-end'}>
<Flex w={'100%'} alignItems={'center'}>
<ChatAvatar src={appAvatar} type={'AI'} />
<Flex
{...controlContainerStyle}
ml={3}
<ChatController
ml={2}
chat={item}
display={index === chatHistory.length - 1 && isChatting ? 'none' : 'flex'}
>
<MyTooltip label={'复制'}>
<MyIcon
{...controlIconStyle}
name={'copy'}
_hover={{ color: 'myBlue.700' }}
onClick={() => onclickCopy(item.value)}
/>
</MyTooltip>
{onDelMessage && (
<MyTooltip label={'删除'}>
<MyIcon
{...controlIconStyle}
name={'delete'}
_hover={{ color: 'red.600' }}
onClick={() => {
setChatHistory((state) =>
state.filter((chat) => chat.dataId !== item.dataId)
);
onDelMessage({
contentId: item.dataId,
index
});
}}
/>
</MyTooltip>
)}
{showVoiceIcon && hasVoiceApi && (
<MyTooltip label={'语音播报'}>
<MyIcon
{...controlIconStyle}
name={'voice'}
_hover={{ color: '#E74694' }}
onClick={() => voiceBroadcast({ text: item.value })}
/>
</MyTooltip>
)}
{/* admin mark icon */}
{showMarkIcon && (
<MyTooltip label={t('chat.Mark')}>
<MyIcon
{...controlIconStyle}
name={'markLight'}
_hover={{ color: '#67c13b' }}
onClick={() => {
showVoiceIcon={showVoiceIcon}
onDelete={
onDelMessage
? () => {
delOneMessage({ dataId: item.dataId, index });
}
: undefined
}
onMark={
showMarkIcon
? () => {
if (!item.dataId) return;
if (item.adminFeedback) {
setAdminMarkData({
@@ -804,67 +679,39 @@ const ChatBox = (
a: item.value
});
}
}}
/>
</MyTooltip>
)}
{feedbackType === FeedbackTypeEnum.admin && (
<MyTooltip label={t('chat.Read User Feedback')}>
<MyIcon
display={item.userFeedback ? 'block' : 'none'}
{...controlIconStyle}
color={'white'}
bg={'#FC9663'}
fontWeight={'bold'}
name={'badLight'}
onClick={() =>
}
: undefined
}
onReadFeedback={
feedbackType === FeedbackTypeEnum.admin
? () =>
setReadFeedbackData({
chatItemId: item.dataId || '',
content: item.userFeedback || '',
isMarked: !!item.adminFeedback
})
}
/>
</MyTooltip>
)}
{feedbackType === FeedbackTypeEnum.user && (
<MyTooltip
label={
item.userFeedback
? `取消反馈。\n您当前反馈内容为:\n${item.userFeedback}`
: '反馈'
}
>
<MyIcon
{...controlIconStyle}
{...(!!item.userFeedback
? {
color: 'white',
bg: '#FC9663',
fontWeight: 'bold',
onClick: () => {
if (!item.dataId) return;
setChatHistory((state) =>
state.map((chatItem) =>
chatItem.dataId === item.dataId
? { ...chatItem, userFeedback: undefined }
: chatItem
)
);
try {
userUpdateChatFeedback({ chatItemId: item.dataId });
} catch (error) {}
}
}
: {
_hover: { color: '#FB7C3C' },
onClick: () => setFeedbackId(item.dataId)
})}
name={'badLight'}
/>
</MyTooltip>
)}
</Flex>
: undefined
}
onFeedback={
feedbackType === FeedbackTypeEnum.user
? item.userFeedback
? () => {
if (!item.dataId) return;
setChatHistory((state) =>
state.map((chatItem) =>
chatItem.dataId === item.dataId
? { ...chatItem, userFeedback: undefined }
: chatItem
)
);
try {
userUpdateChatFeedback({ chatItemId: item.dataId });
} catch (error) {}
}
: () => setFeedbackId(item.dataId)
: undefined
}
/>
{/* chatting status */}
{statusBoxData && index === chatHistory.length - 1 && (
<Flex
@@ -960,7 +807,7 @@ const ChatBox = (
</Box>
</Box>
</Box>
{/* input */}
{/* message input */}
{onStartChat && variableIsFinish && active ? (
<Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(750px, 100%)']} px={[0, 5]}>
<Box
@@ -999,8 +846,8 @@ const ChatBox = (
textarea.style.height = `${textarea.scrollHeight}px`;
}}
onKeyDown={(e) => {
// 触发快捷发送
if (isPc && e.keyCode === 13 && !e.shiftKey) {
// enter send.(pc or iframe && enter and unPress shift)
if ((isPc || window !== parent) && e.keyCode === 13 && !e.shiftKey) {
handleSubmit((data) => sendPrompt(data, TextareaDom.current?.value))();
e.preventDefault();
}
@@ -1089,6 +936,7 @@ const ChatBox = (
}}
/>
)}
{/* admin mark data */}
{showMarkIcon && (
<>
{/* select one dataset to insert markData */}
@@ -1235,3 +1083,207 @@ export const useChatBox = () => {
onExportChat
};
};
function VariableLabel({
required = false,
children
}: {
required?: boolean;
children: React.ReactNode | 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>
);
}
function ChatAvatar({ src, type }: { src?: string; type: 'Human' | 'AI' }) {
const theme = useTheme();
return (
<Box
w={['28px', '34px']}
h={['28px', '34px']}
p={'2px'}
borderRadius={'lg'}
border={theme.borders.base}
boxShadow={'0 0 5px rgba(0,0,0,0.1)'}
bg={type === 'Human' ? 'white' : 'myBlue.100'}
>
<Avatar src={src} w={'100%'} h={'100%'} />
</Box>
);
}
function Empty() {
const { data: chatProblem } = useMarkdown({ url: '/chatProblem.md' });
const { data: versionIntro } = useMarkdown({ url: '/versionIntro.md' });
return (
<Box pt={6} w={'85%'} maxW={'600px'} m={'auto'} alignItems={'center'} justifyContent={'center'}>
{/* version intro */}
<Card p={4} mb={10} minH={'200px'}>
<Markdown source={versionIntro} />
</Card>
<Card p={4} minH={'600px'}>
<Markdown source={chatProblem} />
</Card>
</Box>
);
}
function ChatController({
chat,
display,
showVoiceIcon,
onReadFeedback,
onMark,
onRetry,
onDelete,
onFeedback,
ml,
mr
}: {
chat: ChatSiteItemType;
showVoiceIcon?: boolean;
onRetry?: () => void;
onDelete?: () => void;
onMark?: () => void;
onReadFeedback?: () => void;
onFeedback?: () => void;
} & FlexProps) {
const theme = useTheme();
const { t } = useTranslation();
const { copyData } = useCopyData();
const { audioLoading, audioPlaying, hasAudio, playAudio, cancelAudio } = useAudioPlay({});
const controlIconStyle = {
w: '14px',
cursor: 'pointer',
p: 1,
bg: 'white',
borderRadius: 'lg',
boxShadow: '0 0 5px rgba(0,0,0,0.1)',
border: theme.borders.base,
mr: 3
};
const controlContainerStyle = {
className: 'control',
color: 'myGray.400',
display: 'flex',
pl: 1
};
return (
<Flex {...controlContainerStyle} ml={ml} mr={mr} display={display}>
<MyTooltip label={'复制'}>
<MyIcon
{...controlIconStyle}
name={'copy'}
_hover={{ color: 'myBlue.700' }}
onClick={() => copyData(chat.value)}
/>
</MyTooltip>
{!!onDelete && (
<>
{onRetry && (
<MyTooltip label={t('chat.retry')}>
<MyIcon
{...controlIconStyle}
name={'retryLight'}
_hover={{ color: 'green.500' }}
onClick={onRetry}
/>
</MyTooltip>
)}
<MyTooltip label={'删除'}>
<MyIcon
{...controlIconStyle}
name={'delete'}
_hover={{ color: 'red.600' }}
onClick={onDelete}
/>
</MyTooltip>
</>
)}
{showVoiceIcon &&
hasAudio &&
(audioLoading ? (
<MyTooltip label={'加载中...'}>
<MyIcon {...controlIconStyle} name={'loading'} />
</MyTooltip>
) : audioPlaying ? (
<MyTooltip label={'终止播放'}>
<MyIcon
{...controlIconStyle}
name={'pause'}
_hover={{ color: '#E74694' }}
onClick={() => cancelAudio()}
/>
</MyTooltip>
) : (
<MyTooltip label={'语音播报'}>
<MyIcon
{...controlIconStyle}
name={'voice'}
_hover={{ color: '#E74694' }}
onClick={() => playAudio(chat.value)}
/>
</MyTooltip>
))}
{!!onMark && (
<MyTooltip label={t('chat.Mark')}>
<MyIcon
{...controlIconStyle}
name={'markLight'}
_hover={{ color: '#67c13b' }}
onClick={onMark}
/>
</MyTooltip>
)}
{!!onReadFeedback && (
<MyTooltip label={t('chat.Read User Feedback')}>
<MyIcon
display={chat.userFeedback ? 'block' : 'none'}
{...controlIconStyle}
color={'white'}
bg={'#FC9663'}
fontWeight={'bold'}
name={'badLight'}
onClick={onReadFeedback}
/>
</MyTooltip>
)}
{!!onFeedback && (
<MyTooltip
label={chat.userFeedback ? `取消反馈。\n您当前反馈内容为:\n${chat.userFeedback}` : '反馈'}
>
<MyIcon
{...controlIconStyle}
{...(!!chat.userFeedback
? {
color: 'white',
bg: '#FC9663',
fontWeight: 'bold',
onClick: onFeedback
}
: {
_hover: { color: '#FB7C3C' },
onClick: onFeedback
})}
name={'badLight'}
/>
</MyTooltip>
)}
</Flex>
);
}

View File

@@ -0,0 +1 @@
<?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="1696179048209" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4182" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M373.333333 85.333333H266.666667a53.393333 53.393333 0 0 0-53.333334 53.333334v746.666666a53.393333 53.393333 0 0 0 53.333334 53.333334h106.666666a53.393333 53.393333 0 0 0 53.333334-53.333334V138.666667a53.393333 53.393333 0 0 0-53.333334-53.333334z m10.666667 800a10.666667 10.666667 0 0 1-10.666667 10.666667H266.666667a10.666667 10.666667 0 0 1-10.666667-10.666667V138.666667a10.666667 10.666667 0 0 1 10.666667-10.666667h106.666666a10.666667 10.666667 0 0 1 10.666667 10.666667z m373.333333-800H650.666667a53.393333 53.393333 0 0 0-53.333334 53.333334v746.666666a53.393333 53.393333 0 0 0 53.333334 53.333334h106.666666a53.393333 53.393333 0 0 0 53.333334-53.333334V138.666667a53.393333 53.393333 0 0 0-53.333334-53.333334z m10.666667 800a10.666667 10.666667 0 0 1-10.666667 10.666667H650.666667a10.666667 10.666667 0 0 1-10.666667-10.666667V138.666667a10.666667 10.666667 0 0 1 10.666667-10.666667h106.666666a10.666667 10.666667 0 0 1 10.666667 10.666667z" p-id="4183"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style=" background: rgb(255, 255, 255); display: block; shape-rendering: auto;" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<g transform="rotate(0 50 50)">
<rect x="47.5" y="24" rx="2.5" ry="6" width="5" height="12" fill="#3370ff">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(30 50 50)">
<rect x="47.5" y="24" rx="2.5" ry="6" width="5" height="12" fill="#3370ff">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(60 50 50)">
<rect x="47.5" y="24" rx="2.5" ry="6" width="5" height="12" fill="#3370ff">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(90 50 50)">
<rect x="47.5" y="24" rx="2.5" ry="6" width="5" height="12" fill="#3370ff">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(120 50 50)">
<rect x="47.5" y="24" rx="2.5" ry="6" width="5" height="12" fill="#3370ff">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(150 50 50)">
<rect x="47.5" y="24" rx="2.5" ry="6" width="5" height="12" fill="#3370ff">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(180 50 50)">
<rect x="47.5" y="24" rx="2.5" ry="6" width="5" height="12" fill="#3370ff">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(210 50 50)">
<rect x="47.5" y="24" rx="2.5" ry="6" width="5" height="12" fill="#3370ff">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(240 50 50)">
<rect x="47.5" y="24" rx="2.5" ry="6" width="5" height="12" fill="#3370ff">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(270 50 50)">
<rect x="47.5" y="24" rx="2.5" ry="6" width="5" height="12" fill="#3370ff">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(300 50 50)">
<rect x="47.5" y="24" rx="2.5" ry="6" width="5" height="12" fill="#3370ff">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(330 50 50)">
<rect x="47.5" y="24" rx="2.5" ry="6" width="5" height="12" fill="#3370ff">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
</rect>
</g>
<!-- [ldio] generated by https://loading.io/ --></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -87,7 +87,9 @@ const iconPaths = {
searchLight: () => import('./icons/light/search.svg'),
plusFill: () => import('./icons/fill/plus.svg'),
moveLight: () => import('./icons/light/move.svg'),
questionGuide: () => import('./icons/app/questionGuide.svg')
questionGuide: () => import('./icons/app/questionGuide.svg'),
loading: () => import('./icons/light/loading.svg'),
pause: () => import('./icons/common/pause.svg')
};
export type IconName = keyof typeof iconPaths;

View File

@@ -4,6 +4,7 @@ import ReactMarkdown from 'react-markdown';
import RemarkGfm from 'remark-gfm';
import RemarkMath from 'remark-math';
import RehypeKatex from 'rehype-katex';
import RemarkBreaks from 'remark-breaks';
import { event } from '@/utils/plugin/eventbus';
import 'katex/dist/katex.min.css';
@@ -38,12 +39,14 @@ function MyLink(e: any) {
}
const Guide = ({ text }: { text: string }) => {
const formatText = useMemo(() => text.replace(/\[(.*?)\]($|\n)/g, '[$1]()\n'), [text]);
const formatText = useMemo(
() => text.replace(/\[(.*?)\]($|\n)/g, '[$1]()\n').replace(/\\n/g, '\n&nbsp;'),
[text]
);
return (
<ReactMarkdown
className={`markdown ${styles.markdown}`}
remarkPlugins={[RemarkGfm, RemarkMath]}
remarkPlugins={[RemarkGfm, RemarkMath, RemarkBreaks]}
rehypePlugins={[RehypeKatex]}
components={{
a: MyLink,

View File

@@ -62,6 +62,8 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
[]
);
const formatSource = source.replace(/\\n/g, '\n&nbsp;');
return (
<ReactMarkdown
className={`markdown ${styles.markdown}
@@ -73,9 +75,9 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
components={components}
linkTarget={'_blank'}
>
{source}
{formatSource}
</ReactMarkdown>
);
};
export default Markdown;
export default React.memo(Markdown);

View File

@@ -8,7 +8,7 @@ const PageContainer = ({ children, ...props }: BoxProps) => {
<Box
h={'100%'}
bg={'white'}
borderRadius={[0, '2xl']}
borderRadius={props?.borderRadius || [0, '2xl']}
border={['none', theme.borders.lg]}
overflow={'overlay'}
>

View File

@@ -33,14 +33,8 @@ const DatasetSelectContainer = ({
const { isPc } = useGlobalStore();
return (
<MyModal
isOpen={isOpen}
onClose={onClose}
w={'100%'}
maxW={['90vw', '900px']}
isCentered={!isPc}
>
<Flex flexDirection={'column'} h={['90vh', 'auto']}>
<MyModal isOpen={isOpen} onClose={onClose} w={'100%'} maxW={['90vw', '900px']} isCentered>
<Flex flexDirection={'column'} h={'90vh'}>
<ModalHeader>
{!!parentId ? (
<Flex

View File

@@ -308,7 +308,7 @@ export const AnswerModule: FlowModuleTemplateType = {
value: '',
label: '回复的内容',
description:
'可以使用 \\n 来实现换行。可以通过外部模块输入实现回复,外部模块输入时会覆盖当前填写的内容'
'可以使用 \\n 来实现连续换行。\n\n可以通过外部模块输入实现回复,外部模块输入时会覆盖当前填写的内容'
}
],
outputs: [

View File

@@ -61,6 +61,12 @@ function App({ Component, pageProps }: AppProps) {
url
});
};
// log fastgpt
console.log(
'%cWelcome to FastGPT',
'font-family:Arial; color:#3370ff ; font-size:18px; font-weight:bold;',
`GitHubhttps://github.com/labring/FastGPT`
);
return () => {
window.onerror = null;
};

View File

@@ -67,7 +67,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
res.end('Error connecting to database');
return;
}
console.log('export data');
// create pg select stream
const query = new QueryStream(
@@ -77,16 +76,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
);
const stream = client.query(query);
res.setHeader('Content-Disposition', 'attachment; filename=dataset.csv');
res.setHeader('Content-Type', 'text/csv');
res.write('index,content,source');
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename=dataset.csv; ');
const write = responseWriteController({
res,
readStream: stream
});
write('index,content,source');
// parse data every row
stream.on('data', ({ q, a, source }: { q: string; a: string; source?: string }) => {
if (res.closed) {

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useRef } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { initShareChatInfo } from '@/api/support/outLink';
@@ -35,6 +35,7 @@ const OutLink = ({
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
const { isPc } = useGlobalStore();
const forbidRefresh = useRef(false);
const [isEmbed, setIdEmbed] = useState(true);
const ChatBoxRef = useRef<ComponentRef>(null);
@@ -163,8 +164,12 @@ const OutLink = ({
return loadAppInfo(shareId, chatId, authToken);
});
useEffect(() => {
setIdEmbed(window !== parent);
}, []);
return (
<PageContainer>
<PageContainer {...(isEmbed ? { p: '0 !important', borderRadius: '0' } : {})}>
<Head>
<title>{shareChatData.app.name}</title>
</Head>

View File

@@ -11,7 +11,7 @@ export function responseWriteController({
readStream.resume();
});
return (text: string) => {
return (text: string | Buffer) => {
const writeResult = res.write(text);
if (!writeResult) {
readStream.pause();

View File

@@ -23,7 +23,7 @@ export const dispatchAnswer = (props: Record<string, any>): AnswerResponse => {
res,
event: detail ? sseResponseEventEnum.answer : undefined,
data: textAdaptGptResponse({
text: text.replace?.(/\\n/g, '\n') || ''
text
})
});
}

View File

@@ -1,28 +1,129 @@
export const hasVoiceApi = typeof window !== 'undefined' && 'speechSynthesis' in window;
/**
* voice broadcast
*/
export const voiceBroadcast = ({ text }: { text: string }) => {
window.speechSynthesis?.cancel();
const msg = new SpeechSynthesisUtterance(text);
const voices = window.speechSynthesis?.getVoices?.(); // 获取语言包
const voice = voices.find((item) => {
return item.name === 'Microsoft Yaoyao - Chinese (Simplified, PRC)';
});
if (voice) {
msg.voice = voice;
}
import { useState, useCallback, useEffect, useMemo } from 'react';
import { useToast } from '@/hooks/useToast';
import { getErrText } from '../tools';
window.speechSynthesis?.speak(msg);
export const useAudioPlay = (props?: { ttsUrl?: string }) => {
const { ttsUrl } = props || {};
const { toast } = useToast();
const [audio, setAudio] = useState<HTMLAudioElement>();
const [audioLoading, setAudioLoading] = useState(false);
const [audioPlaying, setAudioPlaying] = useState(false);
msg.onerror = (e) => {
console.log(e);
};
const hasAudio = useMemo(() => {
if (ttsUrl) return true;
const voices = window.speechSynthesis?.getVoices?.() || []; // 获取语言包
const voice = voices.find((item) => {
return item.lang === 'zh-CN';
});
return !!voice;
}, [ttsUrl]);
const playAudio = useCallback(
async (text: string) => {
text = text.replace(/\\n/g, '\n');
try {
if (audio && ttsUrl) {
setAudioLoading(true);
const response = await fetch(ttsUrl, {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
text
})
}).then((res) => res.blob());
const audioUrl = URL.createObjectURL(response);
audio.src = audioUrl;
audio.play();
} else {
// window speech
window.speechSynthesis?.cancel();
const msg = new SpeechSynthesisUtterance(text);
const voices = window.speechSynthesis?.getVoices?.() || []; // 获取语言包
const voice = voices.find((item) => {
return item.lang === 'zh-CN';
});
if (voice) {
msg.onstart = () => {
setAudioPlaying(true);
};
msg.onend = () => {
setAudioPlaying(false);
msg.onstart = null;
msg.onend = null;
};
msg.voice = voice;
window.speechSynthesis?.speak(msg);
}
}
} catch (error) {
toast({
status: 'error',
title: getErrText(error, '语音播报异常')
});
}
setAudioLoading(false);
},
[audio, toast, ttsUrl]
);
const cancelAudio = useCallback(() => {
if (audio) {
audio.pause();
audio.src = '';
}
window.speechSynthesis?.cancel();
setAudioPlaying(false);
}, [audio]);
useEffect(() => {
if (ttsUrl) {
setAudio(new Audio());
} else {
setAudio(undefined);
}
}, [ttsUrl]);
useEffect(() => {
if (audio) {
audio.onplay = () => {
setAudioPlaying(true);
};
audio.onended = () => {
setAudioPlaying(false);
};
audio.onerror = () => {
setAudioPlaying(false);
};
}
const listen = () => {
cancelAudio();
};
window.addEventListener('beforeunload', listen);
return () => {
if (audio) {
audio.onplay = null;
audio.onended = null;
audio.onerror = null;
}
cancelAudio();
window.removeEventListener('beforeunload', listen);
};
}, [audio, cancelAudio]);
useEffect(() => {
return () => {
setAudio(undefined);
};
}, []);
return {
cancel: () => window.speechSynthesis?.cancel()
audioPlaying,
audioLoading,
hasAudio,
playAudio,
cancelAudio
};
};
export const cancelBroadcast = () => {
window.speechSynthesis?.cancel();
};