mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 03:32:23 +08:00
feat: regenerate history switch navigation (#8749)
This commit is contained in:
parent
2c26f77a25
commit
57ec12eb6b
|
@ -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)
|
||||
|
||||
|
||||
|
|
0
web/__mocks__/mime.js
Normal file
0
web/__mocks__/mime.js
Normal file
|
@ -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>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
]
|
441
web/app/components/base/chat/__tests__/realWorldMessages.json
Normal file
441
web/app/components/base/chat/__tests__/realWorldMessages.json
Normal file
|
@ -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"
|
||||
}
|
||||
]
|
258
web/app/components/base/chat/__tests__/utils.spec.ts
Normal file
258
web/app/components/base/chat/__tests__/utils.spec.ts
Normal file
|
@ -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,16 +12,27 @@ 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'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils'
|
||||
|
||||
function appendQAToChatList(newChatList: ChatItem[], item: any) {
|
||||
function getFormattedChatList(messages: any[]) {
|
||||
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') || []
|
||||
newChatList.push({
|
||||
res.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
feedback: item.feedback,
|
||||
|
@ -29,42 +40,16 @@ function appendQAToChatList(newChatList: ChatItem[], item: any) {
|
|||
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}`,
|
||||
})
|
||||
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 }))),
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
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'
|
||||
/>
|
||||
|
|
3
web/babel.config.js
Normal file
3
web/babel.config.js
Normal file
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user