2023-11-20 01:47:07 +00:00
|
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
|
|
import sha256 from 'js-sha256';
|
2025-07-09 06:38:28 +00:00
|
|
|
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
2024-08-26 13:01:29 +00:00
|
|
|
|
|
2025-01-22 09:02:52 +00:00
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
|
|
|
|
import isToday from 'dayjs/plugin/isToday';
|
|
|
|
|
|
import isYesterday from 'dayjs/plugin/isYesterday';
|
2025-01-26 07:44:26 +00:00
|
|
|
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
2025-01-22 09:02:52 +00:00
|
|
|
|
|
|
|
|
|
|
dayjs.extend(relativeTime);
|
|
|
|
|
|
dayjs.extend(isToday);
|
|
|
|
|
|
dayjs.extend(isYesterday);
|
2025-01-26 07:44:26 +00:00
|
|
|
|
dayjs.extend(localizedFormat);
|
2025-01-22 09:02:52 +00:00
|
|
|
|
|
2024-08-26 13:01:29 +00:00
|
|
|
|
import { TTS_RESPONSE_SPLIT } from '$lib/types';
|
2024-05-24 08:40:48 +00:00
|
|
|
|
|
2025-09-05 09:55:04 +00:00
|
|
|
|
import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
|
|
|
|
|
|
2025-04-14 08:56:15 +00:00
|
|
|
|
import { marked } from 'marked';
|
|
|
|
|
|
import markedExtension from '$lib/utils/marked/extension';
|
|
|
|
|
|
import markedKatexExtension from '$lib/utils/marked/katex-extension';
|
|
|
|
|
|
import hljs from 'highlight.js';
|
|
|
|
|
|
|
2023-11-20 01:47:07 +00:00
|
|
|
|
//////////////////////////
|
|
|
|
|
|
// Helper functions
|
|
|
|
|
|
//////////////////////////
|
|
|
|
|
|
|
2024-12-25 05:16:22 +00:00
|
|
|
|
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
|
|
|
2024-11-26 09:39:12 +00:00
|
|
|
|
function escapeRegExp(string: string): string {
|
2024-11-26 09:43:28 +00:00
|
|
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
2024-11-26 09:39:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-22 01:58:29 +00:00
|
|
|
|
export const replaceTokens = (content, sourceIds, char, user) => {
|
2025-03-04 03:48:00 +00:00
|
|
|
|
const tokens = [
|
|
|
|
|
|
{ regex: /{{char}}/gi, replacement: char },
|
|
|
|
|
|
{ regex: /{{user}}/gi, replacement: user },
|
|
|
|
|
|
{
|
|
|
|
|
|
regex: /{{VIDEO_FILE_ID_([a-f0-9-]+)}}/gi,
|
|
|
|
|
|
replacement: (_, fileId) =>
|
|
|
|
|
|
`<video src="${WEBUI_BASE_URL}/api/v1/files/${fileId}/content" controls></video>`
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
regex: /{{HTML_FILE_ID_([a-f0-9-]+)}}/gi,
|
2025-04-30 20:32:29 +00:00
|
|
|
|
replacement: (_, fileId) => `<file type="html" id="${fileId}" />`
|
2025-03-04 03:48:00 +00:00
|
|
|
|
}
|
|
|
|
|
|
];
|
2024-06-17 09:54:56 +00:00
|
|
|
|
|
2025-03-04 03:48:00 +00:00
|
|
|
|
// Replace tokens outside code blocks only
|
|
|
|
|
|
const processOutsideCodeBlocks = (text, replacementFn) => {
|
|
|
|
|
|
return text
|
|
|
|
|
|
.split(/(```[\s\S]*?```|`[\s\S]*?`)/)
|
|
|
|
|
|
.map((segment) => {
|
|
|
|
|
|
return segment.startsWith('```') || segment.startsWith('`')
|
|
|
|
|
|
? segment
|
|
|
|
|
|
: replacementFn(segment);
|
|
|
|
|
|
})
|
|
|
|
|
|
.join('');
|
|
|
|
|
|
};
|
2024-06-17 09:54:56 +00:00
|
|
|
|
|
2025-03-04 03:48:00 +00:00
|
|
|
|
// Apply replacements
|
|
|
|
|
|
content = processOutsideCodeBlocks(content, (segment) => {
|
|
|
|
|
|
tokens.forEach(({ regex, replacement }) => {
|
|
|
|
|
|
if (replacement !== undefined && replacement !== null) {
|
|
|
|
|
|
segment = segment.replace(regex, replacement);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2024-06-17 09:54:56 +00:00
|
|
|
|
|
2025-03-04 03:48:00 +00:00
|
|
|
|
if (Array.isArray(sourceIds)) {
|
|
|
|
|
|
sourceIds.forEach((sourceId, idx) => {
|
2025-03-27 03:46:55 +00:00
|
|
|
|
const regex = new RegExp(`\\[${idx + 1}\\]`, 'g');
|
2025-09-07 22:35:51 +00:00
|
|
|
|
segment = segment.replace(
|
|
|
|
|
|
regex,
|
|
|
|
|
|
`<source_id data="${idx + 1}" title="${encodeURIComponent(sourceId)}" />`
|
|
|
|
|
|
);
|
2025-03-04 03:48:00 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2024-06-19 23:51:29 +00:00
|
|
|
|
|
2025-03-04 03:48:00 +00:00
|
|
|
|
return segment;
|
2024-08-16 15:51:50 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
2024-06-17 09:54:56 +00:00
|
|
|
|
return content;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-08-14 22:08:15 +00:00
|
|
|
|
export const sanitizeResponseContent = (content: string) => {
|
|
|
|
|
|
return content
|
|
|
|
|
|
.replace(/<\|[a-z]*$/, '')
|
|
|
|
|
|
.replace(/<\|[a-z]+\|$/, '')
|
|
|
|
|
|
.replace(/<$/, '')
|
|
|
|
|
|
.replaceAll('<', '<')
|
|
|
|
|
|
.replaceAll('>', '>')
|
2025-06-18 04:12:11 +00:00
|
|
|
|
.replaceAll(/<\|[a-z]+\|>/g, ' ')
|
2024-08-14 22:08:15 +00:00
|
|
|
|
.trim();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const processResponseContent = (content: string) => {
|
2025-05-26 15:53:29 +00:00
|
|
|
|
content = processChineseContent(content);
|
|
|
|
|
|
return content.trim();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function isChineseChar(char: string): boolean {
|
|
|
|
|
|
return /\p{Script=Han}/u.test(char);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Tackle "Model output issue not following the standard Markdown/LaTeX format" in Chinese.
|
|
|
|
|
|
function processChineseContent(content: string): string {
|
2025-05-26 11:35:28 +00:00
|
|
|
|
// This function is used to process the response content before the response content is rendered.
|
|
|
|
|
|
const lines = content.split('\n');
|
|
|
|
|
|
const processedLines = lines.map((line) => {
|
|
|
|
|
|
if (/[\u4e00-\u9fa5]/.test(line)) {
|
2025-05-26 15:53:29 +00:00
|
|
|
|
// Problems caused by Chinese parentheses
|
|
|
|
|
|
/* Discription:
|
2025-06-03 15:30:51 +00:00
|
|
|
|
* When `*` has Chinese delimiters on the inside, markdown parser ignore bold or italic style.
|
2025-05-26 15:53:29 +00:00
|
|
|
|
* - e.g. `**中文名(English)**中文内容` will be parsed directly,
|
|
|
|
|
|
* instead of `<strong>中文名(English)</strong>中文内容`.
|
|
|
|
|
|
* Solution:
|
|
|
|
|
|
* Adding a `space` before and after the bold/italic part can solve the problem.
|
|
|
|
|
|
* - e.g. `**中文名(English)**中文内容` -> ` **中文名(English)** 中文内容`
|
|
|
|
|
|
* Note:
|
|
|
|
|
|
* Similar problem was found with English parentheses and other full delimiters,
|
|
|
|
|
|
* but they are not handled here because they are less likely to appear in LLM output.
|
|
|
|
|
|
* Change the behavior in future if needed.
|
|
|
|
|
|
*/
|
|
|
|
|
|
if (line.includes('*')) {
|
2025-06-03 15:30:51 +00:00
|
|
|
|
// Handle **bold** and *italic*
|
|
|
|
|
|
// 1. With Chinese parentheses
|
|
|
|
|
|
if (/(|)/.test(line)) {
|
|
|
|
|
|
line = processChineseDelimiters(line, '**', '(', ')');
|
|
|
|
|
|
line = processChineseDelimiters(line, '*', '(', ')');
|
|
|
|
|
|
}
|
|
|
|
|
|
// 2. With Chinese quotations
|
|
|
|
|
|
if (/“|”/.test(line)) {
|
|
|
|
|
|
line = processChineseDelimiters(line, '**', '“', '”');
|
|
|
|
|
|
line = processChineseDelimiters(line, '*', '“', '”');
|
|
|
|
|
|
}
|
2025-05-26 15:53:29 +00:00
|
|
|
|
}
|
2025-05-26 11:35:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
return line;
|
|
|
|
|
|
});
|
2025-05-26 13:04:08 +00:00
|
|
|
|
content = processedLines.join('\n');
|
2024-08-14 22:08:15 +00:00
|
|
|
|
|
2025-05-26 15:53:29 +00:00
|
|
|
|
return content;
|
2025-05-26 11:35:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-26 12:43:21 +00:00
|
|
|
|
// Helper function for `processChineseContent`
|
2025-06-03 15:30:51 +00:00
|
|
|
|
function processChineseDelimiters(
|
2025-05-26 11:35:28 +00:00
|
|
|
|
line: string,
|
|
|
|
|
|
symbol: string,
|
|
|
|
|
|
leftSymbol: string,
|
|
|
|
|
|
rightSymbol: string
|
|
|
|
|
|
): string {
|
|
|
|
|
|
// NOTE: If needed, with a little modification, this function can be applied to more cases.
|
|
|
|
|
|
const escapedSymbol = escapeRegExp(symbol);
|
|
|
|
|
|
const regex = new RegExp(
|
|
|
|
|
|
`(.?)(?<!${escapedSymbol})(${escapedSymbol})([^${escapedSymbol}]+)(${escapedSymbol})(?!${escapedSymbol})(.)`,
|
|
|
|
|
|
'g'
|
|
|
|
|
|
);
|
|
|
|
|
|
return line.replace(regex, (match, l, left, content, right, r) => {
|
|
|
|
|
|
const result =
|
|
|
|
|
|
(content.startsWith(leftSymbol) && l && l.length > 0 && isChineseChar(l[l.length - 1])) ||
|
|
|
|
|
|
(content.endsWith(rightSymbol) && r && r.length > 0 && isChineseChar(r[0]));
|
|
|
|
|
|
|
|
|
|
|
|
if (result) {
|
|
|
|
|
|
return `${l} ${left}${content}${right} ${r}`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return match;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-05 15:47:18 +00:00
|
|
|
|
export function unescapeHtml(html: string) {
|
|
|
|
|
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
|
|
|
|
return doc.documentElement.textContent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-25 20:46:06 +00:00
|
|
|
|
export const capitalizeFirstLetter = (string) => {
|
|
|
|
|
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2023-11-20 01:47:07 +00:00
|
|
|
|
export const splitStream = (splitOn) => {
|
|
|
|
|
|
let buffer = '';
|
|
|
|
|
|
return new TransformStream({
|
|
|
|
|
|
transform(chunk, controller) {
|
|
|
|
|
|
buffer += chunk;
|
|
|
|
|
|
const parts = buffer.split(splitOn);
|
|
|
|
|
|
parts.slice(0, -1).forEach((part) => controller.enqueue(part));
|
|
|
|
|
|
buffer = parts[parts.length - 1];
|
|
|
|
|
|
},
|
|
|
|
|
|
flush(controller) {
|
|
|
|
|
|
if (buffer) controller.enqueue(buffer);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const convertMessagesToHistory = (messages) => {
|
2024-01-03 22:33:57 +00:00
|
|
|
|
const history = {
|
2023-11-20 01:47:07 +00:00
|
|
|
|
messages: {},
|
|
|
|
|
|
currentId: null
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let parentMessageId = null;
|
|
|
|
|
|
let messageId = null;
|
|
|
|
|
|
|
|
|
|
|
|
for (const message of messages) {
|
|
|
|
|
|
messageId = uuidv4();
|
|
|
|
|
|
|
|
|
|
|
|
if (parentMessageId !== null) {
|
|
|
|
|
|
history.messages[parentMessageId].childrenIds = [
|
|
|
|
|
|
...history.messages[parentMessageId].childrenIds,
|
|
|
|
|
|
messageId
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
history.messages[messageId] = {
|
|
|
|
|
|
...message,
|
|
|
|
|
|
id: messageId,
|
|
|
|
|
|
parentId: parentMessageId,
|
|
|
|
|
|
childrenIds: []
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
parentMessageId = messageId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
history.currentId = messageId;
|
|
|
|
|
|
return history;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const getGravatarURL = (email) => {
|
|
|
|
|
|
// Trim leading and trailing whitespace from
|
|
|
|
|
|
// an email address and force all characters
|
|
|
|
|
|
// to lower case
|
|
|
|
|
|
const address = String(email).trim().toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
// Create a SHA256 hash of the final string
|
|
|
|
|
|
const hash = sha256(address);
|
|
|
|
|
|
|
|
|
|
|
|
// Grab the actual image URL
|
|
|
|
|
|
return `https://www.gravatar.com/avatar/${hash}`;
|
|
|
|
|
|
};
|
2023-12-19 02:48:51 +00:00
|
|
|
|
|
2024-04-05 22:04:00 +00:00
|
|
|
|
export const canvasPixelTest = () => {
|
|
|
|
|
|
// Test a 1x1 pixel to potentially identify browser/plugin fingerprint blocking or spoofing
|
|
|
|
|
|
// Inspiration: https://github.com/kkapsner/CanvasBlocker/blob/master/test/detectionTest.js
|
2024-04-06 04:02:02 +00:00
|
|
|
|
const canvas = document.createElement('canvas');
|
2024-04-05 22:04:00 +00:00
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
canvas.height = 1;
|
|
|
|
|
|
canvas.width = 1;
|
|
|
|
|
|
const imageData = new ImageData(canvas.width, canvas.height);
|
|
|
|
|
|
const pixelValues = imageData.data;
|
|
|
|
|
|
|
|
|
|
|
|
// Generate RGB test data
|
2024-04-06 04:02:02 +00:00
|
|
|
|
for (let i = 0; i < imageData.data.length; i += 1) {
|
|
|
|
|
|
if (i % 4 !== 3) {
|
2024-04-05 22:04:00 +00:00
|
|
|
|
pixelValues[i] = Math.floor(256 * Math.random());
|
2024-04-06 04:02:02 +00:00
|
|
|
|
} else {
|
2024-04-05 22:04:00 +00:00
|
|
|
|
pixelValues[i] = 255;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ctx.putImageData(imageData, 0, 0);
|
|
|
|
|
|
const p = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
|
|
|
|
|
|
|
|
|
|
// Read RGB data and fail if unmatched
|
2024-04-06 04:02:02 +00:00
|
|
|
|
for (let i = 0; i < p.length; i += 1) {
|
|
|
|
|
|
if (p[i] !== pixelValues[i]) {
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
'canvasPixelTest: Wrong canvas pixel RGB value detected:',
|
|
|
|
|
|
p[i],
|
|
|
|
|
|
'at:',
|
|
|
|
|
|
i,
|
|
|
|
|
|
'expected:',
|
|
|
|
|
|
pixelValues[i]
|
|
|
|
|
|
);
|
|
|
|
|
|
console.log('canvasPixelTest: Canvas blocking or spoofing is likely');
|
2024-04-05 22:04:00 +00:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
2024-04-06 04:02:02 +00:00
|
|
|
|
};
|
2024-04-05 22:04:00 +00:00
|
|
|
|
|
2024-12-25 16:38:51 +00:00
|
|
|
|
export const compressImage = async (imageUrl, maxWidth, maxHeight) => {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
const img = new Image();
|
|
|
|
|
|
img.onload = () => {
|
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
|
let width = img.width;
|
|
|
|
|
|
let height = img.height;
|
2024-12-25 06:28:14 +00:00
|
|
|
|
|
2024-12-25 16:38:51 +00:00
|
|
|
|
// Maintain aspect ratio while resizing
|
2024-12-25 06:28:14 +00:00
|
|
|
|
|
2024-12-25 16:38:51 +00:00
|
|
|
|
if (maxWidth && maxHeight) {
|
|
|
|
|
|
// Resize with both dimensions defined (preserves aspect ratio)
|
2024-12-25 06:28:14 +00:00
|
|
|
|
|
|
|
|
|
|
if (width <= maxWidth && height <= maxHeight) {
|
|
|
|
|
|
resolve(imageUrl);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-25 16:38:51 +00:00
|
|
|
|
if (width / height > maxWidth / maxHeight) {
|
|
|
|
|
|
height = Math.round((maxWidth * height) / width);
|
|
|
|
|
|
width = maxWidth;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
width = Math.round((maxHeight * width) / height);
|
|
|
|
|
|
height = maxHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (maxWidth) {
|
|
|
|
|
|
// Only maxWidth defined
|
2024-12-25 06:28:14 +00:00
|
|
|
|
|
|
|
|
|
|
if (width <= maxWidth) {
|
|
|
|
|
|
resolve(imageUrl);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-25 16:38:51 +00:00
|
|
|
|
height = Math.round((maxWidth * height) / width);
|
|
|
|
|
|
width = maxWidth;
|
|
|
|
|
|
} else if (maxHeight) {
|
|
|
|
|
|
// Only maxHeight defined
|
2024-12-25 06:28:14 +00:00
|
|
|
|
|
|
|
|
|
|
if (height <= maxHeight) {
|
|
|
|
|
|
resolve(imageUrl);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-25 16:38:51 +00:00
|
|
|
|
width = Math.round((maxHeight * width) / height);
|
|
|
|
|
|
height = maxHeight;
|
|
|
|
|
|
}
|
2024-12-25 06:28:14 +00:00
|
|
|
|
|
2024-12-25 16:38:51 +00:00
|
|
|
|
canvas.width = width;
|
|
|
|
|
|
canvas.height = height;
|
2024-12-25 06:28:14 +00:00
|
|
|
|
|
2024-12-25 16:38:51 +00:00
|
|
|
|
const context = canvas.getContext('2d');
|
|
|
|
|
|
context.drawImage(img, 0, 0, width, height);
|
2024-12-25 06:28:14 +00:00
|
|
|
|
|
2024-12-25 16:38:51 +00:00
|
|
|
|
// Get compressed image URL
|
|
|
|
|
|
const compressedUrl = canvas.toDataURL();
|
|
|
|
|
|
resolve(compressedUrl);
|
|
|
|
|
|
};
|
|
|
|
|
|
img.onerror = (error) => reject(error);
|
|
|
|
|
|
img.src = imageUrl;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2024-04-04 19:09:07 +00:00
|
|
|
|
export const generateInitialsImage = (name) => {
|
2024-04-05 03:07:52 +00:00
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
canvas.width = 100;
|
|
|
|
|
|
canvas.height = 100;
|
2024-04-04 19:09:07 +00:00
|
|
|
|
|
2024-04-05 22:04:00 +00:00
|
|
|
|
if (!canvasPixelTest()) {
|
2024-04-06 04:02:02 +00:00
|
|
|
|
console.log(
|
|
|
|
|
|
'generateInitialsImage: failed pixel test, fingerprint evasion is likely. Using default image.'
|
|
|
|
|
|
);
|
2025-07-09 06:38:28 +00:00
|
|
|
|
return `${WEBUI_BASE_URL}/user.png`;
|
2024-04-05 22:04:00 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-05 03:07:52 +00:00
|
|
|
|
ctx.fillStyle = '#F39C12';
|
|
|
|
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
2024-04-04 19:09:07 +00:00
|
|
|
|
|
2024-04-05 03:07:52 +00:00
|
|
|
|
ctx.fillStyle = '#FFFFFF';
|
|
|
|
|
|
ctx.font = '40px Helvetica';
|
|
|
|
|
|
ctx.textAlign = 'center';
|
|
|
|
|
|
ctx.textBaseline = 'middle';
|
2024-04-05 02:56:23 +00:00
|
|
|
|
|
2024-04-05 03:05:39 +00:00
|
|
|
|
const sanitizedName = name.trim();
|
2024-04-05 03:07:52 +00:00
|
|
|
|
const initials =
|
|
|
|
|
|
sanitizedName.length > 0
|
|
|
|
|
|
? sanitizedName[0] +
|
2025-06-20 16:32:23 +00:00
|
|
|
|
(sanitizedName.split(' ').length > 1
|
|
|
|
|
|
? sanitizedName[sanitizedName.lastIndexOf(' ') + 1]
|
|
|
|
|
|
: '')
|
2024-04-05 03:07:52 +00:00
|
|
|
|
: '';
|
2024-04-04 20:26:00 +00:00
|
|
|
|
|
2024-04-05 03:07:52 +00:00
|
|
|
|
ctx.fillText(initials.toUpperCase(), canvas.width / 2, canvas.height / 2);
|
2024-04-04 19:09:07 +00:00
|
|
|
|
|
2024-04-05 03:07:52 +00:00
|
|
|
|
return canvas.toDataURL();
|
2024-04-04 19:09:07 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-01-22 09:02:52 +00:00
|
|
|
|
export const formatDate = (inputDate) => {
|
|
|
|
|
|
const date = dayjs(inputDate);
|
|
|
|
|
|
const now = dayjs();
|
|
|
|
|
|
|
|
|
|
|
|
if (date.isToday()) {
|
2025-01-26 07:44:26 +00:00
|
|
|
|
return `Today at ${date.format('LT')}`;
|
2025-01-22 09:02:52 +00:00
|
|
|
|
} else if (date.isYesterday()) {
|
2025-01-26 07:44:26 +00:00
|
|
|
|
return `Yesterday at ${date.format('LT')}`;
|
2025-01-22 09:02:52 +00:00
|
|
|
|
} else {
|
2025-01-26 07:44:26 +00:00
|
|
|
|
return `${date.format('L')} at ${date.format('LT')}`;
|
2025-01-22 09:02:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-19 15:15:05 +00:00
|
|
|
|
export const copyToClipboard = async (text, html = null, formatted = false) => {
|
2025-04-14 08:56:15 +00:00
|
|
|
|
if (formatted) {
|
2025-07-19 15:15:05 +00:00
|
|
|
|
let styledHtml = '';
|
|
|
|
|
|
if (!html) {
|
|
|
|
|
|
const options = {
|
|
|
|
|
|
throwOnError: false,
|
|
|
|
|
|
highlight: function (code, lang) {
|
|
|
|
|
|
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
|
|
|
|
|
return hljs.highlight(code, { language }).value;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
marked.use(markedKatexExtension(options));
|
|
|
|
|
|
marked.use(markedExtension(options));
|
|
|
|
|
|
// DEVELOPER NOTE: Go to `$lib/components/chat/Messages/Markdown.svelte` to add extra markdown extensions for rendering.
|
2025-04-14 08:56:15 +00:00
|
|
|
|
|
2025-07-19 15:15:05 +00:00
|
|
|
|
const htmlContent = marked.parse(text);
|
2025-04-14 08:56:15 +00:00
|
|
|
|
|
2025-07-19 15:15:05 +00:00
|
|
|
|
// Add basic styling to make the content look better when pasted
|
|
|
|
|
|
styledHtml = `
|
2025-04-14 08:56:15 +00:00
|
|
|
|
<div>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
pre {
|
|
|
|
|
|
background-color: #f6f8fa;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
code {
|
|
|
|
|
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.hljs-keyword { color: #d73a49; }
|
|
|
|
|
|
.hljs-string { color: #032f62; }
|
|
|
|
|
|
.hljs-comment { color: #6a737d; }
|
|
|
|
|
|
.hljs-function { color: #6f42c1; }
|
|
|
|
|
|
.hljs-number { color: #005cc5; }
|
|
|
|
|
|
.hljs-operator { color: #d73a49; }
|
|
|
|
|
|
.hljs-class { color: #6f42c1; }
|
|
|
|
|
|
.hljs-title { color: #6f42c1; }
|
|
|
|
|
|
.hljs-params { color: #24292e; }
|
|
|
|
|
|
.hljs-built_in { color: #005cc5; }
|
|
|
|
|
|
blockquote {
|
|
|
|
|
|
border-left: 4px solid #dfe2e5;
|
|
|
|
|
|
padding-left: 16px;
|
|
|
|
|
|
color: #6a737d;
|
|
|
|
|
|
margin-left: 0;
|
|
|
|
|
|
margin-right: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
table {
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
table, th, td {
|
|
|
|
|
|
border: 1px solid #dfe2e5;
|
|
|
|
|
|
}
|
|
|
|
|
|
th, td {
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
th {
|
|
|
|
|
|
background-color: #f6f8fa;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
${htmlContent}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
2025-07-19 15:15:05 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
// If HTML is provided, use it directly
|
|
|
|
|
|
styledHtml = html;
|
|
|
|
|
|
}
|
2025-04-14 08:56:15 +00:00
|
|
|
|
|
|
|
|
|
|
// Create a blob with HTML content
|
|
|
|
|
|
const blob = new Blob([styledHtml], { type: 'text/html' });
|
2023-12-19 02:48:51 +00:00
|
|
|
|
|
|
|
|
|
|
try {
|
2025-04-14 08:56:15 +00:00
|
|
|
|
// Create a ClipboardItem with HTML content
|
|
|
|
|
|
const data = new ClipboardItem({
|
|
|
|
|
|
'text/html': blob,
|
|
|
|
|
|
'text/plain': new Blob([text], { type: 'text/plain' })
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Write to clipboard
|
|
|
|
|
|
await navigator.clipboard.write([data]);
|
|
|
|
|
|
return true;
|
2023-12-19 02:48:51 +00:00
|
|
|
|
} catch (err) {
|
2025-04-14 08:56:15 +00:00
|
|
|
|
console.error('Error copying formatted content:', err);
|
|
|
|
|
|
// Fallback to plain text
|
|
|
|
|
|
return await copyToClipboard(text);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let result = false;
|
|
|
|
|
|
if (!navigator.clipboard) {
|
|
|
|
|
|
const textArea = document.createElement('textarea');
|
|
|
|
|
|
textArea.value = text;
|
|
|
|
|
|
|
|
|
|
|
|
// Avoid scrolling to bottom
|
|
|
|
|
|
textArea.style.top = '0';
|
|
|
|
|
|
textArea.style.left = '0';
|
|
|
|
|
|
textArea.style.position = 'fixed';
|
|
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(textArea);
|
|
|
|
|
|
textArea.focus();
|
|
|
|
|
|
textArea.select();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const successful = document.execCommand('copy');
|
|
|
|
|
|
const msg = successful ? 'successful' : 'unsuccessful';
|
|
|
|
|
|
console.log('Fallback: Copying text command was ' + msg);
|
|
|
|
|
|
result = true;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Fallback: Oops, unable to copy', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
document.body.removeChild(textArea);
|
|
|
|
|
|
return result;
|
2023-12-19 02:48:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-14 08:56:15 +00:00
|
|
|
|
result = await navigator.clipboard
|
|
|
|
|
|
.writeText(text)
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
console.log('Async: Copying to clipboard was successful!');
|
|
|
|
|
|
return true;
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
|
console.error('Async: Could not copy text: ', error);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2024-04-19 11:34:55 +00:00
|
|
|
|
return result;
|
2023-12-19 02:48:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
2023-12-26 21:10:50 +00:00
|
|
|
|
|
2024-02-25 19:55:15 +00:00
|
|
|
|
export const compareVersion = (latest, current) => {
|
2023-12-27 06:51:52 +00:00
|
|
|
|
return current === '0.0.0'
|
2023-12-27 06:52:53 +00:00
|
|
|
|
? false
|
2024-02-25 19:55:15 +00:00
|
|
|
|
: current.localeCompare(latest, undefined, {
|
2025-06-20 16:32:23 +00:00
|
|
|
|
numeric: true,
|
|
|
|
|
|
sensitivity: 'case',
|
|
|
|
|
|
caseFirst: 'upper'
|
|
|
|
|
|
}) < 0;
|
2023-12-26 21:10:50 +00:00
|
|
|
|
};
|
2024-01-02 08:55:28 +00:00
|
|
|
|
|
2025-04-04 15:11:54 +00:00
|
|
|
|
export const extractCurlyBraceWords = (text) => {
|
|
|
|
|
|
const regex = /\{\{([^}]+)\}\}/g;
|
2024-01-03 22:33:57 +00:00
|
|
|
|
const matches = [];
|
2024-01-02 08:55:28 +00:00
|
|
|
|
let match;
|
|
|
|
|
|
|
|
|
|
|
|
while ((match = regex.exec(text)) !== null) {
|
|
|
|
|
|
matches.push({
|
2025-04-04 15:11:54 +00:00
|
|
|
|
word: match[1].trim(),
|
2024-01-02 08:55:28 +00:00
|
|
|
|
startIndex: match.index,
|
|
|
|
|
|
endIndex: regex.lastIndex - 1
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return matches;
|
|
|
|
|
|
};
|
2024-01-07 08:57:10 +00:00
|
|
|
|
|
2024-08-23 12:31:39 +00:00
|
|
|
|
export const removeLastWordFromString = (inputString, wordString) => {
|
2024-10-19 23:18:14 +00:00
|
|
|
|
console.log('inputString', inputString);
|
|
|
|
|
|
// Split the string by newline characters to handle lines separately
|
|
|
|
|
|
const lines = inputString.split('\n');
|
|
|
|
|
|
|
|
|
|
|
|
// Take the last line to operate only on it
|
|
|
|
|
|
const lastLine = lines.pop();
|
|
|
|
|
|
|
|
|
|
|
|
// Split the last line into an array of words
|
|
|
|
|
|
const words = lastLine.split(' ');
|
2024-08-23 12:31:39 +00:00
|
|
|
|
|
2024-10-19 23:18:14 +00:00
|
|
|
|
// Conditional to check for the last word removal
|
2024-10-19 06:54:35 +00:00
|
|
|
|
if (words.at(-1) === wordString || (wordString === '' && words.at(-1) === '\\#')) {
|
2024-10-19 23:18:14 +00:00
|
|
|
|
words.pop(); // Remove last word if condition is satisfied
|
2024-08-23 12:31:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-19 23:18:14 +00:00
|
|
|
|
// Join the remaining words back into a string and handle space correctly
|
|
|
|
|
|
let updatedLastLine = words.join(' ');
|
|
|
|
|
|
|
|
|
|
|
|
// Add a trailing space to the updated last line if there are still words
|
|
|
|
|
|
if (updatedLastLine !== '') {
|
|
|
|
|
|
updatedLastLine += ' ';
|
2024-08-23 12:43:32 +00:00
|
|
|
|
}
|
2024-08-23 12:31:39 +00:00
|
|
|
|
|
2024-10-19 23:18:14 +00:00
|
|
|
|
// Combine the lines together again, placing the updated last line back in
|
|
|
|
|
|
const resultString = [...lines, updatedLastLine].join('\n');
|
|
|
|
|
|
|
|
|
|
|
|
// Return the final string
|
|
|
|
|
|
console.log('resultString', resultString);
|
|
|
|
|
|
|
2024-08-23 12:31:39 +00:00
|
|
|
|
return resultString;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-01-08 07:43:32 +00:00
|
|
|
|
export const removeFirstHashWord = (inputString) => {
|
|
|
|
|
|
// Split the string into an array of words
|
|
|
|
|
|
const words = inputString.split(' ');
|
|
|
|
|
|
|
|
|
|
|
|
// Find the index of the first word that starts with #
|
|
|
|
|
|
const index = words.findIndex((word) => word.startsWith('#'));
|
|
|
|
|
|
|
|
|
|
|
|
// Remove the first word with #
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
words.splice(index, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Join the remaining words back into a string
|
|
|
|
|
|
const resultString = words.join(' ');
|
|
|
|
|
|
|
|
|
|
|
|
return resultString;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-01-08 09:32:55 +00:00
|
|
|
|
export const transformFileName = (fileName) => {
|
|
|
|
|
|
// Convert to lowercase
|
|
|
|
|
|
const lowerCaseFileName = fileName.toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
// Remove special characters using regular expression
|
|
|
|
|
|
const sanitizedFileName = lowerCaseFileName.replace(/[^\w\s]/g, '');
|
|
|
|
|
|
|
|
|
|
|
|
// Replace spaces with dashes
|
|
|
|
|
|
const finalFileName = sanitizedFileName.replace(/\s+/g, '-');
|
|
|
|
|
|
|
|
|
|
|
|
return finalFileName;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-01-07 08:57:10 +00:00
|
|
|
|
export const calculateSHA256 = async (file) => {
|
|
|
|
|
|
// Create a FileReader to read the file asynchronously
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
|
|
|
|
|
|
// Define a promise to handle the file reading
|
|
|
|
|
|
const readFile = new Promise((resolve, reject) => {
|
|
|
|
|
|
reader.onload = () => resolve(reader.result);
|
|
|
|
|
|
reader.onerror = reject;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Read the file as an ArrayBuffer
|
|
|
|
|
|
reader.readAsArrayBuffer(file);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Wait for the FileReader to finish reading the file
|
|
|
|
|
|
const buffer = await readFile;
|
|
|
|
|
|
|
|
|
|
|
|
// Convert the ArrayBuffer to a Uint8Array
|
|
|
|
|
|
const uint8Array = new Uint8Array(buffer);
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate the SHA-256 hash using Web Crypto API
|
|
|
|
|
|
const hashBuffer = await crypto.subtle.digest('SHA-256', uint8Array);
|
|
|
|
|
|
|
|
|
|
|
|
// Convert the hash to a hexadecimal string
|
|
|
|
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
|
|
|
|
const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
|
|
|
|
|
2024-01-07 09:40:36 +00:00
|
|
|
|
return `${hashHex}`;
|
2024-01-07 08:57:10 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error calculating SHA-256 hash:', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2024-01-17 08:01:11 +00:00
|
|
|
|
|
|
|
|
|
|
export const getImportOrigin = (_chats) => {
|
|
|
|
|
|
// Check what external service chat imports are from
|
2024-01-17 22:23:16 +00:00
|
|
|
|
if ('mapping' in _chats[0]) {
|
|
|
|
|
|
return 'openai';
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'webui';
|
|
|
|
|
|
};
|
2024-01-17 08:01:11 +00:00
|
|
|
|
|
2024-06-16 22:32:26 +00:00
|
|
|
|
export const getUserPosition = async (raw = false) => {
|
|
|
|
|
|
// Get the user's location using the Geolocation API
|
|
|
|
|
|
const position = await new Promise((resolve, reject) => {
|
|
|
|
|
|
navigator.geolocation.getCurrentPosition(resolve, reject);
|
|
|
|
|
|
}).catch((error) => {
|
|
|
|
|
|
console.error('Error getting user location:', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!position) {
|
|
|
|
|
|
return 'Location not available';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Extract the latitude and longitude from the position
|
|
|
|
|
|
const { latitude, longitude } = position.coords;
|
|
|
|
|
|
|
|
|
|
|
|
if (raw) {
|
|
|
|
|
|
return { latitude, longitude };
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return `${latitude.toFixed(3)}, ${longitude.toFixed(3)} (lat, long)`;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-01-17 22:23:16 +00:00
|
|
|
|
const convertOpenAIMessages = (convo) => {
|
2024-01-17 08:01:11 +00:00
|
|
|
|
// Parse OpenAI chat messages and create chat dictionary for creating new chats
|
2024-01-17 22:23:16 +00:00
|
|
|
|
const mapping = convo['mapping'];
|
2024-01-17 08:01:11 +00:00
|
|
|
|
const messages = [];
|
2024-01-17 22:23:16 +00:00
|
|
|
|
let currentId = '';
|
2024-01-19 18:22:28 +00:00
|
|
|
|
let lastId = null;
|
2024-01-17 08:01:11 +00:00
|
|
|
|
|
2024-08-25 00:35:42 +00:00
|
|
|
|
for (const message_id in mapping) {
|
2024-01-17 22:23:16 +00:00
|
|
|
|
const message = mapping[message_id];
|
2024-01-17 08:01:11 +00:00
|
|
|
|
currentId = message_id;
|
2024-01-19 18:22:28 +00:00
|
|
|
|
try {
|
2024-01-27 06:17:28 +00:00
|
|
|
|
if (
|
|
|
|
|
|
messages.length == 0 &&
|
|
|
|
|
|
(message['message'] == null ||
|
|
|
|
|
|
(message['message']['content']['parts']?.[0] == '' &&
|
|
|
|
|
|
message['message']['content']['text'] == null))
|
|
|
|
|
|
) {
|
2024-01-19 18:22:28 +00:00
|
|
|
|
// Skip chat messages with no content
|
|
|
|
|
|
continue;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const new_chat = {
|
|
|
|
|
|
id: message_id,
|
|
|
|
|
|
parentId: lastId,
|
|
|
|
|
|
childrenIds: message['children'] || [],
|
|
|
|
|
|
role: message['message']?.['author']?.['role'] !== 'user' ? 'assistant' : 'user',
|
2024-01-27 06:17:28 +00:00
|
|
|
|
content:
|
|
|
|
|
|
message['message']?.['content']?.['parts']?.[0] ||
|
|
|
|
|
|
message['message']?.['content']?.['text'] ||
|
|
|
|
|
|
'',
|
2024-01-19 18:22:28 +00:00
|
|
|
|
model: 'gpt-3.5-turbo',
|
|
|
|
|
|
done: true,
|
|
|
|
|
|
context: null
|
|
|
|
|
|
};
|
|
|
|
|
|
messages.push(new_chat);
|
|
|
|
|
|
lastId = currentId;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2024-01-27 06:17:28 +00:00
|
|
|
|
console.log('Error with', message, '\nError:', error);
|
2024-01-17 08:01:11 +00:00
|
|
|
|
}
|
2024-01-17 22:23:16 +00:00
|
|
|
|
}
|
2024-01-17 08:01:11 +00:00
|
|
|
|
|
2024-08-25 00:35:42 +00:00
|
|
|
|
const history: Record<PropertyKey, (typeof messages)[number]> = {};
|
2024-01-17 22:23:16 +00:00
|
|
|
|
messages.forEach((obj) => (history[obj.id] = obj));
|
2024-01-17 08:01:11 +00:00
|
|
|
|
|
|
|
|
|
|
const chat = {
|
2024-01-17 22:23:16 +00:00
|
|
|
|
history: {
|
|
|
|
|
|
currentId: currentId,
|
|
|
|
|
|
messages: history // Need to convert this to not a list and instead a json object
|
2024-01-17 08:01:11 +00:00
|
|
|
|
},
|
2024-01-17 22:47:56 +00:00
|
|
|
|
models: ['gpt-3.5-turbo'],
|
2024-01-17 22:23:16 +00:00
|
|
|
|
messages: messages,
|
|
|
|
|
|
options: {},
|
|
|
|
|
|
timestamp: convo['create_time'],
|
2024-01-17 22:47:56 +00:00
|
|
|
|
title: convo['title'] ?? 'New Chat'
|
2024-01-17 22:23:16 +00:00
|
|
|
|
};
|
|
|
|
|
|
return chat;
|
|
|
|
|
|
};
|
2024-01-17 08:01:11 +00:00
|
|
|
|
|
2024-01-19 18:22:28 +00:00
|
|
|
|
const validateChat = (chat) => {
|
2024-10-14 07:13:26 +00:00
|
|
|
|
// Because ChatGPT sometimes has features we can't use like DALL-E or might have corrupted messages, need to validate
|
2024-01-19 18:22:28 +00:00
|
|
|
|
const messages = chat.messages;
|
|
|
|
|
|
|
2024-01-27 06:17:28 +00:00
|
|
|
|
// Check if messages array is empty
|
|
|
|
|
|
if (messages.length === 0) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Last message's children should be an empty array
|
|
|
|
|
|
const lastMessage = messages[messages.length - 1];
|
|
|
|
|
|
if (lastMessage.childrenIds.length !== 0) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// First message's parent should be null
|
|
|
|
|
|
const firstMessage = messages[0];
|
|
|
|
|
|
if (firstMessage.parentId !== null) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Every message's content should be a string
|
2024-08-25 00:35:42 +00:00
|
|
|
|
for (const message of messages) {
|
2024-01-27 06:17:28 +00:00
|
|
|
|
if (typeof message.content !== 'string') {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
2024-01-19 18:22:28 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2024-01-17 22:23:16 +00:00
|
|
|
|
export const convertOpenAIChats = (_chats) => {
|
2024-01-17 08:01:11 +00:00
|
|
|
|
// Create a list of dictionaries with each conversation from import
|
2024-01-17 22:23:16 +00:00
|
|
|
|
const chats = [];
|
2024-01-19 18:22:28 +00:00
|
|
|
|
let failed = 0;
|
2024-08-25 00:35:42 +00:00
|
|
|
|
for (const convo of _chats) {
|
2024-01-17 22:47:56 +00:00
|
|
|
|
const chat = convertOpenAIMessages(convo);
|
|
|
|
|
|
|
2024-01-19 18:22:28 +00:00
|
|
|
|
if (validateChat(chat)) {
|
2024-01-17 22:47:56 +00:00
|
|
|
|
chats.push({
|
|
|
|
|
|
id: convo['id'],
|
|
|
|
|
|
user_id: '',
|
|
|
|
|
|
title: convo['title'],
|
|
|
|
|
|
chat: chat,
|
2025-03-18 06:23:52 +00:00
|
|
|
|
timestamp: convo['create_time']
|
2024-01-17 22:47:56 +00:00
|
|
|
|
});
|
2024-01-27 06:17:28 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
failed++;
|
|
|
|
|
|
}
|
2024-01-17 08:01:11 +00:00
|
|
|
|
}
|
2024-01-27 06:17:28 +00:00
|
|
|
|
console.log(failed, 'Conversations could not be imported');
|
2024-01-17 22:23:16 +00:00
|
|
|
|
return chats;
|
|
|
|
|
|
};
|
2024-01-27 06:17:28 +00:00
|
|
|
|
|
2024-08-25 00:35:42 +00:00
|
|
|
|
export const isValidHttpUrl = (string: string) => {
|
2024-01-27 06:17:28 +00:00
|
|
|
|
let url;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
url = new URL(string);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
|
|
|
|
};
|
2024-02-11 03:20:56 +00:00
|
|
|
|
|
2024-08-25 00:35:42 +00:00
|
|
|
|
export const removeEmojis = (str: string) => {
|
2024-02-11 03:20:56 +00:00
|
|
|
|
// Regular expression to match emojis
|
|
|
|
|
|
const emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g;
|
|
|
|
|
|
|
|
|
|
|
|
// Replace emojis with an empty string
|
|
|
|
|
|
return str.replace(emojiRegex, '');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-08-25 00:35:42 +00:00
|
|
|
|
export const removeFormattings = (str: string) => {
|
2024-12-18 17:18:31 +00:00
|
|
|
|
return (
|
|
|
|
|
|
str
|
|
|
|
|
|
// Block elements (remove completely)
|
|
|
|
|
|
.replace(/(```[\s\S]*?```)/g, '') // Code blocks
|
|
|
|
|
|
.replace(/^\|.*\|$/gm, '') // Tables
|
|
|
|
|
|
// Inline elements (preserve content)
|
|
|
|
|
|
.replace(/(?:\*\*|__)(.*?)(?:\*\*|__)/g, '$1') // Bold
|
|
|
|
|
|
.replace(/(?:[*_])(.*?)(?:[*_])/g, '$1') // Italic
|
|
|
|
|
|
.replace(/~~(.*?)~~/g, '$1') // Strikethrough
|
|
|
|
|
|
.replace(/`([^`]+)`/g, '$1') // Inline code
|
|
|
|
|
|
|
|
|
|
|
|
// Links and images
|
|
|
|
|
|
.replace(/!?\[([^\]]*)\](?:\([^)]+\)|\[[^\]]*\])/g, '$1') // Links & images
|
|
|
|
|
|
.replace(/^\[[^\]]+\]:\s*.*$/gm, '') // Reference definitions
|
|
|
|
|
|
|
|
|
|
|
|
// Block formatting
|
|
|
|
|
|
.replace(/^#{1,6}\s+/gm, '') // Headers
|
|
|
|
|
|
.replace(/^\s*[-*+]\s+/gm, '') // Lists
|
|
|
|
|
|
.replace(/^\s*(?:\d+\.)\s+/gm, '') // Numbered lists
|
|
|
|
|
|
.replace(/^\s*>[> ]*/gm, '') // Blockquotes
|
|
|
|
|
|
.replace(/^\s*:\s+/gm, '') // Definition lists
|
|
|
|
|
|
|
|
|
|
|
|
// Cleanup
|
|
|
|
|
|
.replace(/\[\^[^\]]*\]/g, '') // Footnotes
|
|
|
|
|
|
.replace(/\n{2,}/g, '\n')
|
|
|
|
|
|
); // Multiple newlines
|
2024-06-22 23:13:13 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2024-08-26 13:01:29 +00:00
|
|
|
|
export const cleanText = (content: string) => {
|
2024-08-25 00:35:42 +00:00
|
|
|
|
return removeFormattings(removeEmojis(content.trim()));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-02-04 00:07:43 +00:00
|
|
|
|
export const removeDetails = (content, types) => {
|
|
|
|
|
|
for (const type of types) {
|
|
|
|
|
|
content = content.replace(
|
|
|
|
|
|
new RegExp(`<details\\s+type="${type}"[^>]*>.*?<\\/details>`, 'gis'),
|
|
|
|
|
|
''
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return content;
|
2025-01-22 18:12:09 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-04-12 22:11:03 +00:00
|
|
|
|
export const removeAllDetails = (content) => {
|
|
|
|
|
|
content = content.replace(/<details[^>]*>.*?<\/details>/gis, '');
|
|
|
|
|
|
return content;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-04-03 06:46:39 +00:00
|
|
|
|
export const processDetails = (content) => {
|
|
|
|
|
|
content = removeDetails(content, ['reasoning', 'code_interpreter']);
|
|
|
|
|
|
|
2025-05-29 20:04:13 +00:00
|
|
|
|
// This regex matches <details> tags with type="tool_calls" and captures their attributes to convert them to a string
|
2025-04-03 06:46:39 +00:00
|
|
|
|
const detailsRegex = /<details\s+type="tool_calls"([^>]*)>([\s\S]*?)<\/details>/gis;
|
|
|
|
|
|
const matches = content.match(detailsRegex);
|
|
|
|
|
|
if (matches) {
|
|
|
|
|
|
for (const match of matches) {
|
|
|
|
|
|
const attributesRegex = /(\w+)="([^"]*)"/g;
|
|
|
|
|
|
const attributes = {};
|
|
|
|
|
|
let attributeMatch;
|
|
|
|
|
|
while ((attributeMatch = attributesRegex.exec(match)) !== null) {
|
|
|
|
|
|
attributes[attributeMatch[1]] = attributeMatch[2];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-29 20:04:13 +00:00
|
|
|
|
content = content.replace(match, `"${attributes.result}"`);
|
2025-04-03 06:46:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return content;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-08-25 00:35:42 +00:00
|
|
|
|
// This regular expression matches code blocks marked by triple backticks
|
|
|
|
|
|
const codeBlockRegex = /```[\s\S]*?```/g;
|
2024-06-22 23:33:20 +00:00
|
|
|
|
|
2024-08-25 00:35:42 +00:00
|
|
|
|
export const extractSentences = (text: string) => {
|
|
|
|
|
|
const codeBlocks: string[] = [];
|
2024-06-22 23:33:20 +00:00
|
|
|
|
let index = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Temporarily replace code blocks with placeholders and store the blocks separately
|
|
|
|
|
|
text = text.replace(codeBlockRegex, (match) => {
|
2024-08-25 00:35:42 +00:00
|
|
|
|
const placeholder = `\u0000${index}\u0000`; // Use a unique placeholder
|
2024-06-22 23:33:20 +00:00
|
|
|
|
codeBlocks[index++] = match;
|
|
|
|
|
|
return placeholder;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Split the modified text into sentences based on common punctuation marks, avoiding these blocks
|
2024-07-01 23:04:24 +00:00
|
|
|
|
let sentences = text.split(/(?<=[.!?])\s+/);
|
2024-06-22 23:33:20 +00:00
|
|
|
|
|
|
|
|
|
|
// Restore code blocks and process sentences
|
|
|
|
|
|
sentences = sentences.map((sentence) => {
|
|
|
|
|
|
// Check if the sentence includes a placeholder for a code block
|
|
|
|
|
|
return sentence.replace(/\u0000(\d+)\u0000/g, (_, idx) => codeBlocks[idx]);
|
|
|
|
|
|
});
|
2024-02-11 03:20:56 +00:00
|
|
|
|
|
2024-08-26 13:01:29 +00:00
|
|
|
|
return sentences.map(cleanText).filter(Boolean);
|
2024-08-25 00:35:42 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const extractParagraphsForAudio = (text: string) => {
|
|
|
|
|
|
const codeBlocks: string[] = [];
|
|
|
|
|
|
let index = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Temporarily replace code blocks with placeholders and store the blocks separately
|
|
|
|
|
|
text = text.replace(codeBlockRegex, (match) => {
|
|
|
|
|
|
const placeholder = `\u0000${index}\u0000`; // Use a unique placeholder
|
|
|
|
|
|
codeBlocks[index++] = match;
|
|
|
|
|
|
return placeholder;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Split the modified text into paragraphs based on newlines, avoiding these blocks
|
|
|
|
|
|
let paragraphs = text.split(/\n+/);
|
|
|
|
|
|
|
|
|
|
|
|
// Restore code blocks and process paragraphs
|
|
|
|
|
|
paragraphs = paragraphs.map((paragraph) => {
|
|
|
|
|
|
// Check if the paragraph includes a placeholder for a code block
|
|
|
|
|
|
return paragraph.replace(/\u0000(\d+)\u0000/g, (_, idx) => codeBlocks[idx]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2024-08-26 13:01:29 +00:00
|
|
|
|
return paragraphs.map(cleanText).filter(Boolean);
|
2024-02-11 03:20:56 +00:00
|
|
|
|
};
|
2024-02-11 09:06:25 +00:00
|
|
|
|
|
2024-08-25 00:35:42 +00:00
|
|
|
|
export const extractSentencesForAudio = (text: string) => {
|
2024-06-14 03:15:23 +00:00
|
|
|
|
return extractSentences(text).reduce((mergedTexts, currentText) => {
|
|
|
|
|
|
const lastIndex = mergedTexts.length - 1;
|
|
|
|
|
|
if (lastIndex >= 0) {
|
|
|
|
|
|
const previousText = mergedTexts[lastIndex];
|
|
|
|
|
|
const wordCount = previousText.split(/\s+/).length;
|
2024-08-26 00:00:49 +00:00
|
|
|
|
const charCount = previousText.length;
|
|
|
|
|
|
if (wordCount < 4 || charCount < 50) {
|
2024-06-14 03:15:23 +00:00
|
|
|
|
mergedTexts[lastIndex] = previousText + ' ' + currentText;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
mergedTexts.push(currentText);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
mergedTexts.push(currentText);
|
|
|
|
|
|
}
|
|
|
|
|
|
return mergedTexts;
|
2024-08-25 00:35:42 +00:00
|
|
|
|
}, [] as string[]);
|
2024-06-14 03:15:23 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-05-23 09:40:33 +00:00
|
|
|
|
export const getMessageContentParts = (content: string, splitOn: string = 'punctuation') => {
|
2024-08-26 13:01:29 +00:00
|
|
|
|
const messageContentParts: string[] = [];
|
|
|
|
|
|
|
2025-05-23 09:40:33 +00:00
|
|
|
|
switch (splitOn) {
|
2024-08-26 13:01:29 +00:00
|
|
|
|
default:
|
|
|
|
|
|
case TTS_RESPONSE_SPLIT.PUNCTUATION:
|
|
|
|
|
|
messageContentParts.push(...extractSentencesForAudio(content));
|
|
|
|
|
|
break;
|
|
|
|
|
|
case TTS_RESPONSE_SPLIT.PARAGRAPHS:
|
|
|
|
|
|
messageContentParts.push(...extractParagraphsForAudio(content));
|
|
|
|
|
|
break;
|
|
|
|
|
|
case TTS_RESPONSE_SPLIT.NONE:
|
|
|
|
|
|
messageContentParts.push(cleanText(content));
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return messageContentParts;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-02-11 09:06:25 +00:00
|
|
|
|
export const blobToFile = (blob, fileName) => {
|
|
|
|
|
|
// Create a new File object from the Blob
|
|
|
|
|
|
const file = new File([blob], fileName, { type: blob.type });
|
|
|
|
|
|
return file;
|
|
|
|
|
|
};
|
2024-04-13 17:26:50 +00:00
|
|
|
|
|
2025-01-30 05:56:51 +00:00
|
|
|
|
export const getPromptVariables = (user_name, user_location) => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
'{{USER_NAME}}': user_name,
|
|
|
|
|
|
'{{USER_LOCATION}}': user_location || 'Unknown',
|
|
|
|
|
|
'{{CURRENT_DATETIME}}': getCurrentDateTime(),
|
|
|
|
|
|
'{{CURRENT_DATE}}': getFormattedDate(),
|
|
|
|
|
|
'{{CURRENT_TIME}}': getFormattedTime(),
|
|
|
|
|
|
'{{CURRENT_WEEKDAY}}': getWeekday(),
|
|
|
|
|
|
'{{CURRENT_TIMEZONE}}': getUserTimezone(),
|
|
|
|
|
|
'{{USER_LANGUAGE}}': localStorage.getItem('locale') || 'en-US'
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-05-10 12:17:19 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* This function is used to replace placeholders in a template string with the provided prompt.
|
|
|
|
|
|
* The placeholders can be in the following formats:
|
|
|
|
|
|
* - `{{prompt}}`: This will be replaced with the entire prompt.
|
|
|
|
|
|
* - `{{prompt:start:<length>}}`: This will be replaced with the first <length> characters of the prompt.
|
|
|
|
|
|
* - `{{prompt:end:<length>}}`: This will be replaced with the last <length> characters of the prompt.
|
|
|
|
|
|
* - `{{prompt:middletruncate:<length>}}`: This will be replaced with the prompt truncated to <length> characters, with '...' in the middle.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} template - The template string containing placeholders.
|
|
|
|
|
|
* @param {string} prompt - The string to replace the placeholders with.
|
|
|
|
|
|
* @returns {string} The template string with the placeholders replaced by the prompt.
|
|
|
|
|
|
*/
|
2024-05-31 18:11:28 +00:00
|
|
|
|
export const titleGenerationTemplate = (template: string, prompt: string): string => {
|
|
|
|
|
|
template = template.replace(
|
2024-05-10 12:17:19 +00:00
|
|
|
|
/{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}/g,
|
|
|
|
|
|
(match, startLength, endLength, middleLength) => {
|
|
|
|
|
|
if (match === '{{prompt}}') {
|
|
|
|
|
|
return prompt;
|
|
|
|
|
|
} else if (match.startsWith('{{prompt:start:')) {
|
|
|
|
|
|
return prompt.substring(0, startLength);
|
|
|
|
|
|
} else if (match.startsWith('{{prompt:end:')) {
|
|
|
|
|
|
return prompt.slice(-endLength);
|
|
|
|
|
|
} else if (match.startsWith('{{prompt:middletruncate:')) {
|
|
|
|
|
|
if (prompt.length <= middleLength) {
|
|
|
|
|
|
return prompt;
|
|
|
|
|
|
}
|
|
|
|
|
|
const start = prompt.slice(0, Math.ceil(middleLength / 2));
|
|
|
|
|
|
const end = prompt.slice(-Math.floor(middleLength / 2));
|
|
|
|
|
|
return `${start}...${end}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
2024-05-04 20:11:58 +00:00
|
|
|
|
);
|
2024-05-31 18:11:28 +00:00
|
|
|
|
|
|
|
|
|
|
return template;
|
2024-04-14 21:04:24 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2024-04-14 04:27:00 +00:00
|
|
|
|
export const approximateToHumanReadable = (nanoseconds: number) => {
|
2024-04-14 04:39:10 +00:00
|
|
|
|
const seconds = Math.floor((nanoseconds / 1e9) % 60);
|
|
|
|
|
|
const minutes = Math.floor((nanoseconds / 6e10) % 60);
|
|
|
|
|
|
const hours = Math.floor((nanoseconds / 3.6e12) % 24);
|
2024-04-14 04:27:00 +00:00
|
|
|
|
|
|
|
|
|
|
const results: string[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (seconds >= 0) {
|
|
|
|
|
|
results.push(`${seconds}s`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (minutes > 0) {
|
|
|
|
|
|
results.push(`${minutes}m`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (hours > 0) {
|
|
|
|
|
|
results.push(`${hours}h`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return results.reverse().join(' ');
|
2024-04-13 17:26:50 +00:00
|
|
|
|
};
|
2024-05-04 08:23:02 +00:00
|
|
|
|
|
|
|
|
|
|
export const getTimeRange = (timestamp) => {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
const date = new Date(timestamp * 1000); // Convert Unix timestamp to milliseconds
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate the difference in milliseconds
|
|
|
|
|
|
const diffTime = now.getTime() - date.getTime();
|
|
|
|
|
|
const diffDays = diffTime / (1000 * 3600 * 24);
|
|
|
|
|
|
|
2024-05-04 19:59:23 +00:00
|
|
|
|
const nowDate = now.getDate();
|
|
|
|
|
|
const nowMonth = now.getMonth();
|
|
|
|
|
|
const nowYear = now.getFullYear();
|
|
|
|
|
|
|
|
|
|
|
|
const dateDate = date.getDate();
|
|
|
|
|
|
const dateMonth = date.getMonth();
|
|
|
|
|
|
const dateYear = date.getFullYear();
|
|
|
|
|
|
|
|
|
|
|
|
if (nowYear === dateYear && nowMonth === dateMonth && nowDate === dateDate) {
|
2024-05-04 08:23:02 +00:00
|
|
|
|
return 'Today';
|
2024-05-04 19:59:23 +00:00
|
|
|
|
} else if (nowYear === dateYear && nowMonth === dateMonth && nowDate - dateDate === 1) {
|
2024-05-04 08:23:02 +00:00
|
|
|
|
return 'Yesterday';
|
|
|
|
|
|
} else if (diffDays <= 7) {
|
|
|
|
|
|
return 'Previous 7 days';
|
|
|
|
|
|
} else if (diffDays <= 30) {
|
|
|
|
|
|
return 'Previous 30 days';
|
2024-05-04 19:59:23 +00:00
|
|
|
|
} else if (nowYear === dateYear) {
|
2024-05-04 08:23:02 +00:00
|
|
|
|
return date.toLocaleString('default', { month: 'long' });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return date.getFullYear().toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2024-06-26 17:22:31 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Extract frontmatter as a dictionary from the specified content string.
|
|
|
|
|
|
* @param content {string} - The content string with potential frontmatter.
|
|
|
|
|
|
* @returns {Object} - The extracted frontmatter as a dictionary.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const extractFrontmatter = (content) => {
|
|
|
|
|
|
const frontmatter = {};
|
|
|
|
|
|
let frontmatterStarted = false;
|
|
|
|
|
|
let frontmatterEnded = false;
|
|
|
|
|
|
const frontmatterPattern = /^\s*([a-z_]+):\s*(.*)\s*$/i;
|
|
|
|
|
|
|
|
|
|
|
|
// Split content into lines
|
|
|
|
|
|
const lines = content.split('\n');
|
|
|
|
|
|
|
|
|
|
|
|
// Check if the content starts with triple quotes
|
|
|
|
|
|
if (lines[0].trim() !== '"""') {
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
frontmatterStarted = true;
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 1; i < lines.length; i++) {
|
|
|
|
|
|
const line = lines[i];
|
|
|
|
|
|
|
|
|
|
|
|
if (line.includes('"""')) {
|
|
|
|
|
|
if (frontmatterStarted) {
|
|
|
|
|
|
frontmatterEnded = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (frontmatterStarted && !frontmatterEnded) {
|
|
|
|
|
|
const match = frontmatterPattern.exec(line);
|
|
|
|
|
|
if (match) {
|
|
|
|
|
|
const [, key, value] = match;
|
|
|
|
|
|
frontmatter[key.trim()] = value.trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return frontmatter;
|
|
|
|
|
|
};
|
2024-06-30 03:04:04 +00:00
|
|
|
|
|
|
|
|
|
|
// Function to determine the best matching language
|
|
|
|
|
|
export const bestMatchingLanguage = (supportedLanguages, preferredLanguages, defaultLocale) => {
|
|
|
|
|
|
const languages = supportedLanguages.map((lang) => lang.code);
|
2024-06-30 03:41:06 +00:00
|
|
|
|
|
|
|
|
|
|
const match = preferredLanguages
|
|
|
|
|
|
.map((prefLang) => languages.find((lang) => lang.startsWith(prefLang)))
|
|
|
|
|
|
.find(Boolean);
|
|
|
|
|
|
|
2024-06-30 03:04:04 +00:00
|
|
|
|
return match || defaultLocale;
|
|
|
|
|
|
};
|
2024-09-21 22:02:07 +00:00
|
|
|
|
|
|
|
|
|
|
// Get the date in the format YYYY-MM-DD
|
|
|
|
|
|
export const getFormattedDate = () => {
|
|
|
|
|
|
const date = new Date();
|
2025-02-15 18:37:40 +00:00
|
|
|
|
const year = date.getFullYear();
|
|
|
|
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
|
|
|
|
return `${year}-${month}-${day}`;
|
2024-09-21 22:02:07 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Get the time in the format HH:MM:SS
|
|
|
|
|
|
export const getFormattedTime = () => {
|
|
|
|
|
|
const date = new Date();
|
|
|
|
|
|
return date.toTimeString().split(' ')[0];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Get the current date and time in the format YYYY-MM-DD HH:MM:SS
|
|
|
|
|
|
export const getCurrentDateTime = () => {
|
|
|
|
|
|
return `${getFormattedDate()} ${getFormattedTime()}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Get the user's timezone
|
|
|
|
|
|
export const getUserTimezone = () => {
|
|
|
|
|
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Get the weekday
|
|
|
|
|
|
export const getWeekday = () => {
|
|
|
|
|
|
const date = new Date();
|
|
|
|
|
|
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
|
|
|
|
return weekdays[date.getDay()];
|
|
|
|
|
|
};
|
2024-09-26 18:59:25 +00:00
|
|
|
|
|
|
|
|
|
|
export const createMessagesList = (history, messageId) => {
|
|
|
|
|
|
if (messageId === null) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const message = history.messages[messageId];
|
2025-05-29 04:19:13 +00:00
|
|
|
|
if (message === undefined) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
2024-09-26 18:59:25 +00:00
|
|
|
|
if (message?.parentId) {
|
|
|
|
|
|
return [...createMessagesList(history, message.parentId), message];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return [message];
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2024-09-28 08:53:25 +00:00
|
|
|
|
|
|
|
|
|
|
export const formatFileSize = (size) => {
|
|
|
|
|
|
if (size == null) return 'Unknown size';
|
|
|
|
|
|
if (typeof size !== 'number' || size < 0) return 'Invalid size';
|
|
|
|
|
|
if (size === 0) return '0 B';
|
|
|
|
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
|
|
|
|
let unitIndex = 0;
|
|
|
|
|
|
|
|
|
|
|
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
|
|
|
|
size /= 1024;
|
|
|
|
|
|
unitIndex++;
|
|
|
|
|
|
}
|
|
|
|
|
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const getLineCount = (text) => {
|
2024-09-29 21:08:55 +00:00
|
|
|
|
console.log(typeof text);
|
|
|
|
|
|
return text ? text.split('\n').length : 0;
|
2024-09-28 08:53:25 +00:00
|
|
|
|
};
|
2025-03-27 09:27:56 +00:00
|
|
|
|
|
2025-03-28 08:20:45 +00:00
|
|
|
|
// Helper function to recursively resolve OpenAPI schema into JSON schema format
|
|
|
|
|
|
function resolveSchema(schemaRef, components, resolvedSchemas = new Set()) {
|
|
|
|
|
|
if (!schemaRef) return {};
|
|
|
|
|
|
|
|
|
|
|
|
if (schemaRef['$ref']) {
|
|
|
|
|
|
const refPath = schemaRef['$ref'];
|
|
|
|
|
|
const schemaName = refPath.split('/').pop();
|
|
|
|
|
|
|
|
|
|
|
|
if (resolvedSchemas.has(schemaName)) {
|
|
|
|
|
|
// Avoid infinite recursion on circular references
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
resolvedSchemas.add(schemaName);
|
|
|
|
|
|
const referencedSchema = components.schemas[schemaName];
|
|
|
|
|
|
return resolveSchema(referencedSchema, components, resolvedSchemas);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (schemaRef.type) {
|
|
|
|
|
|
const schemaObj = { type: schemaRef.type };
|
|
|
|
|
|
|
|
|
|
|
|
if (schemaRef.description) {
|
|
|
|
|
|
schemaObj.description = schemaRef.description;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch (schemaRef.type) {
|
|
|
|
|
|
case 'object':
|
|
|
|
|
|
schemaObj.properties = {};
|
|
|
|
|
|
schemaObj.required = schemaRef.required || [];
|
|
|
|
|
|
for (const [propName, propSchema] of Object.entries(schemaRef.properties || {})) {
|
|
|
|
|
|
schemaObj.properties[propName] = resolveSchema(propSchema, components);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 'array':
|
|
|
|
|
|
schemaObj.items = resolveSchema(schemaRef.items, components);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
// for primitive types (string, integer, etc.), just use as is
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
return schemaObj;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// fallback for schemas without explicit type
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Main conversion function
|
2025-03-27 09:27:56 +00:00
|
|
|
|
export const convertOpenApiToToolPayload = (openApiSpec) => {
|
|
|
|
|
|
const toolPayload = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const [path, methods] of Object.entries(openApiSpec.paths)) {
|
|
|
|
|
|
for (const [method, operation] of Object.entries(methods)) {
|
2025-04-19 10:46:06 +00:00
|
|
|
|
if (operation?.operationId) {
|
|
|
|
|
|
const tool = {
|
|
|
|
|
|
name: operation.operationId,
|
|
|
|
|
|
description: operation.description || operation.summary || 'No description available.',
|
|
|
|
|
|
parameters: {
|
|
|
|
|
|
type: 'object',
|
|
|
|
|
|
properties: {},
|
|
|
|
|
|
required: []
|
2025-03-27 09:27:56 +00:00
|
|
|
|
}
|
2025-04-19 10:46:06 +00:00
|
|
|
|
};
|
2025-03-27 09:27:56 +00:00
|
|
|
|
|
2025-04-19 10:46:06 +00:00
|
|
|
|
// Extract path and query parameters
|
|
|
|
|
|
if (operation.parameters) {
|
|
|
|
|
|
operation.parameters.forEach((param) => {
|
|
|
|
|
|
let description = param.schema.description || param.description || '';
|
|
|
|
|
|
if (param.schema.enum && Array.isArray(param.schema.enum)) {
|
|
|
|
|
|
description += `. Possible values: ${param.schema.enum.join(', ')}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
tool.parameters.properties[param.name] = {
|
|
|
|
|
|
type: param.schema.type,
|
|
|
|
|
|
description: description
|
2025-03-28 08:20:45 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-04-19 10:46:06 +00:00
|
|
|
|
if (param.required) {
|
|
|
|
|
|
tool.parameters.required.push(param.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Extract and recursively resolve requestBody if available
|
|
|
|
|
|
if (operation.requestBody) {
|
|
|
|
|
|
const content = operation.requestBody.content;
|
|
|
|
|
|
if (content && content['application/json']) {
|
|
|
|
|
|
const requestSchema = content['application/json'].schema;
|
|
|
|
|
|
const resolvedRequestSchema = resolveSchema(requestSchema, openApiSpec.components);
|
|
|
|
|
|
|
|
|
|
|
|
if (resolvedRequestSchema.properties) {
|
|
|
|
|
|
tool.parameters.properties = {
|
|
|
|
|
|
...tool.parameters.properties,
|
|
|
|
|
|
...resolvedRequestSchema.properties
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (resolvedRequestSchema.required) {
|
|
|
|
|
|
tool.parameters.required = [
|
|
|
|
|
|
...new Set([...tool.parameters.required, ...resolvedRequestSchema.required])
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (resolvedRequestSchema.type === 'array') {
|
|
|
|
|
|
tool.parameters = resolvedRequestSchema; // special case when root schema is an array
|
2025-03-27 09:27:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-19 10:46:06 +00:00
|
|
|
|
toolPayload.push(tool);
|
|
|
|
|
|
}
|
2025-03-27 09:27:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return toolPayload;
|
|
|
|
|
|
};
|
2025-04-17 12:56:57 +00:00
|
|
|
|
|
|
|
|
|
|
export const slugify = (str: string): string => {
|
2025-04-19 10:46:06 +00:00
|
|
|
|
return (
|
|
|
|
|
|
str
|
|
|
|
|
|
// 1. Normalize: separate accented letters into base + combining marks
|
|
|
|
|
|
.normalize('NFD')
|
|
|
|
|
|
// 2. Remove all combining marks (the accents)
|
|
|
|
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
|
|
|
|
// 3. Replace any sequence of whitespace with a single hyphen
|
|
|
|
|
|
.replace(/\s+/g, '-')
|
2025-07-31 11:55:10 +00:00
|
|
|
|
// 4. Remove all characters except alphanumeric characters, hyphens, and underscores
|
|
|
|
|
|
.replace(/[^a-zA-Z0-9-_]/g, '')
|
2025-04-19 10:46:06 +00:00
|
|
|
|
// 5. Convert to lowercase
|
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
|
);
|
2025-04-17 12:56:57 +00:00
|
|
|
|
};
|
2025-07-08 21:49:43 +00:00
|
|
|
|
|
|
|
|
|
|
export const extractInputVariables = (text: string): Record<string, any> => {
|
|
|
|
|
|
const regex = /{{\s*([^|}\s]+)\s*\|\s*([^}]+)\s*}}/g;
|
|
|
|
|
|
const regularRegex = /{{\s*([^|}\s]+)\s*}}/g;
|
|
|
|
|
|
const variables: Record<string, any> = {};
|
|
|
|
|
|
let match;
|
|
|
|
|
|
// Use exec() loop instead of matchAll() for better compatibility
|
|
|
|
|
|
while ((match = regex.exec(text)) !== null) {
|
|
|
|
|
|
const varName = match[1].trim();
|
|
|
|
|
|
const definition = match[2].trim();
|
|
|
|
|
|
variables[varName] = parseVariableDefinition(definition);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Then, extract regular variables (without pipe) - only if not already processed
|
|
|
|
|
|
while ((match = regularRegex.exec(text)) !== null) {
|
|
|
|
|
|
const varName = match[1].trim();
|
|
|
|
|
|
// Only add if not already processed as custom variable
|
|
|
|
|
|
if (!variables.hasOwnProperty(varName)) {
|
|
|
|
|
|
variables[varName] = { type: 'text' }; // Default type for regular variables
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return variables;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const splitProperties = (str: string, delimiter: string): string[] => {
|
|
|
|
|
|
const result: string[] = [];
|
|
|
|
|
|
let current = '';
|
|
|
|
|
|
let depth = 0;
|
|
|
|
|
|
let inString = false;
|
|
|
|
|
|
let escapeNext = false;
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < str.length; i++) {
|
|
|
|
|
|
const char = str[i];
|
|
|
|
|
|
|
|
|
|
|
|
if (escapeNext) {
|
|
|
|
|
|
current += char;
|
|
|
|
|
|
escapeNext = false;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (char === '\\') {
|
|
|
|
|
|
current += char;
|
|
|
|
|
|
escapeNext = true;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (char === '"' && !escapeNext) {
|
|
|
|
|
|
inString = !inString;
|
|
|
|
|
|
current += char;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!inString) {
|
|
|
|
|
|
if (char === '{' || char === '[') {
|
|
|
|
|
|
depth++;
|
|
|
|
|
|
} else if (char === '}' || char === ']') {
|
|
|
|
|
|
depth--;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (char === delimiter && depth === 0) {
|
|
|
|
|
|
result.push(current.trim());
|
|
|
|
|
|
current = '';
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
current += char;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (current.trim()) {
|
|
|
|
|
|
result.push(current.trim());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const parseVariableDefinition = (definition: string): Record<string, any> => {
|
|
|
|
|
|
// Use splitProperties for the main colon delimiter to handle quoted strings
|
|
|
|
|
|
const parts = splitProperties(definition, ':');
|
|
|
|
|
|
const [firstPart, ...propertyParts] = parts;
|
|
|
|
|
|
|
|
|
|
|
|
// Parse type (explicit or implied)
|
|
|
|
|
|
const type = firstPart.startsWith('type=') ? firstPart.slice(5) : firstPart;
|
|
|
|
|
|
|
|
|
|
|
|
// Parse properties using reduce
|
|
|
|
|
|
const properties = propertyParts.reduce((props, part) => {
|
|
|
|
|
|
// Use splitProperties for the equals sign as well, in case there are nested quotes
|
|
|
|
|
|
const equalsParts = splitProperties(part, '=');
|
|
|
|
|
|
const [propertyName, ...valueParts] = equalsParts;
|
|
|
|
|
|
const propertyValue = valueParts.join('='); // Handle values with = signs
|
|
|
|
|
|
|
|
|
|
|
|
return propertyName && propertyValue
|
|
|
|
|
|
? {
|
|
|
|
|
|
...props,
|
|
|
|
|
|
[propertyName.trim()]: parseJsonValue(propertyValue.trim())
|
|
|
|
|
|
}
|
|
|
|
|
|
: props;
|
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
|
|
return { type, ...properties };
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const parseJsonValue = (value: string): any => {
|
|
|
|
|
|
// Remove surrounding quotes if present (for string values)
|
|
|
|
|
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
|
|
|
|
return value.slice(1, -1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if it starts with square or curly brackets (JSON)
|
|
|
|
|
|
if (/^[\[{]/.test(value)) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return JSON.parse(value);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return value; // Return as string if JSON parsing fails
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return value;
|
|
|
|
|
|
};
|
2025-07-09 18:59:37 +00:00
|
|
|
|
|
2025-09-05 09:55:04 +00:00
|
|
|
|
async function ensurePDFjsLoaded() {
|
|
|
|
|
|
if (!window.pdfjsLib) {
|
|
|
|
|
|
const pdfjs = await import('pdfjs-dist');
|
|
|
|
|
|
pdfjs.GlobalWorkerOptions.workerSrc = pdfWorkerUrl;
|
|
|
|
|
|
if (!window.pdfjsLib) {
|
|
|
|
|
|
throw new Error('pdfjsLib is required for PDF extraction');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return window.pdfjsLib;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const extractContentFromFile = async (file: File) => {
|
2025-07-09 18:59:37 +00:00
|
|
|
|
// Known text file extensions for extra fallback
|
|
|
|
|
|
const textExtensions = [
|
|
|
|
|
|
'.txt',
|
|
|
|
|
|
'.md',
|
|
|
|
|
|
'.csv',
|
|
|
|
|
|
'.json',
|
|
|
|
|
|
'.js',
|
|
|
|
|
|
'.ts',
|
|
|
|
|
|
'.css',
|
|
|
|
|
|
'.html',
|
|
|
|
|
|
'.xml',
|
|
|
|
|
|
'.yaml',
|
|
|
|
|
|
'.yml',
|
|
|
|
|
|
'.rtf'
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-09-05 09:55:04 +00:00
|
|
|
|
function getExtension(filename: string) {
|
2025-07-09 18:59:37 +00:00
|
|
|
|
const dot = filename.lastIndexOf('.');
|
|
|
|
|
|
return dot === -1 ? '' : filename.substr(dot).toLowerCase();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Uses pdfjs to extract text from PDF
|
2025-09-05 09:55:04 +00:00
|
|
|
|
async function extractPdfText(file: File) {
|
|
|
|
|
|
const pdfjsLib = await ensurePDFjsLoaded();
|
2025-07-09 18:59:37 +00:00
|
|
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
|
|
|
|
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
|
|
|
|
|
let allText = '';
|
|
|
|
|
|
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
|
|
|
|
|
|
const page = await pdf.getPage(pageNum);
|
|
|
|
|
|
const content = await page.getTextContent();
|
2025-09-05 09:55:04 +00:00
|
|
|
|
const strings = content.items.map((item: any) => item.str);
|
2025-07-09 18:59:37 +00:00
|
|
|
|
allText += strings.join(' ') + '\n';
|
|
|
|
|
|
}
|
|
|
|
|
|
return allText;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Reads file as text using FileReader
|
2025-09-05 09:55:04 +00:00
|
|
|
|
function readAsText(file: File) {
|
2025-07-09 18:59:37 +00:00
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = () => resolve(reader.result);
|
|
|
|
|
|
reader.onerror = reject;
|
|
|
|
|
|
reader.readAsText(file);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const type = file.type || '';
|
|
|
|
|
|
const ext = getExtension(file.name);
|
|
|
|
|
|
|
|
|
|
|
|
// PDF check
|
|
|
|
|
|
if (type === 'application/pdf' || ext === '.pdf') {
|
|
|
|
|
|
return await extractPdfText(file);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Text check (plain or common text-based)
|
|
|
|
|
|
if (type.startsWith('text/') || textExtensions.includes(ext)) {
|
|
|
|
|
|
return await readAsText(file);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback: try to read as text, if decodable
|
|
|
|
|
|
try {
|
|
|
|
|
|
return await readAsText(file);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
throw new Error('Unsupported or non-text file type: ' + (file.name || type));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-08-11 22:07:09 +00:00
|
|
|
|
|
2025-08-20 23:38:26 +00:00
|
|
|
|
export const getAge = (birthDate) => {
|
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
const bDate = new Date(birthDate);
|
|
|
|
|
|
let age = today.getFullYear() - bDate.getFullYear();
|
|
|
|
|
|
const m = today.getMonth() - bDate.getMonth();
|
|
|
|
|
|
|
|
|
|
|
|
if (m < 0 || (m === 0 && today.getDate() < bDate.getDate())) {
|
|
|
|
|
|
age--;
|
|
|
|
|
|
}
|
|
|
|
|
|
return age.toString();
|
|
|
|
|
|
};
|
2025-09-05 12:07:12 +00:00
|
|
|
|
|
|
|
|
|
|
export const convertHeicToJpeg = async (file: File) => {
|
|
|
|
|
|
const { default: heic2any } = await import('heic2any');
|
|
|
|
|
|
try {
|
|
|
|
|
|
return await heic2any({ blob: file, toType: 'image/jpeg' });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
if (err?.message?.includes('already browser readable')) {
|
|
|
|
|
|
return file;
|
|
|
|
|
|
}
|
|
|
|
|
|
throw err;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|