| @@ -105,6 +105,8 @@ class ChatMessageListApi(Resource): | |||
| if rest_count > 0: | |||
| has_more = True | |||
| history_messages = list(reversed(history_messages)) | |||
| return InfiniteScrollPagination(data=history_messages, limit=args["limit"], has_more=has_more) | |||
| @@ -17,6 +17,7 @@ import { createContext, useContext } from 'use-context-selector' | |||
| import { useShallow } from 'zustand/react/shallow' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { UUID_NIL } from '../../base/chat/constants' | |||
| import type { ChatItemInTree } from '../../base/chat/types' | |||
| import VarPanel from './var-panel' | |||
| import cn from '@/utils/classnames' | |||
| import type { FeedbackFunc, FeedbackType, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type' | |||
| @@ -41,6 +42,7 @@ import { useAppContext } from '@/context/app-context' | |||
| import useTimestamp from '@/hooks/use-timestamp' | |||
| import Tooltip from '@/app/components/base/tooltip' | |||
| import { CopyIcon } from '@/app/components/base/copy-icon' | |||
| import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils' | |||
| import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' | |||
| dayjs.extend(utc) | |||
| @@ -139,6 +141,7 @@ function appendQAToChatList(newChatList: IChatItem[], item: any, conversationId: | |||
| })(), | |||
| parentMessageId: `question-${item.id}`, | |||
| }) | |||
| const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || [] | |||
| newChatList.push({ | |||
| id: `question-${item.id}`, | |||
| @@ -193,50 +196,66 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { | |||
| currentLogModalActiveTab: state.currentLogModalActiveTab, | |||
| }))) | |||
| const { t } = useTranslation() | |||
| const [items, setItems] = React.useState<IChatItem[]>([]) | |||
| const fetchedMessages = useRef<ChatMessage[]>([]) | |||
| const [hasMore, setHasMore] = useState(true) | |||
| const [varValues, setVarValues] = useState<Record<string, string>>({}) | |||
| const fetchData = async () => { | |||
| const [allChatItems, setAllChatItems] = useState<IChatItem[]>([]) | |||
| const [chatItemTree, setChatItemTree] = useState<ChatItemInTree[]>([]) | |||
| const [threadChatItems, setThreadChatItems] = useState<IChatItem[]>([]) | |||
| const fetchData = useCallback(async () => { | |||
| try { | |||
| if (!hasMore) | |||
| return | |||
| const params: ChatMessagesRequest = { | |||
| conversation_id: detail.id, | |||
| limit: 10, | |||
| } | |||
| if (items?.[0]?.id) | |||
| params.first_id = items?.[0]?.id.replace('question-', '') | |||
| if (allChatItems.at(-1)?.id) | |||
| params.first_id = allChatItems.at(-1)?.id.replace('question-', '') | |||
| const messageRes = await fetchChatMessages({ | |||
| url: `/apps/${appDetail?.id}/chat-messages`, | |||
| params, | |||
| }) | |||
| if (messageRes.data.length > 0) { | |||
| const varValues = messageRes.data[0].inputs | |||
| const varValues = messageRes.data.at(-1)!.inputs | |||
| setVarValues(varValues) | |||
| } | |||
| fetchedMessages.current = [...fetchedMessages.current, ...messageRes.data] | |||
| const newItems = getFormattedChatList(fetchedMessages.current, detail.id, timezone!, t('appLog.dateTimeFormat') as string) | |||
| setHasMore(messageRes.has_more) | |||
| const newAllChatItems = [ | |||
| ...getFormattedChatList(messageRes.data, detail.id, timezone!, t('appLog.dateTimeFormat') as string), | |||
| ...allChatItems, | |||
| ] | |||
| setAllChatItems(newAllChatItems) | |||
| let tree = buildChatItemTree(newAllChatItems) | |||
| if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) { | |||
| newItems.unshift({ | |||
| tree = [{ | |||
| id: 'introduction', | |||
| isAnswer: true, | |||
| isOpeningStatement: true, | |||
| content: detail?.model_config?.configs?.introduction ?? 'hello', | |||
| feedbackDisabled: true, | |||
| }) | |||
| children: tree, | |||
| }] | |||
| } | |||
| setItems(newItems) | |||
| setHasMore(messageRes.has_more) | |||
| setChatItemTree(tree) | |||
| setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id)) | |||
| } | |||
| catch (err) { | |||
| console.error(err) | |||
| } | |||
| } | |||
| }, [allChatItems, detail.id, hasMore, timezone, t, appDetail, detail?.model_config?.configs?.introduction]) | |||
| const switchSibling = useCallback((siblingMessageId: string) => { | |||
| setThreadChatItems(getThreadMessages(chatItemTree, siblingMessageId)) | |||
| }, [chatItemTree]) | |||
| const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => { | |||
| setItems(items.map((item, i) => { | |||
| setAllChatItems(allChatItems.map((item, i) => { | |||
| if (i === index - 1) { | |||
| return { | |||
| ...item, | |||
| @@ -257,9 +276,9 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { | |||
| } | |||
| return item | |||
| })) | |||
| }, [items]) | |||
| }, [allChatItems]) | |||
| const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => { | |||
| setItems(items.map((item, i) => { | |||
| setAllChatItems(allChatItems.map((item, i) => { | |||
| if (i === index - 1) { | |||
| return { | |||
| ...item, | |||
| @@ -287,9 +306,9 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { | |||
| } | |||
| return item | |||
| })) | |||
| }, [items]) | |||
| }, [allChatItems]) | |||
| const handleAnnotationRemoved = useCallback((index: number) => { | |||
| setItems(items.map((item, i) => { | |||
| setAllChatItems(allChatItems.map((item, i) => { | |||
| if (i === index) { | |||
| return { | |||
| ...item, | |||
| @@ -299,7 +318,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { | |||
| } | |||
| return item | |||
| })) | |||
| }, [items]) | |||
| }, [allChatItems]) | |||
| const fetchInitiated = useRef(false) | |||
| @@ -464,7 +483,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { | |||
| siteInfo={null} | |||
| /> | |||
| </div> | |||
| : (items.length < 8 && !hasMore) | |||
| : threadChatItems.length < 8 | |||
| ? <div className="pt-4 mb-4"> | |||
| <Chat | |||
| config={{ | |||
| @@ -478,7 +497,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { | |||
| }, | |||
| supportFeedback: true, | |||
| } as any} | |||
| chatList={items} | |||
| chatList={threadChatItems} | |||
| onAnnotationAdded={handleAnnotationAdded} | |||
| onAnnotationEdited={handleAnnotationEdited} | |||
| onAnnotationRemoved={handleAnnotationRemoved} | |||
| @@ -487,6 +506,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { | |||
| showPromptLog | |||
| hideProcessDetail | |||
| chatContainerInnerClassName='px-6' | |||
| switchSibling={switchSibling} | |||
| /> | |||
| </div> | |||
| : <div | |||
| @@ -501,7 +521,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { | |||
| {/* Put the scroll bar always on the bottom */} | |||
| <InfiniteScroll | |||
| scrollableTarget="scrollableDiv" | |||
| dataLength={items.length} | |||
| dataLength={threadChatItems.length} | |||
| next={fetchData} | |||
| hasMore={hasMore} | |||
| loader={<div className='text-center text-gray-400 text-xs'>{t('appLog.detail.loading')}...</div>} | |||
| @@ -532,7 +552,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { | |||
| }, | |||
| supportFeedback: true, | |||
| } as any} | |||
| chatList={items} | |||
| chatList={threadChatItems} | |||
| onAnnotationAdded={handleAnnotationAdded} | |||
| onAnnotationEdited={handleAnnotationEdited} | |||
| onAnnotationRemoved={handleAnnotationRemoved} | |||
| @@ -541,6 +561,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { | |||
| showPromptLog | |||
| hideProcessDetail | |||
| chatContainerInnerClassName='px-6' | |||
| switchSibling={switchSibling} | |||
| /> | |||
| </InfiniteScroll> | |||
| </div> | |||
| @@ -0,0 +1,42 @@ | |||
| [ | |||
| { | |||
| "id": "question-1", | |||
| "isAnswer": false, | |||
| "parentMessageId": null | |||
| }, | |||
| { | |||
| "id": "1", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-1" | |||
| }, | |||
| { | |||
| "id": "question-2", | |||
| "isAnswer": false, | |||
| "parentMessageId": "1" | |||
| }, | |||
| { | |||
| "id": "2", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-2" | |||
| }, | |||
| { | |||
| "id": "question-3", | |||
| "isAnswer": false, | |||
| "parentMessageId": "2" | |||
| }, | |||
| { | |||
| "id": "3", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-3" | |||
| }, | |||
| { | |||
| "id": "question-4", | |||
| "isAnswer": false, | |||
| "parentMessageId": "1" | |||
| }, | |||
| { | |||
| "id": "4", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-4" | |||
| } | |||
| ] | |||
| @@ -0,0 +1,42 @@ | |||
| [ | |||
| { | |||
| "id": "question-1", | |||
| "isAnswer": false, | |||
| "parentMessageId": "00000000-0000-0000-0000-000000000000" | |||
| }, | |||
| { | |||
| "id": "1", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-1" | |||
| }, | |||
| { | |||
| "id": "question-2", | |||
| "isAnswer": false, | |||
| "parentMessageId": "00000000-0000-0000-0000-000000000000" | |||
| }, | |||
| { | |||
| "id": "2", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-2" | |||
| }, | |||
| { | |||
| "id": "question-3", | |||
| "isAnswer": false, | |||
| "parentMessageId": "00000000-0000-0000-0000-000000000000" | |||
| }, | |||
| { | |||
| "id": "3", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-3" | |||
| }, | |||
| { | |||
| "id": "question-4", | |||
| "isAnswer": false, | |||
| "parentMessageId": "00000000-0000-0000-0000-000000000000" | |||
| }, | |||
| { | |||
| "id": "4", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-4" | |||
| } | |||
| ] | |||
| @@ -0,0 +1,42 @@ | |||
| [ | |||
| { | |||
| "id": "question-1", | |||
| "isAnswer": false, | |||
| "parentMessageId": "00000000-0000-0000-0000-000000000000" | |||
| }, | |||
| { | |||
| "id": "1", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-1" | |||
| }, | |||
| { | |||
| "id": "question-2", | |||
| "isAnswer": false, | |||
| "parentMessageId": "00000000-0000-0000-0000-000000000000" | |||
| }, | |||
| { | |||
| "id": "2", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-2" | |||
| }, | |||
| { | |||
| "id": "question-3", | |||
| "isAnswer": false, | |||
| "parentMessageId": "2" | |||
| }, | |||
| { | |||
| "id": "3", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-3" | |||
| }, | |||
| { | |||
| "id": "question-4", | |||
| "isAnswer": false, | |||
| "parentMessageId": "1" | |||
| }, | |||
| { | |||
| "id": "4", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-4" | |||
| } | |||
| ] | |||
| @@ -0,0 +1,52 @@ | |||
| [ | |||
| { | |||
| "id": "question-1", | |||
| "isAnswer": false, | |||
| "parentMessageId": null | |||
| }, | |||
| { | |||
| "id": "1", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-1" | |||
| }, | |||
| { | |||
| "id": "question-2", | |||
| "isAnswer": false, | |||
| "parentMessageId": "1" | |||
| }, | |||
| { | |||
| "id": "2", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-2" | |||
| }, | |||
| { | |||
| "id": "question-3", | |||
| "isAnswer": false, | |||
| "parentMessageId": "2" | |||
| }, | |||
| { | |||
| "id": "3", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-3" | |||
| }, | |||
| { | |||
| "id": "question-4", | |||
| "isAnswer": false, | |||
| "parentMessageId": "1" | |||
| }, | |||
| { | |||
| "id": "4", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-4" | |||
| }, | |||
| { | |||
| "id": "question-5", | |||
| "isAnswer": false, | |||
| "parentMessageId": null | |||
| }, | |||
| { | |||
| "id": "5", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-5" | |||
| } | |||
| ] | |||
| @@ -0,0 +1,52 @@ | |||
| [ | |||
| { | |||
| "id": "question-1", | |||
| "isAnswer": false, | |||
| "parentMessageId": "00000000-0000-0000-0000-000000000000" | |||
| }, | |||
| { | |||
| "id": "1", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-1" | |||
| }, | |||
| { | |||
| "id": "question-2", | |||
| "isAnswer": false, | |||
| "parentMessageId": "00000000-0000-0000-0000-000000000000" | |||
| }, | |||
| { | |||
| "id": "2", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-2" | |||
| }, | |||
| { | |||
| "id": "question-3", | |||
| "isAnswer": false, | |||
| "parentMessageId": "00000000-0000-0000-0000-000000000000" | |||
| }, | |||
| { | |||
| "id": "3", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-3" | |||
| }, | |||
| { | |||
| "id": "question-4", | |||
| "isAnswer": false, | |||
| "parentMessageId": "1" | |||
| }, | |||
| { | |||
| "id": "4", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-4" | |||
| }, | |||
| { | |||
| "id": "question-5", | |||
| "isAnswer": false, | |||
| "parentMessageId": null | |||
| }, | |||
| { | |||
| "id": "5", | |||
| "isAnswer": true, | |||
| "parentMessageId": "question-5" | |||
| } | |||
| ] | |||
| @@ -0,0 +1,441 @@ | |||
| [ | |||
| { | |||
| "id": "question-ff4c2b43-48a5-47ad-9dc5-08b34ddba61b", | |||
| "content": "Let's play a game, I say a number , and you response me with another bigger, yet random-looking number. I'll start first, 38", | |||
| "isAnswer": false, | |||
| "message_files": [] | |||
| }, | |||
| { | |||
| "id": "ff4c2b43-48a5-47ad-9dc5-08b34ddba61b", | |||
| "content": "Sure, I'll play! My number is 57. Your turn!", | |||
| "agent_thoughts": [ | |||
| { | |||
| "id": "f9d7ff7c-3a3b-4d9a-a289-657817f4caff", | |||
| "chain_id": null, | |||
| "message_id": "ff4c2b43-48a5-47ad-9dc5-08b34ddba61b", | |||
| "position": 1, | |||
| "thought": "Sure, I'll play! My number is 57. Your turn!", | |||
| "tool": "", | |||
| "tool_labels": {}, | |||
| "tool_input": "", | |||
| "created_at": 1726105791, | |||
| "observation": "", | |||
| "files": [] | |||
| } | |||
| ], | |||
| "feedbackDisabled": false, | |||
| "isAnswer": true, | |||
| "message_files": [], | |||
| "log": [ | |||
| { | |||
| "role": "user", | |||
| "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "assistant", | |||
| "text": "Sure, I'll play! My number is 57. Your turn!", | |||
| "files": [] | |||
| } | |||
| ], | |||
| "workflow_run_id": null, | |||
| "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80", | |||
| "input": { | |||
| "inputs": {}, | |||
| "query": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38" | |||
| }, | |||
| "more": { | |||
| "time": "09/11/2024 09:49 PM", | |||
| "tokens": 49, | |||
| "latency": "1.56" | |||
| }, | |||
| "parentMessageId": "question-ff4c2b43-48a5-47ad-9dc5-08b34ddba61b" | |||
| }, | |||
| { | |||
| "id": "question-73bbad14-d915-499d-87bf-0df14d40779d", | |||
| "content": "58", | |||
| "isAnswer": false, | |||
| "message_files": [], | |||
| "parentMessageId": "ff4c2b43-48a5-47ad-9dc5-08b34ddba61b" | |||
| }, | |||
| { | |||
| "id": "73bbad14-d915-499d-87bf-0df14d40779d", | |||
| "content": "I choose 83. What's your next number?", | |||
| "agent_thoughts": [ | |||
| { | |||
| "id": "f61a3fce-37ac-4f9d-9935-95f97e598dfe", | |||
| "chain_id": null, | |||
| "message_id": "73bbad14-d915-499d-87bf-0df14d40779d", | |||
| "position": 1, | |||
| "thought": "I choose 83. What's your next number?", | |||
| "tool": "", | |||
| "tool_labels": {}, | |||
| "tool_input": "", | |||
| "created_at": 1726105795, | |||
| "observation": "", | |||
| "files": [] | |||
| } | |||
| ], | |||
| "feedbackDisabled": false, | |||
| "isAnswer": true, | |||
| "message_files": [], | |||
| "log": [ | |||
| { | |||
| "role": "user", | |||
| "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "assistant", | |||
| "text": "Sure, I'll play! My number is 57. Your turn!", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "user", | |||
| "text": "58", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "assistant", | |||
| "text": "I choose 83. What's your next number?", | |||
| "files": [] | |||
| } | |||
| ], | |||
| "workflow_run_id": null, | |||
| "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80", | |||
| "input": { | |||
| "inputs": {}, | |||
| "query": "58" | |||
| }, | |||
| "more": { | |||
| "time": "09/11/2024 09:49 PM", | |||
| "tokens": 68, | |||
| "latency": "1.33" | |||
| }, | |||
| "parentMessageId": "question-73bbad14-d915-499d-87bf-0df14d40779d" | |||
| }, | |||
| { | |||
| "id": "question-4c5d0841-1206-463e-95d8-71f812877658", | |||
| "content": "99", | |||
| "isAnswer": false, | |||
| "message_files": [], | |||
| "parentMessageId": "73bbad14-d915-499d-87bf-0df14d40779d" | |||
| }, | |||
| { | |||
| "id": "4c5d0841-1206-463e-95d8-71f812877658", | |||
| "content": "I'll go with 112. Your turn!", | |||
| "agent_thoughts": [ | |||
| { | |||
| "id": "9730d587-9268-4683-9dd9-91a1cab9510b", | |||
| "chain_id": null, | |||
| "message_id": "4c5d0841-1206-463e-95d8-71f812877658", | |||
| "position": 1, | |||
| "thought": "I'll go with 112. Your turn!", | |||
| "tool": "", | |||
| "tool_labels": {}, | |||
| "tool_input": "", | |||
| "created_at": 1726105799, | |||
| "observation": "", | |||
| "files": [] | |||
| } | |||
| ], | |||
| "feedbackDisabled": false, | |||
| "isAnswer": true, | |||
| "message_files": [], | |||
| "log": [ | |||
| { | |||
| "role": "user", | |||
| "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "assistant", | |||
| "text": "Sure, I'll play! My number is 57. Your turn!", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "user", | |||
| "text": "58", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "assistant", | |||
| "text": "I choose 83. What's your next number?", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "user", | |||
| "text": "99", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "assistant", | |||
| "text": "I'll go with 112. Your turn!", | |||
| "files": [] | |||
| } | |||
| ], | |||
| "workflow_run_id": null, | |||
| "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80", | |||
| "input": { | |||
| "inputs": {}, | |||
| "query": "99" | |||
| }, | |||
| "more": { | |||
| "time": "09/11/2024 09:50 PM", | |||
| "tokens": 86, | |||
| "latency": "1.49" | |||
| }, | |||
| "parentMessageId": "question-4c5d0841-1206-463e-95d8-71f812877658" | |||
| }, | |||
| { | |||
| "id": "question-cd5affb0-7bc2-4a6f-be7e-25e74595c9dd", | |||
| "content": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38", | |||
| "isAnswer": false, | |||
| "message_files": [] | |||
| }, | |||
| { | |||
| "id": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd", | |||
| "content": "Sure! My number is 54. Your turn!", | |||
| "agent_thoughts": [ | |||
| { | |||
| "id": "1019cd79-d141-4f9f-880a-fc1441cfd802", | |||
| "chain_id": null, | |||
| "message_id": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd", | |||
| "position": 1, | |||
| "thought": "Sure! My number is 54. Your turn!", | |||
| "tool": "", | |||
| "tool_labels": {}, | |||
| "tool_input": "", | |||
| "created_at": 1726105809, | |||
| "observation": "", | |||
| "files": [] | |||
| } | |||
| ], | |||
| "feedbackDisabled": false, | |||
| "isAnswer": true, | |||
| "message_files": [], | |||
| "log": [ | |||
| { | |||
| "role": "user", | |||
| "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "assistant", | |||
| "text": "Sure! My number is 54. Your turn!", | |||
| "files": [] | |||
| } | |||
| ], | |||
| "workflow_run_id": null, | |||
| "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80", | |||
| "input": { | |||
| "inputs": {}, | |||
| "query": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38" | |||
| }, | |||
| "more": { | |||
| "time": "09/11/2024 09:50 PM", | |||
| "tokens": 46, | |||
| "latency": "1.52" | |||
| }, | |||
| "parentMessageId": "question-cd5affb0-7bc2-4a6f-be7e-25e74595c9dd" | |||
| }, | |||
| { | |||
| "id": "question-324bce32-c98c-435d-a66b-bac974ebb5ed", | |||
| "content": "3306", | |||
| "isAnswer": false, | |||
| "message_files": [], | |||
| "parentMessageId": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd" | |||
| }, | |||
| { | |||
| "id": "324bce32-c98c-435d-a66b-bac974ebb5ed", | |||
| "content": "My number is 4729. Your turn!", | |||
| "agent_thoughts": [ | |||
| { | |||
| "id": "0773bec7-b992-4a53-92b2-20ebaeae8798", | |||
| "chain_id": null, | |||
| "message_id": "324bce32-c98c-435d-a66b-bac974ebb5ed", | |||
| "position": 1, | |||
| "thought": "My number is 4729. Your turn!", | |||
| "tool": "", | |||
| "tool_labels": {}, | |||
| "tool_input": "", | |||
| "created_at": 1726105822, | |||
| "observation": "", | |||
| "files": [] | |||
| } | |||
| ], | |||
| "feedbackDisabled": false, | |||
| "isAnswer": true, | |||
| "message_files": [], | |||
| "log": [ | |||
| { | |||
| "role": "user", | |||
| "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "assistant", | |||
| "text": "Sure! My number is 54. Your turn!", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "user", | |||
| "text": "3306", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "assistant", | |||
| "text": "My number is 4729. Your turn!", | |||
| "files": [] | |||
| } | |||
| ], | |||
| "workflow_run_id": null, | |||
| "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80", | |||
| "input": { | |||
| "inputs": {}, | |||
| "query": "3306" | |||
| }, | |||
| "more": { | |||
| "time": "09/11/2024 09:50 PM", | |||
| "tokens": 66, | |||
| "latency": "1.30" | |||
| }, | |||
| "parentMessageId": "question-324bce32-c98c-435d-a66b-bac974ebb5ed" | |||
| }, | |||
| { | |||
| "id": "question-684b5396-4e91-4043-88e9-aabe48b21acc", | |||
| "content": "3306", | |||
| "isAnswer": false, | |||
| "message_files": [], | |||
| "parentMessageId": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd" | |||
| }, | |||
| { | |||
| "id": "684b5396-4e91-4043-88e9-aabe48b21acc", | |||
| "content": "My number is 4821. Your turn!", | |||
| "agent_thoughts": [ | |||
| { | |||
| "id": "5ca650f3-982c-4399-8b95-9ea241c76707", | |||
| "chain_id": null, | |||
| "message_id": "684b5396-4e91-4043-88e9-aabe48b21acc", | |||
| "position": 1, | |||
| "thought": "My number is 4821. Your turn!", | |||
| "tool": "", | |||
| "tool_labels": {}, | |||
| "tool_input": "", | |||
| "created_at": 1726107812, | |||
| "observation": "", | |||
| "files": [] | |||
| } | |||
| ], | |||
| "feedbackDisabled": false, | |||
| "isAnswer": true, | |||
| "message_files": [], | |||
| "log": [ | |||
| { | |||
| "role": "user", | |||
| "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "assistant", | |||
| "text": "Sure! My number is 54. Your turn!", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "user", | |||
| "text": "3306", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "assistant", | |||
| "text": "My number is 4821. Your turn!", | |||
| "files": [] | |||
| } | |||
| ], | |||
| "workflow_run_id": null, | |||
| "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80", | |||
| "input": { | |||
| "inputs": {}, | |||
| "query": "3306" | |||
| }, | |||
| "more": { | |||
| "time": "09/11/2024 10:23 PM", | |||
| "tokens": 66, | |||
| "latency": "1.48" | |||
| }, | |||
| "parentMessageId": "question-684b5396-4e91-4043-88e9-aabe48b21acc" | |||
| }, | |||
| { | |||
| "id": "question-19904a7b-7494-4ed8-b72c-1d18668cea8c", | |||
| "content": "1003", | |||
| "isAnswer": false, | |||
| "message_files": [], | |||
| "parentMessageId": "684b5396-4e91-4043-88e9-aabe48b21acc" | |||
| }, | |||
| { | |||
| "id": "19904a7b-7494-4ed8-b72c-1d18668cea8c", | |||
| "content": "My number is 1456. Your turn!", | |||
| "agent_thoughts": [ | |||
| { | |||
| "id": "095cacab-afad-4387-a41d-1662578b8b13", | |||
| "chain_id": null, | |||
| "message_id": "19904a7b-7494-4ed8-b72c-1d18668cea8c", | |||
| "position": 1, | |||
| "thought": "My number is 1456. Your turn!", | |||
| "tool": "", | |||
| "tool_labels": {}, | |||
| "tool_input": "", | |||
| "created_at": 1726111024, | |||
| "observation": "", | |||
| "files": [] | |||
| } | |||
| ], | |||
| "feedbackDisabled": false, | |||
| "isAnswer": true, | |||
| "message_files": [], | |||
| "log": [ | |||
| { | |||
| "role": "user", | |||
| "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "assistant", | |||
| "text": "Sure! My number is 54. Your turn!", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "user", | |||
| "text": "3306", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "assistant", | |||
| "text": "My number is 4821. Your turn!", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "user", | |||
| "text": "1003", | |||
| "files": [] | |||
| }, | |||
| { | |||
| "role": "assistant", | |||
| "text": "My number is 1456. Your turn!", | |||
| "files": [] | |||
| } | |||
| ], | |||
| "workflow_run_id": null, | |||
| "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80", | |||
| "input": { | |||
| "inputs": {}, | |||
| "query": "1003" | |||
| }, | |||
| "more": { | |||
| "time": "09/11/2024 11:17 PM", | |||
| "tokens": 86, | |||
| "latency": "1.38" | |||
| }, | |||
| "parentMessageId": "question-19904a7b-7494-4ed8-b72c-1d18668cea8c" | |||
| } | |||
| ] | |||
| @@ -0,0 +1,258 @@ | |||
| import { get } from 'lodash' | |||
| import { buildChatItemTree, getThreadMessages } from '../utils' | |||
| import type { ChatItemInTree } from '../types' | |||
| import branchedTestMessages from './branchedTestMessages.json' | |||
| import legacyTestMessages from './legacyTestMessages.json' | |||
| import mixedTestMessages from './mixedTestMessages.json' | |||
| import multiRootNodesMessages from './multiRootNodesMessages.json' | |||
| import multiRootNodesWithLegacyTestMessages from './multiRootNodesWithLegacyTestMessages.json' | |||
| import realWorldMessages from './realWorldMessages.json' | |||
| function visitNode(tree: ChatItemInTree | ChatItemInTree[], path: string): ChatItemInTree { | |||
| return get(tree, path) | |||
| } | |||
| describe('build chat item tree and get thread messages', () => { | |||
| const tree1 = buildChatItemTree(branchedTestMessages as ChatItemInTree[]) | |||
| it('should build chat item tree1', () => { | |||
| const a1 = visitNode(tree1, '0.children.0') | |||
| expect(a1.id).toBe('1') | |||
| expect(a1.children).toHaveLength(2) | |||
| const a2 = visitNode(a1, 'children.0.children.0') | |||
| expect(a2.id).toBe('2') | |||
| expect(a2.siblingIndex).toBe(0) | |||
| const a3 = visitNode(a2, 'children.0.children.0') | |||
| expect(a3.id).toBe('3') | |||
| const a4 = visitNode(a1, 'children.1.children.0') | |||
| expect(a4.id).toBe('4') | |||
| expect(a4.siblingIndex).toBe(1) | |||
| }) | |||
| it('should get thread messages from tree1, using the last message as the target', () => { | |||
| const threadChatItems1_1 = getThreadMessages(tree1) | |||
| expect(threadChatItems1_1).toHaveLength(4) | |||
| const q1 = visitNode(threadChatItems1_1, '0') | |||
| const a1 = visitNode(threadChatItems1_1, '1') | |||
| const q4 = visitNode(threadChatItems1_1, '2') | |||
| const a4 = visitNode(threadChatItems1_1, '3') | |||
| expect(q1.id).toBe('question-1') | |||
| expect(a1.id).toBe('1') | |||
| expect(q4.id).toBe('question-4') | |||
| expect(a4.id).toBe('4') | |||
| expect(a4.siblingCount).toBe(2) | |||
| expect(a4.siblingIndex).toBe(1) | |||
| }) | |||
| it('should get thread messages from tree1, using the message with id 3 as the target', () => { | |||
| const threadChatItems1_2 = getThreadMessages(tree1, '3') | |||
| expect(threadChatItems1_2).toHaveLength(6) | |||
| const q1 = visitNode(threadChatItems1_2, '0') | |||
| const a1 = visitNode(threadChatItems1_2, '1') | |||
| const q2 = visitNode(threadChatItems1_2, '2') | |||
| const a2 = visitNode(threadChatItems1_2, '3') | |||
| const q3 = visitNode(threadChatItems1_2, '4') | |||
| const a3 = visitNode(threadChatItems1_2, '5') | |||
| expect(q1.id).toBe('question-1') | |||
| expect(a1.id).toBe('1') | |||
| expect(q2.id).toBe('question-2') | |||
| expect(a2.id).toBe('2') | |||
| expect(q3.id).toBe('question-3') | |||
| expect(a3.id).toBe('3') | |||
| expect(a2.siblingCount).toBe(2) | |||
| expect(a2.siblingIndex).toBe(0) | |||
| }) | |||
| const tree2 = buildChatItemTree(legacyTestMessages as ChatItemInTree[]) | |||
| it('should work with legacy chat items', () => { | |||
| expect(tree2).toHaveLength(1) | |||
| const q1 = visitNode(tree2, '0') | |||
| const a1 = visitNode(q1, 'children.0') | |||
| const q2 = visitNode(a1, 'children.0') | |||
| const a2 = visitNode(q2, 'children.0') | |||
| const q3 = visitNode(a2, 'children.0') | |||
| const a3 = visitNode(q3, 'children.0') | |||
| const q4 = visitNode(a3, 'children.0') | |||
| const a4 = visitNode(q4, 'children.0') | |||
| expect(q1.id).toBe('question-1') | |||
| expect(a1.id).toBe('1') | |||
| expect(q2.id).toBe('question-2') | |||
| expect(a2.id).toBe('2') | |||
| expect(q3.id).toBe('question-3') | |||
| expect(a3.id).toBe('3') | |||
| expect(q4.id).toBe('question-4') | |||
| expect(a4.id).toBe('4') | |||
| }) | |||
| it('should get thread messages from tree2, using the last message as the target', () => { | |||
| const threadMessages2 = getThreadMessages(tree2) | |||
| expect(threadMessages2).toHaveLength(8) | |||
| const q1 = visitNode(threadMessages2, '0') | |||
| const a1 = visitNode(threadMessages2, '1') | |||
| const q2 = visitNode(threadMessages2, '2') | |||
| const a2 = visitNode(threadMessages2, '3') | |||
| const q3 = visitNode(threadMessages2, '4') | |||
| const a3 = visitNode(threadMessages2, '5') | |||
| const q4 = visitNode(threadMessages2, '6') | |||
| const a4 = visitNode(threadMessages2, '7') | |||
| expect(q1.id).toBe('question-1') | |||
| expect(a1.id).toBe('1') | |||
| expect(q2.id).toBe('question-2') | |||
| expect(a2.id).toBe('2') | |||
| expect(q3.id).toBe('question-3') | |||
| expect(a3.id).toBe('3') | |||
| expect(q4.id).toBe('question-4') | |||
| expect(a4.id).toBe('4') | |||
| expect(a1.siblingCount).toBe(1) | |||
| expect(a1.siblingIndex).toBe(0) | |||
| expect(a2.siblingCount).toBe(1) | |||
| expect(a2.siblingIndex).toBe(0) | |||
| expect(a3.siblingCount).toBe(1) | |||
| expect(a3.siblingIndex).toBe(0) | |||
| expect(a4.siblingCount).toBe(1) | |||
| expect(a4.siblingIndex).toBe(0) | |||
| }) | |||
| const tree3 = buildChatItemTree(mixedTestMessages as ChatItemInTree[]) | |||
| it('should build mixed chat items tree', () => { | |||
| expect(tree3).toHaveLength(1) | |||
| const a1 = visitNode(tree3, '0.children.0') | |||
| expect(a1.id).toBe('1') | |||
| expect(a1.children).toHaveLength(2) | |||
| const a2 = visitNode(a1, 'children.0.children.0') | |||
| expect(a2.id).toBe('2') | |||
| expect(a2.siblingIndex).toBe(0) | |||
| const a3 = visitNode(a2, 'children.0.children.0') | |||
| expect(a3.id).toBe('3') | |||
| const a4 = visitNode(a1, 'children.1.children.0') | |||
| expect(a4.id).toBe('4') | |||
| expect(a4.siblingIndex).toBe(1) | |||
| }) | |||
| it('should get thread messages from tree3, using the last message as the target', () => { | |||
| const threadMessages3_1 = getThreadMessages(tree3) | |||
| expect(threadMessages3_1).toHaveLength(4) | |||
| const q1 = visitNode(threadMessages3_1, '0') | |||
| const a1 = visitNode(threadMessages3_1, '1') | |||
| const q4 = visitNode(threadMessages3_1, '2') | |||
| const a4 = visitNode(threadMessages3_1, '3') | |||
| expect(q1.id).toBe('question-1') | |||
| expect(a1.id).toBe('1') | |||
| expect(q4.id).toBe('question-4') | |||
| expect(a4.id).toBe('4') | |||
| expect(a4.siblingCount).toBe(2) | |||
| expect(a4.siblingIndex).toBe(1) | |||
| }) | |||
| it('should get thread messages from tree3, using the message with id 3 as the target', () => { | |||
| const threadMessages3_2 = getThreadMessages(tree3, '3') | |||
| expect(threadMessages3_2).toHaveLength(6) | |||
| const q1 = visitNode(threadMessages3_2, '0') | |||
| const a1 = visitNode(threadMessages3_2, '1') | |||
| const q2 = visitNode(threadMessages3_2, '2') | |||
| const a2 = visitNode(threadMessages3_2, '3') | |||
| const q3 = visitNode(threadMessages3_2, '4') | |||
| const a3 = visitNode(threadMessages3_2, '5') | |||
| expect(q1.id).toBe('question-1') | |||
| expect(a1.id).toBe('1') | |||
| expect(q2.id).toBe('question-2') | |||
| expect(a2.id).toBe('2') | |||
| expect(q3.id).toBe('question-3') | |||
| expect(a3.id).toBe('3') | |||
| expect(a2.siblingCount).toBe(2) | |||
| expect(a2.siblingIndex).toBe(0) | |||
| }) | |||
| const tree4 = buildChatItemTree(multiRootNodesMessages as ChatItemInTree[]) | |||
| it('should build multi root nodes chat items tree', () => { | |||
| expect(tree4).toHaveLength(2) | |||
| const a5 = visitNode(tree4, '1.children.0') | |||
| expect(a5.id).toBe('5') | |||
| expect(a5.siblingIndex).toBe(1) | |||
| }) | |||
| it('should get thread messages from tree4, using the last message as the target', () => { | |||
| const threadMessages4 = getThreadMessages(tree4) | |||
| expect(threadMessages4).toHaveLength(2) | |||
| const a1 = visitNode(threadMessages4, '0.children.0') | |||
| expect(a1.id).toBe('5') | |||
| }) | |||
| it('should get thread messages from tree4, using the message with id 2 as the target', () => { | |||
| const threadMessages4_1 = getThreadMessages(tree4, '2') | |||
| expect(threadMessages4_1).toHaveLength(6) | |||
| const a1 = visitNode(threadMessages4_1, '1') | |||
| expect(a1.id).toBe('1') | |||
| const a2 = visitNode(threadMessages4_1, '3') | |||
| expect(a2.id).toBe('2') | |||
| const a3 = visitNode(threadMessages4_1, '5') | |||
| expect(a3.id).toBe('3') | |||
| }) | |||
| const tree5 = buildChatItemTree(multiRootNodesWithLegacyTestMessages as ChatItemInTree[]) | |||
| it('should work with multi root nodes chat items with legacy chat items', () => { | |||
| expect(tree5).toHaveLength(2) | |||
| const q5 = visitNode(tree5, '1') | |||
| expect(q5.id).toBe('question-5') | |||
| expect(q5.parentMessageId).toBe(null) | |||
| const a5 = visitNode(q5, 'children.0') | |||
| expect(a5.id).toBe('5') | |||
| expect(a5.children).toHaveLength(0) | |||
| }) | |||
| it('should get thread messages from tree5, using the last message as the target', () => { | |||
| const threadMessages5 = getThreadMessages(tree5) | |||
| expect(threadMessages5).toHaveLength(2) | |||
| const q5 = visitNode(threadMessages5, '0') | |||
| const a5 = visitNode(threadMessages5, '1') | |||
| expect(q5.id).toBe('question-5') | |||
| expect(a5.id).toBe('5') | |||
| expect(a5.siblingCount).toBe(2) | |||
| expect(a5.siblingIndex).toBe(1) | |||
| }) | |||
| const tree6 = buildChatItemTree(realWorldMessages as ChatItemInTree[]) | |||
| it('should work with real world messages', () => { | |||
| expect(tree6).toMatchSnapshot() | |||
| }) | |||
| it ('should get thread messages from tree6, using the last message as target', () => { | |||
| const threadMessages6_1 = getThreadMessages(tree6) | |||
| expect(threadMessages6_1).toMatchSnapshot() | |||
| }) | |||
| it ('should get thread messages from tree6, using specified message as target', () => { | |||
| const threadMessages6_2 = getThreadMessages(tree6, 'ff4c2b43-48a5-47ad-9dc5-08b34ddba61b') | |||
| expect(threadMessages6_2).toMatchSnapshot() | |||
| }) | |||
| }) | |||
| @@ -19,6 +19,7 @@ import Citation from '@/app/components/base/chat/chat/citation' | |||
| import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item' | |||
| import type { AppData } from '@/models/share' | |||
| import AnswerIcon from '@/app/components/base/answer-icon' | |||
| import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows' | |||
| import cn from '@/utils/classnames' | |||
| import { FileList } from '@/app/components/base/file-uploader' | |||
| @@ -34,6 +35,7 @@ type AnswerProps = { | |||
| hideProcessDetail?: boolean | |||
| appData?: AppData | |||
| noChatInput?: boolean | |||
| switchSibling?: (siblingMessageId: string) => void | |||
| } | |||
| const Answer: FC<AnswerProps> = ({ | |||
| item, | |||
| @@ -47,6 +49,7 @@ const Answer: FC<AnswerProps> = ({ | |||
| hideProcessDetail, | |||
| appData, | |||
| noChatInput, | |||
| switchSibling, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { | |||
| @@ -203,6 +206,23 @@ const Answer: FC<AnswerProps> = ({ | |||
| <Citation data={citation} showHitInfo={config?.supportCitationHitInfo} /> | |||
| ) | |||
| } | |||
| {item.siblingCount && item.siblingCount > 1 && item.siblingIndex !== undefined && <div className="pt-3.5 flex justify-center items-center text-sm"> | |||
| <button | |||
| className={`${item.prevSibling ? 'opacity-100' : 'opacity-65'}`} | |||
| disabled={!item.prevSibling} | |||
| onClick={() => item.prevSibling && switchSibling?.(item.prevSibling)} | |||
| > | |||
| <ChevronRight className="w-[14px] h-[14px] rotate-180 text-gray-500" /> | |||
| </button> | |||
| <span className="px-2 text-xs text-gray-700">{item.siblingIndex + 1} / {item.siblingCount}</span> | |||
| <button | |||
| className={`${item.nextSibling ? 'opacity-100' : 'opacity-65'}`} | |||
| disabled={!item.nextSibling} | |||
| onClick={() => item.nextSibling && switchSibling?.(item.nextSibling)} | |||
| > | |||
| <ChevronRight className="w-[14px] h-[14px] text-gray-500" /> | |||
| </button> | |||
| </div>} | |||
| </div> | |||
| </div> | |||
| <More more={more} /> | |||
| @@ -65,6 +65,7 @@ export type ChatProps = { | |||
| hideProcessDetail?: boolean | |||
| hideLogModal?: boolean | |||
| themeBuilder?: ThemeBuilder | |||
| switchSibling?: (siblingMessageId: string) => void | |||
| showFeatureBar?: boolean | |||
| showFileUpload?: boolean | |||
| onFeatureBarClick?: (state: boolean) => void | |||
| @@ -100,6 +101,7 @@ const Chat: FC<ChatProps> = ({ | |||
| hideProcessDetail, | |||
| hideLogModal, | |||
| themeBuilder, | |||
| switchSibling, | |||
| showFeatureBar, | |||
| showFileUpload, | |||
| onFeatureBarClick, | |||
| @@ -232,6 +234,7 @@ const Chat: FC<ChatProps> = ({ | |||
| chatAnswerContainerInner={chatAnswerContainerInner} | |||
| hideProcessDetail={hideProcessDetail} | |||
| noChatInput={noChatInput} | |||
| switchSibling={switchSibling} | |||
| /> | |||
| ) | |||
| } | |||
| @@ -97,7 +97,11 @@ export type IChatItem = { | |||
| // for agent log | |||
| conversationId?: string | |||
| input?: any | |||
| parentMessageId?: string | |||
| parentMessageId?: string | null | |||
| siblingCount?: number | |||
| siblingIndex?: number | |||
| prevSibling?: string | |||
| nextSibling?: string | |||
| } | |||
| export type Metadata = { | |||
| @@ -65,6 +65,10 @@ export type ChatItem = IChatItem & { | |||
| allFiles?: FileEntity[] | |||
| } | |||
| export type ChatItemInTree = { | |||
| children?: ChatItemInTree[] | |||
| } & IChatItem | |||
| export type OnSend = (message: string, files?: FileEntity[], last_answer?: ChatItem | null) => void | |||
| export type OnRegenerate = (chatItem: ChatItem) => void | |||
| @@ -1,6 +1,7 @@ | |||
| import { addFileInfos, sortAgentSorts } from '../../tools/utils' | |||
| import { UUID_NIL } from './constants' | |||
| import type { ChatItem } from './types' | |||
| import type { IChatItem } from './chat/type' | |||
| import type { ChatItem, ChatItemInTree } from './types' | |||
| import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' | |||
| async function decodeBase64AndDecompress(base64String: string) { | |||
| @@ -81,8 +82,131 @@ function getPrevChatList(fetchedMessages: any[]) { | |||
| return ret.reverse() | |||
| } | |||
| function buildChatItemTree(allMessages: IChatItem[]): ChatItemInTree[] { | |||
| const map: Record<string, ChatItemInTree> = {} | |||
| const rootNodes: ChatItemInTree[] = [] | |||
| const childrenCount: Record<string, number> = {} | |||
| let lastAppendedLegacyAnswer: ChatItemInTree | null = null | |||
| for (let i = 0; i < allMessages.length; i += 2) { | |||
| const question = allMessages[i]! | |||
| const answer = allMessages[i + 1]! | |||
| const isLegacy = question.parentMessageId === UUID_NIL | |||
| const parentMessageId = isLegacy | |||
| ? (lastAppendedLegacyAnswer?.id || '') | |||
| : (question.parentMessageId || '') | |||
| // Process question | |||
| childrenCount[parentMessageId] = (childrenCount[parentMessageId] || 0) + 1 | |||
| const questionNode: ChatItemInTree = { | |||
| ...question, | |||
| children: [], | |||
| } | |||
| map[question.id] = questionNode | |||
| // Process answer | |||
| childrenCount[question.id] = 1 | |||
| const answerNode: ChatItemInTree = { | |||
| ...answer, | |||
| children: [], | |||
| siblingIndex: isLegacy ? 0 : childrenCount[parentMessageId] - 1, | |||
| } | |||
| map[answer.id] = answerNode | |||
| // Connect question and answer | |||
| questionNode.children!.push(answerNode) | |||
| // Append to parent or add to root | |||
| if (isLegacy) { | |||
| if (!lastAppendedLegacyAnswer) | |||
| rootNodes.push(questionNode) | |||
| else | |||
| lastAppendedLegacyAnswer.children!.push(questionNode) | |||
| lastAppendedLegacyAnswer = answerNode | |||
| } | |||
| else { | |||
| if (!parentMessageId) | |||
| rootNodes.push(questionNode) | |||
| else | |||
| map[parentMessageId]?.children!.push(questionNode) | |||
| } | |||
| } | |||
| return rootNodes | |||
| } | |||
| function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): ChatItemInTree[] { | |||
| let ret: ChatItemInTree[] = [] | |||
| let targetNode: ChatItemInTree | undefined | |||
| // find path to the target message | |||
| const stack = tree.toReversed().map(rootNode => ({ | |||
| node: rootNode, | |||
| path: [rootNode], | |||
| })) | |||
| while (stack.length > 0) { | |||
| const { node, path } = stack.pop()! | |||
| if ( | |||
| node.id === targetMessageId | |||
| || (!targetMessageId && !node.children?.length && !stack.length) // if targetMessageId is not provided, we use the last message in the tree as the target | |||
| ) { | |||
| targetNode = node | |||
| ret = path.map((item, index) => { | |||
| if (!item.isAnswer) | |||
| return item | |||
| const parentAnswer = path[index - 2] | |||
| const siblingCount = !parentAnswer ? tree.length : parentAnswer.children!.length | |||
| const prevSibling = !parentAnswer ? tree[item.siblingIndex! - 1]?.children?.[0]?.id : parentAnswer.children![item.siblingIndex! - 1]?.children?.[0].id | |||
| const nextSibling = !parentAnswer ? tree[item.siblingIndex! + 1]?.children?.[0]?.id : parentAnswer.children![item.siblingIndex! + 1]?.children?.[0].id | |||
| return { ...item, siblingCount, prevSibling, nextSibling } | |||
| }) | |||
| break | |||
| } | |||
| if (node.children) { | |||
| for (let i = node.children.length - 1; i >= 0; i--) { | |||
| stack.push({ | |||
| node: node.children[i], | |||
| path: [...path, node.children[i]], | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| // append all descendant messages to the path | |||
| if (targetNode) { | |||
| const stack = [targetNode] | |||
| while (stack.length > 0) { | |||
| const node = stack.pop()! | |||
| if (node !== targetNode) | |||
| ret.push(node) | |||
| if (node.children?.length) { | |||
| const lastChild = node.children.at(-1)! | |||
| if (!lastChild.isAnswer) { | |||
| stack.push(lastChild) | |||
| continue | |||
| } | |||
| const parentAnswer = ret.at(-2) | |||
| const siblingCount = parentAnswer?.children?.length | |||
| const prevSibling = parentAnswer?.children?.at(-2)?.children?.[0]?.id | |||
| stack.push({ ...lastChild, siblingCount, prevSibling }) | |||
| } | |||
| } | |||
| } | |||
| return ret | |||
| } | |||
| export { | |||
| getProcessedInputsFromUrlParams, | |||
| getLastAnswer, | |||
| getPrevChatList, | |||
| getLastAnswer, | |||
| buildChatItemTree, | |||
| getThreadMessages, | |||
| } | |||
| @@ -12,59 +12,44 @@ import { | |||
| import { useWorkflowRun } from '../../hooks' | |||
| import UserInput from './user-input' | |||
| import Chat from '@/app/components/base/chat/chat' | |||
| import type { ChatItem } from '@/app/components/base/chat/types' | |||
| import type { ChatItem, ChatItemInTree } from '@/app/components/base/chat/types' | |||
| import { fetchConversationMessages } from '@/service/debug' | |||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { UUID_NIL } from '@/app/components/base/chat/constants' | |||
| import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' | |||
| function appendQAToChatList(newChatList: ChatItem[], item: any) { | |||
| const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [] | |||
| newChatList.push({ | |||
| id: item.id, | |||
| content: item.answer, | |||
| feedback: item.feedback, | |||
| isAnswer: true, | |||
| citation: item.metadata?.retriever_resources, | |||
| message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))), | |||
| workflow_run_id: item.workflow_run_id, | |||
| }) | |||
| const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || [] | |||
| newChatList.push({ | |||
| id: `question-${item.id}`, | |||
| content: item.query, | |||
| isAnswer: false, | |||
| message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))), | |||
| }) | |||
| } | |||
| import type { IChatItem } from '@/app/components/base/chat/chat/type' | |||
| import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils' | |||
| function getFormattedChatList(messages: any[]) { | |||
| const newChatList: ChatItem[] = [] | |||
| let nextMessageId = null | |||
| for (const item of messages) { | |||
| if (!item.parent_message_id) { | |||
| appendQAToChatList(newChatList, item) | |||
| break | |||
| } | |||
| if (!nextMessageId) { | |||
| appendQAToChatList(newChatList, item) | |||
| nextMessageId = item.parent_message_id | |||
| } | |||
| else { | |||
| if (item.id === nextMessageId || nextMessageId === UUID_NIL) { | |||
| appendQAToChatList(newChatList, item) | |||
| nextMessageId = item.parent_message_id | |||
| } | |||
| } | |||
| } | |||
| return newChatList.reverse() | |||
| const res: ChatItem[] = [] | |||
| messages.forEach((item: any) => { | |||
| const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || [] | |||
| res.push({ | |||
| id: `question-${item.id}`, | |||
| content: item.query, | |||
| isAnswer: false, | |||
| message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))), | |||
| parentMessageId: item.parent_message_id || undefined, | |||
| }) | |||
| const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [] | |||
| res.push({ | |||
| id: item.id, | |||
| content: item.answer, | |||
| feedback: item.feedback, | |||
| isAnswer: true, | |||
| citation: item.metadata?.retriever_resources, | |||
| message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))), | |||
| workflow_run_id: item.workflow_run_id, | |||
| parentMessageId: `question-${item.id}`, | |||
| }) | |||
| }) | |||
| return res | |||
| } | |||
| const ChatRecord = () => { | |||
| const [fetched, setFetched] = useState(false) | |||
| const [chatList, setChatList] = useState<ChatItem[]>([]) | |||
| const [chatItemTree, setChatItemTree] = useState<ChatItemInTree[]>([]) | |||
| const [threadChatItems, setThreadChatItems] = useState<IChatItem[]>([]) | |||
| const appDetail = useAppStore(s => s.appDetail) | |||
| const workflowStore = useWorkflowStore() | |||
| const { handleLoadBackupDraft } = useWorkflowRun() | |||
| @@ -76,20 +61,29 @@ const ChatRecord = () => { | |||
| try { | |||
| setFetched(false) | |||
| const res = await fetchConversationMessages(appDetail.id, currentConversationID) | |||
| setChatList(getFormattedChatList((res as any).data)) | |||
| const newAllChatItems = getFormattedChatList((res as any).data) | |||
| const tree = buildChatItemTree(newAllChatItems) | |||
| setChatItemTree(tree) | |||
| setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id)) | |||
| } | |||
| catch (e) { | |||
| console.error(e) | |||
| } | |||
| finally { | |||
| setFetched(true) | |||
| } | |||
| } | |||
| }, [appDetail, currentConversationID]) | |||
| useEffect(() => { | |||
| handleFetchConversationMessages() | |||
| }, [currentConversationID, appDetail, handleFetchConversationMessages]) | |||
| const switchSibling = useCallback((siblingMessageId: string) => { | |||
| setThreadChatItems(getThreadMessages(chatItemTree, siblingMessageId)) | |||
| }, [chatItemTree]) | |||
| return ( | |||
| <div | |||
| className={` | |||
| @@ -123,7 +117,7 @@ const ChatRecord = () => { | |||
| config={{ | |||
| supportCitationHitInfo: true, | |||
| } as any} | |||
| chatList={chatList} | |||
| chatList={threadChatItems} | |||
| chatContainerClassName='px-3' | |||
| chatContainerInnerClassName='pt-6 w-full max-w-full mx-auto' | |||
| chatFooterClassName='px-4 rounded-b-2xl' | |||
| @@ -132,6 +126,7 @@ const ChatRecord = () => { | |||
| noChatInput | |||
| allToolIcons={{}} | |||
| showPromptLog | |||
| switchSibling={switchSibling} | |||
| noSpacing | |||
| chatAnswerContainerInner='!pr-2' | |||
| /> | |||
| @@ -0,0 +1,3 @@ | |||
| module.exports = { | |||
| presets: ['@babel/preset-env'], | |||
| } | |||
| @@ -99,6 +99,7 @@ const config: Config = { | |||
| // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module | |||
| moduleNameMapper: { | |||
| '^@/components/(.*)$': '<rootDir>/components/$1', | |||
| '^lodash-es$': 'lodash', | |||
| }, | |||
| // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader | |||