feat: Expose the agent's chat window to third parties #1842 (#1897)

### What problem does this PR solve?

feat: Expose the agent's chat window to third parties #1842
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2024-08-09 18:59:16 +08:00
committed by GitHub
parent 37be0ff3d3
commit 827042f72b
14 changed files with 289 additions and 357 deletions

View File

@ -1,72 +0,0 @@
import CopyToClipboard from '@/components/copy-to-clipboard';
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { IToken } from '@/interfaces/database/chat';
import { formatDate } from '@/utils/date';
import { DeleteOutlined } from '@ant-design/icons';
import type { TableProps } from 'antd';
import { Button, Modal, Space, Table } from 'antd';
import { useOperateApiKey } from '../hooks';
const ChatApiKeyModal = ({
visible,
dialogId,
hideModal,
}: IModalProps<any> & { dialogId: string }) => {
const { createToken, removeToken, tokenList, listLoading, creatingLoading } =
useOperateApiKey(visible, dialogId);
const { t } = useTranslate('chat');
const columns: TableProps<IToken>['columns'] = [
{
title: 'Token',
dataIndex: 'token',
key: 'token',
render: (text) => <a>{text}</a>,
},
{
title: t('created'),
dataIndex: 'create_date',
key: 'create_date',
render: (text) => formatDate(text),
},
{
title: t('action'),
key: 'action',
render: (_, record) => (
<Space size="middle">
<CopyToClipboard text={record.token}></CopyToClipboard>
<DeleteOutlined
onClick={() => removeToken(record.token, record.tenant_id)}
/>
</Space>
),
},
];
return (
<>
<Modal
title={t('apiKey')}
open={visible}
onCancel={hideModal}
cancelButtonProps={{ style: { display: 'none' } }}
style={{ top: 300 }}
onOk={hideModal}
width={'50vw'}
>
<Table
columns={columns}
dataSource={tokenList}
rowKey={'token'}
loading={listLoading}
/>
<Button onClick={createToken} loading={creatingLoading}>
{t('createNewKey')}
</Button>
</Modal>
</>
);
};
export default ChatApiKeyModal;

View File

@ -1,21 +0,0 @@
.chartWrapper {
height: 40vh;
overflow: auto;
}
.chartItem {
height: 300px;
padding: 10px 0 50px;
}
.chartLabel {
display: inline-block;
padding-left: 60px;
padding-bottom: 20px;
}
.linkText {
border-radius: 6px;
padding: 6px 10px;
background-color: #eff8ff;
border: 1px;
}

View File

@ -1,144 +0,0 @@
import LineChart from '@/components/line-chart';
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { IDialog, IStats } from '@/interfaces/database/chat';
import { formatDate } from '@/utils/date';
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 ChatApiKeyModal from '../chat-api-key-modal';
import EmbedModal from '../embed-modal';
import {
useFetchStatsOnMount,
usePreviewChat,
useSelectChartStatsList,
useShowEmbedModal,
} from '../hooks';
import styles from './index.less';
const { Paragraph } = Typography;
const { RangePicker } = DatePicker;
const StatsLineChart = ({ statsType }: { statsType: keyof IStats }) => {
const { t } = useTranslate('chat');
const chartList = useSelectChartStatsList();
const list =
chartList[statsType]?.map((x) => ({
...x,
xAxis: formatDate(x.xAxis),
})) ?? [];
return (
<div className={styles.chartItem}>
<b className={styles.chartLabel}>{t(camelCase(statsType))}</b>
<LineChart data={list}></LineChart>
</div>
);
};
const ChatOverviewModal = ({
visible,
hideModal,
dialog,
}: IModalProps<any> & { dialog: IDialog }) => {
const { t } = useTranslate('chat');
const {
visible: apiKeyVisible,
hideModal: hideApiKeyModal,
showModal: showApiKeyModal,
} = useSetModalState();
const {
embedVisible,
hideEmbedModal,
showEmbedModal,
embedToken,
errorContextHolder,
} = useShowEmbedModal(dialog.id);
const { pickerValue, setPickerValue } = useFetchStatsOnMount(visible);
const disabledDate: RangePickerProps['disabledDate'] = (current) => {
return current && current > dayjs().endOf('day');
};
const { handlePreview, contextHolder } = usePreviewChat(dialog.id);
return (
<>
<Modal
title={t('overview')}
open={visible}
onCancel={hideModal}
cancelButtonProps={{ style: { display: 'none' } }}
onOk={hideModal}
width={'100vw'}
okText={t('close', { keyPrefix: 'common' })}
>
<Flex vertical gap={'middle'}>
<Card title={t('backendServiceApi')}>
<Flex gap={8} vertical>
{t('serviceApiEndpoint')}
<Paragraph copyable className={styles.linkText}>
{location.origin}
/v1/api/
</Paragraph>
</Flex>
<Space size={'middle'}>
<Button onClick={showApiKeyModal}>{t('apiKey')}</Button>
<a
href={
'https://github.com/infiniflow/ragflow/blob/main/docs/references/api.md'
}
target="_blank"
rel="noreferrer"
>
<Button>{t('apiReference')}</Button>
</a>
</Space>
</Card>
<Card title={`${dialog.name} Web App`}>
<Flex gap={8} vertical>
<Space size={'middle'}>
<Button onClick={handlePreview}>{t('preview')}</Button>
<Button onClick={showEmbedModal}>{t('embedded')}</Button>
</Space>
</Flex>
</Card>
<Space>
<b>{t('dateRange')}</b>
<RangePicker
disabledDate={disabledDate}
value={pickerValue}
onChange={setPickerValue}
allowClear={false}
/>
</Space>
<div className={styles.chartWrapper}>
<StatsLineChart statsType={'pv'}></StatsLineChart>
<StatsLineChart statsType={'round'}></StatsLineChart>
<StatsLineChart statsType={'speed'}></StatsLineChart>
<StatsLineChart statsType={'thumb_up'}></StatsLineChart>
<StatsLineChart statsType={'tokens'}></StatsLineChart>
<StatsLineChart statsType={'uv'}></StatsLineChart>
</div>
</Flex>
<ChatApiKeyModal
visible={apiKeyVisible}
hideModal={hideApiKeyModal}
dialogId={dialog.id}
></ChatApiKeyModal>
<EmbedModal
token={embedToken}
visible={embedVisible}
hideModal={hideEmbedModal}
></EmbedModal>
{contextHolder}
{errorContextHolder}
</Modal>
</>
);
};
export default ChatOverviewModal;

View File

@ -1,8 +0,0 @@
.codeCard {
.clearCardBody();
}
.codeText {
padding: 10px;
background-color: #e8e8ea;
}

View File

@ -1,70 +0,0 @@
import CopyToClipboard from '@/components/copy-to-clipboard';
import HightLightMarkdown from '@/components/highlight-markdown';
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { Card, Modal, Tabs, TabsProps } from 'antd';
import styles from './index.less';
const EmbedModal = ({
visible,
hideModal,
token = '',
}: IModalProps<any> & { token: string }) => {
const { t } = useTranslate('chat');
const text = `
~~~ html
<iframe
src="${location.origin}/chat/share?shared_id=${token}"
style="width: 100%; height: 100%; min-height: 600px"
frameborder="0"
>
</iframe>
~~~
`;
const items: TabsProps['items'] = [
{
key: '1',
label: t('fullScreenTitle'),
children: (
<Card
title={t('fullScreenDescription')}
extra={<CopyToClipboard text={text}></CopyToClipboard>}
className={styles.codeCard}
>
<HightLightMarkdown>{text}</HightLightMarkdown>
</Card>
),
},
{
key: '2',
label: t('partialTitle'),
children: t('comingSoon'),
},
{
key: '3',
label: t('extensionTitle'),
children: t('comingSoon'),
},
];
const onChange = (key: string) => {
console.log(key);
};
return (
<Modal
title={t('embedModalTitle')}
open={visible}
style={{ top: 300 }}
width={'50vw'}
onOk={hideModal}
onCancel={hideModal}
>
<Tabs defaultActiveKey="1" items={items} onChange={onChange} />
</Modal>
);
};
export default EmbedModal;

