index.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. import { v4 as uuidv4 } from 'uuid';
  2. import sha256 from 'js-sha256';
  3. //////////////////////////
  4. // Helper functions
  5. //////////////////////////
  6. export const sanitizeResponseContent = (content: string) => {
  7. // First, temporarily replace valid <video> tags with a placeholder
  8. const videoTagRegex = /<video\s+src="([^"]+)"\s+controls><\/video>/gi;
  9. const placeholders: string[] = [];
  10. content = content.replace(videoTagRegex, (_, src) => {
  11. const placeholder = `{{VIDEO_${placeholders.length}}}`;
  12. placeholders.push(`<video src="${src}" controls></video>`);
  13. return placeholder;
  14. });
  15. // Now apply the sanitization to the rest of the content
  16. content = content
  17. .replace(/<\|[a-z]*$/, '')
  18. .replace(/<\|[a-z]+\|$/, '')
  19. .replace(/<$/, '')
  20. .replaceAll(/<\|[a-z]+\|>/g, ' ')
  21. .replaceAll('<', '&lt;')
  22. .replaceAll('>', '&gt;')
  23. .trim();
  24. // Replace placeholders with original <video> tags
  25. placeholders.forEach((placeholder, index) => {
  26. content = content.replace(`{{VIDEO_${index}}}`, placeholder);
  27. });
  28. return content.trim();
  29. };
  30. export const replaceTokens = (content, char, user) => {
  31. const charToken = /{{char}}/gi;
  32. const userToken = /{{user}}/gi;
  33. // Replace {{char}} if char is provided
  34. if (char !== undefined && char !== null) {
  35. content = content.replace(charToken, char);
  36. }
  37. // Replace {{user}} if user is provided
  38. if (user !== undefined && user !== null) {
  39. content = content.replace(userToken, user);
  40. }
  41. return content;
  42. };
  43. export const revertSanitizedResponseContent = (content: string) => {
  44. return content.replaceAll('&lt;', '<').replaceAll('&gt;', '>');
  45. };
  46. export const capitalizeFirstLetter = (string) => {
  47. return string.charAt(0).toUpperCase() + string.slice(1);
  48. };
  49. export const splitStream = (splitOn) => {
  50. let buffer = '';
  51. return new TransformStream({
  52. transform(chunk, controller) {
  53. buffer += chunk;
  54. const parts = buffer.split(splitOn);
  55. parts.slice(0, -1).forEach((part) => controller.enqueue(part));
  56. buffer = parts[parts.length - 1];
  57. },
  58. flush(controller) {
  59. if (buffer) controller.enqueue(buffer);
  60. }
  61. });
  62. };
  63. export const convertMessagesToHistory = (messages) => {
  64. const history = {
  65. messages: {},
  66. currentId: null
  67. };
  68. let parentMessageId = null;
  69. let messageId = null;
  70. for (const message of messages) {
  71. messageId = uuidv4();
  72. if (parentMessageId !== null) {
  73. history.messages[parentMessageId].childrenIds = [
  74. ...history.messages[parentMessageId].childrenIds,
  75. messageId
  76. ];
  77. }
  78. history.messages[messageId] = {
  79. ...message,
  80. id: messageId,
  81. parentId: parentMessageId,
  82. childrenIds: []
  83. };
  84. parentMessageId = messageId;
  85. }
  86. history.currentId = messageId;
  87. return history;
  88. };
  89. export const getGravatarURL = (email) => {
  90. // Trim leading and trailing whitespace from
  91. // an email address and force all characters
  92. // to lower case
  93. const address = String(email).trim().toLowerCase();
  94. // Create a SHA256 hash of the final string
  95. const hash = sha256(address);
  96. // Grab the actual image URL
  97. return `https://www.gravatar.com/avatar/${hash}`;
  98. };
  99. export const canvasPixelTest = () => {
  100. // Test a 1x1 pixel to potentially identify browser/plugin fingerprint blocking or spoofing
  101. // Inspiration: https://github.com/kkapsner/CanvasBlocker/blob/master/test/detectionTest.js
  102. const canvas = document.createElement('canvas');
  103. const ctx = canvas.getContext('2d');
  104. canvas.height = 1;
  105. canvas.width = 1;
  106. const imageData = new ImageData(canvas.width, canvas.height);
  107. const pixelValues = imageData.data;
  108. // Generate RGB test data
  109. for (let i = 0; i < imageData.data.length; i += 1) {
  110. if (i % 4 !== 3) {
  111. pixelValues[i] = Math.floor(256 * Math.random());
  112. } else {
  113. pixelValues[i] = 255;
  114. }
  115. }
  116. ctx.putImageData(imageData, 0, 0);
  117. const p = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
  118. // Read RGB data and fail if unmatched
  119. for (let i = 0; i < p.length; i += 1) {
  120. if (p[i] !== pixelValues[i]) {
  121. console.log(
  122. 'canvasPixelTest: Wrong canvas pixel RGB value detected:',
  123. p[i],
  124. 'at:',
  125. i,
  126. 'expected:',
  127. pixelValues[i]
  128. );
  129. console.log('canvasPixelTest: Canvas blocking or spoofing is likely');
  130. return false;
  131. }
  132. }
  133. return true;
  134. };
  135. export const generateInitialsImage = (name) => {
  136. const canvas = document.createElement('canvas');
  137. const ctx = canvas.getContext('2d');
  138. canvas.width = 100;
  139. canvas.height = 100;
  140. if (!canvasPixelTest()) {
  141. console.log(
  142. 'generateInitialsImage: failed pixel test, fingerprint evasion is likely. Using default image.'
  143. );
  144. return '/user.png';
  145. }
  146. ctx.fillStyle = '#F39C12';
  147. ctx.fillRect(0, 0, canvas.width, canvas.height);
  148. ctx.fillStyle = '#FFFFFF';
  149. ctx.font = '40px Helvetica';
  150. ctx.textAlign = 'center';
  151. ctx.textBaseline = 'middle';
  152. const sanitizedName = name.trim();
  153. const initials =
  154. sanitizedName.length > 0
  155. ? sanitizedName[0] +
  156. (sanitizedName.split(' ').length > 1
  157. ? sanitizedName[sanitizedName.lastIndexOf(' ') + 1]
  158. : '')
  159. : '';
  160. ctx.fillText(initials.toUpperCase(), canvas.width / 2, canvas.height / 2);
  161. return canvas.toDataURL();
  162. };
  163. export const copyToClipboard = async (text) => {
  164. let result = false;
  165. if (!navigator.clipboard) {
  166. const textArea = document.createElement('textarea');
  167. textArea.value = text;
  168. // Avoid scrolling to bottom
  169. textArea.style.top = '0';
  170. textArea.style.left = '0';
  171. textArea.style.position = 'fixed';
  172. document.body.appendChild(textArea);
  173. textArea.focus();
  174. textArea.select();
  175. try {
  176. const successful = document.execCommand('copy');
  177. const msg = successful ? 'successful' : 'unsuccessful';
  178. console.log('Fallback: Copying text command was ' + msg);
  179. result = true;
  180. } catch (err) {
  181. console.error('Fallback: Oops, unable to copy', err);
  182. }
  183. document.body.removeChild(textArea);
  184. return result;
  185. }
  186. result = await navigator.clipboard
  187. .writeText(text)
  188. .then(() => {
  189. console.log('Async: Copying to clipboard was successful!');
  190. return true;
  191. })
  192. .catch((error) => {
  193. console.error('Async: Could not copy text: ', error);
  194. return false;
  195. });
  196. return result;
  197. };
  198. export const compareVersion = (latest, current) => {
  199. return current === '0.0.0'
  200. ? false
  201. : current.localeCompare(latest, undefined, {
  202. numeric: true,
  203. sensitivity: 'case',
  204. caseFirst: 'upper'
  205. }) < 0;
  206. };
  207. export const findWordIndices = (text) => {
  208. const regex = /\[([^\]]+)\]/g;
  209. const matches = [];
  210. let match;
  211. while ((match = regex.exec(text)) !== null) {
  212. matches.push({
  213. word: match[1],
  214. startIndex: match.index,
  215. endIndex: regex.lastIndex - 1
  216. });
  217. }
  218. return matches;
  219. };
  220. export const removeFirstHashWord = (inputString) => {
  221. // Split the string into an array of words
  222. const words = inputString.split(' ');
  223. // Find the index of the first word that starts with #
  224. const index = words.findIndex((word) => word.startsWith('#'));
  225. // Remove the first word with #
  226. if (index !== -1) {
  227. words.splice(index, 1);
  228. }
  229. // Join the remaining words back into a string
  230. const resultString = words.join(' ');
  231. return resultString;
  232. };
  233. export const transformFileName = (fileName) => {
  234. // Convert to lowercase
  235. const lowerCaseFileName = fileName.toLowerCase();
  236. // Remove special characters using regular expression
  237. const sanitizedFileName = lowerCaseFileName.replace(/[^\w\s]/g, '');
  238. // Replace spaces with dashes
  239. const finalFileName = sanitizedFileName.replace(/\s+/g, '-');
  240. return finalFileName;
  241. };
  242. export const calculateSHA256 = async (file) => {
  243. // Create a FileReader to read the file asynchronously
  244. const reader = new FileReader();
  245. // Define a promise to handle the file reading
  246. const readFile = new Promise((resolve, reject) => {
  247. reader.onload = () => resolve(reader.result);
  248. reader.onerror = reject;
  249. });
  250. // Read the file as an ArrayBuffer
  251. reader.readAsArrayBuffer(file);
  252. try {
  253. // Wait for the FileReader to finish reading the file
  254. const buffer = await readFile;
  255. // Convert the ArrayBuffer to a Uint8Array
  256. const uint8Array = new Uint8Array(buffer);
  257. // Calculate the SHA-256 hash using Web Crypto API
  258. const hashBuffer = await crypto.subtle.digest('SHA-256', uint8Array);
  259. // Convert the hash to a hexadecimal string
  260. const hashArray = Array.from(new Uint8Array(hashBuffer));
  261. const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join('');
  262. return `${hashHex}`;
  263. } catch (error) {
  264. console.error('Error calculating SHA-256 hash:', error);
  265. throw error;
  266. }
  267. };
  268. export const getImportOrigin = (_chats) => {
  269. // Check what external service chat imports are from
  270. if ('mapping' in _chats[0]) {
  271. return 'openai';
  272. }
  273. return 'webui';
  274. };
  275. export const getUserPosition = async (raw = false) => {
  276. // Get the user's location using the Geolocation API
  277. const position = await new Promise((resolve, reject) => {
  278. navigator.geolocation.getCurrentPosition(resolve, reject);
  279. }).catch((error) => {
  280. console.error('Error getting user location:', error);
  281. throw error;
  282. });
  283. if (!position) {
  284. return 'Location not available';
  285. }
  286. // Extract the latitude and longitude from the position
  287. const { latitude, longitude } = position.coords;
  288. if (raw) {
  289. return { latitude, longitude };
  290. } else {
  291. return `${latitude.toFixed(3)}, ${longitude.toFixed(3)} (lat, long)`;
  292. }
  293. };
  294. const convertOpenAIMessages = (convo) => {
  295. // Parse OpenAI chat messages and create chat dictionary for creating new chats
  296. const mapping = convo['mapping'];
  297. const messages = [];
  298. let currentId = '';
  299. let lastId = null;
  300. for (let message_id in mapping) {
  301. const message = mapping[message_id];
  302. currentId = message_id;
  303. try {
  304. if (
  305. messages.length == 0 &&
  306. (message['message'] == null ||
  307. (message['message']['content']['parts']?.[0] == '' &&
  308. message['message']['content']['text'] == null))
  309. ) {
  310. // Skip chat messages with no content
  311. continue;
  312. } else {
  313. const new_chat = {
  314. id: message_id,
  315. parentId: lastId,
  316. childrenIds: message['children'] || [],
  317. role: message['message']?.['author']?.['role'] !== 'user' ? 'assistant' : 'user',
  318. content:
  319. message['message']?.['content']?.['parts']?.[0] ||
  320. message['message']?.['content']?.['text'] ||
  321. '',
  322. model: 'gpt-3.5-turbo',
  323. done: true,
  324. context: null
  325. };
  326. messages.push(new_chat);
  327. lastId = currentId;
  328. }
  329. } catch (error) {
  330. console.log('Error with', message, '\nError:', error);
  331. }
  332. }
  333. let history = {};
  334. messages.forEach((obj) => (history[obj.id] = obj));
  335. const chat = {
  336. history: {
  337. currentId: currentId,
  338. messages: history // Need to convert this to not a list and instead a json object
  339. },
  340. models: ['gpt-3.5-turbo'],
  341. messages: messages,
  342. options: {},
  343. timestamp: convo['create_time'],
  344. title: convo['title'] ?? 'New Chat'
  345. };
  346. return chat;
  347. };
  348. const validateChat = (chat) => {
  349. // Because ChatGPT sometimes has features we can't use like DALL-E or migh have corrupted messages, need to validate
  350. const messages = chat.messages;
  351. // Check if messages array is empty
  352. if (messages.length === 0) {
  353. return false;
  354. }
  355. // Last message's children should be an empty array
  356. const lastMessage = messages[messages.length - 1];
  357. if (lastMessage.childrenIds.length !== 0) {
  358. return false;
  359. }
  360. // First message's parent should be null
  361. const firstMessage = messages[0];
  362. if (firstMessage.parentId !== null) {
  363. return false;
  364. }
  365. // Every message's content should be a string
  366. for (let message of messages) {
  367. if (typeof message.content !== 'string') {
  368. return false;
  369. }
  370. }
  371. return true;
  372. };
  373. export const convertOpenAIChats = (_chats) => {
  374. // Create a list of dictionaries with each conversation from import
  375. const chats = [];
  376. let failed = 0;
  377. for (let convo of _chats) {
  378. const chat = convertOpenAIMessages(convo);
  379. if (validateChat(chat)) {
  380. chats.push({
  381. id: convo['id'],
  382. user_id: '',
  383. title: convo['title'],
  384. chat: chat,
  385. timestamp: convo['timestamp']
  386. });
  387. } else {
  388. failed++;
  389. }
  390. }
  391. console.log(failed, 'Conversations could not be imported');
  392. return chats;
  393. };
  394. export const isValidHttpUrl = (string) => {
  395. let url;
  396. try {
  397. url = new URL(string);
  398. } catch (_) {
  399. return false;
  400. }
  401. return url.protocol === 'http:' || url.protocol === 'https:';
  402. };
  403. export const removeEmojis = (str) => {
  404. // Regular expression to match emojis
  405. const emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g;
  406. // Replace emojis with an empty string
  407. return str.replace(emojiRegex, '');
  408. };
  409. export const extractSentences = (text) => {
  410. // Split the paragraph into sentences based on common punctuation marks
  411. const sentences = text.split(/(?<=[.!?])\s+/);
  412. return sentences
  413. .map((sentence) => removeEmojis(sentence.trim()))
  414. .filter((sentence) => sentence !== '');
  415. };
  416. export const extractSentencesForAudio = (text) => {
  417. return extractSentences(text).reduce((mergedTexts, currentText) => {
  418. const lastIndex = mergedTexts.length - 1;
  419. if (lastIndex >= 0) {
  420. const previousText = mergedTexts[lastIndex];
  421. const wordCount = previousText.split(/\s+/).length;
  422. if (wordCount < 2) {
  423. mergedTexts[lastIndex] = previousText + ' ' + currentText;
  424. } else {
  425. mergedTexts.push(currentText);
  426. }
  427. } else {
  428. mergedTexts.push(currentText);
  429. }
  430. return mergedTexts;
  431. }, []);
  432. };
  433. export const blobToFile = (blob, fileName) => {
  434. // Create a new File object from the Blob
  435. const file = new File([blob], fileName, { type: blob.type });
  436. return file;
  437. };
  438. /**
  439. * @param {string} template - The template string containing placeholders.
  440. * @returns {string} The template string with the placeholders replaced by the prompt.
  441. */
  442. export const promptTemplate = (
  443. template: string,
  444. user_name?: string,
  445. user_location?: string
  446. ): string => {
  447. // Get the current date
  448. const currentDate = new Date();
  449. // Format the date to YYYY-MM-DD
  450. const formattedDate =
  451. currentDate.getFullYear() +
  452. '-' +
  453. String(currentDate.getMonth() + 1).padStart(2, '0') +
  454. '-' +
  455. String(currentDate.getDate()).padStart(2, '0');
  456. // Format the time to HH:MM:SS AM/PM
  457. const currentTime = currentDate.toLocaleTimeString('en-US', {
  458. hour: 'numeric',
  459. minute: 'numeric',
  460. second: 'numeric',
  461. hour12: true
  462. });
  463. // Replace {{CURRENT_DATETIME}} in the template with the formatted datetime
  464. template = template.replace('{{CURRENT_DATETIME}}', `${formattedDate} ${currentTime}`);
  465. // Replace {{CURRENT_DATE}} in the template with the formatted date
  466. template = template.replace('{{CURRENT_DATE}}', formattedDate);
  467. // Replace {{CURRENT_TIME}} in the template with the formatted time
  468. template = template.replace('{{CURRENT_TIME}}', currentTime);
  469. if (user_name) {
  470. // Replace {{USER_NAME}} in the template with the user's name
  471. template = template.replace('{{USER_NAME}}', user_name);
  472. }
  473. if (user_location) {
  474. // Replace {{USER_LOCATION}} in the template with the current location
  475. template = template.replace('{{USER_LOCATION}}', user_location);
  476. }
  477. return template;
  478. };
  479. /**
  480. * This function is used to replace placeholders in a template string with the provided prompt.
  481. * The placeholders can be in the following formats:
  482. * - `{{prompt}}`: This will be replaced with the entire prompt.
  483. * - `{{prompt:start:<length>}}`: This will be replaced with the first <length> characters of the prompt.
  484. * - `{{prompt:end:<length>}}`: This will be replaced with the last <length> characters of the prompt.
  485. * - `{{prompt:middletruncate:<length>}}`: This will be replaced with the prompt truncated to <length> characters, with '...' in the middle.
  486. *
  487. * @param {string} template - The template string containing placeholders.
  488. * @param {string} prompt - The string to replace the placeholders with.
  489. * @returns {string} The template string with the placeholders replaced by the prompt.
  490. */
  491. export const titleGenerationTemplate = (template: string, prompt: string): string => {
  492. template = template.replace(
  493. /{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}/g,
  494. (match, startLength, endLength, middleLength) => {
  495. if (match === '{{prompt}}') {
  496. return prompt;
  497. } else if (match.startsWith('{{prompt:start:')) {
  498. return prompt.substring(0, startLength);
  499. } else if (match.startsWith('{{prompt:end:')) {
  500. return prompt.slice(-endLength);
  501. } else if (match.startsWith('{{prompt:middletruncate:')) {
  502. if (prompt.length <= middleLength) {
  503. return prompt;
  504. }
  505. const start = prompt.slice(0, Math.ceil(middleLength / 2));
  506. const end = prompt.slice(-Math.floor(middleLength / 2));
  507. return `${start}...${end}`;
  508. }
  509. return '';
  510. }
  511. );
  512. template = promptTemplate(template);
  513. return template;
  514. };
  515. export const approximateToHumanReadable = (nanoseconds: number) => {
  516. const seconds = Math.floor((nanoseconds / 1e9) % 60);
  517. const minutes = Math.floor((nanoseconds / 6e10) % 60);
  518. const hours = Math.floor((nanoseconds / 3.6e12) % 24);
  519. const results: string[] = [];
  520. if (seconds >= 0) {
  521. results.push(`${seconds}s`);
  522. }
  523. if (minutes > 0) {
  524. results.push(`${minutes}m`);
  525. }
  526. if (hours > 0) {
  527. results.push(`${hours}h`);
  528. }
  529. return results.reverse().join(' ');
  530. };
  531. export const getTimeRange = (timestamp) => {
  532. const now = new Date();
  533. const date = new Date(timestamp * 1000); // Convert Unix timestamp to milliseconds
  534. // Calculate the difference in milliseconds
  535. const diffTime = now.getTime() - date.getTime();
  536. const diffDays = diffTime / (1000 * 3600 * 24);
  537. const nowDate = now.getDate();
  538. const nowMonth = now.getMonth();
  539. const nowYear = now.getFullYear();
  540. const dateDate = date.getDate();
  541. const dateMonth = date.getMonth();
  542. const dateYear = date.getFullYear();
  543. if (nowYear === dateYear && nowMonth === dateMonth && nowDate === dateDate) {
  544. return 'Today';
  545. } else if (nowYear === dateYear && nowMonth === dateMonth && nowDate - dateDate === 1) {
  546. return 'Yesterday';
  547. } else if (diffDays <= 7) {
  548. return 'Previous 7 days';
  549. } else if (diffDays <= 30) {
  550. return 'Previous 30 days';
  551. } else if (nowYear === dateYear) {
  552. return date.toLocaleString('default', { month: 'long' });
  553. } else {
  554. return date.getFullYear().toString();
  555. }
  556. };