You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

utils.ts 8.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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. try {
  6. const binaryString = atob(base64String)
  7. const compressedUint8Array = Uint8Array.from(binaryString, char => char.charCodeAt(0))
  8. const decompressedStream = new Response(compressedUint8Array).body?.pipeThrough(new DecompressionStream('gzip'))
  9. const decompressedArrayBuffer = await new Response(decompressedStream).arrayBuffer()
  10. return new TextDecoder().decode(decompressedArrayBuffer)
  11. }
  12. catch {
  13. return undefined
  14. }
  15. }
  16. async function getRawInputsFromUrlParams(): Promise<Record<string, any>> {
  17. const urlParams = new URLSearchParams(window.location.search)
  18. const inputs: Record<string, any> = {}
  19. const entriesArray = Array.from(urlParams.entries())
  20. entriesArray.forEach(([key, value]) => {
  21. const prefixArray = ['sys.', 'user.']
  22. if (!prefixArray.some(prefix => key.startsWith(prefix)))
  23. inputs[key] = decodeURIComponent(value)
  24. })
  25. return inputs
  26. }
  27. async function getProcessedInputsFromUrlParams(): Promise<Record<string, any>> {
  28. const urlParams = new URLSearchParams(window.location.search)
  29. const inputs: Record<string, any> = {}
  30. const entriesArray = Array.from(urlParams.entries())
  31. await Promise.all(
  32. entriesArray.map(async ([key, value]) => {
  33. const prefixArray = ['sys.', 'user.']
  34. if (!prefixArray.some(prefix => key.startsWith(prefix)))
  35. inputs[key] = await decodeBase64AndDecompress(decodeURIComponent(value))
  36. }),
  37. )
  38. return inputs
  39. }
  40. async function getProcessedSystemVariablesFromUrlParams(): Promise<Record<string, any>> {
  41. const urlParams = new URLSearchParams(window.location.search)
  42. const redirectUrl = urlParams.get('redirect_url')
  43. if (redirectUrl) {
  44. const decodedRedirectUrl = decodeURIComponent(redirectUrl)
  45. const queryString = decodedRedirectUrl.split('?')[1]
  46. if (queryString) {
  47. const redirectParams = new URLSearchParams(queryString)
  48. for (const [key, value] of redirectParams.entries())
  49. urlParams.set(key, value)
  50. }
  51. }
  52. const systemVariables: Record<string, any> = {}
  53. const entriesArray = Array.from(urlParams.entries())
  54. await Promise.all(
  55. entriesArray.map(async ([key, value]) => {
  56. if (key.startsWith('sys.'))
  57. systemVariables[key.slice(4)] = await decodeBase64AndDecompress(decodeURIComponent(value))
  58. }),
  59. )
  60. return systemVariables
  61. }
  62. async function getProcessedUserVariablesFromUrlParams(): Promise<Record<string, any>> {
  63. const urlParams = new URLSearchParams(window.location.search)
  64. const userVariables: Record<string, any> = {}
  65. const entriesArray = Array.from(urlParams.entries())
  66. await Promise.all(
  67. entriesArray.map(async ([key, value]) => {
  68. if (key.startsWith('user.'))
  69. userVariables[key.slice(5)] = await decodeBase64AndDecompress(decodeURIComponent(value))
  70. }),
  71. )
  72. return userVariables
  73. }
  74. async function getRawUserVariablesFromUrlParams(): Promise<Record<string, any>> {
  75. const urlParams = new URLSearchParams(window.location.search)
  76. const userVariables: Record<string, any> = {}
  77. const entriesArray = Array.from(urlParams.entries())
  78. entriesArray.forEach(([key, value]) => {
  79. if (key.startsWith('user.'))
  80. userVariables[key.slice(5)] = decodeURIComponent(value)
  81. })
  82. return userVariables
  83. }
  84. function isValidGeneratedAnswer(item?: ChatItem | ChatItemInTree): boolean {
  85. return !!item && item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement
  86. }
  87. function getLastAnswer<T extends ChatItem | ChatItemInTree>(chatList: T[]): T | null {
  88. for (let i = chatList.length - 1; i >= 0; i--) {
  89. const item = chatList[i]
  90. if (isValidGeneratedAnswer(item))
  91. return item
  92. }
  93. return null
  94. }
  95. /**
  96. * Build a chat item tree from a chat list
  97. * @param allMessages - The chat list, sorted from oldest to newest
  98. * @returns The chat item tree
  99. */
  100. function buildChatItemTree(allMessages: IChatItem[]): ChatItemInTree[] {
  101. const map: Record<string, ChatItemInTree> = {}
  102. const rootNodes: ChatItemInTree[] = []
  103. const childrenCount: Record<string, number> = {}
  104. let lastAppendedLegacyAnswer: ChatItemInTree | null = null
  105. for (let i = 0; i < allMessages.length; i += 2) {
  106. const question = allMessages[i]!
  107. const answer = allMessages[i + 1]!
  108. const isLegacy = question.parentMessageId === UUID_NIL
  109. const parentMessageId = isLegacy
  110. ? (lastAppendedLegacyAnswer?.id || '')
  111. : (question.parentMessageId || '')
  112. // Process question
  113. childrenCount[parentMessageId] = (childrenCount[parentMessageId] || 0) + 1
  114. const questionNode: ChatItemInTree = {
  115. ...question,
  116. children: [],
  117. }
  118. map[question.id] = questionNode
  119. // Process answer
  120. childrenCount[question.id] = 1
  121. const answerNode: ChatItemInTree = {
  122. ...answer,
  123. children: [],
  124. siblingIndex: isLegacy ? 0 : childrenCount[parentMessageId] - 1,
  125. }
  126. map[answer.id] = answerNode
  127. // Connect question and answer
  128. questionNode.children!.push(answerNode)
  129. // Append to parent or add to root
  130. if (isLegacy) {
  131. if (!lastAppendedLegacyAnswer)
  132. rootNodes.push(questionNode)
  133. else
  134. lastAppendedLegacyAnswer.children!.push(questionNode)
  135. lastAppendedLegacyAnswer = answerNode
  136. }
  137. else {
  138. if (
  139. !parentMessageId
  140. || !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
  141. )
  142. rootNodes.push(questionNode)
  143. else
  144. map[parentMessageId]?.children!.push(questionNode)
  145. }
  146. }
  147. return rootNodes
  148. }
  149. function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): ChatItemInTree[] {
  150. let ret: ChatItemInTree[] = []
  151. let targetNode: ChatItemInTree | undefined
  152. // find path to the target message
  153. const stack = tree.slice().reverse().map(rootNode => ({
  154. node: rootNode,
  155. path: [rootNode],
  156. }))
  157. while (stack.length > 0) {
  158. const { node, path } = stack.pop()!
  159. if (
  160. node.id === targetMessageId
  161. || (!targetMessageId && !node.children?.length && !stack.length) // if targetMessageId is not provided, we use the last message in the tree as the target
  162. ) {
  163. targetNode = node
  164. ret = path.map((item, index) => {
  165. if (!item.isAnswer)
  166. return item
  167. const parentAnswer = path[index - 2]
  168. const siblingCount = !parentAnswer ? tree.length : parentAnswer.children!.length
  169. const prevSibling = !parentAnswer ? tree[item.siblingIndex! - 1]?.children?.[0]?.id : parentAnswer.children![item.siblingIndex! - 1]?.children?.[0].id
  170. const nextSibling = !parentAnswer ? tree[item.siblingIndex! + 1]?.children?.[0]?.id : parentAnswer.children![item.siblingIndex! + 1]?.children?.[0].id
  171. return { ...item, siblingCount, prevSibling, nextSibling }
  172. })
  173. break
  174. }
  175. if (node.children) {
  176. for (let i = node.children.length - 1; i >= 0; i--) {
  177. stack.push({
  178. node: node.children[i],
  179. path: [...path, node.children[i]],
  180. })
  181. }
  182. }
  183. }
  184. // append all descendant messages to the path
  185. if (targetNode) {
  186. const stack = [targetNode]
  187. while (stack.length > 0) {
  188. const node = stack.pop()!
  189. if (node !== targetNode)
  190. ret.push(node)
  191. if (node.children?.length) {
  192. const lastChild = node.children.at(-1)!
  193. if (!lastChild.isAnswer) {
  194. stack.push(lastChild)
  195. continue
  196. }
  197. const parentAnswer = ret.at(-2)
  198. const siblingCount = parentAnswer?.children?.length
  199. const prevSibling = parentAnswer?.children?.at(-2)?.children?.[0]?.id
  200. stack.push({ ...lastChild, siblingCount, prevSibling })
  201. }
  202. }
  203. }
  204. return ret
  205. }
  206. export {
  207. getRawInputsFromUrlParams,
  208. getProcessedInputsFromUrlParams,
  209. getProcessedSystemVariablesFromUrlParams,
  210. getProcessedUserVariablesFromUrlParams,
  211. getRawUserVariablesFromUrlParams,
  212. isValidGeneratedAnswer,
  213. getLastAnswer,
  214. buildChatItemTree,
  215. getThreadMessages,
  216. }