View File

@ -1,20 +1,14 @@
import { MessageType } from '@/constants/chat';
import { fileIconMap } from '@/constants/common';
import {
useCreateToken,
useFetchConversation,
useFetchConversationList,
useFetchDialog,
useFetchDialogList,
useFetchStats,
useListToken,
useRemoveConversation,
useRemoveDialog,
useRemoveToken,
useSelectConversationList,
useSelectDialogList,
useSelectStats,
useSelectTokenList,
useSetDialog,
useUpdateConversation,
} from '@/hooks/chat-hooks';
@ -25,16 +19,9 @@ import {
} from '@/hooks/common-hooks';
import { useSendMessageWithSse } from '@/hooks/logic-hooks';
import { useOneNamespaceEffectsLoading } from '@/hooks/store-hooks';
import {
IAnswer,
IConversation,
IDialog,
IStats,
} from '@/interfaces/database/chat';
import { IAnswer, IConversation, IDialog } from '@/interfaces/database/chat';
import { IChunk } from '@/interfaces/database/knowledge';
import { getFileExtension } from '@/utils';
import { message } from 'antd';
import dayjs, { Dayjs } from 'dayjs';
import omit from 'lodash/omit';
import trim from 'lodash/trim';
import {
@ -773,202 +760,3 @@ export const useSendButtonDisabled = (value: string) => {
return trim(value) === '';
};
//#endregion
//#region API provided for external calls
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>([
dayjs(),
dayjs().subtract(7, 'day'),
]);
useEffect(() => {
if (visible && Array.isArray(pickerValue) && pickerValue[0]) {
fetchStats({
fromDate: getDay(pickerValue[0]),
toDate: getDay(pickerValue[1] ?? dayjs()),
});
}
}, [fetchStats, pickerValue, visible]);
return {
pickerValue,
setPickerValue,
};
};
export const useOperateApiKey = (visible: boolean, dialogId: string) => {
const removeToken = useRemoveToken();
const createToken = useCreateToken(dialogId);
const listToken = useListToken();
const tokenList = useSelectTokenList();
const creatingLoading = useOneNamespaceEffectsLoading('chatModel', [
'createToken',
]);
const listLoading = useOneNamespaceEffectsLoading('chatModel', ['list']);
const showDeleteConfirm = useShowDeleteConfirm();
const onRemoveToken = (token: string, tenantId: string) => {
showDeleteConfirm({
onOk: () => removeToken({ dialogId, tokens: [token], tenantId }),
});
};
useEffect(() => {
if (visible && dialogId) {
listToken(dialogId);
}
}, [listToken, dialogId, visible]);
return {
removeToken: onRemoveToken,
createToken,
tokenList,
creatingLoading,
listLoading,
};
};
type ChartStatsType = {
[k in keyof IStats]: Array<{ xAxis: string; yAxis: number }>;
};
export const useSelectChartStatsList = (): ChartStatsType => {
const stats: IStats = useSelectStats();
// const stats = {
// pv: [
// ['2024-06-01', 1],
// ['2024-07-24', 3],
// ['2024-09-01', 10],
// ],
// uv: [
// ['2024-02-01', 0],
// ['2024-03-01', 99],
// ['2024-05-01', 3],
// ],
// speed: [
// ['2024-09-01', 2],
// ['2024-09-01', 3],
// ],
// tokens: [
// ['2024-09-01', 1],
// ['2024-09-01', 3],
// ],
// round: [
// ['2024-09-01', 0],
// ['2024-09-01', 3],
// ],
// thumb_up: [
// ['2024-09-01', 3],
// ['2024-09-01', 9],
// ],
// };
return Object.keys(stats).reduce((pre, cur) => {
const item = stats[cur as keyof IStats];
if (item.length > 0) {
pre[cur as keyof IStats] = item.map((x) => ({
xAxis: x[0] as string,
yAxis: x[1] as number,
}));
}
return pre;
}, {} as ChartStatsType);
};
export const useShowTokenEmptyError = () => {
const [messageApi, contextHolder] = message.useMessage();
const { t } = useTranslate('chat');
const showTokenEmptyError = useCallback(() => {
messageApi.error(t('tokenError'));
}, [messageApi, t]);
return { showTokenEmptyError, contextHolder };
};
const getUrlWithToken = (token: string) => {
const { protocol, host } = window.location;
return `${protocol}//${host}/chat/share?shared_id=${token}`;
};
const useFetchTokenListBeforeOtherStep = (dialogId: string) => {
const { showTokenEmptyError, contextHolder } = useShowTokenEmptyError();
const listToken = useListToken();
const tokenList = useSelectTokenList();
const token =
Array.isArray(tokenList) && tokenList.length > 0 ? tokenList[0].token : '';
const handleOperate = useCallback(async () => {
const data = await listToken(dialogId);
const list = data.data;
if (data.retcode === 0 && Array.isArray(list) && list.length > 0) {
return list[0]?.token;
} else {
showTokenEmptyError();
return false;
}
}, [dialogId, listToken, showTokenEmptyError]);
return {
token,
contextHolder,
handleOperate,
};
};
export const useShowEmbedModal = (dialogId: string) => {
const {
visible: embedVisible,
hideModal: hideEmbedModal,
showModal: showEmbedModal,
} = useSetModalState();
const { handleOperate, token, contextHolder } =
useFetchTokenListBeforeOtherStep(dialogId);
const handleShowEmbedModal = useCallback(async () => {
const succeed = await handleOperate();
if (succeed) {
showEmbedModal();
}
}, [handleOperate, showEmbedModal]);
return {
showEmbedModal: handleShowEmbedModal,
hideEmbedModal,
embedVisible,
embedToken: token,
errorContextHolder: contextHolder,
};
};
export const usePreviewChat = (dialogId: string) => {
const { handleOperate, contextHolder } =
useFetchTokenListBeforeOtherStep(dialogId);
const open = useCallback((t: string) => {
window.open(getUrlWithToken(t), '_blank');
}, []);
const handlePreview = useCallback(async () => {
const token = await handleOperate();
if (token) {
open(token);
}
}, [handleOperate, open]);
return {
handlePreview,
contextHolder,
};
};
//#endregion

