| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
- import { MessageType } from '@/constants/chat';
- import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
- import { useSelectUserInfo } from '@/hooks/userSettingHook';
- import { IReference, Message } from '@/interfaces/database/chat';
- import { Avatar, Button, Flex, Input, List, Popover, Space } from 'antd';
- import classNames from 'classnames';
- import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
- import reactStringReplace from 'react-string-replace';
- import {
- useFetchConversationOnMount,
- useGetFileIcon,
- useSendMessage,
- } from '../hooks';
-
- import Image from '@/components/image';
- import NewDocumentLink from '@/components/new-document-link';
- import { useSelectFileThumbnails } from '@/hooks/knowledgeHook';
- import { InfoCircleOutlined } from '@ant-design/icons';
- import Markdown from 'react-markdown';
- 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, 3));
-
- const rehypeWrapReference = () => {
- return function wrapTextTransform(tree: any) {
- visitParents(tree, 'text', (node, ancestors) => {
- if (ancestors.at(-1).tagName !== 'custom-typography') {
- node.type = 'element';
- node.tagName = 'custom-typography';
- node.properties = {};
- node.children = [{ type: 'text', value: node.value }];
- }
- });
- };
- };
-
- const MessageItem = ({
- item,
- reference,
- }: {
- item: Message;
- reference: IReference;
- }) => {
- const userInfo = useSelectUserInfo();
- const fileThumbnails = useSelectFileThumbnails();
-
- const isAssistant = item.role === MessageType.Assistant;
-
- 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;
- return (
- <Flex
- key={chunkItem?.chunk_id}
- gap={10}
- className={styles.referencePopoverWrapper}
- >
- <Popover
- placement="topRight"
- content={
- <Image
- id={chunkItem?.img_id}
- className={styles.referenceImagePreview}
- ></Image>
- }
- >
- <Image
- id={chunkItem?.img_id}
- className={styles.referenceChunkImage}
- ></Image>
- </Popover>
- <Space direction={'vertical'}>
- <div>{chunkItem?.content_with_weight}</div>
- {documentId && (
- <Flex gap={'middle'}>
- <img src={fileThumbnails[documentId]} alt="" />
- <NewDocumentLink documentId={documentId}>
- {document?.doc_name}
- </NewDocumentLink>
- </Flex>
- )}
- </Space>
- </Flex>
- );
- },
- [reference, fileThumbnails],
- );
-
- 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]);
-
- 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 ? (
- userInfo.avatar ?? (
- <Avatar
- size={40}
- src={
- userInfo.avatar ??
- 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
- }
- />
- )
- ) : (
- <AssistantIcon></AssistantIcon>
- )}
- <Flex vertical gap={8} flex={1}>
- <b>{isAssistant ? '' : userInfo.nickname}</b>
- <div className={styles.messageText}>
- <Markdown
- rehypePlugins={[rehypeWrapReference]}
- components={
- {
- 'custom-typography': ({ children }: { children: string }) =>
- renderReference(children),
- } as any
- }
- >
- {item.content}
- </Markdown>
- </div>
- {isAssistant && referenceDocumentList.length > 0 && (
- <List
- bordered
- dataSource={referenceDocumentList}
- renderItem={(item) => (
- <List.Item>
- {/* <SvgIcon name={getFileIcon(item.doc_name)}></SvgIcon> */}
- <Flex gap={'middle'}>
- <img src={fileThumbnails[item.doc_id]}></img>
- <NewDocumentLink documentId={item.doc_id}>
- {item.doc_name}
- </NewDocumentLink>
- </Flex>
- </List.Item>
- )}
- />
- )}
- </Flex>
- </div>
- </section>
- </div>
- );
- };
-
- const ChatContainer = () => {
- const [value, setValue] = useState('');
- const {
- ref,
- currentConversation: conversation,
- addNewestConversation,
- } = useFetchConversationOnMount();
- const { sendMessage } = useSendMessage();
-
- const loading = useOneNamespaceEffectsLoading('chatModel', [
- 'completeConversation',
- ]);
- useGetFileIcon();
-
- const handlePressEnter = () => {
- if (!loading) {
- setValue('');
- addNewestConversation(value);
- sendMessage(value);
- }
- };
-
- const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
- setValue(e.target.value);
- };
-
- return (
- <Flex flex={1} className={styles.chatContainer} vertical>
- <Flex flex={1} vertical className={styles.messageContainer}>
- <div>
- {conversation?.message?.map((message) => {
- const assistantMessages = conversation?.message
- ?.filter((x) => x.role === MessageType.Assistant)
- .slice(1);
- const referenceIndex = assistantMessages.findIndex(
- (x) => x.id === message.id,
- );
- const reference = conversation.reference[referenceIndex];
- return (
- <MessageItem
- key={message.id}
- item={message}
- reference={reference}
- ></MessageItem>
- );
- })}
- </div>
- <div ref={ref} />
- </Flex>
- <Input
- size="large"
- placeholder="Message Resume Assistant..."
- value={value}
- suffix={
- <Button type="primary" onClick={handlePressEnter} loading={loading}>
- Send
- </Button>
- }
- onPressEnter={handlePressEnter}
- onChange={handleInputChange}
- />
- </Flex>
- );
- };
-
- export default ChatContainer;
|