fix: replace some pictures of chunk method #437 (#438)

### What problem does this PR solve?

some chunk method pictures are not in English #437

feat: set the height of both html and body to 100%
feat: add SharedChat
feat: add shared hooks

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
balibabu 2024-04-18 19:27:53 +08:00 committed by GitHub
parent fe2f5205fc
commit 1dada69daa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1336 additions and 733 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 545 KiB

After

Width:  |  Height:  |  Size: 406 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 388 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 321 KiB

After

Width:  |  Height:  |  Size: 467 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 311 KiB

After

Width:  |  Height:  |  Size: 966 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 599 KiB

After

Width:  |  Height:  |  Size: 515 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 872 KiB

After

Width:  |  Height:  |  Size: 196 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 296 KiB

View File

@ -1,6 +1,19 @@
@import url(./inter.less);
html {
height: 100%;
}
body {
font-family: Inter;
margin: 0;
height: 100%;
}
#root {
height: 100%;
}
.ant-app {
height: 100%;
}

View File

@ -4,7 +4,7 @@ import {
IStats,
IToken,
} from '@/interfaces/database/chat';
import { useCallback } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'umi';
export const useFetchDialogList = () => {
@ -248,3 +248,78 @@ export const useSelectStats = () => {
};
//#endregion
//#region shared chat
export const useCreateSharedConversation = () => {
const dispatch = useDispatch();
const createSharedConversation = useCallback(
(userId?: string) => {
return dispatch<any>({
type: 'chatModel/createExternalConversation',
payload: { userId },
});
},
[dispatch],
);
return createSharedConversation;
};
export const useFetchSharedConversation = () => {
const dispatch = useDispatch();
const fetchSharedConversation = useCallback(
(conversationId: string) => {
return dispatch<any>({
type: 'chatModel/getExternalConversation',
payload: conversationId,
});
},
[dispatch],
);
return fetchSharedConversation;
};
export const useCompleteSharedConversation = () => {
const dispatch = useDispatch();
const completeSharedConversation = useCallback(
(payload: any) => {
return dispatch<any>({
type: 'chatModel/completeExternalConversation',
payload: payload,
});
},
[dispatch],
);
return completeSharedConversation;
};
export const useCreatePublicUrlToken = (dialogId: string, visible: boolean) => {
const [token, setToken] = useState();
const createToken = useCreateToken(dialogId);
const { protocol, host } = window.location;
const urlWithToken = `${protocol}//${host}/chat/share?shared_id=${token}`;
const createUrlToken = useCallback(async () => {
if (visible) {
const data = await createToken();
const urlToken = data.data?.token;
if (urlToken) {
setToken(urlToken);
}
}
}, [createToken, visible]);
useEffect(() => {
createUrlToken();
}, [createUrlToken]);
return { token, createUrlToken, urlWithToken };
};
//#endregion

View File

@ -33,9 +33,9 @@
.messageEmpty {
width: 300px;
}
.referenceIcon {
padding: 0 6px;
}
// .referenceIcon {
// padding: 0 6px;
// }
}
.messageItemLeft {
@ -46,24 +46,24 @@
text-align: right;
}
.referencePopoverWrapper {
max-width: 50vw;
}
// .referencePopoverWrapper {
// max-width: 50vw;
// }
.referenceChunkImage {
width: 10vw;
object-fit: contain;
}
// .referenceChunkImage {
// width: 10vw;
// object-fit: contain;
// }
.referenceImagePreview {
max-width: 45vw;
max-height: 45vh;
}
.chunkContentText {
.chunkText;
max-height: 45vh;
overflow-y: auto;
}
.documentLink {
padding: 0;
}
// .referenceImagePreview {
// max-width: 45vw;
// max-height: 45vh;
// }
// .chunkContentText {
// .chunkText;
// max-height: 45vh;
// overflow-y: auto;
// }
// .documentLink {
// padding: 0;
// }

View File

