### 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)
Before Width: | Height: | Size: 545 KiB After Width: | Height: | Size: 406 KiB |
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 388 KiB |
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 467 KiB |
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 966 KiB |
Before Width: | Height: | Size: 599 KiB After Width: | Height: | Size: 515 KiB |
Before Width: | Height: | Size: 872 KiB After Width: | Height: | Size: 196 KiB |
Before Width: | Height: | Size: 366 KiB After Width: | Height: | Size: 296 KiB |
@ -1,6 +1,19 @@
|
|||||||
@import url(./inter.less);
|
@import url(./inter.less);
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Inter;
|
font-family: Inter;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-app {
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
IStats,
|
IStats,
|
||||||
IToken,
|
IToken,
|
||||||
} from '@/interfaces/database/chat';
|
} from '@/interfaces/database/chat';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'umi';
|
import { useDispatch, useSelector } from 'umi';
|
||||||
|
|
||||||
export const useFetchDialogList = () => {
|
export const useFetchDialogList = () => {
|
||||||
@ -248,3 +248,78 @@ export const useSelectStats = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//#endregion
|
//#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
|
||||||
|
@ -33,9 +33,9 @@
|
|||||||
.messageEmpty {
|
.messageEmpty {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
.referenceIcon {
|
// .referenceIcon {
|
||||||
padding: 0 6px;
|
// padding: 0 6px;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
.messageItemLeft {
|
.messageItemLeft {
|
||||||
@ -46,24 +46,24 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.referencePopoverWrapper {
|
// .referencePopoverWrapper {
|
||||||
max-width: 50vw;
|
// max-width: 50vw;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.referenceChunkImage {
|
// .referenceChunkImage {
|
||||||
width: 10vw;
|
// width: 10vw;
|
||||||
object-fit: contain;
|
// object-fit: contain;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.referenceImagePreview {
|
// .referenceImagePreview {
|
||||||
max-width: 45vw;
|
// max-width: 45vw;
|
||||||
max-height: 45vh;
|
// max-height: 45vh;
|
||||||
}
|
// }
|
||||||
.chunkContentText {
|
// .chunkContentText {
|
||||||
.chunkText;
|
// .chunkText;
|
||||||
max-height: 45vh;
|
// max-height: 45vh;
|
||||||
overflow-y: auto;
|
// overflow-y: auto;
|
||||||
}
|
// }
|
||||||
.documentLink {
|
// .documentLink {
|
||||||
padding: 0;
|
// padding: 0;
|
||||||
}
|
// }
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
|
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
|
||||||
import Image from '@/components/image';
|
|
||||||
import NewDocumentLink from '@/components/new-document-link';
|
import NewDocumentLink from '@/components/new-document-link';
|
||||||
import DocumentPreviewer from '@/components/pdf-previewer';
|
import DocumentPreviewer from '@/components/pdf-previewer';
|
||||||
import { MessageType } from '@/constants/chat';
|
import { MessageType } from '@/constants/chat';
|
||||||
@ -7,7 +6,6 @@ import { useSelectFileThumbnails } from '@/hooks/knowledgeHook';
|
|||||||
import { useSelectUserInfo } from '@/hooks/userSettingHook';
|
import { useSelectUserInfo } from '@/hooks/userSettingHook';
|
||||||
import { IReference, Message } from '@/interfaces/database/chat';
|
import { IReference, Message } from '@/interfaces/database/chat';
|
||||||
import { IChunk } from '@/interfaces/database/knowledge';
|
import { IChunk } from '@/interfaces/database/knowledge';
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
@ -15,18 +13,11 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
Input,
|
Input,
|
||||||
List,
|
List,
|
||||||
Popover,
|
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Space,
|
|
||||||
Spin,
|
Spin,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { 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 {
|
import {
|
||||||
useClickDrawer,
|
useClickDrawer,
|
||||||
useFetchConversationOnMount,
|
useFetchConversationOnMount,
|
||||||
@ -35,33 +26,13 @@ import {
|
|||||||
useSelectConversationLoading,
|
useSelectConversationLoading,
|
||||||
useSendMessage,
|
useSendMessage,
|
||||||
} from '../hooks';
|
} from '../hooks';
|
||||||
|
import MarkdownContent from '../markdown-content';
|
||||||
|
|
||||||
import SvgIcon from '@/components/svg-icon';
|
import SvgIcon from '@/components/svg-icon';
|
||||||
import { useTranslate } from '@/hooks/commonHooks';
|
import { useTranslate } from '@/hooks/commonHooks';
|
||||||
import { getExtension, isPdf } from '@/utils/documentUtils';
|
import { getExtension, isPdf } from '@/utils/documentUtils';
|
||||||
import styles from './index.less';
|
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 = ({
|
const MessageItem = ({
|
||||||
item,
|
item,
|
||||||
reference,
|
reference,
|
||||||
@ -76,100 +47,6 @@ const MessageItem = ({
|
|||||||
|
|
||||||
const isAssistant = item.role === MessageType.Assistant;
|
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(() => {
|
const referenceDocumentList = useMemo(() => {
|
||||||
return reference?.doc_aggs ?? [];
|
return reference?.doc_aggs ?? [];
|
||||||
}, [reference?.doc_aggs]);
|
}, [reference?.doc_aggs]);
|
||||||
@ -207,38 +84,11 @@ const MessageItem = ({
|
|||||||
<b>{isAssistant ? '' : userInfo.nickname}</b>
|
<b>{isAssistant ? '' : userInfo.nickname}</b>
|
||||||
<div className={styles.messageText}>
|
<div className={styles.messageText}>
|
||||||
{item.content !== '' ? (
|
{item.content !== '' ? (
|
||||||
<Markdown
|
<MarkdownContent
|
||||||
rehypePlugins={[rehypeWrapReference]}
|
content={item.content}
|
||||||
remarkPlugins={[remarkGfm]}
|
reference={reference}
|
||||||
components={
|
clickDocumentButton={clickDocumentButton}
|
||||||
{
|
></MarkdownContent>
|
||||||
'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>
|
|
||||||
) : (
|
) : (
|
||||||
<Skeleton active className={styles.messageEmpty} />
|
<Skeleton active className={styles.messageEmpty} />
|
||||||
)}
|
)}
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
|
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||||
import LineChart from '@/components/line-chart';
|
import LineChart from '@/components/line-chart';
|
||||||
|
import { useCreatePublicUrlToken } from '@/hooks/chatHooks';
|
||||||
import { useSetModalState, useTranslate } from '@/hooks/commonHooks';
|
import { useSetModalState, useTranslate } from '@/hooks/commonHooks';
|
||||||
import { IModalProps } from '@/interfaces/common';
|
import { IModalProps } from '@/interfaces/common';
|
||||||
import { IDialog, IStats } from '@/interfaces/database/chat';
|
import { IDialog, IStats } from '@/interfaces/database/chat';
|
||||||
|
import { ReloadOutlined } from '@ant-design/icons';
|
||||||
import { Button, Card, DatePicker, Flex, Modal, Space, Typography } from 'antd';
|
import { Button, Card, DatePicker, Flex, Modal, Space, Typography } from 'antd';
|
||||||
import { RangePickerProps } from 'antd/es/date-picker';
|
import { RangePickerProps } from 'antd/es/date-picker';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import camelCase from 'lodash/camelCase';
|
import camelCase from 'lodash/camelCase';
|
||||||
|
import { Link } from 'umi';
|
||||||
import ChatApiKeyModal from '../chat-api-key-modal';
|
import ChatApiKeyModal from '../chat-api-key-modal';
|
||||||
import { useFetchStatsOnMount, useSelectChartStatsList } from '../hooks';
|
import { useFetchStatsOnMount, useSelectChartStatsList } from '../hooks';
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
@ -20,6 +24,10 @@ const ChatOverviewModal = ({
|
|||||||
}: IModalProps<any> & { dialog: IDialog }) => {
|
}: IModalProps<any> & { dialog: IDialog }) => {
|
||||||
const { t } = useTranslate('chat');
|
const { t } = useTranslate('chat');
|
||||||
const chartList = useSelectChartStatsList();
|
const chartList = useSelectChartStatsList();
|
||||||
|
const { urlWithToken, createUrlToken, token } = useCreatePublicUrlToken(
|
||||||
|
dialog.id,
|
||||||
|
visible,
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
visible: apiKeyVisible,
|
visible: apiKeyVisible,
|
||||||
@ -45,14 +53,20 @@ const ChatOverviewModal = ({
|
|||||||
<Card title={dialog.name}>
|
<Card title={dialog.name}>
|
||||||
<Flex gap={8} vertical>
|
<Flex gap={8} vertical>
|
||||||
{t('publicUrl')}
|
{t('publicUrl')}
|
||||||
<Paragraph copyable className={styles.linkText}>
|
<Flex className={styles.linkText} gap={10}>
|
||||||
This is a copyable text.
|
<span>{urlWithToken}</span>
|
||||||
</Paragraph>
|
<CopyToClipboard text={urlWithToken}></CopyToClipboard>
|
||||||
|
<ReloadOutlined onClick={createUrlToken} />
|
||||||
|
</Flex>
|
||||||
|
<Space size={'middle'}>
|
||||||
|
<Button>
|
||||||
|
<Link to={`/chat/share?shared_id=${token}`} target="_blank">
|
||||||
|
{t('preview')}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button>{t('embedded')}</Button>
|
||||||
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Space size={'middle'}>
|
|
||||||
<Button>{t('preview')}</Button>
|
|
||||||
<Button>{t('embedded')}</Button>
|
|
||||||
</Space>
|
|
||||||
</Card>
|
</Card>
|
||||||
<Card title={t('backendServiceApi')}>
|
<Card title={t('backendServiceApi')}>
|
||||||
<Flex gap={8} vertical>
|
<Flex gap={8} vertical>
|
||||||
|
@ -715,6 +715,8 @@ export const useGetSendButtonDisabled = () => {
|
|||||||
|
|
||||||
type RangeValue = [Dayjs | null, Dayjs | null] | null;
|
type RangeValue = [Dayjs | null, Dayjs | null] | null;
|
||||||
|
|
||||||
|
const getDay = (date: Dayjs) => date.format('YYYY-MM-DD');
|
||||||
|
|
||||||
export const useFetchStatsOnMount = (visible: boolean) => {
|
export const useFetchStatsOnMount = (visible: boolean) => {
|
||||||
const fetchStats = useFetchStats();
|
const fetchStats = useFetchStats();
|
||||||
const [pickerValue, setPickerValue] = useState<RangeValue>([
|
const [pickerValue, setPickerValue] = useState<RangeValue>([
|
||||||
@ -724,7 +726,10 @@ export const useFetchStatsOnMount = (visible: boolean) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && Array.isArray(pickerValue) && pickerValue[0]) {
|
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]);
|
}, [fetchStats, pickerValue, visible]);
|
||||||
|
|
||||||
|
25
web/src/pages/chat/markdown-content/index.less
Normal 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;
|
||||||
|
}
|
173
web/src/pages/chat/markdown-content/index.tsx
Normal 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;
|
@ -158,7 +158,7 @@ const model: DvaModel<ChatModelState> = {
|
|||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
*completeConversation({ payload }, { call, put }) {
|
*completeConversation({ payload }, { call }) {
|
||||||
const { data } = yield call(chatService.completeConversation, payload);
|
const { data } = yield call(chatService.completeConversation, payload);
|
||||||
// if (data.retcode === 0) {
|
// if (data.retcode === 0) {
|
||||||
// yield put({
|
// yield put({
|
||||||
@ -192,7 +192,7 @@ const model: DvaModel<ChatModelState> = {
|
|||||||
});
|
});
|
||||||
message.success(i18n.t('message.created'));
|
message.success(i18n.t('message.created'));
|
||||||
}
|
}
|
||||||
return data.retcode;
|
return data;
|
||||||
},
|
},
|
||||||
*listToken({ payload }, { call, put }) {
|
*listToken({ payload }, { call, put }) {
|
||||||
const { data } = yield call(chatService.listToken, payload);
|
const { data } = yield call(chatService.listToken, payload);
|
||||||
@ -232,13 +232,13 @@ const model: DvaModel<ChatModelState> = {
|
|||||||
chatService.createExternalConversation,
|
chatService.createExternalConversation,
|
||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
if (data.retcode === 0) {
|
// if (data.retcode === 0) {
|
||||||
yield put({
|
// yield put({
|
||||||
type: 'getExternalConversation',
|
// type: 'getExternalConversation',
|
||||||
payload: { conversation_id: payload.conversationId },
|
// payload: data.data.id,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
return data.retcode;
|
return data;
|
||||||
},
|
},
|
||||||
*getExternalConversation({ payload }, { call }) {
|
*getExternalConversation({ payload }, { call }) {
|
||||||
const { data } = yield call(
|
const { data } = yield call(
|
||||||
@ -246,7 +246,7 @@ const model: DvaModel<ChatModelState> = {
|
|||||||
null,
|
null,
|
||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
return data.retcode;
|
return data;
|
||||||
},
|
},
|
||||||
*completeExternalConversation({ payload }, { call }) {
|
*completeExternalConversation({ payload }, { call }) {
|
||||||
const { data } = yield call(
|
const { data } = yield call(
|
||||||
|
50
web/src/pages/chat/share/index.less
Normal 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;
|
||||||
|
}
|
53
web/src/pages/chat/share/index.tsx
Normal 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;
|
122
web/src/pages/chat/share/large.tsx
Normal 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);
|
32
web/src/pages/chat/share/shared-markdown.tsx
Normal 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;
|
192
web/src/pages/chat/shared-hooks.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
@ -4,6 +4,11 @@ const routes = [
|
|||||||
component: '@/pages/login',
|
component: '@/pages/login',
|
||||||
layout: false,
|
layout: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/chat/share',
|
||||||
|
component: '@/pages/chat/share',
|
||||||
|
layout: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: '@/layouts',
|
component: '@/layouts',
|
||||||
|
@ -76,7 +76,7 @@ const methods = {
|
|||||||
},
|
},
|
||||||
createExternalConversation: {
|
createExternalConversation: {
|
||||||
url: createExternalConversation,
|
url: createExternalConversation,
|
||||||
method: 'post',
|
method: 'get',
|
||||||
},
|
},
|
||||||
getExternalConversation: {
|
getExternalConversation: {
|
||||||
url: getExternalConversation,
|
url: getExternalConversation,
|
||||||
|
@ -15,3 +15,8 @@ export const convertTheKeysOfTheObjectToSnake = (data: unknown) => {
|
|||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getSearchValue = (key: string) => {
|
||||||
|
const params = new URL(document.location as any).searchParams;
|
||||||
|
return params.get(key);
|
||||||
|
};
|
||||||
|
@ -4,7 +4,7 @@ import authorizationUtil from '@/utils/authorizationUtil';
|
|||||||
import { message, notification } from 'antd';
|
import { message, notification } from 'antd';
|
||||||
import { history } from 'umi';
|
import { history } from 'umi';
|
||||||
import { RequestMethod, extend } from 'umi-request';
|
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
|
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) => {
|
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 data = convertTheKeysOfTheObjectToSnake(options.data);
|
||||||
const params = convertTheKeysOfTheObjectToSnake(options.params);
|
const params = convertTheKeysOfTheObjectToSnake(options.params);
|
||||||
|
|
||||||
|