misc.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. // @ts-expect-error this package does not have typing
  2. import TextLineStream from 'textlinestream';
  3. import {
  4. APIMessage,
  5. APIMessageContentPart,
  6. LlamaCppServerProps,
  7. Message,
  8. } from './types';
  9. // ponyfill for missing ReadableStream asyncIterator on Safari
  10. import { asyncIterator } from '@sec-ant/readable-stream/ponyfill/asyncIterator';
  11. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  12. export const isString = (x: any) => !!x.toLowerCase;
  13. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  14. export const isBoolean = (x: any) => x === true || x === false;
  15. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  16. export const isNumeric = (n: any) => !isString(n) && !isNaN(n) && !isBoolean(n);
  17. export const escapeAttr = (str: string) =>
  18. str.replace(/>/g, '>').replace(/"/g, '"');
  19. // wrapper for SSE
  20. export async function* getSSEStreamAsync(fetchResponse: Response) {
  21. if (!fetchResponse.body) throw new Error('Response body is empty');
  22. const lines: ReadableStream<string> = fetchResponse.body
  23. .pipeThrough(new TextDecoderStream())
  24. .pipeThrough(new TextLineStream());
  25. // @ts-expect-error asyncIterator complains about type, but it should work
  26. for await (const line of asyncIterator(lines)) {
  27. //if (isDev) console.log({ line });
  28. if (line.startsWith('data:') && !line.endsWith('[DONE]')) {
  29. const data = JSON.parse(line.slice(5));
  30. yield data;
  31. } else if (line.startsWith('error:')) {
  32. const data = JSON.parse(line.slice(6));
  33. throw new Error(data.message || 'Unknown error');
  34. }
  35. }
  36. }
  37. // copy text to clipboard
  38. export const copyStr = (textToCopy: string) => {
  39. // Navigator clipboard api needs a secure context (https)
  40. if (navigator.clipboard && window.isSecureContext) {
  41. navigator.clipboard.writeText(textToCopy);
  42. } else {
  43. // Use the 'out of viewport hidden text area' trick
  44. const textArea = document.createElement('textarea');
  45. textArea.value = textToCopy;
  46. // Move textarea out of the viewport so it's not visible
  47. textArea.style.position = 'absolute';
  48. textArea.style.left = '-999999px';
  49. document.body.prepend(textArea);
  50. textArea.select();
  51. document.execCommand('copy');
  52. }
  53. };
  54. /**
  55. * filter out redundant fields upon sending to API
  56. * also format extra into text
  57. */
  58. export function normalizeMsgsForAPI(messages: Readonly<Message[]>) {
  59. return messages.map((msg) => {
  60. if (msg.role !== 'user' || !msg.extra) {
  61. return {
  62. role: msg.role,
  63. content: msg.content,
  64. } as APIMessage;
  65. }
  66. // extra content first, then user text message in the end
  67. // this allow re-using the same cache prefix for long context
  68. const contentArr: APIMessageContentPart[] = [];
  69. for (const extra of msg.extra ?? []) {
  70. if (extra.type === 'context') {
  71. contentArr.push({
  72. type: 'text',
  73. text: extra.content,
  74. });
  75. } else if (extra.type === 'textFile') {
  76. contentArr.push({
  77. type: 'text',
  78. text: `File: ${extra.name}\nContent:\n\n${extra.content}`,
  79. });
  80. } else if (extra.type === 'imageFile') {
  81. contentArr.push({
  82. type: 'image_url',
  83. image_url: { url: extra.base64Url },
  84. });
  85. } else {
  86. throw new Error('Unknown extra type');
  87. }
  88. }
  89. // add user message to the end
  90. contentArr.push({
  91. type: 'text',
  92. text: msg.content,
  93. });
  94. return {
  95. role: msg.role,
  96. content: contentArr,
  97. };
  98. }) as APIMessage[];
  99. }
  100. /**
  101. * recommended for DeepsSeek-R1, filter out content between <think> and </think> tags
  102. */
  103. export function filterThoughtFromMsgs(messages: APIMessage[]) {
  104. console.debug({ messages });
  105. return messages.map((msg) => {
  106. if (msg.role !== 'assistant') {
  107. return msg;
  108. }
  109. // assistant message is always a string
  110. const contentStr = msg.content as string;
  111. return {
  112. role: msg.role,
  113. content:
  114. msg.role === 'assistant'
  115. ? contentStr.split('</think>').at(-1)!.trim()
  116. : contentStr,
  117. } as APIMessage;
  118. });
  119. }
  120. export function classNames(classes: Record<string, boolean>): string {
  121. return Object.entries(classes)
  122. .filter(([_, value]) => value)
  123. .map(([key, _]) => key)
  124. .join(' ');
  125. }
  126. export const delay = (ms: number) =>
  127. new Promise((resolve) => setTimeout(resolve, ms));
  128. export const throttle = <T extends unknown[]>(
  129. callback: (...args: T) => void,
  130. delay: number
  131. ) => {
  132. let isWaiting = false;
  133. return (...args: T) => {
  134. if (isWaiting) {
  135. return;
  136. }
  137. callback(...args);
  138. isWaiting = true;
  139. setTimeout(() => {
  140. isWaiting = false;
  141. }, delay);
  142. };
  143. };
  144. export const cleanCurrentUrl = (removeQueryParams: string[]) => {
  145. const url = new URL(window.location.href);
  146. removeQueryParams.forEach((param) => {
  147. url.searchParams.delete(param);
  148. });
  149. window.history.replaceState({}, '', url.toString());
  150. };
  151. export const getServerProps = async (
  152. baseUrl: string,
  153. apiKey?: string
  154. ): Promise<LlamaCppServerProps> => {
  155. try {
  156. const response = await fetch(`${baseUrl}/props`, {
  157. headers: {
  158. 'Content-Type': 'application/json',
  159. ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
  160. },
  161. });
  162. if (!response.ok) {
  163. throw new Error('Failed to fetch server props');
  164. }
  165. const data = await response.json();
  166. return data as LlamaCppServerProps;
  167. } catch (error) {
  168. console.error('Error fetching server props:', error);
  169. throw error;
  170. }
  171. };