mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-22 04:06:18 +00:00
perf: markdown more wrap (#365)
This commit is contained in:
@@ -15,8 +15,9 @@ OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
# 通用key。可以是 openai 的也可以是 oneapi 的。
|
||||
# 此处逻辑:优先走 ONEAPI_URL,如果填写了 ONEAPI_URL,key 也需要是 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=/
|
||||
|
@@ -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,
|
||||
|
@@ -237,7 +237,7 @@
|
||||
"key tips": "你可以使用 API 秘钥访问一些特定的接口"
|
||||
},
|
||||
"outlink": {
|
||||
"Copy Iframe": "复制嵌入",
|
||||
"Copy Iframe": "嵌入网页",
|
||||
"Copy Link": "复制",
|
||||
"Create API Key": "创建新 Key",
|
||||
"Create Link": "创建链接",
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
1
projects/app/src/components/Icon/icons/common/pause.svg
Normal file
1
projects/app/src/components/Icon/icons/common/pause.svg
Normal 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 |
52
projects/app/src/components/Icon/icons/light/loading.svg
Normal file
52
projects/app/src/components/Icon/icons/light/loading.svg
Normal 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 |
@@ -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;
|
||||
|
@@ -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 '),
|
||||
[text]
|
||||
);
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className={`markdown ${styles.markdown}`}
|
||||
remarkPlugins={[RemarkGfm, RemarkMath]}
|
||||
remarkPlugins={[RemarkGfm, RemarkMath, RemarkBreaks]}
|
||||
rehypePlugins={[RehypeKatex]}
|
||||
components={{
|
||||
a: MyLink,
|
||||
|
@@ -62,6 +62,8 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
|
||||
[]
|
||||
);
|
||||
|
||||
const formatSource = source.replace(/\\n/g, '\n ');
|
||||
|
||||
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);
|
||||
|
@@ -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'}
|
||||
>
|
||||
|
@@ -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
|
||||
|
@@ -308,7 +308,7 @@ export const AnswerModule: FlowModuleTemplateType = {
|
||||
value: '',
|
||||
label: '回复的内容',
|
||||
description:
|
||||
'可以使用 \\n 来实现换行。也可以通过外部模块输入实现回复,外部模块输入时会覆盖当前填写的内容'
|
||||
'可以使用 \\n 来实现连续换行。\n\n可以通过外部模块输入实现回复,外部模块输入时会覆盖当前填写的内容'
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
|
@@ -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;',
|
||||
`GitHub:https://github.com/labring/FastGPT`
|
||||
);
|
||||
return () => {
|
||||
window.onerror = null;
|
||||
};
|
||||
|
@@ -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) {
|
||||
|
@@ -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>
|
||||
|
@@ -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();
|
||||
|
@@ -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
|
||||
})
|
||||
});
|
||||
}
|
||||
|
@@ -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();
|
||||
};
|
||||
|
Reference in New Issue
Block a user