utils.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import { UUID_NIL } from './constants'
  2. import type { IChatItem } from './chat/type'
  3. import type { ChatItem, ChatItemInTree } from './types'
  4. async function decodeBase64AndDecompress(base64String: string) {
  5. const binaryString = atob(base64String)
  6. const compressedUint8Array = Uint8Array.from(binaryString, char => char.charCodeAt(0))
  7. const decompressedStream = new Response(compressedUint8Array).body?.pipeThrough(new DecompressionStream('gzip'))
  8. const decompressedArrayBuffer = await new Response(decompressedStream).arrayBuffer()
  9. return new TextDecoder().decode(decompressedArrayBuffer)
  10. }
  11. async function getProcessedInputsFromUrlParams(): Promise<Record<string, any>> {
  12. const urlParams = new URLSearchParams(window.location.search)
  13. const inputs: Record<string, any> = {}
  14. await Promise.all(
  15. urlParams.entries().map(async ([key, value]) => {
  16. inputs[key] = await decodeBase64AndDecompress(decodeURIComponent(value))
  17. }),
  18. )
  19. return inputs
  20. }
  21. function isValidGeneratedAnswer(item?: ChatItem | ChatItemInTree): boolean {
  22. return !!item && item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement
  23. }
  24. function getLastAnswer<T extends ChatItem | ChatItemInTree>(chatList: T[]): T | null {
  25. for (let i = chatList.length - 1; i >= 0; i--) {
  26. const item = chatList[i]
  27. if (isValidGeneratedAnswer(item))
  28. return item
  29. }
  30. return null
  31. }
  32. /**
  33. * Build a chat item tree from a chat list
  34. * @param allMessages - The chat list, sorted from oldest to newest
  35. * @returns The chat item tree
  36. */
  37. function buildChatItemTree(allMessages: IChatItem[]): ChatItemInTree[] {
  38. const map: Record<string, ChatItemInTree> = {}
  39. const rootNodes: ChatItemInTree[] = []
  40. const childrenCount: Record<string, number> = {}
  41. let lastAppendedLegacyAnswer: ChatItemInTree | null = null
  42. for (let i = 0; i < allMessages.length; i += 2) {
  43. const question = allMessages[i]!
  44. const answer = allMessages[i + 1]!
  45. const isLegacy = question.parentMessageId === UUID_NIL
  46. const parentMessageId = isLegacy
  47. ? (lastAppendedLegacyAnswer?.id || '')
  48. : (question.parentMessageId || '')
  49. // Process question
  50. childrenCount[parentMessageId] = (childrenCount[parentMessageId] || 0) + 1
  51. const questionNode: ChatItemInTree = {
  52. ...question,
  53. children: [],
  54. }
  55. map[question.id] = questionNode
  56. // Process answer
  57. childrenCount[question.id] = 1
  58. const answerNode: ChatItemInTree = {
  59. ...answer,
  60. children: [],
  61. siblingIndex: isLegacy ? 0 : childrenCount[parentMessageId] - 1,
  62. }
  63. map[answer.id] = answerNode
  64. // Connect question and answer
  65. questionNode.children!.push(answerNode)
  66. // Append to parent or add to root
  67. if (isLegacy) {
  68. if (!lastAppendedLegacyAnswer)
  69. rootNodes.push(questionNode)
  70. else
  71. lastAppendedLegacyAnswer.children!.push(questionNode)
  72. lastAppendedLegacyAnswer = answerNode
  73. }
  74. else {
  75. if (
  76. !parentMessageId
  77. || !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
  78. )
  79. rootNodes.push(questionNode)
  80. else
  81. map[parentMessageId]?.children!.push(questionNode)
  82. }
  83. }
  84. return rootNodes
  85. }
  86. function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): ChatItemInTree[] {
  87. let ret: ChatItemInTree[] = []
  88. let targetNode: ChatItemInTree | undefined
  89. // find path to the target message
  90. const stack = tree.slice().reverse().map(rootNode => ({
  91. node: rootNode,
  92. path: [rootNode],
  93. }))
  94. while (stack.length > 0) {
  95. const { node, path } = stack.pop()!
  96. if (
  97. node.id === targetMessageId
  98. || (!targetMessageId && !node.children?.length && !stack.length) // if targetMessageId is not provided, we use the last message in the tree as the target
  99. ) {
  100. targetNode = node
  101. ret = path.map((item, index) => {
  102. if (!item.isAnswer)
  103. return item
  104. const parentAnswer = path[index - 2]
  105. const siblingCount = !parentAnswer ? tree.length : parentAnswer.children!.length
  106. const prevSibling = !parentAnswer ? tree[item.siblingIndex! - 1]?.children?.[0]?.id : parentAnswer.children![item.siblingIndex! - 1]?.children?.[0].id
  107. const nextSibling = !parentAnswer ? tree[item.siblingIndex! + 1]?.children?.[0]?.id : parentAnswer.children![item.siblingIndex! + 1]?.children?.[0].id
  108. return { ...item, siblingCount, prevSibling, nextSibling }
  109. })
  110. break
  111. }
  112. if (node.children) {
  113. for (let i = node.children.length - 1; i >= 0; i--) {
  114. stack.push({
  115. node: node.children[i],
  116. path: [...path, node.children[i]],
  117. })
  118. }
  119. }
  120. }
  121. // append all descendant messages to the path
  122. if (targetNode) {
  123. const stack = [targetNode]
  124. while (stack.length > 0) {
  125. const node = stack.pop()!
  126. if (node !== targetNode)
  127. ret.push(node)
  128. if (node.children?.length) {
  129. const lastChild = node.children.at(-1)!
  130. if (!lastChild.isAnswer) {
  131. stack.push(lastChild)
  132. continue
  133. }
  134. const parentAnswer = ret.at(-2)
  135. const siblingCount = parentAnswer?.children?.length
  136. const prevSibling = parentAnswer?.children?.at(-2)?.children?.[0]?.id
  137. stack.push({ ...lastChild, siblingCount, prevSibling })
  138. }
  139. }
  140. }
  141. return ret
  142. }
  143. export {
  144. getProcessedInputsFromUrlParams,
  145. isValidGeneratedAnswer,
  146. getLastAnswer,
  147. buildChatItemTree,
  148. getThreadMessages,
  149. }