misc.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. // @ts-expect-error this package does not have typing
  2. import TextLineStream from 'textlinestream';
  3. import { APIMessage, Message } from './types';
  4. // ponyfill for missing ReadableStream asyncIterator on Safari
  5. import { asyncIterator } from '@sec-ant/readable-stream/ponyfill/asyncIterator';
  6. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  7. export const isString = (x: any) => !!x.toLowerCase;
  8. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  9. export const isBoolean = (x: any) => x === true || x === false;
  10. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  11. export const isNumeric = (n: any) => !isString(n) && !isNaN(n) && !isBoolean(n);
  12. export const escapeAttr = (str: string) =>
  13. str.replace(/>/g, '>').replace(/"/g, '"');
  14. // wrapper for SSE
  15. export async function* getSSEStreamAsync(fetchResponse: Response) {
  16. if (!fetchResponse.body) throw new Error('Response body is empty');
  17. const lines: ReadableStream<string> = fetchResponse.body
  18. .pipeThrough(new TextDecoderStream())
  19. .pipeThrough(new TextLineStream());
  20. // @ts-expect-error asyncIterator complains about type, but it should work
  21. for await (const line of asyncIterator(lines)) {
  22. //if (isDev) console.log({ line });
  23. if (line.startsWith('data:') && !line.endsWith('[DONE]')) {
  24. const data = JSON.parse(line.slice(5));
  25. yield data;
  26. } else if (line.startsWith('error:')) {
  27. const data = JSON.parse(line.slice(6));
  28. throw new Error(data.message || 'Unknown error');
  29. }
  30. }
  31. }
  32. // copy text to clipboard
  33. export const copyStr = (textToCopy: string) => {
  34. // Navigator clipboard api needs a secure context (https)
  35. if (navigator.clipboard && window.isSecureContext) {
  36. navigator.clipboard.writeText(textToCopy);
  37. } else {
  38. // Use the 'out of viewport hidden text area' trick
  39. const textArea = document.createElement('textarea');
  40. textArea.value = textToCopy;
  41. // Move textarea out of the viewport so it's not visible
  42. textArea.style.position = 'absolute';
  43. textArea.style.left = '-999999px';
  44. document.body.prepend(textArea);
  45. textArea.select();
  46. document.execCommand('copy');
  47. }
  48. };
  49. /**
  50. * filter out redundant fields upon sending to API
  51. * also format extra into text
  52. */
  53. export function normalizeMsgsForAPI(messages: Readonly<Message[]>) {
  54. return messages.map((msg) => {
  55. let newContent = '';
  56. for (const extra of msg.extra ?? []) {
  57. if (extra.type === 'context') {
  58. newContent += `${extra.content}\n\n`;
  59. }
  60. }
  61. newContent += msg.content;
  62. return {
  63. role: msg.role,
  64. content: newContent,
  65. };
  66. }) as APIMessage[];
  67. }
  68. /**
  69. * recommended for DeepsSeek-R1, filter out content between <think> and </think> tags
  70. */
  71. export function filterThoughtFromMsgs(messages: APIMessage[]) {
  72. return messages.map((msg) => {
  73. return {
  74. role: msg.role,
  75. content:
  76. msg.role === 'assistant'
  77. ? msg.content.split('</think>').at(-1)!.trim()
  78. : msg.content,
  79. } as APIMessage;
  80. });
  81. }
  82. export function classNames(classes: Record<string, boolean>): string {
  83. return Object.entries(classes)
  84. .filter(([_, value]) => value)
  85. .map(([key, _]) => key)
  86. .join(' ');
  87. }
  88. export const delay = (ms: number) =>
  89. new Promise((resolve) => setTimeout(resolve, ms));
  90. export const throttle = <T extends unknown[]>(
  91. callback: (...args: T) => void,
  92. delay: number
  93. ) => {
  94. let isWaiting = false;
  95. return (...args: T) => {
  96. if (isWaiting) {
  97. return;
  98. }
  99. callback(...args);
  100. isWaiting = true;
  101. setTimeout(() => {
  102. isWaiting = false;
  103. }, delay);
  104. };
  105. };
  106. export const cleanCurrentUrl = (removeQueryParams: string[]) => {
  107. const url = new URL(window.location.href);
  108. removeQueryParams.forEach((param) => {
  109. url.searchParams.delete(param);
  110. });
  111. window.history.replaceState({}, '', url.toString());
  112. };