| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 | 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        || !allMessages.some(item => item.id === parentMessageId) // parent message might not be fetched yet, in this case we will append the question to the root nodes      )        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,  getPrevChatList,  getLastAnswer,  buildChatItemTree,  getThreadMessages,}
 |