| if rest_count > 0: | if rest_count > 0: | ||||
| has_more = True | has_more = True | ||||
| history_messages = list(reversed(history_messages)) | |||||
| return InfiniteScrollPagination(data=history_messages, limit=args["limit"], has_more=has_more) | return InfiniteScrollPagination(data=history_messages, limit=args["limit"], has_more=has_more) | ||||
| import { useShallow } from 'zustand/react/shallow' | import { useShallow } from 'zustand/react/shallow' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { UUID_NIL } from '../../base/chat/constants' | import { UUID_NIL } from '../../base/chat/constants' | ||||
| import type { ChatItemInTree } from '../../base/chat/types' | |||||
| import VarPanel from './var-panel' | import VarPanel from './var-panel' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import type { FeedbackFunc, FeedbackType, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type' | import type { FeedbackFunc, FeedbackType, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type' | ||||
| import useTimestamp from '@/hooks/use-timestamp' | import useTimestamp from '@/hooks/use-timestamp' | ||||
| import Tooltip from '@/app/components/base/tooltip' | import Tooltip from '@/app/components/base/tooltip' | ||||
| import { CopyIcon } from '@/app/components/base/copy-icon' | 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' | import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' | ||||
| dayjs.extend(utc) | dayjs.extend(utc) | ||||
| })(), | })(), | ||||
| parentMessageId: `question-${item.id}`, | parentMessageId: `question-${item.id}`, | ||||
| }) | }) | ||||
| const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || [] | const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || [] | ||||
| newChatList.push({ | newChatList.push({ | ||||
| id: `question-${item.id}`, | id: `question-${item.id}`, | ||||
| currentLogModalActiveTab: state.currentLogModalActiveTab, | currentLogModalActiveTab: state.currentLogModalActiveTab, | ||||
| }))) | }))) | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const [items, setItems] = React.useState<IChatItem[]>([]) | |||||
| const fetchedMessages = useRef<ChatMessage[]>([]) | |||||
| const [hasMore, setHasMore] = useState(true) | const [hasMore, setHasMore] = useState(true) | ||||
| const [varValues, setVarValues] = useState<Record<string, string>>({}) | 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 { | try { | ||||
| if (!hasMore) | if (!hasMore) | ||||
| return | return | ||||
| const params: ChatMessagesRequest = { | const params: ChatMessagesRequest = { | ||||
| conversation_id: detail.id, | conversation_id: detail.id, | ||||
| limit: 10, | 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({ | const messageRes = await fetchChatMessages({ | ||||
| url: `/apps/${appDetail?.id}/chat-messages`, | url: `/apps/${appDetail?.id}/chat-messages`, | ||||
| params, | params, | ||||
| }) | }) | ||||
| if (messageRes.data.length > 0) { | if (messageRes.data.length > 0) { | ||||
| const varValues = messageRes.data[0].inputs | |||||
| const varValues = messageRes.data.at(-1)!.inputs | |||||
| setVarValues(varValues) | 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) { | if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) { | ||||
| newItems.unshift({ | |||||
| tree = [{ | |||||
| id: 'introduction', | id: 'introduction', | ||||
| isAnswer: true, | isAnswer: true, | ||||
| isOpeningStatement: true, | isOpeningStatement: true, | ||||
| content: detail?.model_config?.configs?.introduction ?? 'hello', | content: detail?.model_config?.configs?.introduction ?? 'hello', | ||||
| feedbackDisabled: true, | feedbackDisabled: true, | ||||
| }) | |||||
| children: tree, | |||||
| }] | |||||
| } | } | ||||
| setItems(newItems) | |||||
| setHasMore(messageRes.has_more) | |||||
| setChatItemTree(tree) | |||||
| setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id)) | |||||
| } | } | ||||
| catch (err) { | catch (err) { | ||||
| console.error(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) => { | const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => { | ||||
| setItems(items.map((item, i) => { | |||||
| setAllChatItems(allChatItems.map((item, i) => { | |||||
| if (i === index - 1) { | if (i === index - 1) { | ||||
| return { | return { | ||||
| ...item, | ...item, | ||||
| } | } | ||||
| return item | return item | ||||
| })) | })) | ||||
| }, [items]) | |||||
| }, [allChatItems]) | |||||
| const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => { | 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) { | if (i === index - 1) { | ||||
| return { | return { | ||||
| ...item, | ...item, | ||||
| } | } | ||||
| return item | return item | ||||
| })) | })) | ||||
| }, [items]) | |||||
| }, [allChatItems]) | |||||
| const handleAnnotationRemoved = useCallback((index: number) => { | const handleAnnotationRemoved = useCallback((index: number) => { | ||||
| setItems(items.map((item, i) => { | |||||
| setAllChatItems(allChatItems.map((item, i) => { | |||||
| if (i === index) { | if (i === index) { | ||||
| return { | return { | ||||
| ...item, | ...item, | ||||
| } | } | ||||
| return item | return item | ||||
| })) | })) | ||||
| }, [items]) | |||||
| }, [allChatItems]) | |||||
| const fetchInitiated = useRef(false) | const fetchInitiated = useRef(false) | ||||
| siteInfo={null} | siteInfo={null} | ||||
| /> | /> | ||||
| </div> | </div> | ||||
| : (items.length < 8 && !hasMore) | |||||
| : threadChatItems.length < 8 | |||||
| ? <div className="pt-4 mb-4"> | ? <div className="pt-4 mb-4"> | ||||
| <Chat | <Chat | ||||
| config={{ | config={{ | ||||
| }, | }, | ||||
| supportFeedback: true, | supportFeedback: true, | ||||
| } as any} | } as any} | ||||
| chatList={items} | |||||
| chatList={threadChatItems} | |||||
| onAnnotationAdded={handleAnnotationAdded} | onAnnotationAdded={handleAnnotationAdded} | ||||
| onAnnotationEdited={handleAnnotationEdited} | onAnnotationEdited={handleAnnotationEdited} | ||||
| onAnnotationRemoved={handleAnnotationRemoved} | onAnnotationRemoved={handleAnnotationRemoved} | ||||
| showPromptLog | showPromptLog | ||||
| hideProcessDetail | hideProcessDetail | ||||
| chatContainerInnerClassName='px-6' | chatContainerInnerClassName='px-6' | ||||
| switchSibling={switchSibling} | |||||
| /> | /> | ||||
| </div> | </div> | ||||
| : <div | : <div | ||||
| {/* Put the scroll bar always on the bottom */} | {/* Put the scroll bar always on the bottom */} | ||||
| <InfiniteScroll | <InfiniteScroll | ||||
| scrollableTarget="scrollableDiv" | scrollableTarget="scrollableDiv" | ||||
| dataLength={items.length} | |||||
| dataLength={threadChatItems.length} | |||||
| next={fetchData} | next={fetchData} | ||||
| hasMore={hasMore} | hasMore={hasMore} | ||||
| loader={<div className='text-center text-gray-400 text-xs'>{t('appLog.detail.loading')}...</div>} | loader={<div className='text-center text-gray-400 text-xs'>{t('appLog.detail.loading')}...</div>} | ||||
| }, | }, | ||||
| supportFeedback: true, | supportFeedback: true, | ||||
| } as any} | } as any} | ||||
| chatList={items} | |||||
| chatList={threadChatItems} | |||||
| onAnnotationAdded={handleAnnotationAdded} | onAnnotationAdded={handleAnnotationAdded} | ||||
| onAnnotationEdited={handleAnnotationEdited} | onAnnotationEdited={handleAnnotationEdited} | ||||
| onAnnotationRemoved={handleAnnotationRemoved} | onAnnotationRemoved={handleAnnotationRemoved} | ||||
| showPromptLog | showPromptLog | ||||
| hideProcessDetail | hideProcessDetail | ||||
| chatContainerInnerClassName='px-6' | chatContainerInnerClassName='px-6' | ||||
| switchSibling={switchSibling} | |||||
| /> | /> | ||||
| </InfiniteScroll> | </InfiniteScroll> | ||||
| </div> | </div> |
| [ | |||||
| { | |||||
| "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-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" | |||||
| } | |||||
| ] |
| [ | |||||
| { | |||||
| "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" | |||||
| } | |||||
| ] |
| [ | |||||
| { | |||||
| "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" | |||||
| } | |||||
| ] |
| [ | |||||
| { | |||||
| "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" | |||||
| } | |||||
| ] |
| [ | |||||
| { | |||||
| "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" | |||||
| } | |||||
| ] |
| 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() | |||||
| }) | |||||
| }) |
| import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item' | import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item' | ||||
| import type { AppData } from '@/models/share' | import type { AppData } from '@/models/share' | ||||
| import AnswerIcon from '@/app/components/base/answer-icon' | 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 cn from '@/utils/classnames' | ||||
| import { FileList } from '@/app/components/base/file-uploader' | import { FileList } from '@/app/components/base/file-uploader' | ||||
| hideProcessDetail?: boolean | hideProcessDetail?: boolean | ||||
| appData?: AppData | appData?: AppData | ||||
| noChatInput?: boolean | noChatInput?: boolean | ||||
| switchSibling?: (siblingMessageId: string) => void | |||||
| } | } | ||||
| const Answer: FC<AnswerProps> = ({ | const Answer: FC<AnswerProps> = ({ | ||||
| item, | item, | ||||
| hideProcessDetail, | hideProcessDetail, | ||||
| appData, | appData, | ||||
| noChatInput, | noChatInput, | ||||
| switchSibling, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { | const { | ||||
| <Citation data={citation} showHitInfo={config?.supportCitationHitInfo} /> | <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> | ||||
| </div> | </div> | ||||
| <More more={more} /> | <More more={more} /> |
| hideProcessDetail?: boolean | hideProcessDetail?: boolean | ||||
| hideLogModal?: boolean | hideLogModal?: boolean | ||||
| themeBuilder?: ThemeBuilder | themeBuilder?: ThemeBuilder | ||||
| switchSibling?: (siblingMessageId: string) => void | |||||
| showFeatureBar?: boolean | showFeatureBar?: boolean | ||||
| showFileUpload?: boolean | showFileUpload?: boolean | ||||
| onFeatureBarClick?: (state: boolean) => void | onFeatureBarClick?: (state: boolean) => void | ||||
| hideProcessDetail, | hideProcessDetail, | ||||
| hideLogModal, | hideLogModal, | ||||
| themeBuilder, | themeBuilder, | ||||
| switchSibling, | |||||
| showFeatureBar, | showFeatureBar, | ||||
| showFileUpload, | showFileUpload, | ||||
| onFeatureBarClick, | onFeatureBarClick, | ||||
| chatAnswerContainerInner={chatAnswerContainerInner} | chatAnswerContainerInner={chatAnswerContainerInner} | ||||
| hideProcessDetail={hideProcessDetail} | hideProcessDetail={hideProcessDetail} | ||||
| noChatInput={noChatInput} | noChatInput={noChatInput} | ||||
| switchSibling={switchSibling} | |||||
| /> | /> | ||||
| ) | ) | ||||
| } | } |
| // for agent log | // for agent log | ||||
| conversationId?: string | conversationId?: string | ||||
| input?: any | input?: any | ||||
| parentMessageId?: string | |||||
| parentMessageId?: string | null | |||||
| siblingCount?: number | |||||
| siblingIndex?: number | |||||
| prevSibling?: string | |||||
| nextSibling?: string | |||||
| } | } | ||||
| export type Metadata = { | export type Metadata = { |
| allFiles?: FileEntity[] | allFiles?: FileEntity[] | ||||
| } | } | ||||
| export type ChatItemInTree = { | |||||
| children?: ChatItemInTree[] | |||||
| } & IChatItem | |||||
| export type OnSend = (message: string, files?: FileEntity[], last_answer?: ChatItem | null) => void | export type OnSend = (message: string, files?: FileEntity[], last_answer?: ChatItem | null) => void | ||||
| export type OnRegenerate = (chatItem: ChatItem) => void | export type OnRegenerate = (chatItem: ChatItem) => void |
| import { addFileInfos, sortAgentSorts } from '../../tools/utils' | import { addFileInfos, sortAgentSorts } from '../../tools/utils' | ||||
| import { UUID_NIL } from './constants' | 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' | import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' | ||||
| async function decodeBase64AndDecompress(base64String: string) { | async function decodeBase64AndDecompress(base64String: string) { | ||||
| return ret.reverse() | 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 { | export { | ||||
| getProcessedInputsFromUrlParams, | getProcessedInputsFromUrlParams, | ||||
| getLastAnswer, | |||||
| getPrevChatList, | getPrevChatList, | ||||
| getLastAnswer, | |||||
| buildChatItemTree, | |||||
| getThreadMessages, | |||||
| } | } |
| import { useWorkflowRun } from '../../hooks' | import { useWorkflowRun } from '../../hooks' | ||||
| import UserInput from './user-input' | import UserInput from './user-input' | ||||
| import Chat from '@/app/components/base/chat/chat' | 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 { fetchConversationMessages } from '@/service/debug' | ||||
| import { useStore as useAppStore } from '@/app/components/app/store' | import { useStore as useAppStore } from '@/app/components/app/store' | ||||
| import Loading from '@/app/components/base/loading' | 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' | 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[]) { | 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 ChatRecord = () => { | ||||
| const [fetched, setFetched] = useState(false) | 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 appDetail = useAppStore(s => s.appDetail) | ||||
| const workflowStore = useWorkflowStore() | const workflowStore = useWorkflowStore() | ||||
| const { handleLoadBackupDraft } = useWorkflowRun() | const { handleLoadBackupDraft } = useWorkflowRun() | ||||
| try { | try { | ||||
| setFetched(false) | setFetched(false) | ||||
| const res = await fetchConversationMessages(appDetail.id, currentConversationID) | 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) { | catch (e) { | ||||
| console.error(e) | |||||
| } | } | ||||
| finally { | finally { | ||||
| setFetched(true) | setFetched(true) | ||||
| } | } | ||||
| } | } | ||||
| }, [appDetail, currentConversationID]) | }, [appDetail, currentConversationID]) | ||||
| useEffect(() => { | useEffect(() => { | ||||
| handleFetchConversationMessages() | handleFetchConversationMessages() | ||||
| }, [currentConversationID, appDetail, handleFetchConversationMessages]) | }, [currentConversationID, appDetail, handleFetchConversationMessages]) | ||||
| const switchSibling = useCallback((siblingMessageId: string) => { | |||||
| setThreadChatItems(getThreadMessages(chatItemTree, siblingMessageId)) | |||||
| }, [chatItemTree]) | |||||
| return ( | return ( | ||||
| <div | <div | ||||
| className={` | className={` | ||||
| config={{ | config={{ | ||||
| supportCitationHitInfo: true, | supportCitationHitInfo: true, | ||||
| } as any} | } as any} | ||||
| chatList={chatList} | |||||
| chatList={threadChatItems} | |||||
| chatContainerClassName='px-3' | chatContainerClassName='px-3' | ||||
| chatContainerInnerClassName='pt-6 w-full max-w-full mx-auto' | chatContainerInnerClassName='pt-6 w-full max-w-full mx-auto' | ||||
| chatFooterClassName='px-4 rounded-b-2xl' | chatFooterClassName='px-4 rounded-b-2xl' | ||||
| noChatInput | noChatInput | ||||
| allToolIcons={{}} | allToolIcons={{}} | ||||
| showPromptLog | showPromptLog | ||||
| switchSibling={switchSibling} | |||||
| noSpacing | noSpacing | ||||
| chatAnswerContainerInner='!pr-2' | chatAnswerContainerInner='!pr-2' | ||||
| /> | /> |
| module.exports = { | |||||
| presets: ['@babel/preset-env'], | |||||
| } |
| // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module | // 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: { | moduleNameMapper: { | ||||
| '^@/components/(.*)$': '<rootDir>/components/$1', | '^@/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 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader |