Spaces:
Paused
Paused
| import { addFileInfos, sortAgentSorts } from '../../tools/utils' | |
| import { UUID_NIL } from './constants' | |
| 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) { | |
| const binaryString = atob(base64String) | |
| const compressedUint8Array = Uint8Array.from(binaryString, char => char.charCodeAt(0)) | |
| const decompressedStream = new Response(compressedUint8Array).body?.pipeThrough(new DecompressionStream('gzip')) | |
| const decompressedArrayBuffer = await new Response(decompressedStream).arrayBuffer() | |
| return new TextDecoder().decode(decompressedArrayBuffer) | |
| } | |
| function getProcessedInputsFromUrlParams(): Record<string, any> { | |
| const urlParams = new URLSearchParams(window.location.search) | |
| const inputs: Record<string, any> = {} | |
| urlParams.forEach(async (value, key) => { | |
| inputs[key] = await decodeBase64AndDecompress(decodeURIComponent(value)) | |
| }) | |
| return inputs | |
| } | |
| function getLastAnswer(chatList: ChatItem[]) { | |
| for (let i = chatList.length - 1; i >= 0; i--) { | |
| const item = chatList[i] | |
| if (item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement) | |
| return item | |
| } | |
| return null | |
| } | |
| function appendQAToChatList(chatList: ChatItem[], item: any) { | |
| // we append answer first and then question since will reverse the whole chatList later | |
| const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [] | |
| chatList.push({ | |
| id: item.id, | |
| content: item.answer, | |
| agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files), | |
| feedback: item.feedback, | |
| isAnswer: true, | |
| citation: item.retriever_resources, | |
| message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))), | |
| }) | |
| const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || [] | |
| chatList.push({ | |
| id: `question-${item.id}`, | |
| content: item.query, | |
| isAnswer: false, | |
| message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))), | |
| }) | |
| } | |
| /** | |
| * Computes the latest thread messages from all messages of the conversation. | |
| * Same logic as backend codebase `api/core/prompt/utils/extract_thread_messages.py` | |
| * | |
| * @param fetchedMessages - The history chat list data from the backend, sorted by created_at in descending order. This includes all flattened history messages of the conversation. | |
| * @returns An array of ChatItems representing the latest thread. | |
| */ | |
| function getPrevChatList(fetchedMessages: any[]) { | |
| const ret: ChatItem[] = [] | |
| let nextMessageId = null | |
| for (const item of fetchedMessages) { | |
| if (!item.parent_message_id) { | |
| appendQAToChatList(ret, item) | |
| break | |
| } | |
| if (!nextMessageId) { | |
| appendQAToChatList(ret, item) | |
| nextMessageId = item.parent_message_id | |
| } | |
| else { | |
| if (item.id === nextMessageId || nextMessageId === UUID_NIL) { | |
| appendQAToChatList(ret, item) | |
| nextMessageId = item.parent_message_id | |
| } | |
| } | |
| } | |
| 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) | |
| } | |
| } | |
| // If no messages have parentMessageId=null (indicating a root node), | |
| // then we likely have a partial chat history. In this case, | |
| // use the first available message as the root node. | |
| if (rootNodes.length === 0 && allMessages.length > 0) | |
| rootNodes.push(map[allMessages[0]!.id]!) | |
| 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, | |
| getPrevChatList, | |
| getLastAnswer, | |
| buildChatItemTree, | |
| getThreadMessages, | |
| } | |