diff --git a/web/src/pages/chat/chat-api-key-modal/index.tsx b/web/src/components/api-service/chat-api-key-modal/index.tsx similarity index 93% rename from web/src/pages/chat/chat-api-key-modal/index.tsx rename to web/src/components/api-service/chat-api-key-modal/index.tsx index 9099040904e..46e6761a603 100644 --- a/web/src/pages/chat/chat-api-key-modal/index.tsx +++ b/web/src/components/api-service/chat-api-key-modal/index.tsx @@ -9,12 +9,12 @@ import { Button, Modal, Space, Table } from 'antd'; import { useOperateApiKey } from '../hooks'; const ChatApiKeyModal = ({ - visible, dialogId, hideModal, -}: IModalProps & { dialogId: string }) => { + idKey, +}: IModalProps & { dialogId: string; idKey: string }) => { const { createToken, removeToken, tokenList, listLoading, creatingLoading } = - useOperateApiKey(visible, dialogId); + useOperateApiKey(dialogId, idKey); const { t } = useTranslate('chat'); const columns: TableProps['columns'] = [ @@ -48,7 +48,7 @@ const ChatApiKeyModal = ({ <> { const ChatOverviewModal = ({ visible, hideModal, - dialog, -}: IModalProps & { dialog: IDialog }) => { + id, + name = '', + idKey, +}: IModalProps & { id: string; name?: string; idKey: string }) => { const { t } = useTranslate('chat'); const { visible: apiKeyVisible, @@ -54,15 +56,15 @@ const ChatOverviewModal = ({ showEmbedModal, embedToken, errorContextHolder, - } = useShowEmbedModal(dialog.id); + } = useShowEmbedModal(id, idKey); - const { pickerValue, setPickerValue } = useFetchStatsOnMount(visible); + const { pickerValue, setPickerValue } = useFetchNextStats(); const disabledDate: RangePickerProps['disabledDate'] = (current) => { return current && current > dayjs().endOf('day'); }; - const { handlePreview, contextHolder } = usePreviewChat(dialog.id); + const { handlePreview, contextHolder } = usePreviewChat(id, idKey); return ( <> @@ -97,7 +99,7 @@ const ChatOverviewModal = ({ - + @@ -124,11 +126,13 @@ const ChatOverviewModal = ({ - + {apiKeyVisible && ( + + )} { + const { removeToken } = useRemoveNextToken(); + const { createToken, loading: creatingLoading } = useCreateNextToken(); + const { data: tokenList, loading: listLoading } = useFetchTokenList({ + [idKey]: dialogId, + }); + + const showDeleteConfirm = useShowDeleteConfirm(); + + const onRemoveToken = (token: string, tenantId: string) => { + showDeleteConfirm({ + onOk: () => removeToken({ dialogId, tokens: [token], tenantId }), + }); + }; + + const onCreateToken = useCallback(() => { + createToken({ [idKey]: dialogId }); + }, [createToken, idKey, dialogId]); + + return { + removeToken: onRemoveToken, + createToken: onCreateToken, + tokenList, + creatingLoading, + listLoading, + }; +}; + +type ChartStatsType = { + [k in keyof IStats]: Array<{ xAxis: string; yAxis: number }>; +}; + +export const useSelectChartStatsList = (): ChartStatsType => { + const { data: stats } = useFetchNextStats(); + + 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, idKey: string) => { + const { showTokenEmptyError, contextHolder } = useShowTokenEmptyError(); + + const { data: tokenList, refetch } = useFetchTokenList({ [idKey]: dialogId }); + + const token = + Array.isArray(tokenList) && tokenList.length > 0 ? tokenList[0].token : ''; + + const handleOperate = useCallback(async () => { + const ret = await refetch(); + const list = ret.data; + if (Array.isArray(list) && list.length > 0) { + return list[0]?.token; + } else { + showTokenEmptyError(); + return false; + } + }, [showTokenEmptyError, refetch]); + + return { + token, + contextHolder, + handleOperate, + }; +}; + +export const useShowEmbedModal = (dialogId: string, idKey: string) => { + const { + visible: embedVisible, + hideModal: hideEmbedModal, + showModal: showEmbedModal, + } = useSetModalState(); + + const { handleOperate, token, contextHolder } = + useFetchTokenListBeforeOtherStep(dialogId, idKey); + + 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, idKey: string) => { + const { handleOperate, contextHolder } = useFetchTokenListBeforeOtherStep( + dialogId, + idKey, + ); + + 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, + }; +}; diff --git a/web/src/hooks/chat-hooks.ts b/web/src/hooks/chat-hooks.ts index 467accc42ab..84089f0eae3 100644 --- a/web/src/hooks/chat-hooks.ts +++ b/web/src/hooks/chat-hooks.ts @@ -4,7 +4,10 @@ import { IStats, IToken, } from '@/interfaces/database/chat'; -import { useCallback } from 'react'; +import chatService from '@/services/chat-service'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import dayjs, { Dayjs } from 'dayjs'; +import { useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'umi'; export const useFetchDialogList = () => { @@ -175,79 +178,108 @@ export const useCompleteConversation = () => { // #region API provided for external calls -export const useCreateToken = (dialogId: string) => { +export const useCreateToken = (params: Record) => { const dispatch = useDispatch(); const createToken = useCallback(() => { return dispatch({ type: 'chatModel/createToken', - payload: { dialogId }, + payload: params, }); - }, [dispatch, dialogId]); + }, [dispatch, params]); return createToken; }; -export const useListToken = () => { - const dispatch = useDispatch(); - - const listToken = useCallback( - (dialogId: string) => { - return dispatch({ - type: 'chatModel/listToken', - payload: { dialogId }, - }); +export const useCreateNextToken = () => { + const queryClient = useQueryClient(); + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: ['createToken'], + mutationFn: async (params: Record) => { + const { data } = await chatService.createToken(params); + if (data.retcode === 0) { + queryClient.invalidateQueries({ queryKey: ['fetchTokenList'] }); + } + return data?.data ?? []; }, - [dispatch], - ); + }); - return listToken; + return { data, loading, createToken: mutateAsync }; }; -export const useSelectTokenList = () => { - const tokenList: IToken[] = useSelector( - (state: any) => state.chatModel.tokenList, - ); - - return tokenList; -}; - -export const useRemoveToken = () => { - const dispatch = useDispatch(); - - const removeToken = useCallback( - (payload: { tenantId: string; dialogId: string; tokens: string[] }) => { - return dispatch({ - type: 'chatModel/removeToken', - payload: payload, - }); +export const useFetchTokenList = (params: Record) => { + const { + data, + isFetching: loading, + refetch, + } = useQuery({ + queryKey: ['fetchTokenList', params], + initialData: [], + gcTime: 0, + queryFn: async () => { + const { data } = await chatService.listToken(params); + + return data?.data ?? []; }, - [dispatch], - ); + }); - return removeToken; + return { data, loading, refetch }; }; -export const useFetchStats = () => { - const dispatch = useDispatch(); - - const fetchStats = useCallback( - (payload: any) => { - return dispatch({ - type: 'chatModel/getStats', - payload, - }); +export const useRemoveNextToken = () => { + const queryClient = useQueryClient(); + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: ['removeToken'], + mutationFn: async (params: { + tenantId: string; + dialogId: string; + tokens: string[]; + }) => { + const { data } = await chatService.removeToken(params); + if (data.retcode === 0) { + queryClient.invalidateQueries({ queryKey: ['fetchTokenList'] }); + } + return data?.data ?? []; }, - [dispatch], - ); + }); - return fetchStats; + return { data, loading, removeToken: mutateAsync }; }; -export const useSelectStats = () => { - const stats: IStats = useSelector((state: any) => state.chatModel.stats); +type RangeValue = [Dayjs | null, Dayjs | null] | null; + +const getDay = (date?: Dayjs) => date?.format('YYYY-MM-DD'); + +export const useFetchNextStats = () => { + const [pickerValue, setPickerValue] = useState([ + dayjs(), + dayjs().subtract(7, 'day'), + ]); + const { data, isFetching: loading } = useQuery({ + queryKey: ['fetchStats', pickerValue], + initialData: {} as IStats, + gcTime: 0, + queryFn: async () => { + if (Array.isArray(pickerValue) && pickerValue[0]) { + const { data } = await chatService.getStats({ + fromDate: getDay(pickerValue[0]), + toDate: getDay(pickerValue[1] ?? dayjs()), + }); + return data?.data ?? {}; + } + return {}; + }, + }); - return stats; + return { data, loading, pickerValue, setPickerValue }; }; //#endregion diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 0a80be56fc7..a5720766b20 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -783,6 +783,7 @@ The above is the content you need to summarize.`, '15d': '12 days', '30d': '30 days', }, + publish: 'Publish', }, footer: { profile: 'All rights reserved @ React', diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index e8ff4ba1677..8ddb7cd250d 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -741,6 +741,7 @@ export default { '15d': '12天', '30d': '30天', }, + publish: '發布', }, footer: { profile: '“保留所有權利 @ react”', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index c3f54bf3dce..b9a91c77461 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -759,6 +759,7 @@ export default { '15d': '12天', '30d': '30天', }, + publish: '发布', }, footer: { profile: 'All rights reserved @ React', diff --git a/web/src/pages/chat/hooks.ts b/web/src/pages/chat/hooks.ts index 205cbfec21d..f0e98d1a93f 100644 --- a/web/src/pages/chat/hooks.ts +++ b/web/src/pages/chat/hooks.ts @@ -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([ - 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 diff --git a/web/src/pages/chat/index.tsx b/web/src/pages/chat/index.tsx index bca68cca9b2..ac6f66e9a54 100644 --- a/web/src/pages/chat/index.tsx +++ b/web/src/pages/chat/index.tsx @@ -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} > - + {overviewVisible && ( + + )} ); }; diff --git a/web/src/pages/chat/model.ts b/web/src/pages/chat/model.ts index 4fc66d0cd0d..3e32492d100 100644 --- a/web/src/pages/chat/model.ts +++ b/web/src/pages/chat/model.ts @@ -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 = { @@ -32,8 +23,6 @@ const model: DvaModel = { currentDialog: {}, conversationList: [], currentConversation: {} as IClientConversation, - tokenList: [], - stats: {} as IStats, }, reducers: { save(state, action) { @@ -71,18 +60,6 @@ const model: DvaModel = { 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 = { } 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, diff --git a/web/src/pages/flow/header/index.tsx b/web/src/pages/flow/header/index.tsx index da2ac25d71d..e89113d2422 100644 --- a/web/src/pages/flow/header/index.tsx +++ b/web/src/pages/flow/header/index.tsx @@ -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) => { + + {overviewVisible && ( + + )} ); };