### What problem does this PR solve? Feat: Add log-detail page,Improve the style of chat boxes #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| ></ReferenceDocumentList> | ></ReferenceDocumentList> | ||||
| )} | )} | ||||
| {isAssistant && currentEventListWithoutMessageById && ( | {isAssistant && currentEventListWithoutMessageById && ( | ||||
| <WorkFlowTimeline | |||||
| currentEventListWithoutMessage={currentEventListWithoutMessageById( | |||||
| item.id, | |||||
| )} | |||||
| currentMessageId={item.id} | |||||
| canvasId={conversationId} | |||||
| /> | |||||
| <div className="mt-4"> | |||||
| <WorkFlowTimeline | |||||
| currentEventListWithoutMessage={currentEventListWithoutMessageById( | |||||
| item.id, | |||||
| )} | |||||
| currentMessageId={item.id} | |||||
| canvasId={conversationId} | |||||
| /> | |||||
| </div> | |||||
| )} | )} | ||||
| {isUser && ( | {isUser && ( | ||||
| <UploadedMessageFiles files={item.files}></UploadedMessageFiles> | <UploadedMessageFiles files={item.files}></UploadedMessageFiles> |
| const [date, setDate] = useState<DateRange | undefined>( | const [date, setDate] = useState<DateRange | undefined>( | ||||
| selectDateRange || { from: today, to: today }, | selectDateRange || { from: today, to: today }, | ||||
| ); | ); | ||||
| useEffect(() => { | |||||
| setDate(selectDateRange); | |||||
| }, [selectDateRange]); | |||||
| const onChange = (e: DateRange | undefined) => { | const onChange = (e: DateRange | undefined) => { | ||||
| if (!e) return; | if (!e) return; | ||||
| setDate(e); | setDate(e); |
| export interface IAnswerEvent<T> { | export interface IAnswerEvent<T> { | ||||
| event: MessageEventType; | event: MessageEventType; | ||||
| message_id: string; | message_id: string; | ||||
| session_id: string; | |||||
| created_at: number; | created_at: number; | ||||
| task_id: string; | task_id: string; | ||||
| data: T; | data: T; |
| const { id: agentId } = useParams(); | const { id: agentId } = useParams(); | ||||
| const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | ||||
| const inputs = useSelectBeginNodeDataInputs(); | const inputs = useSelectBeginNodeDataInputs(); | ||||
| const [sessionId, setSessionId] = useState<string | null>(null); | |||||
| const { send, answerList, done, stopOutputMessage } = useSendMessageBySSE( | const { send, answerList, done, stopOutputMessage } = useSendMessageBySSE( | ||||
| url || api.runCanvas, | url || api.runCanvas, | ||||
| ); | ); | ||||
| return answerList[0]?.message_id; | return answerList[0]?.message_id; | ||||
| }, [answerList]); | }, [answerList]); | ||||
| useEffect(() => { | |||||
| if (answerList[0]?.session_id) { | |||||
| setSessionId(answerList[0]?.session_id); | |||||
| } | |||||
| }, [answerList]); | |||||
| const { findReferenceByMessageId } = useFindMessageReference(answerList); | const { findReferenceByMessageId } = useFindMessageReference(answerList); | ||||
| const prologue = useGetBeginNodePrologue(); | const prologue = useGetBeginNodePrologue(); | ||||
| const { | const { | ||||
| params.inputs = transferInputsArrayToObject(query); // begin operator inputs | params.inputs = transferInputsArrayToObject(query); // begin operator inputs | ||||
| params.files = uploadResponseList; | params.files = uploadResponseList; | ||||
| params.session_id = sessionId; | |||||
| } | } | ||||
| const res = await send(params); | const res = await send(params); | ||||
| }, | }, | ||||
| [ | [ | ||||
| agentId, | agentId, | ||||
| sessionId, | |||||
| send, | send, | ||||
| inputs, | inputs, | ||||
| uploadResponseList, | uploadResponseList, |
| setEventList([]); | setEventList([]); | ||||
| }, []); | }, []); | ||||
| const addEventList = useCallback( | |||||
| (events: IEventList, message_id: string) => { | |||||
| const nextList = [...eventList]; | |||||
| events.forEach((x) => { | |||||
| if (nextList.every((y) => y !== x)) { | |||||
| nextList.push(x); | |||||
| } | |||||
| }); | |||||
| setEventList(nextList); | |||||
| setMessageIdPool((prev) => ({ ...prev, [message_id]: nextList })); | |||||
| }, | |||||
| [eventList], | |||||
| ); | |||||
| const addEventList = useCallback((events: IEventList, message_id: string) => { | |||||
| setEventList((x) => { | |||||
| const list = [...x, ...events]; | |||||
| setMessageIdPool((prev) => ({ ...prev, [message_id]: list })); | |||||
| return list; | |||||
| }); | |||||
| }, []); | |||||
| const currentEventListWithoutMessage = useMemo(() => { | const currentEventListWithoutMessage = useMemo(() => { | ||||
| const list = messageIdPool[currentMessageId]?.filter( | const list = messageIdPool[currentMessageId]?.filter( |
| const ToolTimelineItem = ({ tools }: { tools: Record<string, any>[] }) => { | const ToolTimelineItem = ({ tools }: { tools: Record<string, any>[] }) => { | ||||
| if (!tools || tools.length === 0 || !Array.isArray(tools)) return null; | if (!tools || tools.length === 0 || !Array.isArray(tools)) return null; | ||||
| const blackList = ['analyze_task', 'add_memory', 'gen_citations']; | |||||
| const blackList = ['add_memory', 'gen_citations']; | |||||
| const filteredTools = tools.filter( | const filteredTools = tools.filter( | ||||
| (tool) => !blackList.includes(tool.tool_name), | (tool) => !blackList.includes(tool.tool_name), | ||||
| ); | ); | ||||
| const capitalizeWords = (str: string, separator: string = '_'): string => { | |||||
| if (!str) return ''; | |||||
| return str | |||||
| .split(separator) | |||||
| .map((word) => { | |||||
| return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); | |||||
| }) | |||||
| .join(' '); | |||||
| }; | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| {filteredTools?.map((tool, idx) => { | {filteredTools?.map((tool, idx) => { | ||||
| return ( | return ( | ||||
| <TimelineItem | <TimelineItem | ||||
| key={idx} | |||||
| key={'tool_' + idx} | |||||
| step={idx} | step={idx} | ||||
| className="group-data-[orientation=vertical]/timeline:ms-10 group-data-[orientation=vertical]/timeline:not-last:pb-8" | className="group-data-[orientation=vertical]/timeline:ms-10 group-data-[orientation=vertical]/timeline:not-last:pb-8" | ||||
| > | > | ||||
| <AccordionItem value={idx.toString()}> | <AccordionItem value={idx.toString()}> | ||||
| <AccordionTrigger> | <AccordionTrigger> | ||||
| <div className="flex gap-2 items-center"> | <div className="flex gap-2 items-center"> | ||||
| <span>{tool.tool_name}</span> | |||||
| <span> | |||||
| {tool.path + ' '} | |||||
| {capitalizeWords(tool.tool_name, '_')} | |||||
| </span> | |||||
| <span className="text-text-sub-title text-xs"> | <span className="text-text-sub-title text-xs"> | ||||
| {/* 0:00 | {/* 0:00 | ||||
| {x.data.elapsed_time?.toString().slice(0, 6)} */} | {x.data.elapsed_time?.toString().slice(0, 6)} */} |
| import MessageItem from '@/components/next-message-item'; | |||||
| import { Modal } from '@/components/ui/modal'; | |||||
| import { useFetchAgent } from '@/hooks/use-agent-request'; | |||||
| import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | |||||
| import { IAgentLogMessage } from '@/interfaces/database/agent'; | |||||
| import { IReferenceObject, Message } from '@/interfaces/database/chat'; | |||||
| import { buildMessageUuidWithRole } from '@/utils/chat'; | |||||
| import React from 'react'; | |||||
| import { IMessage } from '../chat/interface'; | |||||
| interface CustomModalProps { | |||||
| isOpen: boolean; | |||||
| onClose: () => void; | |||||
| message: IAgentLogMessage[]; | |||||
| reference: IReferenceObject; | |||||
| } | |||||
| export const AgentLogDetailModal: React.FC<CustomModalProps> = ({ | |||||
| isOpen, | |||||
| onClose, | |||||
| message: derivedMessages, | |||||
| reference, | |||||
| }) => { | |||||
| const { data: userInfo } = useFetchUserInfo(); | |||||
| const { data: canvasInfo } = useFetchAgent(); | |||||
| return ( | |||||
| <Modal | |||||
| open={isOpen} | |||||
| onCancel={onClose} | |||||
| showfooter={false} | |||||
| footer={null} | |||||
| title={derivedMessages?.length ? derivedMessages[0]?.content : ''} | |||||
| className="!w-[900px]" | |||||
| > | |||||
| <div className="flex items-start mb-4 flex-col gap-4 justify-start"> | |||||
| <div> | |||||
| {derivedMessages?.map((message, i) => { | |||||
| return ( | |||||
| <MessageItem | |||||
| key={buildMessageUuidWithRole( | |||||
| message as Partial<Message | IMessage>, | |||||
| )} | |||||
| nickname={userInfo.nickname} | |||||
| avatar={userInfo.avatar} | |||||
| avatarDialog={canvasInfo.avatar} | |||||
| item={message as IMessage} | |||||
| reference={reference} | |||||
| index={i} | |||||
| showLikeButton={false} | |||||
| showLog={false} | |||||
| ></MessageItem> | |||||
| ); | |||||
| })} | |||||
| </div> | |||||
| </div> | |||||
| </Modal> | |||||
| ); | |||||
| }; |
| import { Spin } from '@/components/ui/spin'; | import { Spin } from '@/components/ui/spin'; | ||||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | ||||
| import { useFetchAgentLog } from '@/hooks/use-agent-request'; | import { useFetchAgentLog } from '@/hooks/use-agent-request'; | ||||
| import { IAgentLogResponse } from '@/interfaces/database/agent'; | |||||
| import { | |||||
| IAgentLogMessage, | |||||
| IAgentLogResponse, | |||||
| } from '@/interfaces/database/agent'; | |||||
| import { IReferenceObject } from '@/interfaces/database/chat'; | |||||
| import { useQueryClient } from '@tanstack/react-query'; | |||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState } from 'react'; | ||||
| import { useParams } from 'umi'; | import { useParams } from 'umi'; | ||||
| import { DateRange } from '../../components/originui/calendar/index'; | import { DateRange } from '../../components/originui/calendar/index'; | ||||
| TableRow, | TableRow, | ||||
| } from '../../components/ui/table'; | } from '../../components/ui/table'; | ||||
| import { useFetchDataOnMount } from '../agent/hooks/use-fetch-data'; | import { useFetchDataOnMount } from '../agent/hooks/use-fetch-data'; | ||||
| import { AgentLogDetailModal } from './agent-log-detail-modal'; | |||||
| const getStartOfToday = (): Date => { | |||||
| const today = new Date(); | |||||
| today.setHours(0, 0, 0, 0); | |||||
| return today; | |||||
| }; | |||||
| const getEndOfToday = (): Date => { | |||||
| const today = new Date(); | |||||
| today.setHours(23, 59, 59, 999); | |||||
| return today; | |||||
| }; | |||||
| const AgentLogPage: React.FC = () => { | const AgentLogPage: React.FC = () => { | ||||
| const { navigateToAgentList, navigateToAgent } = useNavigatePage(); | const { navigateToAgentList, navigateToAgent } = useNavigatePage(); | ||||
| const { flowDetail: agentDetail } = useFetchDataOnMount(); | const { flowDetail: agentDetail } = useFetchDataOnMount(); | ||||
| const { id: canvasId } = useParams(); | const { id: canvasId } = useParams(); | ||||
| const today = new Date(); | |||||
| const queryClient = useQueryClient(); | |||||
| const init = { | const init = { | ||||
| keywords: '', | keywords: '', | ||||
| from_date: today, | |||||
| to_date: today, | |||||
| from_date: getStartOfToday(), | |||||
| to_date: getEndOfToday(), | |||||
| orderby: 'create_time', | orderby: 'create_time', | ||||
| desc: false, | desc: false, | ||||
| page: 1, | page: 1, | ||||
| }); | }); | ||||
| }; | }; | ||||
| const handleClickSearch = () => { | |||||
| setPagination({ ...pagination, current: 1 }); | |||||
| handleSearch(); | |||||
| queryClient.invalidateQueries({ | |||||
| queryKey: ['fetchAgentLog'], | |||||
| }); | |||||
| }; | |||||
| useEffect(() => { | useEffect(() => { | ||||
| handleSearch(); | handleSearch(); | ||||
| }, [pagination.current, pagination.pageSize, sortConfig]); | }, [pagination.current, pagination.pageSize, sortConfig]); | ||||
| const handleReset = () => { | const handleReset = () => { | ||||
| setSearchParams(init); | setSearchParams(init); | ||||
| setKeywords(init.keywords); | |||||
| setCurrentDate({ from: init.from_date, to: init.to_date }); | |||||
| }; | |||||
| const [openModal, setOpenModal] = useState(false); | |||||
| const [modalData, setModalData] = useState<IAgentLogResponse>(); | |||||
| const showLogDetail = (item: IAgentLogResponse) => { | |||||
| setModalData(item); | |||||
| setOpenModal(true); | |||||
| }; | }; | ||||
| return ( | return ( | ||||
| <div className=" text-white"> | <div className=" text-white"> | ||||
| <PageHeader> | <PageHeader> | ||||
| <span className="whitespace-nowrap">Latest Date</span> | <span className="whitespace-nowrap">Latest Date</span> | ||||
| <TimeRangePicker | <TimeRangePicker | ||||
| onSelect={handleDateRangeChange} | onSelect={handleDateRangeChange} | ||||
| selectDateRange={{ from: currentDate.from, to: currentDate.to }} | |||||
| selectDateRange={currentDate} | |||||
| /> | /> | ||||
| </div> | </div> | ||||
| <button | <button | ||||
| type="button" | type="button" | ||||
| className="bg-foreground text-text-title-invert px-4 py-1 rounded" | className="bg-foreground text-text-title-invert px-4 py-1 rounded" | ||||
| onClick={() => { | onClick={() => { | ||||
| setPagination({ ...pagination, current: 1 }); | |||||
| handleSearch(); | |||||
| handleClickSearch(); | |||||
| }} | }} | ||||
| > | > | ||||
| Search | Search | ||||
| )} | )} | ||||
| {!loading && | {!loading && | ||||
| data?.map((item) => ( | data?.map((item) => ( | ||||
| <TableRow key={item.id}> | |||||
| <TableRow | |||||
| key={item.id} | |||||
| onClick={() => { | |||||
| showLogDetail(item); | |||||
| }} | |||||
| > | |||||
| {columns.map((column) => ( | {columns.map((column) => ( | ||||
| <TableCell key={column.dataIndex}> | <TableCell key={column.dataIndex}> | ||||
| {column.render | {column.render | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <AgentLogDetailModal | |||||
| isOpen={openModal} | |||||
| message={modalData?.message as IAgentLogMessage[]} | |||||
| reference={modalData?.reference as unknown as IReferenceObject} | |||||
| onClose={() => setOpenModal(false)} | |||||
| /> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| }; | }; |
| } | } | ||||
| return ( | return ( | ||||
| <section className="h-[100vh]"> | |||||
| <section className={cn('flex flex-1 flex-col p-2.5 h-full')}> | |||||
| <div className={cn('flex flex-1 flex-col overflow-auto pr-2')}> | |||||
| <div> | |||||
| {derivedMessages?.map((message, i) => { | |||||
| return ( | |||||
| <MessageItem | |||||
| visibleAvatar={visibleAvatar} | |||||
| conversationId={conversationId} | |||||
| currentEventListWithoutMessageById={ | |||||
| currentEventListWithoutMessageById | |||||
| } | |||||
| setCurrentMessageId={setCurrentMessageId} | |||||
| key={buildMessageUuidWithRole(message)} | |||||
| avatarDialog={avatarData.avatar} | |||||
| item={message} | |||||
| nickname="You" | |||||
| reference={findReferenceByMessageId(message.id)} | |||||
| loading={ | |||||
| message.role === MessageType.Assistant && | |||||
| sendLoading && | |||||
| derivedMessages?.length - 1 === i | |||||
| } | |||||
| index={i} | |||||
| clickDocumentButton={clickDocumentButton} | |||||
| showLikeButton={false} | |||||
| showLoudspeaker={false} | |||||
| showLog={false} | |||||
| sendLoading={sendLoading} | |||||
| ></MessageItem> | |||||
| ); | |||||
| })} | |||||
| <section className="h-[100vh] flex justify-center items-center"> | |||||
| <div className=" w-[80vw]"> | |||||
| <div className="flex flex-1 flex-col p-2.5 h-[90vh] border rounded-lg"> | |||||
| <div | |||||
| className={cn('flex flex-1 flex-col overflow-auto m-auto w-5/6')} | |||||
| > | |||||
| <div> | |||||
| {derivedMessages?.map((message, i) => { | |||||
| return ( | |||||
| <MessageItem | |||||
| visibleAvatar={visibleAvatar} | |||||
| conversationId={conversationId} | |||||
| currentEventListWithoutMessageById={ | |||||
| currentEventListWithoutMessageById | |||||
| } | |||||
| setCurrentMessageId={setCurrentMessageId} | |||||
| key={buildMessageUuidWithRole(message)} | |||||
| avatarDialog={avatarData.avatar} | |||||
| item={message} | |||||
| nickname="You" | |||||
| reference={findReferenceByMessageId(message.id)} | |||||
| loading={ | |||||
| message.role === MessageType.Assistant && | |||||
| sendLoading && | |||||
| derivedMessages?.length - 1 === i | |||||
| } | |||||
| index={i} | |||||
| clickDocumentButton={clickDocumentButton} | |||||
| showLikeButton={false} | |||||
| showLoudspeaker={false} | |||||
| showLog={false} | |||||
| sendLoading={sendLoading} | |||||
| ></MessageItem> | |||||
| ); | |||||
| })} | |||||
| </div> | |||||
| <div ref={ref} /> | |||||
| </div> | |||||
| <div className="flex w-full justify-center mb-8"> | |||||
| <div className="w-5/6"> | |||||
| <NextMessageInput | |||||
| isShared | |||||
| value={value} | |||||
| disabled={hasError} | |||||
| sendDisabled={sendDisabled} | |||||
| conversationId={conversationId} | |||||
| onInputChange={handleInputChange} | |||||
| onPressEnter={handlePressEnter} | |||||
| sendLoading={sendLoading} | |||||
| stopOutputMessage={stopOutputMessage} | |||||
| onUpload={handleUploadFile} | |||||
| isUploading={loading} | |||||
| ></NextMessageInput> | |||||
| </div> | |||||
| </div> | </div> | ||||
| <div ref={ref} /> | |||||
| </div> | </div> | ||||
| <NextMessageInput | |||||
| isShared | |||||
| value={value} | |||||
| disabled={hasError} | |||||
| sendDisabled={sendDisabled} | |||||
| conversationId={conversationId} | |||||
| onInputChange={handleInputChange} | |||||
| onPressEnter={handlePressEnter} | |||||
| sendLoading={sendLoading} | |||||
| stopOutputMessage={stopOutputMessage} | |||||
| onUpload={handleUploadFile} | |||||
| isUploading={loading} | |||||
| ></NextMessageInput> | |||||
| </section> | |||||
| </div> | |||||
| {visible && ( | {visible && ( | ||||
| <PdfDrawer | <PdfDrawer | ||||
| visible={visible} | visible={visible} |