index.ts 44 KB

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