| | |
| | import TextLineStream from 'textlinestream'; |
| | import { APIMessage, Message } from './types'; |
| |
|
| | |
| | import { asyncIterator } from '@sec-ant/readable-stream/ponyfill/asyncIterator'; |
| |
|
| | |
| | export const isString = (x: any) => !!x.toLowerCase; |
| | |
| | export const isBoolean = (x: any) => x === true || x === false; |
| | |
| | export const isNumeric = (n: any) => !isString(n) && !isNaN(n) && !isBoolean(n); |
| | export const escapeAttr = (str: string) => |
| | str.replace(/>/g, '>').replace(/"/g, '"'); |
| |
|
| | |
| | export async function* getSSEStreamAsync(fetchResponse: Response) { |
| | if (!fetchResponse.body) throw new Error('Response body is empty'); |
| | const lines: ReadableStream<string> = fetchResponse.body |
| | .pipeThrough(new TextDecoderStream()) |
| | .pipeThrough(new TextLineStream()); |
| | |
| | for await (const line of asyncIterator(lines)) { |
| | |
| | if (line.startsWith('data:') && !line.endsWith('[DONE]')) { |
| | const data = JSON.parse(line.slice(5)); |
| | yield data; |
| | } else if (line.startsWith('error:')) { |
| | const data = JSON.parse(line.slice(6)); |
| | throw new Error(data.message || 'Unknown error'); |
| | } |
| | } |
| | } |
| |
|
| | |
| | export const copyStr = (textToCopy: string) => { |
| | |
| | if (navigator.clipboard && window.isSecureContext) { |
| | navigator.clipboard.writeText(textToCopy); |
| | } else { |
| | |
| | const textArea = document.createElement('textarea'); |
| | textArea.value = textToCopy; |
| | |
| | textArea.style.position = 'absolute'; |
| | textArea.style.left = '-999999px'; |
| | document.body.prepend(textArea); |
| | textArea.select(); |
| | document.execCommand('copy'); |
| | } |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | export function normalizeMsgsForAPI(messages: Readonly<Message[]>) { |
| | return messages.map((msg) => { |
| | let newContent = ''; |
| |
|
| | for (const extra of msg.extra ?? []) { |
| | if (extra.type === 'context') { |
| | newContent += `${extra.content}\n\n`; |
| | } |
| | } |
| |
|
| | newContent += msg.content; |
| |
|
| | return { |
| | role: msg.role, |
| | content: newContent, |
| | }; |
| | }) as APIMessage[]; |
| | } |
| |
|
| | |
| | |
| | |
| | export function filterThoughtFromMsgs(messages: APIMessage[]) { |
| | return messages.map((msg) => { |
| | return { |
| | role: msg.role, |
| | content: |
| | msg.role === 'assistant' |
| | ? msg.content.split('</think>').at(-1)!.trim() |
| | : msg.content, |
| | } as APIMessage; |
| | }); |
| | } |
| |
|
| | export function classNames(classes: Record<string, boolean>): string { |
| | return Object.entries(classes) |
| | .filter(([_, value]) => value) |
| | .map(([key, _]) => key) |
| | .join(' '); |
| | } |
| |
|
| | export const delay = (ms: number) => |
| | new Promise((resolve) => setTimeout(resolve, ms)); |
| |
|
| | export const throttle = <T extends unknown[]>( |
| | callback: (...args: T) => void, |
| | delay: number |
| | ) => { |
| | let isWaiting = false; |
| |
|
| | return (...args: T) => { |
| | if (isWaiting) { |
| | return; |
| | } |
| |
|
| | callback(...args); |
| | isWaiting = true; |
| |
|
| | setTimeout(() => { |
| | isWaiting = false; |
| | }, delay); |
| | }; |
| | }; |
| |
|
| | export const cleanCurrentUrl = (removeQueryParams: string[]) => { |
| | const url = new URL(window.location.href); |
| | removeQueryParams.forEach((param) => { |
| | url.searchParams.delete(param); |
| | }); |
| | window.history.replaceState({}, '', url.toString()); |
| | }; |
| |
|