index.ts 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387
  1. import { v4 as uuidv4 } from 'uuid';
  2. import sha256 from 'js-sha256';
  3. import dayjs from 'dayjs';
  4. import relativeTime from 'dayjs/plugin/relativeTime';
  5. import isToday from 'dayjs/plugin/isToday';
  6. import isYesterday from 'dayjs/plugin/isYesterday';
  7. import localizedFormat from 'dayjs/plugin/localizedFormat';
  8. dayjs.extend(relativeTime);
  9. dayjs.extend(isToday);
  10. dayjs.extend(isYesterday);
  11. dayjs.extend(localizedFormat);
  12. import { WEBUI_BASE_URL } from '$lib/constants';
  13. import { TTS_RESPONSE_SPLIT } from '$lib/types';
  14. import { marked } from 'marked';
  15. import markedExtension from '$lib/utils/marked/extension';
  16. import markedKatexExtension from '$lib/utils/marked/katex-extension';
  17. import hljs from 'highlight.js';
  18. //////////////////////////
  19. // Helper functions
  20. //////////////////////////
  21. export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
  22. function escapeRegExp(string: string): string {
  23. return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  24. }
  25. export const replaceTokens = (content, sourceIds, char, user) => {
  26. const tokens = [
  27. { regex: /{{char}}/gi, replacement: char },
  28. { regex: /{{user}}/gi, replacement: user },
  29. {
  30. regex: /{{VIDEO_FILE_ID_([a-f0-9-]+)}}/gi,
  31. replacement: (_, fileId) =>
  32. `<video src="${WEBUI_BASE_URL}/api/v1/files/${fileId}/content" controls></video>`
  33. },
  34. {
  35. regex: /{{HTML_FILE_ID_([a-f0-9-]+)}}/gi,
  36. replacement: (_, fileId) => `<file type="html" id="${fileId}" />`
  37. }
  38. ];
  39. // Replace tokens outside code blocks only
  40. const processOutsideCodeBlocks = (text, replacementFn) => {
  41. return text
  42. .split(/(```[\s\S]*?```|`[\s\S]*?`)/)
  43. .map((segment) => {
  44. return segment.startsWith('```') || segment.startsWith('`')
  45. ? segment
  46. : replacementFn(segment);
  47. })
  48. .join('');
  49. };
  50. // Apply replacements
  51. content = processOutsideCodeBlocks(content, (segment) => {
  52. tokens.forEach(({ regex, replacement }) => {
  53. if (replacement !== undefined && replacement !== null) {
  54. segment = segment.replace(regex, replacement);
  55. }
  56. });
  57. if (Array.isArray(sourceIds)) {
  58. sourceIds.forEach((sourceId, idx) => {
  59. const regex = new RegExp(`\\[${idx + 1}\\]`, 'g');
  60. segment = segment.replace(regex, `<source_id data="${idx + 1}" title="${sourceId}" />`);
  61. });
  62. }
  63. return segment;
  64. });
  65. return content;
  66. };
  67. export const sanitizeResponseContent = (content: string) => {
  68. return content
  69. .replace(/<\|[a-z]*$/, '')
  70. .replace(/<\|[a-z]+\|$/, '')
  71. .replace(/<$/, '')
  72. .replaceAll(/<\|[a-z]+\|>/g, ' ')
  73. .replaceAll('<', '&lt;')
  74. .replaceAll('>', '&gt;')
  75. .trim();
  76. };
  77. export const processResponseContent = (content: string) => {
  78. content = processChineseContent(content);
  79. return content.trim();
  80. };
  81. function isChineseChar(char: string): boolean {
  82. return /\p{Script=Han}/u.test(char);
  83. }
  84. // Tackle "Model output issue not following the standard Markdown/LaTeX format" in Chinese.
  85. function processChineseContent(content: string): string {
  86. // This function is used to process the response content before the response content is rendered.
  87. const lines = content.split('\n');
  88. const processedLines = lines.map((line) => {
  89. if (/[\u4e00-\u9fa5]/.test(line)) {
  90. // Problems caused by Chinese parentheses
  91. /* Discription:
  92. * When `*` has Chinese parentheses on the inside, markdown parser ignore bold or italic style.
  93. * - e.g. `**中文名(English)**中文内容` will be parsed directly,
  94. * instead of `<strong>中文名(English)</strong>中文内容`.
  95. * Solution:
  96. * Adding a `space` before and after the bold/italic part can solve the problem.
  97. * - e.g. `**中文名(English)**中文内容` -> ` **中文名(English)** 中文内容`
  98. * Note:
  99. * Similar problem was found with English parentheses and other full delimiters,
  100. * but they are not handled here because they are less likely to appear in LLM output.
  101. * Change the behavior in future if needed.
  102. */
  103. if (line.includes('*')) {
  104. // Handle **bold** with Chinese parentheses
  105. line = processChineseParentheses(line, '**', '(', ')');
  106. // Handle *italic* with Chinese parentheses
  107. line = processChineseParentheses(line, '*', '(', ')');
  108. }
  109. }
  110. return line;
  111. });
  112. content = processedLines.join('\n');
  113. return content;
  114. }
  115. // Helper function for `processChineseContent`
  116. function processChineseParentheses(
  117. line: string,
  118. symbol: string,
  119. leftSymbol: string,
  120. rightSymbol: string
  121. ): string {
  122. // NOTE: If needed, with a little modification, this function can be applied to more cases.
  123. const escapedSymbol = escapeRegExp(symbol);
  124. const regex = new RegExp(
  125. `(.?)(?<!${escapedSymbol})(${escapedSymbol})([^${escapedSymbol}]+)(${escapedSymbol})(?!${escapedSymbol})(.)`,
  126. 'g'
  127. );
  128. return line.replace(regex, (match, l, left, content, right, r) => {
  129. const result =
  130. (content.startsWith(leftSymbol) && l && l.length > 0 && isChineseChar(l[l.length - 1])) ||
  131. (content.endsWith(rightSymbol) && r && r.length > 0 && isChineseChar(r[0]));
  132. if (result) {
  133. return `${l} ${left}${content}${right} ${r}`;
  134. } else {
  135. return match;
  136. }
  137. });
  138. }
  139. export function unescapeHtml(html: string) {
  140. const doc = new DOMParser().parseFromString(html, 'text/html');
  141. return doc.documentElement.textContent;
  142. }
  143. export const capitalizeFirstLetter = (string) => {
  144. return string.charAt(0).toUpperCase() + string.slice(1);
  145. };
  146. export const splitStream = (splitOn) => {
  147. let buffer = '';
  148. return new TransformStream({
  149. transform(chunk, controller) {
  150. buffer += chunk;
  151. const parts = buffer.split(splitOn);
  152. parts.slice(0, -1).forEach((part) => controller.enqueue(part));
  153. buffer = parts[parts.length - 1];
  154. },
  155. flush(controller) {
  156. if (buffer) controller.enqueue(buffer);
  157. }
  158. });
  159. };
  160. export const convertMessagesToHistory = (messages) => {
  161. const history = {
  162. messages: {},
  163. currentId: null
  164. };
  165. let parentMessageId = null;
  166. let messageId = null;
  167. for (const message of messages) {
  168. messageId = uuidv4();
  169. if (parentMessageId !== null) {
  170. history.messages[parentMessageId].childrenIds = [
  171. ...history.messages[parentMessageId].childrenIds,
  172. messageId
  173. ];
  174. }
  175. history.messages[messageId] = {
  176. ...message,
  177. id: messageId,
  178. parentId: parentMessageId,
  179. childrenIds: []
  180. };
  181. parentMessageId = messageId;
  182. }
  183. history.currentId = messageId;
  184. return history;
  185. };
  186. export const getGravatarURL = (email) => {
  187. // Trim leading and trailing whitespace from
  188. // an email address and force all characters
  189. // to lower case
  190. const address = String(email).trim().toLowerCase();
  191. // Create a SHA256 hash of the final string
  192. const hash = sha256(address);
  193. // Grab the actual image URL
  194. return `https://www.gravatar.com/avatar/${hash}`;
  195. };
  196. export const canvasPixelTest = () => {
  197. // Test a 1x1 pixel to potentially identify browser/plugin fingerprint blocking or spoofing
  198. // Inspiration: https://github.com/kkapsner/CanvasBlocker/blob/master/test/detectionTest.js
  199. const canvas = document.createElement('canvas');
  200. const ctx = canvas.getContext('2d');
  201. canvas.height = 1;
  202. canvas.width = 1;
  203. const imageData = new ImageData(canvas.width, canvas.height);
  204. const pixelValues = imageData.data;
  205. // Generate RGB test data
  206. for (let i = 0; i < imageData.data.length; i += 1) {
  207. if (i % 4 !== 3) {
  208. pixelValues[i] = Math.floor(256 * Math.random());
  209. } else {
  210. pixelValues[i] = 255;
  211. }
  212. }
  213. ctx.putImageData(imageData, 0, 0);
  214. const p = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
  215. // Read RGB data and fail if unmatched
  216. for (let i = 0; i < p.length; i += 1) {
  217. if (p[i] !== pixelValues[i]) {
  218. console.log(
  219. 'canvasPixelTest: Wrong canvas pixel RGB value detected:',
  220. p[i],
  221. 'at:',
  222. i,
  223. 'expected:',
  224. pixelValues[i]
  225. );
  226. console.log('canvasPixelTest: Canvas blocking or spoofing is likely');
  227. return false;
  228. }
  229. }
  230. return true;
  231. };
  232. export const compressImage = async (imageUrl, maxWidth, maxHeight) => {
  233. return new Promise((resolve, reject) => {
  234. const img = new Image();
  235. img.onload = () => {
  236. const canvas = document.createElement('canvas');
  237. let width = img.width;
  238. let height = img.height;
  239. // Maintain aspect ratio while resizing
  240. if (maxWidth && maxHeight) {
  241. // Resize with both dimensions defined (preserves aspect ratio)
  242. if (width <= maxWidth && height <= maxHeight) {
  243. resolve(imageUrl);
  244. return;
  245. }
  246. if (width / height > maxWidth / maxHeight) {
  247. height = Math.round((maxWidth * height) / width);
  248. width = maxWidth;
  249. } else {
  250. width = Math.round((maxHeight * width) / height);
  251. height = maxHeight;
  252. }
  253. } else if (maxWidth) {
  254. // Only maxWidth defined
  255. if (width <= maxWidth) {
  256. resolve(imageUrl);
  257. return;
  258. }
  259. height = Math.round((maxWidth * height) / width);
  260. width = maxWidth;
  261. } else if (maxHeight) {
  262. // Only maxHeight defined
  263. if (height <= maxHeight) {
  264. resolve(imageUrl);
  265. return;
  266. }
  267. width = Math.round((maxHeight * width) / height);
  268. height = maxHeight;
  269. }
  270. canvas.width = width;
  271. canvas.height = height;
  272. const context = canvas.getContext('2d');
  273. context.drawImage(img, 0, 0, width, height);
  274. // Get compressed image URL
  275. const compressedUrl = canvas.toDataURL();
  276. resolve(compressedUrl);
  277. };
  278. img.onerror = (error) => reject(error);
  279. img.src = imageUrl;
  280. });
  281. };
  282. export const generateInitialsImage = (name) => {
  283. const canvas = document.createElement('canvas');
  284. const ctx = canvas.getContext('2d');
  285. canvas.width = 100;
  286. canvas.height = 100;
  287. if (!canvasPixelTest()) {
  288. console.log(
  289. 'generateInitialsImage: failed pixel test, fingerprint evasion is likely. Using default image.'
  290. );
  291. return '/user.png';
  292. }
  293. ctx.fillStyle = '#F39C12';
  294. ctx.fillRect(0, 0, canvas.width, canvas.height);
  295. ctx.fillStyle = '#FFFFFF';
  296. ctx.font = '40px Helvetica';
  297. ctx.textAlign = 'center';
  298. ctx.textBaseline = 'middle';
  299. const sanitizedName = name.trim();
  300. const initials =
  301. sanitizedName.length > 0
  302. ? sanitizedName[0] +
  303. (sanitizedName.split(' ').length > 1
  304. ? sanitizedName[sanitizedName.lastIndexOf(' ') + 1]
  305. : '')
  306. : '';
  307. ctx.fillText(initials.toUpperCase(), canvas.width / 2, canvas.height / 2);
  308. return canvas.toDataURL();
  309. };
  310. export const formatDate = (inputDate) => {
  311. const date = dayjs(inputDate);
  312. const now = dayjs();
  313. if (date.isToday()) {
  314. return `Today at ${date.format('LT')}`;
  315. } else if (date.isYesterday()) {
  316. return `Yesterday at ${date.format('LT')}`;
  317. } else {
  318. return `${date.format('L')} at ${date.format('LT')}`;
  319. }
  320. };
  321. export const copyToClipboard = async (text, formatted = false) => {
  322. if (formatted) {
  323. const options = {
  324. throwOnError: false,
  325. highlight: function (code, lang) {
  326. const language = hljs.getLanguage(lang) ? lang : 'plaintext';
  327. return hljs.highlight(code, { language }).value;
  328. }
  329. };
  330. marked.use(markedKatexExtension(options));
  331. marked.use(markedExtension(options));
  332. // DEVELOPER NOTE: Go to `$lib/components/chat/Messages/Markdown.svelte` to add extra markdown extensions for rendering.
  333. const htmlContent = marked.parse(text);
  334. // Add basic styling to make the content look better when pasted
  335. const styledHtml = `
  336. <div>
  337. <style>
  338. pre {
  339. background-color: #f6f8fa;
  340. border-radius: 6px;
  341. padding: 16px;
  342. overflow: auto;
  343. }
  344. code {
  345. font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
  346. font-size: 14px;
  347. }
  348. .hljs-keyword { color: #d73a49; }
  349. .hljs-string { color: #032f62; }
  350. .hljs-comment { color: #6a737d; }
  351. .hljs-function { color: #6f42c1; }
  352. .hljs-number { color: #005cc5; }
  353. .hljs-operator { color: #d73a49; }
  354. .hljs-class { color: #6f42c1; }
  355. .hljs-title { color: #6f42c1; }
  356. .hljs-params { color: #24292e; }
  357. .hljs-built_in { color: #005cc5; }
  358. blockquote {
  359. border-left: 4px solid #dfe2e5;
  360. padding-left: 16px;
  361. color: #6a737d;
  362. margin-left: 0;
  363. margin-right: 0;
  364. }
  365. table {
  366. border-collapse: collapse;
  367. width: 100%;
  368. margin-bottom: 16px;
  369. }
  370. table, th, td {
  371. border: 1px solid #dfe2e5;
  372. }
  373. th, td {
  374. padding: 8px 12px;
  375. }
  376. th {
  377. background-color: #f6f8fa;
  378. }
  379. </style>
  380. ${htmlContent}
  381. </div>
  382. `;
  383. // Create a blob with HTML content
  384. const blob = new Blob([styledHtml], { type: 'text/html' });
  385. try {
  386. // Create a ClipboardItem with HTML content
  387. const data = new ClipboardItem({
  388. 'text/html': blob,
  389. 'text/plain': new Blob([text], { type: 'text/plain' })
  390. });
  391. // Write to clipboard
  392. await navigator.clipboard.write([data]);
  393. return true;
  394. } catch (err) {
  395. console.error('Error copying formatted content:', err);
  396. // Fallback to plain text
  397. return await copyToClipboard(text);
  398. }
  399. } else {
  400. let result = false;
  401. if (!navigator.clipboard) {
  402. const textArea = document.createElement('textarea');
  403. textArea.value = text;
  404. // Avoid scrolling to bottom
  405. textArea.style.top = '0';
  406. textArea.style.left = '0';
  407. textArea.style.position = 'fixed';
  408. document.body.appendChild(textArea);
  409. textArea.focus();
  410. textArea.select();
  411. try {
  412. const successful = document.execCommand('copy');
  413. const msg = successful ? 'successful' : 'unsuccessful';
  414. console.log('Fallback: Copying text command was ' + msg);
  415. result = true;
  416. } catch (err) {
  417. console.error('Fallback: Oops, unable to copy', err);
  418. }
  419. document.body.removeChild(textArea);
  420. return result;
  421. }
  422. result = await navigator.clipboard
  423. .writeText(text)
  424. .then(() => {
  425. console.log('Async: Copying to clipboard was successful!');
  426. return true;
  427. })
  428. .catch((error) => {
  429. console.error('Async: Could not copy text: ', error);
  430. return false;
  431. });
  432. return result;
  433. }
  434. };
  435. export const compareVersion = (latest, current) => {
  436. return current === '0.0.0'
  437. ? false
  438. : current.localeCompare(latest, undefined, {
  439. numeric: true,
  440. sensitivity: 'case',
  441. caseFirst: 'upper'
  442. }) < 0;
  443. };
  444. export const extractCurlyBraceWords = (text) => {
  445. const regex = /\{\{([^}]+)\}\}/g;
  446. const matches = [];
  447. let match;
  448. while ((match = regex.exec(text)) !== null) {
  449. matches.push({
  450. word: match[1].trim(),
  451. startIndex: match.index,
  452. endIndex: regex.lastIndex - 1
  453. });
  454. }
  455. return matches;
  456. };
  457. export const removeLastWordFromString = (inputString, wordString) => {
  458. console.log('inputString', inputString);
  459. // Split the string by newline characters to handle lines separately
  460. const lines = inputString.split('\n');
  461. // Take the last line to operate only on it
  462. const lastLine = lines.pop();
  463. // Split the last line into an array of words
  464. const words = lastLine.split(' ');
  465. // Conditional to check for the last word removal
  466. if (words.at(-1) === wordString || (wordString === '' && words.at(-1) === '\\#')) {
  467. words.pop(); // Remove last word if condition is satisfied
  468. }
  469. // Join the remaining words back into a string and handle space correctly
  470. let updatedLastLine = words.join(' ');
  471. // Add a trailing space to the updated last line if there are still words
  472. if (updatedLastLine !== '') {
  473. updatedLastLine += ' ';
  474. }
  475. // Combine the lines together again, placing the updated last line back in
  476. const resultString = [...lines, updatedLastLine].join('\n');
  477. // Return the final string
  478. console.log('resultString', resultString);
  479. return resultString;
  480. };
  481. export const removeFirstHashWord = (inputString) => {
  482. // Split the string into an array of words
  483. const words = inputString.split(' ');
  484. // Find the index of the first word that starts with #
  485. const index = words.findIndex((word) => word.startsWith('#'));
  486. // Remove the first word with #
  487. if (index !== -1) {
  488. words.splice(index, 1);
  489. }
  490. // Join the remaining words back into a string
  491. const resultString = words.join(' ');
  492. return resultString;
  493. };
  494. export const transformFileName = (fileName) => {
  495. // Convert to lowercase
  496. const lowerCaseFileName = fileName.toLowerCase();
  497. // Remove special characters using regular expression
  498. const sanitizedFileName = lowerCaseFileName.replace(/[^\w\s]/g, '');
  499. // Replace spaces with dashes
  500. const finalFileName = sanitizedFileName.replace(/\s+/g, '-');
  501. return finalFileName;
  502. };
  503. export const calculateSHA256 = async (file) => {
  504. // Create a FileReader to read the file asynchronously
  505. const reader = new FileReader();
  506. // Define a promise to handle the file reading
  507. const readFile = new Promise((resolve, reject) => {
  508. reader.onload = () => resolve(reader.result);
  509. reader.onerror = reject;
  510. });
  511. // Read the file as an ArrayBuffer
  512. reader.readAsArrayBuffer(file);
  513. try {
  514. // Wait for the FileReader to finish reading the file
  515. const buffer = await readFile;
  516. // Convert the ArrayBuffer to a Uint8Array
  517. const uint8Array = new Uint8Array(buffer);
  518. // Calculate the SHA-256 hash using Web Crypto API
  519. const hashBuffer = await crypto.subtle.digest('SHA-256', uint8Array);
  520. // Convert the hash to a hexadecimal string
  521. const hashArray = Array.from(new Uint8Array(hashBuffer));
  522. const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join('');
  523. return `${hashHex}`;
  524. } catch (error) {
  525. console.error('Error calculating SHA-256 hash:', error);
  526. throw error;
  527. }
  528. };
  529. export const getImportOrigin = (_chats) => {
  530. // Check what external service chat imports are from
  531. if ('mapping' in _chats[0]) {
  532. return 'openai';
  533. }
  534. return 'webui';
  535. };
  536. export const getUserPosition = async (raw = false) => {
  537. // Get the user's location using the Geolocation API
  538. const position = await new Promise((resolve, reject) => {
  539. navigator.geolocation.getCurrentPosition(resolve, reject);
  540. }).catch((error) => {
  541. console.error('Error getting user location:', error);
  542. throw error;
  543. });
  544. if (!position) {
  545. return 'Location not available';
  546. }
  547. // Extract the latitude and longitude from the position
  548. const { latitude, longitude } = position.coords;
  549. if (raw) {
  550. return { latitude, longitude };
  551. } else {
  552. return `${latitude.toFixed(3)}, ${longitude.toFixed(3)} (lat, long)`;
  553. }
  554. };
  555. const convertOpenAIMessages = (convo) => {
  556. // Parse OpenAI chat messages and create chat dictionary for creating new chats
  557. const mapping = convo['mapping'];
  558. const messages = [];
  559. let currentId = '';
  560. let lastId = null;
  561. for (const message_id in mapping) {
  562. const message = mapping[message_id];
  563. currentId = message_id;
  564. try {
  565. if (
  566. messages.length == 0 &&
  567. (message['message'] == null ||
  568. (message['message']['content']['parts']?.[0] == '' &&
  569. message['message']['content']['text'] == null))
  570. ) {
  571. // Skip chat messages with no content
  572. continue;
  573. } else {
  574. const new_chat = {
  575. id: message_id,
  576. parentId: lastId,
  577. childrenIds: message['children'] || [],
  578. role: message['message']?.['author']?.['role'] !== 'user' ? 'assistant' : 'user',
  579. content:
  580. message['message']?.['content']?.['parts']?.[0] ||
  581. message['message']?.['content']?.['text'] ||
  582. '',
  583. model: 'gpt-3.5-turbo',
  584. done: true,
  585. context: null
  586. };
  587. messages.push(new_chat);
  588. lastId = currentId;
  589. }
  590. } catch (error) {
  591. console.log('Error with', message, '\nError:', error);
  592. }
  593. }
  594. const history: Record<PropertyKey, (typeof messages)[number]> = {};
  595. messages.forEach((obj) => (history[obj.id] = obj));
  596. const chat = {
  597. history: {
  598. currentId: currentId,
  599. messages: history // Need to convert this to not a list and instead a json object
  600. },
  601. models: ['gpt-3.5-turbo'],
  602. messages: messages,
  603. options: {},
  604. timestamp: convo['create_time'],
  605. title: convo['title'] ?? 'New Chat'
  606. };
  607. return chat;
  608. };
  609. const validateChat = (chat) => {
  610. // Because ChatGPT sometimes has features we can't use like DALL-E or might have corrupted messages, need to validate
  611. const messages = chat.messages;
  612. // Check if messages array is empty
  613. if (messages.length === 0) {
  614. return false;
  615. }
  616. // Last message's children should be an empty array
  617. const lastMessage = messages[messages.length - 1];
  618. if (lastMessage.childrenIds.length !== 0) {
  619. return false;
  620. }
  621. // First message's parent should be null
  622. const firstMessage = messages[0];
  623. if (firstMessage.parentId !== null) {
  624. return false;
  625. }
  626. // Every message's content should be a string
  627. for (const message of messages) {
  628. if (typeof message.content !== 'string') {
  629. return false;
  630. }
  631. }
  632. return true;
  633. };
  634. export const convertOpenAIChats = (_chats) => {
  635. // Create a list of dictionaries with each conversation from import
  636. const chats = [];
  637. let failed = 0;
  638. for (const convo of _chats) {
  639. const chat = convertOpenAIMessages(convo);
  640. if (validateChat(chat)) {
  641. chats.push({
  642. id: convo['id'],
  643. user_id: '',
  644. title: convo['title'],
  645. chat: chat,
  646. timestamp: convo['create_time']
  647. });
  648. } else {
  649. failed++;
  650. }
  651. }
  652. console.log(failed, 'Conversations could not be imported');
  653. return chats;
  654. };
  655. export const isValidHttpUrl = (string: string) => {
  656. let url;
  657. try {
  658. url = new URL(string);
  659. } catch (_) {
  660. return false;
  661. }
  662. return url.protocol === 'http:' || url.protocol === 'https:';
  663. };
  664. export const removeEmojis = (str: string) => {
  665. // Regular expression to match emojis
  666. const emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g;
  667. // Replace emojis with an empty string
  668. return str.replace(emojiRegex, '');
  669. };
  670. export const removeFormattings = (str: string) => {
  671. return (
  672. str
  673. // Block elements (remove completely)
  674. .replace(/(```[\s\S]*?```)/g, '') // Code blocks
  675. .replace(/^\|.*\|$/gm, '') // Tables
  676. // Inline elements (preserve content)
  677. .replace(/(?:\*\*|__)(.*?)(?:\*\*|__)/g, '$1') // Bold
  678. .replace(/(?:[*_])(.*?)(?:[*_])/g, '$1') // Italic
  679. .replace(/~~(.*?)~~/g, '$1') // Strikethrough
  680. .replace(/`([^`]+)`/g, '$1') // Inline code
  681. // Links and images
  682. .replace(/!?\[([^\]]*)\](?:\([^)]+\)|\[[^\]]*\])/g, '$1') // Links & images
  683. .replace(/^\[[^\]]+\]:\s*.*$/gm, '') // Reference definitions
  684. // Block formatting
  685. .replace(/^#{1,6}\s+/gm, '') // Headers
  686. .replace(/^\s*[-*+]\s+/gm, '') // Lists
  687. .replace(/^\s*(?:\d+\.)\s+/gm, '') // Numbered lists
  688. .replace(/^\s*>[> ]*/gm, '') // Blockquotes
  689. .replace(/^\s*:\s+/gm, '') // Definition lists
  690. // Cleanup
  691. .replace(/\[\^[^\]]*\]/g, '') // Footnotes
  692. .replace(/\n{2,}/g, '\n')
  693. ); // Multiple newlines
  694. };
  695. export const cleanText = (content: string) => {
  696. return removeFormattings(removeEmojis(content.trim()));
  697. };
  698. export const removeDetails = (content, types) => {
  699. for (const type of types) {
  700. content = content.replace(
  701. new RegExp(`<details\\s+type="${type}"[^>]*>.*?<\\/details>`, 'gis'),
  702. ''
  703. );
  704. }
  705. return content;
  706. };
  707. export const removeAllDetails = (content) => {
  708. content = content.replace(/<details[^>]*>.*?<\/details>/gis, '');
  709. return content;
  710. };
  711. export const processDetails = (content) => {
  712. content = removeDetails(content, ['reasoning', 'code_interpreter']);
  713. // This regex matches <details> tags with type="tool_calls" and captures their attributes to convert them to <tool_calls> tags
  714. const detailsRegex = /<details\s+type="tool_calls"([^>]*)>([\s\S]*?)<\/details>/gis;
  715. const matches = content.match(detailsRegex);
  716. if (matches) {
  717. for (const match of matches) {
  718. const attributesRegex = /(\w+)="([^"]*)"/g;
  719. const attributes = {};
  720. let attributeMatch;
  721. while ((attributeMatch = attributesRegex.exec(match)) !== null) {
  722. attributes[attributeMatch[1]] = attributeMatch[2];
  723. }
  724. content = content.replace(
  725. match,
  726. `<tool_calls name="${attributes.name}" result="${attributes.result}"/>`
  727. );
  728. }
  729. }
  730. return content;
  731. };
  732. // This regular expression matches code blocks marked by triple backticks
  733. const codeBlockRegex = /```[\s\S]*?```/g;
  734. export const extractSentences = (text: string) => {
  735. const codeBlocks: string[] = [];
  736. let index = 0;
  737. // Temporarily replace code blocks with placeholders and store the blocks separately
  738. text = text.replace(codeBlockRegex, (match) => {
  739. const placeholder = `\u0000${index}\u0000`; // Use a unique placeholder
  740. codeBlocks[index++] = match;
  741. return placeholder;
  742. });
  743. // Split the modified text into sentences based on common punctuation marks, avoiding these blocks
  744. let sentences = text.split(/(?<=[.!?])\s+/);
  745. // Restore code blocks and process sentences
  746. sentences = sentences.map((sentence) => {
  747. // Check if the sentence includes a placeholder for a code block
  748. return sentence.replace(/\u0000(\d+)\u0000/g, (_, idx) => codeBlocks[idx]);
  749. });
  750. return sentences.map(cleanText).filter(Boolean);
  751. };
  752. export const extractParagraphsForAudio = (text: string) => {
  753. const codeBlocks: string[] = [];
  754. let index = 0;
  755. // Temporarily replace code blocks with placeholders and store the blocks separately
  756. text = text.replace(codeBlockRegex, (match) => {
  757. const placeholder = `\u0000${index}\u0000`; // Use a unique placeholder
  758. codeBlocks[index++] = match;
  759. return placeholder;
  760. });
  761. // Split the modified text into paragraphs based on newlines, avoiding these blocks
  762. let paragraphs = text.split(/\n+/);
  763. // Restore code blocks and process paragraphs
  764. paragraphs = paragraphs.map((paragraph) => {
  765. // Check if the paragraph includes a placeholder for a code block
  766. return paragraph.replace(/\u0000(\d+)\u0000/g, (_, idx) => codeBlocks[idx]);
  767. });
  768. return paragraphs.map(cleanText).filter(Boolean);
  769. };
  770. export const extractSentencesForAudio = (text: string) => {
  771. return extractSentences(text).reduce((mergedTexts, currentText) => {
  772. const lastIndex = mergedTexts.length - 1;
  773. if (lastIndex >= 0) {
  774. const previousText = mergedTexts[lastIndex];
  775. const wordCount = previousText.split(/\s+/).length;
  776. const charCount = previousText.length;
  777. if (wordCount < 4 || charCount < 50) {
  778. mergedTexts[lastIndex] = previousText + ' ' + currentText;
  779. } else {
  780. mergedTexts.push(currentText);
  781. }
  782. } else {
  783. mergedTexts.push(currentText);
  784. }
  785. return mergedTexts;
  786. }, [] as string[]);
  787. };
  788. export const getMessageContentParts = (content: string, splitOn: string = 'punctuation') => {
  789. const messageContentParts: string[] = [];
  790. switch (splitOn) {
  791. default:
  792. case TTS_RESPONSE_SPLIT.PUNCTUATION:
  793. messageContentParts.push(...extractSentencesForAudio(content));
  794. break;
  795. case TTS_RESPONSE_SPLIT.PARAGRAPHS:
  796. messageContentParts.push(...extractParagraphsForAudio(content));
  797. break;
  798. case TTS_RESPONSE_SPLIT.NONE:
  799. messageContentParts.push(cleanText(content));
  800. break;
  801. }
  802. return messageContentParts;
  803. };
  804. export const blobToFile = (blob, fileName) => {
  805. // Create a new File object from the Blob
  806. const file = new File([blob], fileName, { type: blob.type });
  807. return file;
  808. };
  809. export const getPromptVariables = (user_name, user_location) => {
  810. return {
  811. '{{USER_NAME}}': user_name,
  812. '{{USER_LOCATION}}': user_location || 'Unknown',
  813. '{{CURRENT_DATETIME}}': getCurrentDateTime(),
  814. '{{CURRENT_DATE}}': getFormattedDate(),
  815. '{{CURRENT_TIME}}': getFormattedTime(),
  816. '{{CURRENT_WEEKDAY}}': getWeekday(),
  817. '{{CURRENT_TIMEZONE}}': getUserTimezone(),
  818. '{{USER_LANGUAGE}}': localStorage.getItem('locale') || 'en-US'
  819. };
  820. };
  821. /**
  822. * @param {string} template - The template string containing placeholders.
  823. * @returns {string} The template string with the placeholders replaced by the prompt.
  824. */
  825. export const promptTemplate = (
  826. template: string,
  827. user_name?: string,
  828. user_location?: string
  829. ): string => {
  830. // Get the current date
  831. const currentDate = new Date();
  832. // Format the date to YYYY-MM-DD
  833. const formattedDate =
  834. currentDate.getFullYear() +
  835. '-' +
  836. String(currentDate.getMonth() + 1).padStart(2, '0') +
  837. '-' +
  838. String(currentDate.getDate()).padStart(2, '0');
  839. // Format the time to HH:MM:SS AM/PM
  840. const currentTime = currentDate.toLocaleTimeString('en-US', {
  841. hour: 'numeric',
  842. minute: 'numeric',
  843. second: 'numeric',
  844. hour12: true
  845. });
  846. // Get the current weekday
  847. const currentWeekday = getWeekday();
  848. // Get the user's timezone
  849. const currentTimezone = getUserTimezone();
  850. // Get the user's language
  851. const userLanguage = localStorage.getItem('locale') || 'en-US';
  852. // Replace {{CURRENT_DATETIME}} in the template with the formatted datetime
  853. template = template.replace('{{CURRENT_DATETIME}}', `${formattedDate} ${currentTime}`);
  854. // Replace {{CURRENT_DATE}} in the template with the formatted date
  855. template = template.replace('{{CURRENT_DATE}}', formattedDate);
  856. // Replace {{CURRENT_TIME}} in the template with the formatted time
  857. template = template.replace('{{CURRENT_TIME}}', currentTime);
  858. // Replace {{CURRENT_WEEKDAY}} in the template with the current weekday
  859. template = template.replace('{{CURRENT_WEEKDAY}}', currentWeekday);
  860. // Replace {{CURRENT_TIMEZONE}} in the template with the user's timezone
  861. template = template.replace('{{CURRENT_TIMEZONE}}', currentTimezone);
  862. // Replace {{USER_LANGUAGE}} in the template with the user's language
  863. template = template.replace('{{USER_LANGUAGE}}', userLanguage);
  864. if (user_name) {
  865. // Replace {{USER_NAME}} in the template with the user's name
  866. template = template.replace('{{USER_NAME}}', user_name);
  867. }
  868. if (user_location) {
  869. // Replace {{USER_LOCATION}} in the template with the current location
  870. template = template.replace('{{USER_LOCATION}}', user_location);
  871. } else {
  872. // Replace {{USER_LOCATION}} in the template with 'Unknown' if no location is provided
  873. template = template.replace('{{USER_LOCATION}}', 'LOCATION_UNKNOWN');
  874. }
  875. return template;
  876. };
  877. /**
  878. * This function is used to replace placeholders in a template string with the provided prompt.
  879. * The placeholders can be in the following formats:
  880. * - `{{prompt}}`: This will be replaced with the entire prompt.
  881. * - `{{prompt:start:<length>}}`: This will be replaced with the first <length> characters of the prompt.
  882. * - `{{prompt:end:<length>}}`: This will be replaced with the last <length> characters of the prompt.
  883. * - `{{prompt:middletruncate:<length>}}`: This will be replaced with the prompt truncated to <length> characters, with '...' in the middle.
  884. *
  885. * @param {string} template - The template string containing placeholders.
  886. * @param {string} prompt - The string to replace the placeholders with.
  887. * @returns {string} The template string with the placeholders replaced by the prompt.
  888. */
  889. export const titleGenerationTemplate = (template: string, prompt: string): string => {
  890. template = template.replace(
  891. /{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}/g,
  892. (match, startLength, endLength, middleLength) => {
  893. if (match === '{{prompt}}') {
  894. return prompt;
  895. } else if (match.startsWith('{{prompt:start:')) {
  896. return prompt.substring(0, startLength);
  897. } else if (match.startsWith('{{prompt:end:')) {
  898. return prompt.slice(-endLength);
  899. } else if (match.startsWith('{{prompt:middletruncate:')) {
  900. if (prompt.length <= middleLength) {
  901. return prompt;
  902. }
  903. const start = prompt.slice(0, Math.ceil(middleLength / 2));
  904. const end = prompt.slice(-Math.floor(middleLength / 2));
  905. return `${start}...${end}`;
  906. }
  907. return '';
  908. }
  909. );
  910. template = promptTemplate(template);
  911. return template;
  912. };
  913. export const approximateToHumanReadable = (nanoseconds: number) => {
  914. const seconds = Math.floor((nanoseconds / 1e9) % 60);
  915. const minutes = Math.floor((nanoseconds / 6e10) % 60);
  916. const hours = Math.floor((nanoseconds / 3.6e12) % 24);
  917. const results: string[] = [];
  918. if (seconds >= 0) {
  919. results.push(`${seconds}s`);
  920. }
  921. if (minutes > 0) {
  922. results.push(`${minutes}m`);
  923. }
  924. if (hours > 0) {
  925. results.push(`${hours}h`);
  926. }
  927. return results.reverse().join(' ');
  928. };
  929. export const getTimeRange = (timestamp) => {
  930. const now = new Date();
  931. const date = new Date(timestamp * 1000); // Convert Unix timestamp to milliseconds
  932. // Calculate the difference in milliseconds
  933. const diffTime = now.getTime() - date.getTime();
  934. const diffDays = diffTime / (1000 * 3600 * 24);
  935. const nowDate = now.getDate();
  936. const nowMonth = now.getMonth();
  937. const nowYear = now.getFullYear();
  938. const dateDate = date.getDate();
  939. const dateMonth = date.getMonth();
  940. const dateYear = date.getFullYear();
  941. if (nowYear === dateYear && nowMonth === dateMonth && nowDate === dateDate) {
  942. return 'Today';
  943. } else if (nowYear === dateYear && nowMonth === dateMonth && nowDate - dateDate === 1) {
  944. return 'Yesterday';
  945. } else if (diffDays <= 7) {
  946. return 'Previous 7 days';
  947. } else if (diffDays <= 30) {
  948. return 'Previous 30 days';
  949. } else if (nowYear === dateYear) {
  950. return date.toLocaleString('default', { month: 'long' });
  951. } else {
  952. return date.getFullYear().toString();
  953. }
  954. };
  955. /**
  956. * Extract frontmatter as a dictionary from the specified content string.
  957. * @param content {string} - The content string with potential frontmatter.
  958. * @returns {Object} - The extracted frontmatter as a dictionary.
  959. */
  960. export const extractFrontmatter = (content) => {
  961. const frontmatter = {};
  962. let frontmatterStarted = false;
  963. let frontmatterEnded = false;
  964. const frontmatterPattern = /^\s*([a-z_]+):\s*(.*)\s*$/i;
  965. // Split content into lines
  966. const lines = content.split('\n');
  967. // Check if the content starts with triple quotes
  968. if (lines[0].trim() !== '"""') {
  969. return {};
  970. }
  971. frontmatterStarted = true;
  972. for (let i = 1; i < lines.length; i++) {
  973. const line = lines[i];
  974. if (line.includes('"""')) {
  975. if (frontmatterStarted) {
  976. frontmatterEnded = true;
  977. break;
  978. }
  979. }
  980. if (frontmatterStarted && !frontmatterEnded) {
  981. const match = frontmatterPattern.exec(line);
  982. if (match) {
  983. const [, key, value] = match;
  984. frontmatter[key.trim()] = value.trim();
  985. }
  986. }
  987. }
  988. return frontmatter;
  989. };
  990. // Function to determine the best matching language
  991. export const bestMatchingLanguage = (supportedLanguages, preferredLanguages, defaultLocale) => {
  992. const languages = supportedLanguages.map((lang) => lang.code);
  993. const match = preferredLanguages
  994. .map((prefLang) => languages.find((lang) => lang.startsWith(prefLang)))
  995. .find(Boolean);
  996. return match || defaultLocale;
  997. };
  998. // Get the date in the format YYYY-MM-DD
  999. export const getFormattedDate = () => {
  1000. const date = new Date();
  1001. const year = date.getFullYear();
  1002. const month = String(date.getMonth() + 1).padStart(2, '0');
  1003. const day = String(date.getDate()).padStart(2, '0');
  1004. return `${year}-${month}-${day}`;
  1005. };
  1006. // Get the time in the format HH:MM:SS
  1007. export const getFormattedTime = () => {
  1008. const date = new Date();
  1009. return date.toTimeString().split(' ')[0];
  1010. };
  1011. // Get the current date and time in the format YYYY-MM-DD HH:MM:SS
  1012. export const getCurrentDateTime = () => {
  1013. return `${getFormattedDate()} ${getFormattedTime()}`;
  1014. };
  1015. // Get the user's timezone
  1016. export const getUserTimezone = () => {
  1017. return Intl.DateTimeFormat().resolvedOptions().timeZone;
  1018. };
  1019. // Get the weekday
  1020. export const getWeekday = () => {
  1021. const date = new Date();
  1022. const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
  1023. return weekdays[date.getDay()];
  1024. };
  1025. export const createMessagesList = (history, messageId) => {
  1026. if (messageId === null) {
  1027. return [];
  1028. }
  1029. const message = history.messages[messageId];
  1030. if (message === undefined) {
  1031. return [];
  1032. }
  1033. if (message?.parentId) {
  1034. return [...createMessagesList(history, message.parentId), message];
  1035. } else {
  1036. return [message];
  1037. }
  1038. };
  1039. export const formatFileSize = (size) => {
  1040. if (size == null) return 'Unknown size';
  1041. if (typeof size !== 'number' || size < 0) return 'Invalid size';
  1042. if (size === 0) return '0 B';
  1043. const units = ['B', 'KB', 'MB', 'GB', 'TB'];
  1044. let unitIndex = 0;
  1045. while (size >= 1024 && unitIndex < units.length - 1) {
  1046. size /= 1024;
  1047. unitIndex++;
  1048. }
  1049. return `${size.toFixed(1)} ${units[unitIndex]}`;
  1050. };
  1051. export const getLineCount = (text) => {
  1052. console.log(typeof text);
  1053. return text ? text.split('\n').length : 0;
  1054. };
  1055. // Helper function to recursively resolve OpenAPI schema into JSON schema format
  1056. function resolveSchema(schemaRef, components, resolvedSchemas = new Set()) {
  1057. if (!schemaRef) return {};
  1058. if (schemaRef['$ref']) {
  1059. const refPath = schemaRef['$ref'];
  1060. const schemaName = refPath.split('/').pop();
  1061. if (resolvedSchemas.has(schemaName)) {
  1062. // Avoid infinite recursion on circular references
  1063. return {};
  1064. }
  1065. resolvedSchemas.add(schemaName);
  1066. const referencedSchema = components.schemas[schemaName];
  1067. return resolveSchema(referencedSchema, components, resolvedSchemas);
  1068. }
  1069. if (schemaRef.type) {
  1070. const schemaObj = { type: schemaRef.type };
  1071. if (schemaRef.description) {
  1072. schemaObj.description = schemaRef.description;
  1073. }
  1074. switch (schemaRef.type) {
  1075. case 'object':
  1076. schemaObj.properties = {};
  1077. schemaObj.required = schemaRef.required || [];
  1078. for (const [propName, propSchema] of Object.entries(schemaRef.properties || {})) {
  1079. schemaObj.properties[propName] = resolveSchema(propSchema, components);
  1080. }
  1081. break;
  1082. case 'array':
  1083. schemaObj.items = resolveSchema(schemaRef.items, components);
  1084. break;
  1085. default:
  1086. // for primitive types (string, integer, etc.), just use as is
  1087. break;
  1088. }
  1089. return schemaObj;
  1090. }
  1091. // fallback for schemas without explicit type
  1092. return {};
  1093. }
  1094. // Main conversion function
  1095. export const convertOpenApiToToolPayload = (openApiSpec) => {
  1096. const toolPayload = [];
  1097. for (const [path, methods] of Object.entries(openApiSpec.paths)) {
  1098. for (const [method, operation] of Object.entries(methods)) {
  1099. if (operation?.operationId) {
  1100. const tool = {
  1101. type: 'function',
  1102. name: operation.operationId,
  1103. description: operation.description || operation.summary || 'No description available.',
  1104. parameters: {
  1105. type: 'object',
  1106. properties: {},
  1107. required: []
  1108. }
  1109. };
  1110. // Extract path and query parameters
  1111. if (operation.parameters) {
  1112. operation.parameters.forEach((param) => {
  1113. let description = param.schema.description || param.description || '';
  1114. if (param.schema.enum && Array.isArray(param.schema.enum)) {
  1115. description += `. Possible values: ${param.schema.enum.join(', ')}`;
  1116. }
  1117. tool.parameters.properties[param.name] = {
  1118. type: param.schema.type,
  1119. description: description
  1120. };
  1121. if (param.required) {
  1122. tool.parameters.required.push(param.name);
  1123. }
  1124. });
  1125. }
  1126. // Extract and recursively resolve requestBody if available
  1127. if (operation.requestBody) {
  1128. const content = operation.requestBody.content;
  1129. if (content && content['application/json']) {
  1130. const requestSchema = content['application/json'].schema;
  1131. const resolvedRequestSchema = resolveSchema(requestSchema, openApiSpec.components);
  1132. if (resolvedRequestSchema.properties) {
  1133. tool.parameters.properties = {
  1134. ...tool.parameters.properties,
  1135. ...resolvedRequestSchema.properties
  1136. };
  1137. if (resolvedRequestSchema.required) {
  1138. tool.parameters.required = [
  1139. ...new Set([...tool.parameters.required, ...resolvedRequestSchema.required])
  1140. ];
  1141. }
  1142. } else if (resolvedRequestSchema.type === 'array') {
  1143. tool.parameters = resolvedRequestSchema; // special case when root schema is an array
  1144. }
  1145. }
  1146. }
  1147. toolPayload.push(tool);
  1148. }
  1149. }
  1150. }
  1151. return toolPayload;
  1152. };
  1153. export const slugify = (str: string): string => {
  1154. return (
  1155. str
  1156. // 1. Normalize: separate accented letters into base + combining marks
  1157. .normalize('NFD')
  1158. // 2. Remove all combining marks (the accents)
  1159. .replace(/[\u0300-\u036f]/g, '')
  1160. // 3. Replace any sequence of whitespace with a single hyphen
  1161. .replace(/\s+/g, '-')
  1162. // 4. Remove all characters except alphanumeric characters and hyphens
  1163. .replace(/[^a-zA-Z0-9-]/g, '')
  1164. // 5. Convert to lowercase
  1165. .toLowerCase()
  1166. );
  1167. };