@ -1,5 +1,4 @@
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
import Image from '@/components/image';
import NewDocumentLink from '@/components/new-document-link';
import DocumentPreviewer from '@/components/pdf-previewer';
import { MessageType } from '@/constants/chat';
@ -7,7 +6,6 @@ import { useSelectFileThumbnails } from '@/hooks/knowledgeHook';
import { useSelectUserInfo } from '@/hooks/userSettingHook';
import { IReference, Message } from '@/interfaces/database/chat';
import { IChunk } from '@/interfaces/database/knowledge';
import { InfoCircleOutlined } from '@ant-design/icons';
import {
Avatar,
Button,
@ -15,18 +13,11 @@ import {
Flex,
Input,
List,
Popover,
Skeleton,
Space,
Spin,
} from 'antd';
import classNames from 'classnames';
import { useCallback, useMemo } from 'react';
import Markdown from 'react-markdown';
import reactStringReplace from 'react-string-replace';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import remarkGfm from 'remark-gfm';
import { visitParents } from 'unist-util-visit-parents';
import { useMemo } from 'react';
import {
useClickDrawer,
useFetchConversationOnMount,
@ -35,33 +26,13 @@ import {
useSelectConversationLoading,
useSendMessage,
} from '../hooks';
import MarkdownContent from '../markdown-content';
import SvgIcon from '@/components/svg-icon';
import { useTranslate } from '@/hooks/commonHooks';
import { getExtension, isPdf } from '@/utils/documentUtils';
import styles from './index.less';
const reg = /(#{2}\d+\${2})/g;
const getChunkIndex = (match: string) => Number(match.slice(2, -2));
const rehypeWrapReference = () => {
return function wrapTextTransform(tree: any) {
visitParents(tree, 'text', (node, ancestors) => {
const latestAncestor = ancestors.at(-1);
if (
latestAncestor.tagName !== 'custom-typography' &&
latestAncestor.tagName !== 'code'
) {
node.type = 'element';
node.tagName = 'custom-typography';
node.properties = {};
node.children = [{ type: 'text', value: node.value }];
}
});
};
};
const MessageItem = ({
item,
reference,
@ -76,100 +47,6 @@ const MessageItem = ({
const isAssistant = item.role === MessageType.Assistant;
const handleDocumentButtonClick = useCallback(
(documentId: string, chunk: IChunk, isPdf: boolean) => () => {
if (!isPdf) {
return;
}
clickDocumentButton(documentId, chunk);
},
[clickDocumentButton],
);
const getPopoverContent = useCallback(
(chunkIndex: number) => {
const chunks = reference?.chunks ?? [];
const chunkItem = chunks[chunkIndex];
const document = reference?.doc_aggs.find(
(x) => x?.doc_id === chunkItem?.doc_id,
);
const documentId = document?.doc_id;
const fileThumbnail = documentId ? fileThumbnails[documentId] : '';
const fileExtension = documentId ? getExtension(document?.doc_name) : '';
const imageId = chunkItem?.img_id;
return (
<Flex
key={chunkItem?.chunk_id}
gap={10}
className={styles.referencePopoverWrapper}
>
{imageId && (
<Popover
placement="left"
content={
<Image
id={imageId}
className={styles.referenceImagePreview}
></Image>
}
>
<Image
id={imageId}
className={styles.referenceChunkImage}
></Image>
</Popover>
)}
<Space direction={'vertical'}>
<div
dangerouslySetInnerHTML={{
__html: chunkItem?.content_with_weight,
}}
className={styles.chunkContentText}
></div>
{documentId && (
<Flex gap={'small'}>
{fileThumbnail ? (
<img src={fileThumbnail} alt="" />
) : (
<SvgIcon
name={`file-icon/${fileExtension}`}
width={24}
></SvgIcon>
)}
<Button
type="link"
className={styles.documentLink}
onClick={handleDocumentButtonClick(
documentId,
chunkItem,
fileExtension === 'pdf',
)}
>
{document?.doc_name}
</Button>
</Flex>
)}
</Space>
</Flex>
);
},
[reference, fileThumbnails, handleDocumentButtonClick],
);
const renderReference = useCallback(
(text: string) => {
return reactStringReplace(text, reg, (match, i) => {
const chunkIndex = getChunkIndex(match);
return (
<Popover content={getPopoverContent(chunkIndex)}>
<InfoCircleOutlined key={i} className={styles.referenceIcon} />
</Popover>
);
});
},
[getPopoverContent],
);
const referenceDocumentList = useMemo(() => {
return reference?.doc_aggs ?? [];
}, [reference?.doc_aggs]);
@ -207,38 +84,11 @@ const MessageItem = ({
<b>{isAssistant ? '' : userInfo.nickname}</b>
<div className={styles.messageText}>
{item.content !== '' ? (
<Markdown
rehypePlugins={[rehypeWrapReference]}
remarkPlugins={[remarkGfm]}
components={
{
'custom-typography': ({
children,
}: {
children: string;
}) => renderReference(children),
code(props: any) {
const { children, className, node, ...rest } = props;
const match = /language-(\w+)/.exec(className || '');
return match ? (
<SyntaxHighlighter
{...rest}
PreTag="div"
language={match[1]}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code {...rest} className={className}>
{children}
</code>
);
},
} as any
}
>
{item.content}
</Markdown>
<MarkdownContent
content={item.content}
reference={reference}
clickDocumentButton={clickDocumentButton}
></MarkdownContent>
) : (
<Skeleton active className={styles.messageEmpty} />
)}

View File

@ -1,11 +1,15 @@
import CopyToClipboard from '@/components/copy-to-clipboard';
import LineChart from '@/components/line-chart';
import { useCreatePublicUrlToken } from '@/hooks/chatHooks';
import { useSetModalState, useTranslate } from '@/hooks/commonHooks';
import { IModalProps } from '@/interfaces/common';
import { IDialog, IStats } from '@/interfaces/database/chat';
import { ReloadOutlined } from '@ant-design/icons';
import { Button, Card, DatePicker, Flex, Modal, Space, Typography } from 'antd';
import { RangePickerProps } from 'antd/es/date-picker';
import dayjs from 'dayjs';
import camelCase from 'lodash/camelCase';
import { Link } from 'umi';
import ChatApiKeyModal from '../chat-api-key-modal';
import { useFetchStatsOnMount, useSelectChartStatsList } from '../hooks';
import styles from './index.less';
@ -20,6 +24,10 @@ const ChatOverviewModal = ({
}: IModalProps<any> & { dialog: IDialog }) => {
const { t } = useTranslate('chat');
const chartList = useSelectChartStatsList();
const { urlWithToken, createUrlToken, token } = useCreatePublicUrlToken(
dialog.id,
visible,
);
const {
visible: apiKeyVisible,
@ -45,14 +53,20 @@ const ChatOverviewModal = ({
<Card title={dialog.name}>
<Flex gap={8} vertical>
{t('publicUrl')}
<Paragraph copyable className={styles.linkText}>
This is a copyable text.
</Paragraph>
<Flex className={styles.linkText} gap={10}>
<span>{urlWithToken}</span>
<CopyToClipboard text={urlWithToken}></CopyToClipboard>
<ReloadOutlined onClick={createUrlToken} />
</Flex>
<Space size={'middle'}>
<Button>{t('preview')}</Button>
<Button>
<Link to={`/chat/share?shared_id=${token}`} target="_blank">
{t('preview')}
</Link>
</Button>
<Button>{t('embedded')}</Button>
</Space>
</Flex>
</Card>
<Card title={t('backendServiceApi')}>
<Flex gap={8} vertical>

View File

@ -715,6 +715,8 @@ export const useGetSendButtonDisabled = () => {
type RangeValue = [Dayjs | null, Dayjs | null] | null;
const getDay = (date: Dayjs) => date.format('YYYY-MM-DD');
export const useFetchStatsOnMount = (visible: boolean) => {
const fetchStats = useFetchStats();
const [pickerValue, setPickerValue] = useState<RangeValue>([
@ -724,7 +726,10 @@ export const useFetchStatsOnMount = (visible: boolean) => {
useEffect(() => {
if (visible && Array.isArray(pickerValue) && pickerValue[0]) {
fetchStats({ fromDate: pickerValue[0], toDate: pickerValue[1] });
fetchStats({
fromDate: getDay(pickerValue[0]),
toDate: getDay(pickerValue[1] ?? dayjs()),
});
}
}, [fetchStats, pickerValue, visible]);

View File

@ -0,0 +1,25 @@
.referencePopoverWrapper {
max-width: 50vw;
}
.referenceChunkImage {
width: 10vw;
object-fit: contain;
}
.referenceImagePreview {
max-width: 45vw;
max-height: 45vh;
}
.chunkContentText {
.chunkText;
max-height: 45vh;
overflow-y: auto;
}
.documentLink {
padding: 0;
}
.referenceIcon {
padding: 0 6px;
}

View File

@ -0,0 +1,173 @@
import Image from '@/components/image';
import SvgIcon from '@/components/svg-icon';
import { useSelectFileThumbnails } from '@/hooks/knowledgeHook';
import { IReference } from '@/interfaces/database/chat';
import { IChunk } from '@/interfaces/database/knowledge';
import { getExtension } from '@/utils/documentUtils';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Button, Flex, Popover, Space } from 'antd';
import { useCallback } from 'react';
import Markdown from 'react-markdown';
import reactStringReplace from 'react-string-replace';
import SyntaxHighlighter from 'react-syntax-highlighter';
import remarkGfm from 'remark-gfm';
import { visitParents } from 'unist-util-visit-parents';
import styles from './index.less';
const reg = /(#{2}\d+\${2})/g;
const getChunkIndex = (match: string) => Number(match.slice(2, -2));
// TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
const MarkdownContent = ({
reference,
clickDocumentButton,
content,
}: {
content: string;
reference: IReference;
clickDocumentButton: (documentId: string, chunk: IChunk) => void;
}) => {
const fileThumbnails = useSelectFileThumbnails();
const handleDocumentButtonClick = useCallback(
(documentId: string, chunk: IChunk, isPdf: boolean) => () => {
if (!isPdf) {
return;
}
clickDocumentButton(documentId, chunk);
},
[clickDocumentButton],
);
const rehypeWrapReference = () => {
return function wrapTextTransform(tree: any) {
visitParents(tree, 'text', (node, ancestors) => {
const latestAncestor = ancestors.at(-1);
if (
latestAncestor.tagName !== 'custom-typography' &&
latestAncestor.tagName !== 'code'
) {
node.type = 'element';
node.tagName = 'custom-typography';
node.properties = {};
node.children = [{ type: 'text', value: node.value }];
}
});
};
};
const getPopoverContent = useCallback(
(chunkIndex: number) => {
const chunks = reference?.chunks ?? [];
const chunkItem = chunks[chunkIndex];
const document = reference?.doc_aggs.find(
(x) => x?.doc_id === chunkItem?.doc_id,
);
const documentId = document?.doc_id;
const fileThumbnail = documentId ? fileThumbnails[documentId] : '';
const fileExtension = documentId ? getExtension(document?.doc_name) : '';
const imageId = chunkItem?.img_id;
return (
<Flex
key={chunkItem?.chunk_id}
gap={10}
className={styles.referencePopoverWrapper}
>
{imageId && (
<Popover
placement="left"
content={
<Image
id={imageId}
className={styles.referenceImagePreview}
></Image>
}
>
<Image
id={imageId}
className={styles.referenceChunkImage}
></Image>
</Popover>
)}
<Space direction={'vertical'}>
<div
dangerouslySetInnerHTML={{
__html: chunkItem?.content_with_weight,
}}
className={styles.chunkContentText}
></div>
{documentId && (
<Flex gap={'small'}>
{fileThumbnail ? (
<img src={fileThumbnail} alt="" />
) : (
<SvgIcon
name={`file-icon/${fileExtension}`}
width={24}
></SvgIcon>
)}
<Button
type="link"
className={styles.documentLink}
onClick={handleDocumentButtonClick(
documentId,
chunkItem,
fileExtension === 'pdf',
)}
>
{document?.doc_name}
</Button>
</Flex>
)}
</Space>
</Flex>
);
},
[reference, fileThumbnails, handleDocumentButtonClick],
);
const renderReference = useCallback(
(text: string) => {
return reactStringReplace(text, reg, (match, i) => {
const chunkIndex = getChunkIndex(match);
return (
<Popover content={getPopoverContent(chunkIndex)}>
<InfoCircleOutlined key={i} className={styles.referenceIcon} />
</Popover>
);
});
},
[getPopoverContent],
);
return (
<Markdown
rehypePlugins={[rehypeWrapReference]}
remarkPlugins={[remarkGfm]}
components={
{
'custom-typography': ({ children }: { children: string }) =>
renderReference(children),
code(props: any) {
const { children, className, node, ...rest } = props;
const match = /language-(\w+)/.exec(className || '');
return match ? (
<SyntaxHighlighter {...rest} PreTag="div" language={match[1]}>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code {...rest} className={className}>
{children}
</code>
);
},
} as any
}
>
{content}
</Markdown>
);
};
export default MarkdownContent;

View File

@ -158,7 +158,7 @@ const model: DvaModel<ChatModelState> = {
}
return data;
},
*completeConversation({ payload }, { call, put }) {
*completeConversation({ payload }, { call }) {
const { data } = yield call(chatService.completeConversation, payload);
// if (data.retcode === 0) {
// yield put({
@ -192,7 +192,7 @@ const model: DvaModel<ChatModelState> = {
});
message.success(i18n.t('message.created'));
}
return data.retcode;
return data;
},
*listToken({ payload }, { call, put }) {
const { data } = yield call(chatService.listToken, payload);
@ -232,13 +232,13 @@ const model: DvaModel<ChatModelState> = {
chatService.createExternalConversation,
payload,
);
if (data.retcode === 0) {
yield put({
type: 'getExternalConversation',
payload: { conversation_id: payload.conversationId },
});
}
return data.retcode;
// if (data.retcode === 0) {
// yield put({
// type: 'getExternalConversation',
// payload: data.data.id,
// });
// }
return data;
},
*getExternalConversation({ payload }, { call }) {
const { data } = yield call(
@ -246,7 +246,7 @@ const model: DvaModel<ChatModelState> = {
null,
payload,
);
return data.retcode;
return data;
},
*completeExternalConversation({ payload }, { call }) {
const { data } = yield call(

View File

@ -0,0 +1,50 @@
.chatWrapper {
height: 100%;
}
.chatContainer {
padding: 10px;
box-sizing: border-box;
height: 100%;
.messageContainer {
overflow-y: auto;
padding-right: 6px;
}
}
.messageItem {
padding: 24px 0;
.messageItemSection {
display: inline-block;
}
.messageItemSectionLeft {
width: 70%;
}
.messageItemSectionRight {
width: 40%;
}
.messageItemContent {
display: inline-flex;
gap: 20px;
}
.messageItemContentReverse {
flex-direction: row-reverse;
}
.messageText {
.chunkText();
padding: 0 14px;
background-color: rgba(249, 250, 251, 1);
word-break: break-all;
}
.messageEmpty {
width: 300px;
}
}
.messageItemLeft {
text-align: left;
}
.messageItemRight {
text-align: right;
}

View File

@ -0,0 +1,53 @@
import { useEffect } from 'react';
import {
useCreateSharedConversationOnMount,
useSelectCurrentSharedConversation,
useSendSharedMessage,
} from '../shared-hooks';
import ChatContainer from './large';
import styles from './index.less';
const SharedChat = () => {
const { conversationId } = useCreateSharedConversationOnMount();
const {
currentConversation,
addNewestConversation,
removeLatestMessage,
ref,
loading,
setCurrentConversation,
} = useSelectCurrentSharedConversation(conversationId);
const {
handlePressEnter,
handleInputChange,
value,
loading: sendLoading,
} = useSendSharedMessage(
currentConversation,
addNewestConversation,
removeLatestMessage,
setCurrentConversation,
);
useEffect(() => {
console.info(location.href);
}, []);
return (
<div className={styles.chatWrapper}>
<ChatContainer
value={value}
handleInputChange={handleInputChange}
handlePressEnter={handlePressEnter}
loading={loading}
sendLoading={sendLoading}
conversation={currentConversation}
ref={ref}
></ChatContainer>
</div>
);
};
export default SharedChat;

View File

@ -0,0 +1,122 @@
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
import { MessageType } from '@/constants/chat';
import { useTranslate } from '@/hooks/commonHooks';
import { Message } from '@/interfaces/database/chat';
import { Avatar, Button, Flex, Input, Skeleton, Spin } from 'antd';
import classNames from 'classnames';
import { useSelectConversationLoading } from '../hooks';
import React, { ChangeEventHandler, forwardRef } from 'react';
import { IClientConversation } from '../interface';
import styles from './index.less';
import SharedMarkdown from './shared-markdown';
const MessageItem = ({ item }: { item: Message }) => {
const isAssistant = item.role === MessageType.Assistant;
return (
<div
className={classNames(styles.messageItem, {
[styles.messageItemLeft]: item.role === MessageType.Assistant,
[styles.messageItemRight]: item.role === MessageType.User,
})}
>
<section
className={classNames(styles.messageItemSection, {
[styles.messageItemSectionLeft]: item.role === MessageType.Assistant,
[styles.messageItemSectionRight]: item.role === MessageType.User,
})}
>
<div
className={classNames(styles.messageItemContent, {
[styles.messageItemContentReverse]: item.role === MessageType.User,
})}
>
{item.role === MessageType.User ? (
<Avatar
size={40}
src={
'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
}
/>
) : (
<AssistantIcon></AssistantIcon>
)}
<Flex vertical gap={8} flex={1}>
<b>{isAssistant ? '' : 'You'}</b>
<div className={styles.messageText}>
{item.content !== '' ? (
<SharedMarkdown content={item.content}></SharedMarkdown>
) : (
<Skeleton active className={styles.messageEmpty} />
)}
</div>
</Flex>
</div>
</section>
</div>
);
};
interface IProps {
handlePressEnter(): void;
handleInputChange: ChangeEventHandler<HTMLInputElement>;
value: string;
loading: boolean;
sendLoading: boolean;
conversation: IClientConversation;
ref: React.LegacyRef<any>;
}
const ChatContainer = (
{
handlePressEnter,
handleInputChange,
value,
loading: sendLoading,
conversation,
}: IProps,
ref: React.LegacyRef<any>,
) => {
const loading = useSelectConversationLoading();
const { t } = useTranslate('chat');
return (
<>
<Flex flex={1} className={styles.chatContainer} vertical>
<Flex flex={1} vertical className={styles.messageContainer}>
<div>
<Spin spinning={loading}>
{conversation?.message?.map((message) => {
return (
<MessageItem key={message.id} item={message}></MessageItem>
);
})}
</Spin>
</div>
<div ref={ref} />
</Flex>
<Input
size="large"
placeholder={t('sendPlaceholder')}
value={value}
// disabled={disabled}
suffix={
<Button
type="primary"
onClick={handlePressEnter}
loading={sendLoading}
// disabled={disabled}
>
{t('send')}
</Button>
}
onPressEnter={handlePressEnter}
onChange={handleInputChange}
/>
</Flex>
</>
);
};
export default forwardRef(ChatContainer);

View File

@ -0,0 +1,32 @@
import Markdown from 'react-markdown';
import SyntaxHighlighter from 'react-syntax-highlighter';
import remarkGfm from 'remark-gfm';
const SharedMarkdown = ({ content }: { content: string }) => {
return (
<Markdown
remarkPlugins={[remarkGfm]}
components={
{
code(props: any) {
const { children, className, node, ...rest } = props;
const match = /language-(\w+)/.exec(className || '');
return match ? (
<SyntaxHighlighter {...rest} PreTag="div" language={match[1]}>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code {...rest} className={className}>
{children}
</code>
);
},
} as any
}
>
{content}
</Markdown>
);
};
export default SharedMarkdown;

View File

@ -0,0 +1,192 @@
import { MessageType } from '@/constants/chat';
import {
useCompleteSharedConversation,
useCreateSharedConversation,
useFetchSharedConversation,
} from '@/hooks/chatHooks';
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
import omit from 'lodash/omit';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from 'react';
import { useSearchParams } from 'umi';
import { v4 as uuid } from 'uuid';
import { useHandleMessageInputChange, useScrollToBottom } from './hooks';
import { IClientConversation, IMessage } from './interface';
export const useCreateSharedConversationOnMount = () => {
const [currentQueryParameters] = useSearchParams();
const [conversationId, setConversationId] = useState('');
const createConversation = useCreateSharedConversation();
const sharedId = currentQueryParameters.get('shared_id');
const userId = currentQueryParameters.get('user_id');
const setConversation = useCallback(async () => {
console.info(sharedId);
if (sharedId) {
const data = await createConversation(userId ?? undefined);
const id = data.data?.id;
if (id) {
setConversationId(id);
}
}
}, [createConversation, sharedId, userId]);
useEffect(() => {
setConversation();
}, [setConversation]);
return { conversationId };
};
export const useSelectCurrentSharedConversation = (conversationId: string) => {
const [currentConversation, setCurrentConversation] =
useState<IClientConversation>({} as IClientConversation);
const fetchConversation = useFetchSharedConversation();
const loading = useOneNamespaceEffectsLoading('chatModel', [
'getExternalConversation',
]);
const ref = useScrollToBottom(currentConversation);
const addNewestConversation = useCallback((message: string) => {
setCurrentConversation((pre) => {
return {
...pre,
message: [
...(pre.message ?? []),
{
role: MessageType.User,
content: message,
id: uuid(),
} as IMessage,
{
role: MessageType.Assistant,
content: '',
id: uuid(),
reference: [],
} as IMessage,
],
};
});
}, []);
const removeLatestMessage = useCallback(() => {
setCurrentConversation((pre) => {
const nextMessages = pre.message.slice(0, -2);
return {
...pre,
message: nextMessages,
};
});
}, []);
const fetchConversationOnMount = useCallback(async () => {
if (conversationId) {
const data = await fetchConversation(conversationId);
if (data.retcode === 0) {
setCurrentConversation(data.data);
}
}
}, [conversationId, fetchConversation]);
useEffect(() => {
fetchConversationOnMount();
}, [fetchConversationOnMount]);
return {
currentConversation,
addNewestConversation,
removeLatestMessage,
loading,
ref,
setCurrentConversation,
};
};
export const useSendSharedMessage = (
conversation: IClientConversation,
addNewestConversation: (message: string) => void,
removeLatestMessage: () => void,
setCurrentConversation: Dispatch<SetStateAction<IClientConversation>>,
) => {
const conversationId = conversation.id;
const loading = useOneNamespaceEffectsLoading('chatModel', [
'completeExternalConversation',
]);
const setConversation = useCreateSharedConversation();
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
const fetchConversation = useFetchSharedConversation();
const completeConversation = useCompleteSharedConversation();
const sendMessage = useCallback(
async (message: string, id?: string) => {
const retcode = await completeConversation({
conversation_id: id ?? conversationId,
messages: [
...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')),
{
role: MessageType.User,
content: message,
},
],
});
if (retcode === 0) {
const data = await fetchConversation(conversationId);
if (data.retcode === 0) {
setCurrentConversation(data.data);
}
} else {
// cancel loading
setValue(message);
removeLatestMessage();
}
},
[
conversationId,
conversation?.message,
fetchConversation,
removeLatestMessage,
setValue,
completeConversation,
setCurrentConversation,
],
);
const handleSendMessage = useCallback(
async (message: string) => {
if (conversationId !== '') {
sendMessage(message);
} else {
const data = await setConversation('user id');
if (data.retcode === 0) {
const id = data.data.id;
sendMessage(message, id);
}
}
},
[conversationId, setConversation, sendMessage],
);
const handlePressEnter = () => {
if (!loading) {
setValue('');
addNewestConversation(value);
handleSendMessage(value.trim());
}
};
return {
handlePressEnter,
handleInputChange,
value,
loading,
};
};

View File

@ -4,6 +4,11 @@ const routes = [
component: '@/pages/login',
layout: false,
},
{
path: '/chat/share',
component: '@/pages/chat/share',
layout: false,
},
{
path: '/',
component: '@/layouts',

View File

@ -76,7 +76,7 @@ const methods = {
},
createExternalConversation: {
url: createExternalConversation,
method: 'post',
method: 'get',
},
getExternalConversation: {
url: getExternalConversation,

View File

@ -15,3 +15,8 @@ export const convertTheKeysOfTheObjectToSnake = (data: unknown) => {
}
return data;
};
export const getSearchValue = (key: string) => {
const params = new URL(document.location as any).searchParams;
return params.get(key);
};

View File

@ -4,7 +4,7 @@ import authorizationUtil from '@/utils/authorizationUtil';
import { message, notification } from 'antd';
import { history } from 'umi';
import { RequestMethod, extend } from 'umi-request';
import { convertTheKeysOfTheObjectToSnake } from './commonUtil';
import { convertTheKeysOfTheObjectToSnake, getSearchValue } from './commonUtil';
const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.'; // 手动中断请求。errorHandler 抛出的error message
@ -87,7 +87,10 @@ const request: RequestMethod = extend({
});
request.interceptors.request.use((url: string, options: any) => {
const authorization = authorizationUtil.getAuthorization();
const sharedId = getSearchValue('shared_id');
const authorization = sharedId
? 'Bearer ' + sharedId
: authorizationUtil.getAuthorization();
const data = convertTheKeysOfTheObjectToSnake(options.data);
const params = convertTheKeysOfTheObjectToSnake(options.params);