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