{item.content !== '' ? (
-
renderReference(children),
- code(props: any) {
- const { children, className, node, ...rest } = props;
- const match = /language-(\w+)/.exec(className || '');
- return match ? (
-
- {String(children).replace(/\n$/, '')}
-
- ) : (
-
- {children}
-
- );
- },
- } as any
- }
- >
- {item.content}
-
+
) : (
)}
diff --git a/web/src/pages/chat/chat-overview-modal/index.tsx b/web/src/pages/chat/chat-overview-modal/index.tsx
index 9d3336dc5..c8a1c4126 100644
--- a/web/src/pages/chat/chat-overview-modal/index.tsx
+++ b/web/src/pages/chat/chat-overview-modal/index.tsx
@@ -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
& { 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 = ({
{t('publicUrl')}
-
- This is a copyable text.
-
+
+ {urlWithToken}
+
+
+
+
+
+
+
-
-
-
-
diff --git a/web/src/pages/chat/hooks.ts b/web/src/pages/chat/hooks.ts
index 10100b48c..cf5c85831 100644
--- a/web/src/pages/chat/hooks.ts
+++ b/web/src/pages/chat/hooks.ts
@@ -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([
@@ -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]);
diff --git a/web/src/pages/chat/markdown-content/index.less b/web/src/pages/chat/markdown-content/index.less
new file mode 100644
index 000000000..ee1301bff
--- /dev/null
+++ b/web/src/pages/chat/markdown-content/index.less
@@ -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;
+}
diff --git a/web/src/pages/chat/markdown-content/index.tsx b/web/src/pages/chat/markdown-content/index.tsx
new file mode 100644
index 000000000..c32166f19
--- /dev/null
+++ b/web/src/pages/chat/markdown-content/index.tsx
@@ -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 (
+
+ {imageId && (
+
+ }
+ >
+
+
+ )}
+
+
+ {documentId && (
+
+ {fileThumbnail ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
+
+ );
+ },
+ [reference, fileThumbnails, handleDocumentButtonClick],
+ );
+
+ const renderReference = useCallback(
+ (text: string) => {
+ return reactStringReplace(text, reg, (match, i) => {
+ const chunkIndex = getChunkIndex(match);
+ return (
+
+
+
+ );
+ });
+ },
+ [getPopoverContent],
+ );
+
+ return (
+
+ renderReference(children),
+ code(props: any) {
+ const { children, className, node, ...rest } = props;
+ const match = /language-(\w+)/.exec(className || '');
+ return match ? (
+
+ {String(children).replace(/\n$/, '')}
+
+ ) : (
+
+ {children}
+
+ );
+ },
+ } as any
+ }
+ >
+ {content}
+
+ );
+};
+
+export default MarkdownContent;
diff --git a/web/src/pages/chat/model.ts b/web/src/pages/chat/model.ts
index c2498a012..5c302a272 100644
--- a/web/src/pages/chat/model.ts
+++ b/web/src/pages/chat/model.ts
@@ -158,7 +158,7 @@ const model: DvaModel = {
}
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 = {
});
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 = {
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 = {
null,
payload,
);
- return data.retcode;
+ return data;
},
*completeExternalConversation({ payload }, { call }) {
const { data } = yield call(
diff --git a/web/src/pages/chat/share/index.less b/web/src/pages/chat/share/index.less
new file mode 100644
index 000000000..54e555011
--- /dev/null
+++ b/web/src/pages/chat/share/index.less
@@ -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;
+}
diff --git a/web/src/pages/chat/share/index.tsx b/web/src/pages/chat/share/index.tsx
new file mode 100644
index 000000000..00d91cdfe
--- /dev/null
+++ b/web/src/pages/chat/share/index.tsx
@@ -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 (
+
+
+
+ );
+};
+
+export default SharedChat;
diff --git a/web/src/pages/chat/share/large.tsx b/web/src/pages/chat/share/large.tsx
new file mode 100644
index 000000000..1e510af66
--- /dev/null
+++ b/web/src/pages/chat/share/large.tsx
@@ -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 (
+
+
+
+ {item.role === MessageType.User ? (
+
+ ) : (
+
+ )}
+
+ {isAssistant ? '' : 'You'}
+
+ {item.content !== '' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ );
+};
+
+interface IProps {
+ handlePressEnter(): void;
+ handleInputChange: ChangeEventHandler;
+ value: string;
+ loading: boolean;
+ sendLoading: boolean;
+ conversation: IClientConversation;
+ ref: React.LegacyRef;
+}
+
+const ChatContainer = (
+ {
+ handlePressEnter,
+ handleInputChange,
+ value,
+ loading: sendLoading,
+ conversation,
+ }: IProps,
+ ref: React.LegacyRef,
+) => {
+ const loading = useSelectConversationLoading();
+ const { t } = useTranslate('chat');
+
+ return (
+ <>
+
+
+
+
+ {conversation?.message?.map((message) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {t('send')}
+
+ }
+ onPressEnter={handlePressEnter}
+ onChange={handleInputChange}
+ />
+
+ >
+ );
+};
+
+export default forwardRef(ChatContainer);
diff --git a/web/src/pages/chat/share/shared-markdown.tsx b/web/src/pages/chat/share/shared-markdown.tsx
new file mode 100644
index 000000000..2c1a3c040
--- /dev/null
+++ b/web/src/pages/chat/share/shared-markdown.tsx
@@ -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 (
+
+ {String(children).replace(/\n$/, '')}
+
+ ) : (
+
+ {children}
+
+ );
+ },
+ } as any
+ }
+ >
+ {content}
+
+ );
+};
+
+export default SharedMarkdown;
diff --git a/web/src/pages/chat/shared-hooks.ts b/web/src/pages/chat/shared-hooks.ts
new file mode 100644
index 000000000..bb511a0ef
--- /dev/null
+++ b/web/src/pages/chat/shared-hooks.ts
@@ -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({} 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>,
+) => {
+ 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,
+ };
+};
diff --git a/web/src/routes.ts b/web/src/routes.ts
index b6d9e9bcc..ff6187052 100644
--- a/web/src/routes.ts
+++ b/web/src/routes.ts
@@ -4,6 +4,11 @@ const routes = [
component: '@/pages/login',
layout: false,
},
+ {
+ path: '/chat/share',
+ component: '@/pages/chat/share',
+ layout: false,
+ },
{
path: '/',
component: '@/layouts',
diff --git a/web/src/services/chatService.ts b/web/src/services/chatService.ts
index 0b4567560..496ed6754 100644
--- a/web/src/services/chatService.ts
+++ b/web/src/services/chatService.ts
@@ -76,7 +76,7 @@ const methods = {
},
createExternalConversation: {
url: createExternalConversation,
- method: 'post',
+ method: 'get',
},
getExternalConversation: {
url: getExternalConversation,
diff --git a/web/src/utils/commonUtil.ts b/web/src/utils/commonUtil.ts
index 9263e99d0..9cda1a995 100644
--- a/web/src/utils/commonUtil.ts
+++ b/web/src/utils/commonUtil.ts
@@ -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);
+};
diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts
index 91e7d4b38..39d30da42 100644
--- a/web/src/utils/request.ts
+++ b/web/src/utils/request.ts
@@ -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);