mirror of
https://github.com/langgenius/dify.git
synced 2024-11-15 19:22:36 +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:
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
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 { 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'
|
||||||
|
@ -41,6 +42,7 @@ import { useAppContext } from '@/context/app-context'
|
||||||
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)
|
||||||
|
@ -139,6 +141,7 @@ function appendQAToChatList(newChatList: IChatItem[], item: any, conversationId:
|
||||||
})(),
|
})(),
|
||||||
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}`,
|
||||||
|
@ -193,50 +196,66 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||||
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)
|
if (allChatItems.at(-1)?.id)
|
||||||
params.first_id = items?.[0]?.id.replace('question-', '')
|
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]
|
setHasMore(messageRes.has_more)
|
||||||
const newItems = getFormattedChatList(fetchedMessages.current, detail.id, timezone!, t('appLog.dateTimeFormat') as string)
|
|
||||||
|
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)
|
setChatItemTree(tree)
|
||||||
setHasMore(messageRes.has_more)
|
|
||||||
|
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,
|
||||||
|
@ -257,9 +276,9 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||||
}
|
}
|
||||||
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,
|
||||||
|
@ -287,9 +306,9 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||||
}
|
}
|
||||||
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,
|
||||||
|
@ -299,7 +318,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||||
}
|
}
|
||||||
return item
|
return item
|
||||||
}))
|
}))
|
||||||
}, [items])
|
}, [allChatItems])
|
||||||
|
|
||||||
const fetchInitiated = useRef(false)
|
const fetchInitiated = useRef(false)
|
||||||
|
|
||||||
|
@ -464,7 +483,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||||
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={{
|
||||||
|
@ -478,7 +497,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||||
},
|
},
|
||||||
supportFeedback: true,
|
supportFeedback: true,
|
||||||
} as any}
|
} as any}
|
||||||
chatList={items}
|
chatList={threadChatItems}
|
||||||
onAnnotationAdded={handleAnnotationAdded}
|
onAnnotationAdded={handleAnnotationAdded}
|
||||||
onAnnotationEdited={handleAnnotationEdited}
|
onAnnotationEdited={handleAnnotationEdited}
|
||||||
onAnnotationRemoved={handleAnnotationRemoved}
|
onAnnotationRemoved={handleAnnotationRemoved}
|
||||||
|
@ -487,6 +506,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||||
showPromptLog
|
showPromptLog
|
||||||
hideProcessDetail
|
hideProcessDetail
|
||||||
chatContainerInnerClassName='px-6'
|
chatContainerInnerClassName='px-6'
|
||||||
|
switchSibling={switchSibling}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
: <div
|
: <div
|
||||||
|
@ -501,7 +521,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||||
{/* 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>}
|
||||||
|
@ -532,7 +552,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||||
},
|
},
|
||||||
supportFeedback: true,
|
supportFeedback: true,
|
||||||
} as any}
|
} as any}
|
||||||
chatList={items}
|
chatList={threadChatItems}
|
||||||
onAnnotationAdded={handleAnnotationAdded}
|
onAnnotationAdded={handleAnnotationAdded}
|
||||||
onAnnotationEdited={handleAnnotationEdited}
|
onAnnotationEdited={handleAnnotationEdited}
|
||||||
onAnnotationRemoved={handleAnnotationRemoved}
|
onAnnotationRemoved={handleAnnotationRemoved}
|
||||||
|
@ -541,6 +561,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||||
showPromptLog
|
showPromptLog
|
||||||
hideProcessDetail
|
hideProcessDetail
|
||||||
chatContainerInnerClassName='px-6'
|
chatContainerInnerClassName='px-6'
|
||||||
|
switchSibling={switchSibling}
|
||||||
/>
|
/>
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
</div>
|
</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 { 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'
|
||||||
|
|
||||||
|
@ -34,6 +35,7 @@ type AnswerProps = {
|
||||||
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,
|
||||||
|
@ -47,6 +49,7 @@ const Answer: FC<AnswerProps> = ({
|
||||||
hideProcessDetail,
|
hideProcessDetail,
|
||||||
appData,
|
appData,
|
||||||
noChatInput,
|
noChatInput,
|
||||||
|
switchSibling,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
|
@ -203,6 +206,23 @@ const Answer: FC<AnswerProps> = ({
|
||||||
<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} />
|
||||||
|
|
|
@ -65,6 +65,7 @@ export type ChatProps = {
|
||||||
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
|
||||||
|
@ -100,6 +101,7 @@ const Chat: FC<ChatProps> = ({
|
||||||
hideProcessDetail,
|
hideProcessDetail,
|
||||||
hideLogModal,
|
hideLogModal,
|
||||||
themeBuilder,
|
themeBuilder,
|
||||||
|
switchSibling,
|
||||||
showFeatureBar,
|
showFeatureBar,
|
||||||
showFileUpload,
|
showFileUpload,
|
||||||
onFeatureBarClick,
|
onFeatureBarClick,
|
||||||
|
@ -232,6 +234,7 @@ const Chat: FC<ChatProps> = ({
|
||||||
chatAnswerContainerInner={chatAnswerContainerInner}
|
chatAnswerContainerInner={chatAnswerContainerInner}
|
||||||
hideProcessDetail={hideProcessDetail}
|
hideProcessDetail={hideProcessDetail}
|
||||||
noChatInput={noChatInput}
|
noChatInput={noChatInput}
|
||||||
|
switchSibling={switchSibling}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,7 +97,11 @@ export type IChatItem = {
|
||||||
// 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 = {
|
||||||
|
|
|
@ -65,6 +65,10 @@ export type ChatItem = IChatItem & {
|
||||||
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
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
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) {
|
||||||
|
@ -81,8 +82,131 @@ function getPrevChatList(fetchedMessages: any[]) {
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,59 +12,44 @@ import {
|
||||||
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'
|
||||||
|
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||||
function appendQAToChatList(newChatList: ChatItem[], item: any) {
|
import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils'
|
||||||
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 }))),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFormattedChatList(messages: any[]) {
|
function getFormattedChatList(messages: any[]) {
|
||||||
const newChatList: ChatItem[] = []
|
const res: ChatItem[] = []
|
||||||
let nextMessageId = null
|
messages.forEach((item: any) => {
|
||||||
for (const item of messages) {
|
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
|
||||||
if (!item.parent_message_id) {
|
res.push({
|
||||||
appendQAToChatList(newChatList, item)
|
id: `question-${item.id}`,
|
||||||
break
|
content: item.query,
|
||||||
}
|
isAnswer: false,
|
||||||
|
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||||
if (!nextMessageId) {
|
parentMessageId: item.parent_message_id || undefined,
|
||||||
appendQAToChatList(newChatList, item)
|
})
|
||||||
nextMessageId = item.parent_message_id
|
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
|
||||||
}
|
res.push({
|
||||||
else {
|
id: item.id,
|
||||||
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
|
content: item.answer,
|
||||||
appendQAToChatList(newChatList, item)
|
feedback: item.feedback,
|
||||||
nextMessageId = item.parent_message_id
|
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,
|
||||||
return newChatList.reverse()
|
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()
|
||||||
|
@ -76,20 +61,29 @@ const ChatRecord = () => {
|
||||||
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={`
|
||||||
|
@ -123,7 +117,7 @@ const ChatRecord = () => {
|
||||||
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'
|
||||||
|
@ -132,6 +126,7 @@ const ChatRecord = () => {
|
||||||
noChatInput
|
noChatInput
|
||||||
allToolIcons={{}}
|
allToolIcons={{}}
|
||||||
showPromptLog
|
showPromptLog
|
||||||
|
switchSibling={switchSibling}
|
||||||
noSpacing
|
noSpacing
|
||||||
chatAnswerContainerInner='!pr-2'
|
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
|
// 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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user