mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
markdown guide
This commit is contained in:
@@ -89,8 +89,6 @@ export const streamFetch = ({
|
||||
});
|
||||
read();
|
||||
} catch (err: any) {
|
||||
console.log(111111111111111111);
|
||||
|
||||
if (err?.message === 'The user aborted a request.') {
|
||||
return resolve({
|
||||
responseText,
|
||||
|
@@ -17,6 +17,7 @@ import { useUserStore } from '@/store/user';
|
||||
import { Types } from 'mongoose';
|
||||
import { HUMAN_ICON, quoteLenKey, rawSearchKey } from '@/constants/chat';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { EventNameEnum } from '../Markdown/constant';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import Avatar from '@/components/Avatar';
|
||||
|
||||
@@ -236,7 +237,7 @@ const ChatBox = (
|
||||
* user confirm send prompt
|
||||
*/
|
||||
const sendPrompt = useCallback(
|
||||
async (data: Record<string, any> = {}) => {
|
||||
async (data: Record<string, any> = {}, inputVal = '') => {
|
||||
if (isChatting) {
|
||||
toast({
|
||||
title: '正在聊天中...请等待结束',
|
||||
@@ -245,8 +246,7 @@ const ChatBox = (
|
||||
return;
|
||||
}
|
||||
// get input value
|
||||
const value = TextareaDom.current?.value || '';
|
||||
const val = value.trim().replace(/\n\s*/g, '\n');
|
||||
const val = inputVal.trim().replace(/\n\s*/g, '\n');
|
||||
|
||||
if (!val) {
|
||||
toast({
|
||||
@@ -320,7 +320,7 @@ const ChatBox = (
|
||||
});
|
||||
|
||||
if (!err?.responseText) {
|
||||
resetInputVal(value);
|
||||
resetInputVal(inputVal);
|
||||
setChatHistory(newChatList.slice(0, newChatList.length - 2));
|
||||
}
|
||||
|
||||
@@ -403,11 +403,25 @@ const ChatBox = (
|
||||
{/* avatar */}
|
||||
<ChatAvatar src={appAvatar} order={1} mr={['6px', 2]} />
|
||||
{/* message */}
|
||||
<Flex order={2} pt={2} maxW={`calc(100% - ${isLargeWidth ? '75px' : '58px'})`}>
|
||||
<Card bg={'white'} px={4} py={3} borderRadius={'0 8px 8px 8px'}>
|
||||
{welcomeText}
|
||||
</Card>
|
||||
</Flex>
|
||||
<Card
|
||||
order={2}
|
||||
mt={2}
|
||||
px={4}
|
||||
py={3}
|
||||
bg={'white'}
|
||||
maxW={`calc(100% - ${isLargeWidth ? '75px' : '58px'})`}
|
||||
borderRadius={'0 8px 8px 8px'}
|
||||
>
|
||||
<Markdown
|
||||
source={`~~~guide \n${welcomeText}`}
|
||||
isChatting={false}
|
||||
onClick={(e) => {
|
||||
const val = e?.data;
|
||||
if (e?.event !== EventNameEnum.guideClick || !val) return;
|
||||
handleSubmit((data) => sendPrompt(data, val))();
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Flex>
|
||||
)}
|
||||
{/* variable input */}
|
||||
@@ -416,54 +430,59 @@ const ChatBox = (
|
||||
{/* avatar */}
|
||||
<ChatAvatar src={appAvatar} order={1} mr={['6px', 2]} />
|
||||
{/* message */}
|
||||
<Flex order={2} pt={2} maxW={`calc(100% - ${isLargeWidth ? '75px' : '58px'})`}>
|
||||
<Card bg={'white'} px={4} py={3} borderRadius={'0 8px 8px 8px'}>
|
||||
<Box>
|
||||
{variableModules.map((item) => (
|
||||
<Box w={'min(100%,300px)'} key={item.id} mb={4}>
|
||||
<VariableLabel required={item.required}>{item.label}</VariableLabel>
|
||||
{item.type === VariableInputEnum.input && (
|
||||
<Input
|
||||
isDisabled={variableIsFinish}
|
||||
{...register(item.key, {
|
||||
required: item.required
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.select && (
|
||||
<MySelect
|
||||
width={'100%'}
|
||||
isDisabled={variableIsFinish}
|
||||
list={(item.enums || []).map((item) => ({
|
||||
label: item.value,
|
||||
value: item.value
|
||||
}))}
|
||||
value={getValues(item.key)}
|
||||
onchange={(e) => {
|
||||
setValue(item.key, e);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
{!variableIsFinish && (
|
||||
<Button
|
||||
leftIcon={<MyIcon name={'chatFill'} w={'16px'} />}
|
||||
size={'sm'}
|
||||
maxW={'100px'}
|
||||
borderRadius={'lg'}
|
||||
onClick={handleSubmit((data) => {
|
||||
onUpdateVariable?.(data);
|
||||
setVariables(data);
|
||||
<Card
|
||||
order={2}
|
||||
mt={2}
|
||||
flex={1}
|
||||
bg={'white'}
|
||||
px={4}
|
||||
py={3}
|
||||
borderRadius={'0 8px 8px 8px'}
|
||||
maxW={'min(100%,400px)'}
|
||||
>
|
||||
{variableModules.map((item) => (
|
||||
<Box key={item.id} mb={4}>
|
||||
<VariableLabel required={item.required}>{item.label}</VariableLabel>
|
||||
{item.type === VariableInputEnum.input && (
|
||||
<Input
|
||||
isDisabled={variableIsFinish}
|
||||
{...register(item.key, {
|
||||
required: item.required
|
||||
})}
|
||||
>
|
||||
{'开始对话'}
|
||||
</Button>
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.select && (
|
||||
<MySelect
|
||||
width={'100%'}
|
||||
isDisabled={variableIsFinish}
|
||||
list={(item.enums || []).map((item) => ({
|
||||
label: item.value,
|
||||
value: item.value
|
||||
}))}
|
||||
value={getValues(item.key)}
|
||||
onchange={(e) => {
|
||||
setValue(item.key, e);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Card>
|
||||
</Flex>
|
||||
))}
|
||||
{!variableIsFinish && (
|
||||
<Button
|
||||
leftIcon={<MyIcon name={'chatFill'} w={'16px'} />}
|
||||
size={'sm'}
|
||||
maxW={'100px'}
|
||||
borderRadius={'lg'}
|
||||
onClick={handleSubmit((data) => {
|
||||
onUpdateVariable?.(data);
|
||||
setVariables(data);
|
||||
})}
|
||||
>
|
||||
{'开始对话'}
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
@@ -650,7 +669,7 @@ const ChatBox = (
|
||||
onKeyDown={(e) => {
|
||||
// 触发快捷发送
|
||||
if (e.keyCode === 13 && !e.shiftKey) {
|
||||
handleSubmit(sendPrompt)();
|
||||
handleSubmit((data) => sendPrompt(data, TextareaDom.current?.value))();
|
||||
e.preventDefault();
|
||||
}
|
||||
// 全选内容
|
||||
@@ -685,7 +704,9 @@ const ChatBox = (
|
||||
height={['18px', '20px']}
|
||||
cursor={'pointer'}
|
||||
color={'gray.500'}
|
||||
onClick={handleSubmit(sendPrompt)}
|
||||
onClick={() => {
|
||||
handleSubmit((data) => sendPrompt(data, TextareaDom.current?.value))();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
49
client/src/components/Markdown/chat/Guide.tsx
Normal file
49
client/src/components/Markdown/chat/Guide.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import RemarkGfm from 'remark-gfm';
|
||||
import RemarkMath from 'remark-math';
|
||||
import RehypeKatex from 'rehype-katex';
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
import styles from '../index.module.scss';
|
||||
import { EventNameEnum } from '../constant';
|
||||
|
||||
const Guide = ({ text, onClick }: { text: string; onClick?: (e: any) => void }) => {
|
||||
const formatText = useMemo(() => text.replace(/\[(.*?)\]/g, '[$1]()'), [text]);
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className={`markdown ${styles.markdown}`}
|
||||
remarkPlugins={[RemarkGfm, RemarkMath]}
|
||||
rehypePlugins={[RehypeKatex]}
|
||||
components={{
|
||||
a({ children }: any) {
|
||||
return (
|
||||
<Box as={'li'} py={1} m={0}>
|
||||
<Box
|
||||
as={'span'}
|
||||
color={'blue.600'}
|
||||
textDecoration={'underline'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
if (!onClick) return;
|
||||
onClick({
|
||||
event: EventNameEnum.guideClick,
|
||||
data: String(children)
|
||||
});
|
||||
}}
|
||||
>
|
||||
{String(children)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{formatText}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Guide);
|
3
client/src/components/Markdown/constant.ts
Normal file
3
client/src/components/Markdown/constant.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export enum EventNameEnum {
|
||||
guideClick = 'guideClick'
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import RemarkGfm from 'remark-gfm';
|
||||
import RemarkMath from 'remark-math';
|
||||
@@ -7,19 +7,27 @@ import RemarkBreaks from 'remark-breaks';
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
import styles from './index.module.scss';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import Link from './Link';
|
||||
import CodeLight from './CodeLight';
|
||||
import MermaidCodeBlock from './img/MermaidCodeBlock';
|
||||
import MdImage from './img/Image';
|
||||
|
||||
function Code({ inline, className, children }: any) {
|
||||
const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'));
|
||||
const MdImage = dynamic(() => import('./img/Image'));
|
||||
const ChatGuide = dynamic(() => import('./chat/Guide'));
|
||||
|
||||
function Code({ inline, className, children, onClick }: any) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const codeType = match?.[1];
|
||||
|
||||
if (match?.[1] === 'mermaid') {
|
||||
if (codeType === 'mermaid') {
|
||||
return <MermaidCodeBlock code={String(children)} />;
|
||||
}
|
||||
|
||||
if (codeType === 'guide') {
|
||||
return <ChatGuide text={String(children)} onClick={onClick} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeLight className={className} inline={inline} match={match}>
|
||||
{children}
|
||||
@@ -31,7 +39,23 @@ function Image({ src }: { src?: string }) {
|
||||
return <MdImage src={src} />;
|
||||
}
|
||||
|
||||
const Markdown = ({ source, isChatting = false }: { source: string; isChatting?: boolean }) => {
|
||||
const Markdown = ({
|
||||
source,
|
||||
isChatting = false,
|
||||
onClick
|
||||
}: {
|
||||
source: string;
|
||||
isChatting?: boolean;
|
||||
onClick?: (e: any) => void;
|
||||
}) => {
|
||||
const components = useRef({
|
||||
a: Link,
|
||||
img: Image,
|
||||
pre: 'div',
|
||||
p: 'div',
|
||||
code: (props: any) => <Code {...props} onClick={onClick} />
|
||||
});
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className={`markdown ${styles.markdown}
|
||||
@@ -39,13 +63,8 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
|
||||
`}
|
||||
remarkPlugins={[RemarkGfm, RemarkMath, RemarkBreaks]}
|
||||
rehypePlugins={[RehypeKatex]}
|
||||
components={{
|
||||
a: Link,
|
||||
img: Image,
|
||||
pre: 'div',
|
||||
p: 'div',
|
||||
code: Code
|
||||
}}
|
||||
// @ts-ignore
|
||||
components={components.current}
|
||||
>
|
||||
{source}
|
||||
</ReactMarkdown>
|
||||
|
@@ -28,7 +28,7 @@ const Settings = ({ appId }: { appId: string }) => {
|
||||
content: '确认删除该应用?'
|
||||
});
|
||||
const [settingAppInfo, setSettingAppInfo] = useState<AppSchema>();
|
||||
const [fullScreen, setFullScreen] = useState(true);
|
||||
const [fullScreen, setFullScreen] = useState(false);
|
||||
|
||||
/* 点击删除 */
|
||||
const handleDelModel = useCallback(async () => {
|
||||
|
@@ -12,7 +12,7 @@ const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
|
||||
const welcomePlaceholder =
|
||||
'每次对话开始前,发送一个初始内容。可使用的特殊标记:\n[快捷按键]: 用户点击后可以直接发送该问题';
|
||||
'每次对话开始前,发送一个初始内容。支持标准 Markdown 语法,可使用的额外标记:\n[快捷按键]: 用户点击后可以直接发送该问题';
|
||||
|
||||
const NodeUserGuide = ({
|
||||
data: { inputs, outputs, onChangeNode, ...props }
|
||||
|
@@ -3,15 +3,23 @@ import { useChatBox } from '@/components/ChatBox';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { Menu, MenuButton, MenuList, MenuItem, Box } from '@chakra-ui/react';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const ToolMenu = ({ history }: { history: ChatItemType[] }) => {
|
||||
const { onExportChat } = useChatBox();
|
||||
const router = useRouter();
|
||||
const menuList = useRef([
|
||||
// {
|
||||
// icon: 'shareLight',
|
||||
// label: '分享对话',
|
||||
// onClick: () => {}
|
||||
// },
|
||||
{
|
||||
icon: 'chatLight',
|
||||
label: '新对话',
|
||||
onClick: () => {
|
||||
router.push({
|
||||
query: {
|
||||
appId: router.query?.appId
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'apiLight',
|
||||
label: 'HTML导出',
|
||||
|
Reference in New Issue
Block a user