View File

@ -41,10 +41,10 @@ import {
useSelectFirstDialogOnMount,
} from './hooks';
import ChatOverviewModal from '@/components/api-service/chat-overview-modal';
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import { useSetSelectedRecord } from '@/hooks/logic-hooks';
import { IDialog } from '@/interfaces/database/chat';
import ChatOverviewModal from './chat-overview-modal';
import styles from './index.less';
const { Text } = Typography;
@ -371,11 +371,15 @@ const Chat = () => {
initialName={initialConversationName}
loading={conversationRenameLoading}
></RenameModal>
<ChatOverviewModal
visible={overviewVisible}
hideModal={hideOverviewModal}
dialog={currentRecord}
></ChatOverviewModal>
{overviewVisible && (
<ChatOverviewModal
visible={overviewVisible}
hideModal={hideOverviewModal}
id={currentRecord.id}
name={currentRecord.name}
idKey="dialogId"
></ChatOverviewModal>
)}
</Flex>
);
};

View File

@ -1,14 +1,7 @@
import {
IConversation,
IDialog,
IStats,
IToken,
Message,
} from '@/interfaces/database/chat';
import { IConversation, IDialog, Message } from '@/interfaces/database/chat';
import i18n from '@/locales/config';
import chatService from '@/services/chat-service';
import { message } from 'antd';
import omit from 'lodash/omit';
import { DvaModel } from 'umi';
import { v4 as uuid } from 'uuid';
import { IClientConversation, IMessage } from './interface';
@ -20,8 +13,6 @@ export interface ChatModelState {
currentDialog: IDialog;
conversationList: IConversation[];
currentConversation: IClientConversation;
tokenList: IToken[];
stats: IStats;
}
const model: DvaModel<ChatModelState> = {
@ -32,8 +23,6 @@ const model: DvaModel<ChatModelState> = {
currentDialog: <IDialog>{},
conversationList: [],
currentConversation: {} as IClientConversation,
tokenList: [],
stats: {} as IStats,
},
reducers: {
save(state, action) {
@ -71,18 +60,6 @@ const model: DvaModel<ChatModelState> = {
currentConversation: { ...payload, message: messageList },
};
},
setTokenList(state, { payload }) {
return {
...state,
tokenList: payload,
};
},
setStats(state, { payload }) {
return {
...state,
stats: payload,
};
},
},
effects: {
@ -183,51 +160,6 @@ const model: DvaModel<ChatModelState> = {
}
return data.retcode;
},
*createToken({ payload }, { call, put }) {
const { data } = yield call(chatService.createToken, payload);
if (data.retcode === 0) {
yield put({
type: 'listToken',
payload: payload,
});
message.success(i18n.t('message.created'));
}
return data;
},
*listToken({ payload }, { call, put }) {
const { data } = yield call(chatService.listToken, payload);
if (data.retcode === 0) {
yield put({
type: 'setTokenList',
payload: data.data,
});
}
return data;
},
*removeToken({ payload }, { call, put }) {
const { data } = yield call(
chatService.removeToken,
omit(payload, ['dialogId']),
);
if (data.retcode === 0) {
message.success(i18n.t('message.deleted'));
yield put({
type: 'listToken',
payload: { dialog_id: payload.dialogId },
});
}
return data.retcode;
},
*getStats({ payload }, { call, put }) {
const { data } = yield call(chatService.getStats, payload);
if (data.retcode === 0) {
yield put({
type: 'setStats',
payload: data.data,
});
}
return data.retcode;
},
*createExternalConversation({ payload }, { call, put }) {
const { data } = yield call(
chatService.createExternalConversation,

View File

@ -1,8 +1,9 @@
import { useTranslate } from '@/hooks/common-hooks';
import ChatOverviewModal from '@/components/api-service/chat-overview-modal';
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import { useFetchFlow } from '@/hooks/flow-hooks';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { Button, Flex, Space } from 'antd';
import { Link } from 'umi';
import { Link, useParams } from 'umi';
import { useSaveGraph, useSaveGraphBeforeOpeningDebugDrawer } from '../hooks';
import styles from './index.less';
@ -15,6 +16,12 @@ const FlowHeader = ({ showChatDrawer }: IProps) => {
const handleRun = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer);
const { data } = useFetchFlow();
const { t } = useTranslate('flow');
const {
visible: overviewVisible,
hideModal: hideOverviewModal,
showModal: showOverviewModal,
} = useSetModalState();
const { id } = useParams();
return (
<>
@ -37,8 +44,19 @@ const FlowHeader = ({ showChatDrawer }: IProps) => {
<Button type="primary" onClick={saveGraph}>
<b>{t('save')}</b>
</Button>
<Button type="primary" onClick={showOverviewModal}>
<b>{t('publish')}</b>
</Button>
</Space>
</Flex>
{overviewVisible && (
<ChatOverviewModal
visible={overviewVisible}
hideModal={hideOverviewModal}
id={id!}
idKey="canvasId"
></ChatOverviewModal>
)}
</>
);
};