| import type { Writable } from 'svelte/store'; |
| import { v4 as uuidv4 } from 'uuid'; |
| import sha256 from 'js-sha256'; |
| import DOMPurify from 'dompurify'; |
| import { WEBUI_BASE_URL } from '$lib/constants'; |
|
|
| import dayjs from 'dayjs'; |
| import relativeTime from 'dayjs/plugin/relativeTime'; |
| import isToday from 'dayjs/plugin/isToday'; |
| import isYesterday from 'dayjs/plugin/isYesterday'; |
| import localizedFormat from 'dayjs/plugin/localizedFormat'; |
|
|
| dayjs.extend(relativeTime); |
| dayjs.extend(isToday); |
| dayjs.extend(isYesterday); |
| dayjs.extend(localizedFormat); |
|
|
| import { TTS_RESPONSE_SPLIT } from '$lib/types'; |
|
|
| import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.mjs?url'; |
|
|
| import { marked } from 'marked'; |
| import markedExtension from '$lib/utils/marked/extension'; |
| import markedKatexExtension from '$lib/utils/marked/katex-extension'; |
| import hljs from 'highlight.js'; |
| import { decode } from 'html-entities'; |
|
|
| |
| |
| |
| |
| |
|
|
| export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); |
|
|
| export const formatNumber = (num: number): string => { |
| return new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format( |
| num |
| ); |
| }; |
|
|
| function escapeRegExp(string: string): string { |
| return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| } |
|
|
| |
| export const replaceOutsideCode = (content: string, replacer: (str: string) => string) => { |
| return content |
| .split(/(```[\s\S]*?```|`[\s\S]*?`)/) |
| .map((segment) => { |
| return segment.startsWith('```') || segment.startsWith('`') ? segment : replacer(segment); |
| }) |
| .join(''); |
| }; |
|
|
| export const replaceTokens = (content, char, user) => { |
| if (!content.includes('{{')) return content; |
| 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, |
| replacement: (_, fileId) => `<file type="html" id="${fileId}" />` |
| } |
| ]; |
|
|
| |
| content = replaceOutsideCode(content, (segment) => { |
| tokens.forEach(({ regex, replacement }) => { |
| if (replacement !== undefined && replacement !== null) { |
| segment = segment.replace(regex, replacement); |
| } |
| }); |
|
|
| return segment; |
| }); |
|
|
| return content; |
| }; |
|
|
| export const sanitizeResponseContent = (content: string) => { |
| return content |
| .replace(/<\|[a-z]*$/, '') |
| .replace(/<\|[a-z]+\|$/, '') |
| .replace(/<$/, '') |
| .replaceAll('<', '<') |
| .replaceAll('>', '>') |
| .replaceAll(/<\|[a-z]+\|>/g, ' ') |
| .trim(); |
| }; |
|
|
| export const processResponseContent = (content: string) => { |
| content = processChineseContent(content); |
| return content.trim(); |
| }; |
|
|
| function isChineseChar(char: string): boolean { |
| return /\p{Script=Han}/u.test(char); |
| } |
|
|
| |
| function processChineseContent(content: string): string { |
| |
| if (!/[\u4e00-\u9fa5]/.test(content)) return content; |
| const lines = content.split('\n'); |
| const processedLines = lines.map((line) => { |
| if (/[\u4e00-\u9fa5]/.test(line)) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if (line.includes('*')) { |
| |
| |
| if (/(|)/.test(line)) { |
| line = processChineseDelimiters(line, '**', '(', ')'); |
| line = processChineseDelimiters(line, '*', '(', ')'); |
| } |
| |
| if (/“|”/.test(line)) { |
| line = processChineseDelimiters(line, '**', '“', '”'); |
| line = processChineseDelimiters(line, '*', '“', '”'); |
| } |
| } |
| } |
| return line; |
| }); |
| content = processedLines.join('\n'); |
|
|
| return content; |
| } |
|
|
| |
| function processChineseDelimiters( |
| line: string, |
| symbol: string, |
| leftSymbol: string, |
| rightSymbol: string |
| ): string { |
| |
| 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; |
| } |
| }); |
| } |
|
|
| export function unescapeHtml(html: string): string { |
| return decode(html); |
| } |
|
|
| export const capitalizeFirstLetter = (string) => { |
| return string.charAt(0).toUpperCase() + string.slice(1); |
| }; |
|
|
| 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) => { |
| const history = { |
| 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 sanitizeHistory = (history) => { |
| if (!history?.messages || typeof history.messages !== 'object') return; |
|
|
| |
| for (const [id, message] of Object.entries(history.messages)) { |
| if (!message || typeof message !== 'object') { |
| delete history.messages[id]; |
| } |
| } |
|
|
| |
| for (const [id, message] of Object.entries(history.messages)) { |
| if (message.id !== id) message.id = id; |
| if (!Array.isArray(message.childrenIds)) message.childrenIds = []; |
| } |
|
|
| |
| const parentByChildId = {}; |
| for (const [id, message] of Object.entries(history.messages)) { |
| for (const childId of message.childrenIds) { |
| parentByChildId[childId] = id; |
| } |
| } |
|
|
| |
| for (const [id, message] of Object.entries(history.messages)) { |
| |
| if (message.role && message.parentId !== undefined) continue; |
|
|
| if (message.parentId === undefined) { |
| message.parentId = parentByChildId[id] ?? null; |
| } |
|
|
| if (!message.role) { |
| const parent = message.parentId ? history.messages[message.parentId] : null; |
| message.role = |
| parent?.role === 'user' |
| ? 'assistant' |
| : parent?.role === 'assistant' |
| ? 'user' |
| : message.model || message.usage || message.done !== undefined |
| ? 'assistant' |
| : 'user'; |
| } |
| } |
|
|
| |
| for (const message of Object.values(history.messages)) { |
| message.childrenIds = message.childrenIds.filter((childId) => history.messages[childId]); |
| } |
|
|
| |
| const currentMessage = history.messages?.[history.currentId]; |
| if (!currentMessage?.id || !currentMessage?.role) { |
| let latestLeafId = null; |
| let latestTimestamp = -1; |
| for (const [id, message] of Object.entries(history.messages)) { |
| if (message.childrenIds.length === 0 && (message.timestamp ?? 0) > latestTimestamp) { |
| latestLeafId = id; |
| latestTimestamp = message.timestamp ?? 0; |
| } |
| } |
| history.currentId = latestLeafId ?? Object.keys(history.messages)[0] ?? null; |
| } |
| }; |
|
|
| export const getGravatarURL = (email) => { |
| |
| |
| |
| const address = String(email).trim().toLowerCase(); |
|
|
| |
| const hash = sha256(address); |
|
|
| |
| return `https://www.gravatar.com/avatar/${hash}`; |
| }; |
|
|
| export const canvasPixelTest = () => { |
| |
| |
| const canvas = document.createElement('canvas'); |
| const ctx = canvas.getContext('2d'); |
| canvas.height = 1; |
| canvas.width = 1; |
| const imageData = new ImageData(canvas.width, canvas.height); |
| const pixelValues = imageData.data; |
|
|
| |
| for (let i = 0; i < imageData.data.length; i += 1) { |
| if (i % 4 !== 3) { |
| pixelValues[i] = Math.floor(256 * Math.random()); |
| } else { |
| pixelValues[i] = 255; |
| } |
| } |
|
|
| ctx.putImageData(imageData, 0, 0); |
| const p = ctx.getImageData(0, 0, canvas.width, canvas.height).data; |
|
|
| |
| 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'); |
| return false; |
| } |
| } |
|
|
| return true; |
| }; |
|
|
| let resizeImageWarmupDone = false; |
| |
| |
| |
| |
| async function resizeImageToDataURL( |
| img: HTMLImageElement, |
| width: number, |
| height: number, |
| mimeType = 'image/jpeg' |
| ): Promise<string> { |
| const canvas = document.createElement('canvas'); |
| canvas.width = width; |
| canvas.height = height; |
| canvas.getContext('2d')?.drawImage(img, 0, 0, width, height); |
|
|
| const toDataURL = () => canvas.toDataURL(mimeType); |
|
|
| if ( |
| !resizeImageWarmupDone && |
| canvas.toBlob && |
| /android|iphone|ipad|ipod/i.test(navigator?.userAgent) |
| ) { |
| resizeImageWarmupDone = true; |
| return new Promise((resolve) => { |
| canvas.toBlob((blob) => { |
| if (!blob) { |
| resolve(toDataURL()); |
| return; |
| } |
| const reader = new FileReader(); |
| reader.onload = () => resolve(String(reader.result)); |
| reader.onerror = () => resolve(toDataURL()); |
| reader.readAsDataURL(blob); |
| }, mimeType); |
| }); |
| } |
| return Promise.resolve(toDataURL()); |
| } |
|
|
| export const compressImage = async (imageUrl, maxWidth, maxHeight) => { |
| return new Promise((resolve, reject) => { |
| const img = new Image(); |
| img.onload = async () => { |
| let width = img.width; |
| let height = img.height; |
|
|
| |
|
|
| if (maxWidth && maxHeight) { |
| |
|
|
| if (width <= maxWidth && height <= maxHeight) { |
| resolve(imageUrl); |
| return; |
| } |
|
|
| 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) { |
| |
|
|
| if (width <= maxWidth) { |
| resolve(imageUrl); |
| return; |
| } |
|
|
| height = Math.round((maxWidth * height) / width); |
| width = maxWidth; |
| } else if (maxHeight) { |
| |
|
|
| if (height <= maxHeight) { |
| resolve(imageUrl); |
| return; |
| } |
|
|
| width = Math.round((maxHeight * width) / height); |
| height = maxHeight; |
| } |
|
|
| const mimeType = imageUrl.match(/^data:([^;]+);/)?.[1] ?? 'image/jpeg'; |
| resolve(await resizeImageToDataURL(img, width, height, mimeType)); |
| }; |
| img.onerror = (error) => reject(error); |
| img.src = imageUrl; |
| }); |
| }; |
| export const generateInitialsImage = (name) => { |
| const canvas = document.createElement('canvas'); |
| const ctx = canvas.getContext('2d'); |
| canvas.width = 100; |
| canvas.height = 100; |
|
|
| if (!canvasPixelTest()) { |
| console.log( |
| 'generateInitialsImage: failed pixel test, fingerprint evasion is likely. Using default image.' |
| ); |
| return `${WEBUI_BASE_URL}/user.png`; |
| } |
|
|
| ctx.fillStyle = '#F39C12'; |
| ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
|
| ctx.fillStyle = '#FFFFFF'; |
| ctx.font = '40px Helvetica'; |
| ctx.textAlign = 'center'; |
| ctx.textBaseline = 'middle'; |
|
|
| const sanitizedName = name.trim(); |
| const initials = |
| sanitizedName.length > 0 |
| ? sanitizedName[0] + |
| (sanitizedName.split(' ').length > 1 |
| ? sanitizedName[sanitizedName.lastIndexOf(' ') + 1] |
| : '') |
| : ''; |
|
|
| ctx.fillText(initials.toUpperCase(), canvas.width / 2, canvas.height / 2); |
|
|
| return canvas.toDataURL(); |
| }; |
|
|
| export const formatDate = (inputDate) => { |
| const date = dayjs(inputDate); |
|
|
| if (date.isToday()) { |
| return `Today at {{LOCALIZED_TIME}}`; |
| } else if (date.isYesterday()) { |
| return `Yesterday at {{LOCALIZED_TIME}}`; |
| } else { |
| return `{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}`; |
| } |
| }; |
|
|
| export const copyToClipboard = async (text, html = null, formatted = false) => { |
| if (formatted) { |
| 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)); |
| |
|
|
| const htmlContent = marked.parse(text); |
|
|
| |
| styledHtml = ` |
| <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> |
| `; |
| } else { |
| |
| styledHtml = html; |
| } |
|
|
| |
| const blob = new Blob([styledHtml], { type: 'text/html' }); |
|
|
| try { |
| |
| const data = new ClipboardItem({ |
| 'text/html': blob, |
| 'text/plain': new Blob([text], { type: 'text/plain' }) |
| }); |
|
|
| |
| await navigator.clipboard.write([data]); |
| return true; |
| } catch (err) { |
| console.error('Error copying formatted content:', err); |
| |
| return await copyToClipboard(text); |
| } |
| } else { |
| let result = false; |
| if (!navigator.clipboard) { |
| const span = document.createElement('span'); |
| span.textContent = text; |
| span.style.whiteSpace = 'pre'; |
| span.style.position = 'fixed'; |
| span.style.top = '0'; |
| span.style.left = '0'; |
| span.style.opacity = '0'; |
| document.body.appendChild(span); |
|
|
| const range = document.createRange(); |
| range.selectNodeContents(span); |
| const selection = window.getSelection(); |
| selection?.removeAllRanges(); |
| selection?.addRange(range); |
|
|
| try { |
| const successful = document.execCommand('copy'); |
| const msg = successful ? 'successful' : 'unsuccessful'; |
| console.log('Fallback: Copying text command was ' + msg); |
| result = successful; |
| } catch (err) { |
| console.error('Fallback: Oops, unable to copy', err); |
| } |
|
|
| selection?.removeAllRanges(); |
| document.body.removeChild(span); |
| return result; |
| } |
|
|
| 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; |
| }); |
|
|
| return result; |
| } |
| }; |
|
|
| export const compareVersion = (latest, current) => { |
| return current === '0.0.0' |
| ? false |
| : current.localeCompare(latest, undefined, { |
| numeric: true, |
| sensitivity: 'case', |
| caseFirst: 'upper' |
| }) < 0; |
| }; |
|
|
| export const extractCurlyBraceWords = (text) => { |
| const regex = /\{\{([^}]+)\}\}/g; |
| const matches = []; |
| let match; |
|
|
| while ((match = regex.exec(text)) !== null) { |
| matches.push({ |
| word: match[1].trim(), |
| startIndex: match.index, |
| endIndex: regex.lastIndex - 1 |
| }); |
| } |
|
|
| return matches; |
| }; |
|
|
| export const removeLastWordFromString = (inputString, wordString) => { |
| console.log('inputString', inputString); |
| |
| const lines = inputString.split('\n'); |
|
|
| |
| const lastLine = lines.pop(); |
|
|
| |
| const words = lastLine.split(' '); |
|
|
| |
| if (words.at(-1) === wordString || (wordString === '' && words.at(-1) === '\\#')) { |
| words.pop(); |
| } |
|
|
| |
| let updatedLastLine = words.join(' '); |
|
|
| |
| if (updatedLastLine !== '') { |
| updatedLastLine += ' '; |
| } |
|
|
| |
| const resultString = [...lines, updatedLastLine].join('\n'); |
|
|
| |
| console.log('resultString', resultString); |
|
|
| return resultString; |
| }; |
|
|
| export const removeFirstHashWord = (inputString) => { |
| |
| const words = inputString.split(' '); |
|
|
| |
| const index = words.findIndex((word) => word.startsWith('#')); |
|
|
| |
| if (index !== -1) { |
| words.splice(index, 1); |
| } |
|
|
| |
| const resultString = words.join(' '); |
|
|
| return resultString; |
| }; |
|
|
| export const transformFileName = (fileName) => { |
| |
| const lowerCaseFileName = fileName.toLowerCase(); |
|
|
| |
| const sanitizedFileName = lowerCaseFileName.replace(/[^\w\s]/g, ''); |
|
|
| |
| const finalFileName = sanitizedFileName.replace(/\s+/g, '-'); |
|
|
| return finalFileName; |
| }; |
|
|
| export const calculateSHA256 = async (file) => { |
| |
| const reader = new FileReader(); |
|
|
| |
| const readFile = new Promise((resolve, reject) => { |
| reader.onload = () => resolve(reader.result); |
| reader.onerror = reject; |
| }); |
|
|
| |
| reader.readAsArrayBuffer(file); |
|
|
| try { |
| |
| const buffer = await readFile; |
|
|
| |
| const uint8Array = new Uint8Array(buffer); |
|
|
| |
| const hashBuffer = await crypto.subtle.digest('SHA-256', uint8Array); |
|
|
| |
| const hashArray = Array.from(new Uint8Array(hashBuffer)); |
| const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join(''); |
|
|
| return `${hashHex}`; |
| } catch (error) { |
| console.error('Error calculating SHA-256 hash:', error); |
| throw error; |
| } |
| }; |
|
|
| export const getImportOrigin = (_chats) => { |
| |
| |
| |
| if (_chats.some((chat) => 'mapping' in chat)) { |
| return 'openai'; |
| } |
| return 'webui'; |
| }; |
|
|
| export const getUserPosition = async (raw = false) => { |
| |
| 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'; |
| } |
|
|
| |
| const { latitude, longitude } = position.coords; |
|
|
| if (raw) { |
| return { latitude, longitude }; |
| } else { |
| return `${latitude.toFixed(3)}, ${longitude.toFixed(3)} (lat, long)`; |
| } |
| }; |
|
|
| const extractOpenAIMessageContent = (message): string => { |
| |
| |
| try { |
| const parts = message?.['content']?.['parts']; |
| if (Array.isArray(parts)) { |
| const textParts = parts.filter((p) => typeof p === 'string'); |
| if (textParts.length > 0) return textParts.join('\n'); |
| } |
| return message?.['content']?.['text'] || ''; |
| } catch { |
| return ''; |
| } |
| }; |
|
|
| const convertOpenAIMessages = (convo) => { |
| |
| const mapping = convo['mapping']; |
| const messages = []; |
| let currentId = ''; |
| let lastId = null; |
| const uniqueModels = new Set<string>(); |
|
|
| for (const message_id in mapping) { |
| const message = mapping[message_id]; |
| currentId = message_id; |
| try { |
| if ( |
| messages.length == 0 && |
| (message['message'] == null || |
| (message['message']['content']['parts']?.[0] == '' && |
| message['message']['content']['text'] == null)) |
| ) { |
| |
| continue; |
| } else { |
| const role = message['message']?.['author']?.['role']; |
| |
| if (role === 'system' || role === 'tool') { |
| continue; |
| } |
|
|
| const model = message['message']?.['metadata']?.['model_slug'] || 'gpt-3.5-turbo'; |
| const timestamp = message['message']?.['create_time'] |
| ? Math.floor(message['message']['create_time']) |
| : undefined; |
|
|
| const new_chat: Record<string, any> = { |
| id: message_id, |
| parentId: lastId, |
| childrenIds: message['children'] || [], |
| role: role !== 'user' ? 'assistant' : 'user', |
| content: extractOpenAIMessageContent(message['message']), |
| model, |
| done: true, |
| context: null, |
| ...(timestamp !== undefined && { timestamp }) |
| }; |
|
|
| if (role !== 'user') { |
| uniqueModels.add(model); |
| } |
|
|
| messages.push(new_chat); |
| lastId = currentId; |
| } |
| } catch (error) { |
| console.log('Error with', message, '\nError:', error); |
| } |
| } |
|
|
| |
| |
| if (messages.length > 0) { |
| messages[messages.length - 1].childrenIds = []; |
| } |
|
|
| const history: Record<PropertyKey, (typeof messages)[number]> = {}; |
| messages.forEach((obj) => (history[obj.id] = obj)); |
|
|
| const chat = { |
| history: { |
| currentId: currentId, |
| messages: history |
| }, |
| models: uniqueModels.size > 0 ? [...uniqueModels] : ['gpt-3.5-turbo'], |
| messages: messages, |
| options: {}, |
| timestamp: convo['create_time'], |
| title: convo['title'] ?? 'New Chat' |
| }; |
| return chat; |
| }; |
|
|
| const validateChat = (chat) => { |
| |
| const messages = chat.messages; |
|
|
| |
| if (messages.length === 0) { |
| return false; |
| } |
|
|
| |
| for (const message of messages) { |
| if (typeof message.content !== 'string') { |
| return false; |
| } |
| } |
|
|
| return true; |
| }; |
|
|
| export const convertOpenAIChats = (_chats) => { |
| |
| const chats = []; |
| let failed = 0; |
| let skipped = 0; |
| for (const convo of _chats) { |
| |
| if (!('mapping' in convo)) { |
| skipped++; |
| console.log( |
| 'Skipping non-conversation entry (folder/project):', |
| convo['title'] ?? convo['id'] |
| ); |
| continue; |
| } |
|
|
| const chat = convertOpenAIMessages(convo); |
|
|
| if (validateChat(chat)) { |
| |
| |
| const createdAt = convo['create_time'] ? Math.floor(convo['create_time']) : null; |
| const updatedAt = convo['update_time'] ? Math.floor(convo['update_time']) : createdAt; |
| chats.push({ |
| id: convo['id'], |
| user_id: '', |
| title: convo['title'], |
| chat: chat, |
| created_at: createdAt, |
| updated_at: updatedAt |
| }); |
| } else { |
| failed++; |
| } |
| } |
| console.log(failed, 'Conversations could not be imported'); |
| if (skipped > 0) { |
| console.log(skipped, 'Non-conversation entries (folders/projects) were skipped'); |
| } |
| return chats; |
| }; |
|
|
| export const isValidHttpUrl = (string: string) => { |
| let url; |
|
|
| try { |
| url = new URL(string); |
| } catch (_) { |
| return false; |
| } |
|
|
| return url.protocol === 'http:' || url.protocol === 'https:'; |
| }; |
|
|
| export const isYoutubeUrl = (url: string) => { |
| return ( |
| url.startsWith('https://www.youtube.com') || |
| url.startsWith('https://youtu.be') || |
| url.startsWith('https://youtube.com') || |
| url.startsWith('https://m.youtube.com') |
| ); |
| }; |
|
|
| export const removeEmojis = (str: string) => { |
| |
| |
| |
| |
| |
| const emojiRegex = /\p{RGI_Emoji}/gv; |
|
|
| |
| return str.replace(emojiRegex, ''); |
| }; |
|
|
| export const removeFormattings = (str: string) => { |
| return ( |
| str |
| |
| .replace(/(```[\s\S]*?```)/g, '') |
| .replace(/^\|.*\|$/gm, '') |
| |
| .replace(/(?:\*\*|__)(.*?)(?:\*\*|__)/g, '$1') |
| .replace(/(?:[*_])(.*?)(?:[*_])/g, '$1') |
| .replace(/~~(.*?)~~/g, '$1') |
| .replace(/`([^`]+)`/g, '$1') |
|
|
| |
| .replace(/!?\[([^\]]*)\](?:\([^)]+\)|\[[^\]]*\])/g, '$1') |
| .replace(/^\[[^\]]+\]:\s*.*$/gm, '') |
|
|
| |
| .replace(/^#{1,6}\s+/gm, '') |
| .replace(/^\s*[-*+]\s+/gm, '') |
| .replace(/^\s*(?:\d+\.)\s+/gm, '') |
| .replace(/^\s*>[> ]*/gm, '') |
| .replace(/^\s*:\s+/gm, '') |
|
|
| |
| .replace(/\[\^[^\]]*\]/g, '') |
| .replace(/\n{2,}/g, '\n') |
| ); |
| }; |
|
|
| export const cleanText = (content: string) => { |
| return removeFormattings(removeEmojis(content.trim())); |
| }; |
|
|
| export const removeDetails = (content, types) => { |
| return replaceOutsideCode(content, (segment) => { |
| for (const type of types) { |
| segment = segment.replace( |
| new RegExp(`<details\\s+type="${type}"[^>]*>.*?<\\/details>`, 'gis'), |
| '' |
| ); |
| } |
| return segment; |
| }).trim(); |
| }; |
|
|
| export const removeAllDetails = (content) => { |
| |
| |
| |
| |
| content = content.replace(/<details[^>]*>[\s\S]*?<\/details>/gi, ''); |
| |
| return replaceOutsideCode(content, (segment) => { |
| return segment.replace(/<details[^>]*>.*?<\/details>/gis, ''); |
| }).trim(); |
| }; |
|
|
| export const processDetails = (content) => { |
| content = removeDetails(content, ['reasoning', 'code_interpreter']); |
|
|
| |
| 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]; |
| } |
|
|
| |
| let resultText = ''; |
| if (attributes.result) { |
| resultText = unescapeHtml(attributes.result); |
| } else { |
| |
| const bodyMatch = match.match(/<summary>[\s\S]*?<\/summary>\s*([\s\S]*?)\s*<\/details>/i); |
| if (bodyMatch && bodyMatch[1].trim()) { |
| resultText = unescapeHtml(bodyMatch[1].trim()); |
| } |
| } |
| if (resultText) { |
| content = content.replace(match, resultText); |
| } |
| } |
| } |
|
|
| return content; |
| }; |
|
|
| |
| const codeBlockRegex = /```[\s\S]*?```/g; |
|
|
| export const extractSentences = (text: string) => { |
| const codeBlocks: string[] = []; |
| let index = 0; |
|
|
| |
| text = text.replace(codeBlockRegex, (match) => { |
| const placeholder = `\u0000${index}\u0000`; |
| codeBlocks[index++] = match; |
| return placeholder; |
| }); |
|
|
| |
| let sentences = text.split(/(?<=[.!?])\s+|\n+/); |
|
|
| |
| sentences = sentences.map((sentence) => { |
| |
| return sentence.replace(/\u0000(\d+)\u0000/g, (_, idx) => codeBlocks[idx]); |
| }); |
|
|
| return sentences.map(cleanText).filter(Boolean); |
| }; |
|
|
| export const extractParagraphsForAudio = (text: string) => { |
| const codeBlocks: string[] = []; |
| let index = 0; |
|
|
| |
| text = text.replace(codeBlockRegex, (match) => { |
| const placeholder = `\u0000${index}\u0000`; |
| codeBlocks[index++] = match; |
| return placeholder; |
| }); |
|
|
| |
| let paragraphs = text.split(/\n+/); |
|
|
| |
| paragraphs = paragraphs.map((paragraph) => { |
| |
| return paragraph.replace(/\u0000(\d+)\u0000/g, (_, idx) => codeBlocks[idx]); |
| }); |
|
|
| return paragraphs.map(cleanText).filter(Boolean); |
| }; |
|
|
| export const extractSentencesForAudio = (text: string) => { |
| return extractSentences(text).reduce((mergedTexts, currentText) => { |
| const lastIndex = mergedTexts.length - 1; |
| if (lastIndex >= 0) { |
| const previousText = mergedTexts[lastIndex]; |
| const wordCount = previousText.split(/\s+/).length; |
| const charCount = previousText.length; |
| if (wordCount < 4 || charCount < 50) { |
| mergedTexts[lastIndex] = previousText + ' ' + currentText; |
| } else { |
| mergedTexts.push(currentText); |
| } |
| } else { |
| mergedTexts.push(currentText); |
| } |
| return mergedTexts; |
| }, [] as string[]); |
| }; |
|
|
| export const getMessageContentParts = (content: string, splitOn: string = 'punctuation') => { |
| |
| |
| |
| |
| |
| |
| |
| |
| content = content.replace(/<details[^>]*>[\s\S]*?<\/details>/gi, ''); |
|
|
| const messageContentParts: string[] = []; |
|
|
| switch (splitOn) { |
| 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; |
| }; |
|
|
| export const blobToFile = (blob, fileName) => { |
| |
| const file = new File([blob], fileName, { type: blob.type }); |
| return file; |
| }; |
|
|
| export const getPromptVariables = (user_name, user_location, user_email = '') => { |
| return { |
| '{{USER_NAME}}': user_name, |
| '{{USER_EMAIL}}': user_email || 'Unknown', |
| '{{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' |
| }; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export const titleGenerationTemplate = (template: string, prompt: string): string => { |
| template = template.replace( |
| /{{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 ''; |
| } |
| ); |
|
|
| return template; |
| }; |
|
|
| export const approximateToHumanReadable = (nanoseconds: number) => { |
| const seconds = Math.floor((nanoseconds / 1e9) % 60); |
| const minutes = Math.floor((nanoseconds / 6e10) % 60); |
| const hours = Math.floor((nanoseconds / 3.6e12) % 24); |
|
|
| 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(' '); |
| }; |
|
|
| |
| const MONTH_NAMES = [ |
| 'January', |
| 'February', |
| 'March', |
| 'April', |
| 'May', |
| 'June', |
| 'July', |
| 'August', |
| 'September', |
| 'October', |
| 'November', |
| 'December' |
| ]; |
|
|
| export const getTimeRange = (timestamp) => { |
| const now = new Date(); |
| const date = new Date(timestamp * 1000); |
|
|
| |
| const diffTime = now.getTime() - date.getTime(); |
| const diffDays = diffTime / (1000 * 3600 * 24); |
|
|
| 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) { |
| return 'Today'; |
| } else if (nowYear === dateYear && nowMonth === dateMonth && nowDate - dateDate === 1) { |
| return 'Yesterday'; |
| } else if (diffDays <= 7) { |
| return 'Previous 7 days'; |
| } else if (diffDays <= 30) { |
| return 'Previous 30 days'; |
| } else if (nowYear === dateYear) { |
| return MONTH_NAMES[dateMonth]; |
| } else { |
| return date.getFullYear().toString(); |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| export const extractFrontmatter = (content) => { |
| const frontmatter = {}; |
| let frontmatterStarted = false; |
| let frontmatterEnded = false; |
| const frontmatterPattern = /^\s*([a-z_]+):\s*(.*)\s*$/i; |
|
|
| |
| const lines = content.split('\n'); |
|
|
| |
| 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; |
| }; |
|
|
| |
| export const bestMatchingLanguage = (supportedLanguages, preferredLanguages, defaultLocale) => { |
| const languages = supportedLanguages.map((lang) => lang.code); |
|
|
| const match = preferredLanguages |
| .map((prefLang) => languages.find((lang) => lang.startsWith(prefLang))) |
| .find(Boolean); |
|
|
| return match || defaultLocale; |
| }; |
|
|
| |
| export const getFormattedDate = () => { |
| const date = new Date(); |
| 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}`; |
| }; |
|
|
| |
| export const getFormattedTime = () => { |
| const date = new Date(); |
| return date.toTimeString().split(' ')[0]; |
| }; |
|
|
| |
| export const getCurrentDateTime = () => { |
| return `${getFormattedDate()} ${getFormattedTime()}`; |
| }; |
|
|
| |
| export const getUserTimezone = () => { |
| return Intl.DateTimeFormat().resolvedOptions().timeZone; |
| }; |
|
|
| |
| export const getWeekday = () => { |
| const date = new Date(); |
| const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; |
| return weekdays[date.getDay()]; |
| }; |
|
|
| export const createMessagesList = (history, messageId) => { |
| const list = []; |
| let currentId = messageId; |
|
|
| while (currentId !== null && currentId !== undefined) { |
| const message = history.messages[currentId]; |
| if (message === undefined) { |
| break; |
| } |
| list.push(message); |
| currentId = message.parentId; |
| } |
|
|
| return list.reverse(); |
| }; |
|
|
| 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) => { |
| console.log(typeof text); |
| return text ? text.split('\n').length : 0; |
| }; |
|
|
| |
| 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)) { |
| |
| 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: |
| |
| break; |
| } |
|
|
| |
| for (const keyword of ['oneOf', 'anyOf', 'allOf']) { |
| if (Array.isArray(schemaRef[keyword])) { |
| schemaObj[keyword] = schemaRef[keyword].map((inner) => |
| resolveSchema(inner, components, resolvedSchemas) |
| ); |
| } |
| } |
|
|
| return schemaObj; |
| } |
|
|
| |
| const compositionObj: Record<string, any> = {}; |
| let hasComposition = false; |
| for (const keyword of ['oneOf', 'anyOf', 'allOf']) { |
| if (Array.isArray(schemaRef[keyword])) { |
| compositionObj[keyword] = schemaRef[keyword].map((inner) => |
| resolveSchema(inner, components, resolvedSchemas) |
| ); |
| hasComposition = true; |
| } |
| } |
| if (hasComposition) { |
| if (schemaRef.description) compositionObj.description = schemaRef.description; |
| return compositionObj; |
| } |
|
|
| |
| return {}; |
| } |
|
|
| |
| |
| const OPENAPI_HTTP_METHODS = new Set([ |
| 'get', |
| 'put', |
| 'post', |
| 'delete', |
| 'options', |
| 'head', |
| 'patch', |
| 'trace' |
| ]); |
|
|
| |
| export const convertOpenApiToToolPayload = (openApiSpec) => { |
| const toolPayload = []; |
|
|
| |
| if (!openApiSpec || !openApiSpec.paths) { |
| return toolPayload; |
| } |
|
|
| for (const [path, methods] of Object.entries(openApiSpec.paths)) { |
| if (!methods || typeof methods !== 'object') continue; |
|
|
| |
| |
| const pathLevelParams: any[] = Array.isArray((methods as any).parameters) |
| ? (methods as any).parameters |
| : []; |
|
|
| for (const [method, operation] of Object.entries(methods)) { |
| if (!OPENAPI_HTTP_METHODS.has(method)) continue; |
| if (!operation || typeof operation !== 'object') continue; |
| if ((operation as any)?.operationId) { |
| const tool = { |
| name: (operation as any).operationId, |
| description: |
| (operation as any).description || |
| (operation as any).summary || |
| 'No description available.', |
| parameters: { |
| type: 'object', |
| properties: {}, |
| required: [] |
| } |
| }; |
|
|
| |
| |
| |
| const opParams: any[] = Array.isArray((operation as any).parameters) |
| ? (operation as any).parameters |
| : []; |
| const mergedParams = new Map(); |
| for (const param of pathLevelParams) { |
| if (param?.name) mergedParams.set(`${param.name}:${param.in ?? ''}`, param); |
| } |
| for (const param of opParams) { |
| if (param?.name) mergedParams.set(`${param.name}:${param.in ?? ''}`, param); |
| } |
|
|
| |
| for (const param of mergedParams.values()) { |
| const paramName = param?.name; |
| if (!paramName) continue; |
| const paramSchema = param?.schema ?? {}; |
| let description = paramSchema.description || param.description || ''; |
| if (paramSchema.enum && Array.isArray(paramSchema.enum)) { |
| description += `. Possible values: ${paramSchema.enum.join(', ')}`; |
| } |
| tool.parameters.properties[paramName] = { |
| type: paramSchema.type, |
| description: description |
| }; |
|
|
| if (param.required) { |
| tool.parameters.required.push(paramName); |
| } |
| } |
|
|
| |
| if ((operation as any).requestBody) { |
| const content = (operation as any).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; |
| } |
| } |
| } |
|
|
| toolPayload.push(tool); |
| } |
| } |
| } |
|
|
| return toolPayload; |
| }; |
|
|
| export const slugify = (str: string): string => { |
| return ( |
| str |
| |
| .normalize('NFD') |
| |
| .replace(/[\u0300-\u036f]/g, '') |
| |
| .replace(/\s+/g, '-') |
| |
| .replace(/[^a-zA-Z0-9-_]/g, '') |
| |
| .toLowerCase() |
| ); |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export const nameToId = (name: string): string => { |
| return name |
| .normalize('NFD') |
| .replace(/[\u0300-\u036f]/g, '') |
| .replace(/[^\w]+/g, '_') |
| .replace(/^_+|_+$/g, '') |
| .toLowerCase(); |
| }; |
|
|
| 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; |
| |
| while ((match = regex.exec(text)) !== null) { |
| const varName = match[1].trim(); |
| const definition = match[2].trim(); |
| variables[varName] = parseVariableDefinition(definition); |
| } |
| |
| while ((match = regularRegex.exec(text)) !== null) { |
| const varName = match[1].trim(); |
| |
| if (!variables.hasOwnProperty(varName)) { |
| variables[varName] = { type: 'text' }; |
| } |
| } |
| 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> => { |
| |
| const parts = splitProperties(definition, ':'); |
| const [firstPart, ...propertyParts] = parts; |
|
|
| |
| const type = firstPart.startsWith('type=') ? firstPart.slice(5) : firstPart; |
|
|
| |
| const properties = propertyParts.reduce( |
| (props, part) => { |
| const trimmed = part.trim(); |
| if (!trimmed) return props; |
|
|
| |
| const equalsParts = splitProperties(trimmed, '='); |
|
|
| if (equalsParts.length === 1) { |
| |
| const flagName = equalsParts[0].trim(); |
| if (flagName.length > 0) { |
| return { ...props, [flagName]: true }; |
| } |
| return props; |
| } |
|
|
| const [propertyName, ...valueParts] = equalsParts; |
| const propertyValueRaw = valueParts.join('='); |
|
|
| if (!propertyName || propertyValueRaw == null) return props; |
|
|
| return { |
| ...props, |
| [propertyName.trim()]: parseJsonValue(propertyValueRaw.trim()) |
| }; |
| }, |
| {} as Record<string, any> |
| ); |
|
|
| return { type, ...properties }; |
| }; |
| export const parseJsonValue = (value: string): any => { |
| |
| if (value.startsWith('"') && value.endsWith('"')) { |
| return value.slice(1, -1); |
| } |
|
|
| |
| if (/^[\[{]/.test(value)) { |
| try { |
| return JSON.parse(value); |
| } catch { |
| return value; |
| } |
| } |
|
|
| return value; |
| }; |
|
|
| type ReadableStreamWithAsyncIterator<T> = ReadableStream<T> & { |
| [Symbol.asyncIterator]?: () => AsyncIterableIterator<T>; |
| }; |
|
|
| function ensureReadableStreamAsyncIterator() { |
| if (typeof ReadableStream === 'undefined') { |
| return; |
| } |
|
|
| const prototype = ReadableStream.prototype as ReadableStreamWithAsyncIterator<unknown>; |
| if (prototype[Symbol.asyncIterator]) { |
| return; |
| } |
|
|
| Object.defineProperty(prototype, Symbol.asyncIterator, { |
| value: async function* (this: ReadableStream<unknown>) { |
| const reader = this.getReader(); |
| try { |
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) { |
| return; |
| } |
| yield value; |
| } |
| } finally { |
| reader.releaseLock(); |
| } |
| }, |
| configurable: true, |
| writable: true |
| }); |
| } |
|
|
| async function ensurePDFjsLoaded() { |
| ensureReadableStreamAsyncIterator(); |
| 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) => { |
| |
| const textExtensions = [ |
| '.txt', |
| '.md', |
| '.csv', |
| '.json', |
| '.js', |
| '.ts', |
| '.css', |
| '.html', |
| '.xml', |
| '.yaml', |
| '.yml', |
| '.rtf' |
| ]; |
|
|
| function getExtension(filename: string) { |
| const dot = filename.lastIndexOf('.'); |
| return dot === -1 ? '' : filename.substr(dot).toLowerCase(); |
| } |
|
|
| |
| async function extractPdfText(file: File) { |
| const pdfjsLib = await ensurePDFjsLoaded(); |
| 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(); |
| const strings = content.items.map((item: any) => item.str); |
| allText += strings.join(' ') + '\n'; |
| } |
| return allText; |
| } |
|
|
| |
| function readAsText(file: File) { |
| return new Promise((resolve, reject) => { |
| const reader = new FileReader(); |
| reader.onload = () => resolve(reader.result); |
| reader.onerror = reject; |
| reader.readAsText(file); |
| }); |
| } |
|
|
| async function extractDocxText(file: File) { |
| const [arrayBuffer, { default: mammoth }] = await Promise.all([ |
| file.arrayBuffer(), |
| import('mammoth') |
| ]); |
| const result = await mammoth.extractRawText({ arrayBuffer }); |
| return result.value; |
| } |
|
|
| const type = file.type || ''; |
| const ext = getExtension(file.name); |
|
|
| |
| if (type === 'application/pdf' || ext === '.pdf') { |
| return await extractPdfText(file); |
| } |
|
|
| |
| if ( |
| type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || |
| ext === '.docx' |
| ) { |
| return await extractDocxText(file); |
| } |
|
|
| |
| if (type.startsWith('text/') || textExtensions.includes(ext)) { |
| return await readAsText(file); |
| } |
|
|
| |
| try { |
| return await readAsText(file); |
| } catch (err) { |
| throw new Error('Unsupported or non-text file type: ' + (file.name || type)); |
| } |
| }; |
|
|
| 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(); |
| }; |
|
|
| 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; |
| } |
| }; |
|
|
| export const decodeString = (str: string) => { |
| try { |
| return decodeURIComponent(str); |
| } catch (e) { |
| return str; |
| } |
| }; |
|
|
| export const initMermaid = async () => { |
| const { default: mermaid } = await import('mermaid'); |
| mermaid.initialize({ |
| startOnLoad: false, |
| theme: document.documentElement.classList.contains('dark') ? 'dark' : 'default', |
| securityLevel: 'loose' |
| }); |
| return mermaid; |
| }; |
|
|
| const cleanupMermaidTempElements = (id: string) => { |
| if (typeof document === 'undefined') { |
| return; |
| } |
|
|
| document.getElementById(id)?.remove(); |
| document.getElementById(`d${id}`)?.remove(); |
| document.getElementById(`i${id}`)?.remove(); |
| }; |
|
|
| |
| |
| export const sanitizeSvg = (svg: string): string => |
| DOMPurify.sanitize(svg, { |
| USE_PROFILES: { svg: true, svgFilters: true }, |
| WHOLE_DOCUMENT: false, |
| ADD_TAGS: ['style', 'foreignObject'], |
| ADD_ATTR: [ |
| 'class', |
| 'style', |
| 'id', |
| 'data-*', |
| 'viewBox', |
| 'preserveAspectRatio', |
| 'markerWidth', |
| 'markerHeight', |
| 'markerUnits', |
| 'refX', |
| 'refY', |
| 'orient', |
| 'href', |
| 'xlink:href', |
| 'dominant-baseline', |
| 'text-anchor', |
| 'clipPathUnits', |
| 'filterUnits', |
| 'patternUnits', |
| 'patternContentUnits', |
| 'maskUnits', |
| 'role', |
| 'aria-label', |
| 'aria-labelledby', |
| 'aria-hidden', |
| 'tabindex' |
| ], |
| SANITIZE_DOM: true |
| }); |
|
|
| export const renderMermaidDiagram = async ( |
| mermaid: typeof import('mermaid').default, |
| code: string, |
| renderId?: string |
| ) => { |
| const id = renderId ?? `mermaid-${uuidv4()}`; |
| try { |
| const parseResult = await mermaid.parse(code, { suppressErrors: false }); |
| if (parseResult) { |
| const { svg } = await mermaid.render(id, code); |
| return sanitizeSvg(svg); |
| } |
| return ''; |
| } finally { |
| |
| cleanupMermaidTempElements(id); |
| } |
| }; |
|
|
| export const renderVegaVisualization = async (spec: string, i18n?: any) => { |
| const vega = await import('vega'); |
| const parsedSpec = JSON.parse(spec); |
| let vegaSpec = parsedSpec; |
| if (parsedSpec.$schema && parsedSpec.$schema.includes('vega-lite')) { |
| const vegaLite = await import('vega-lite'); |
| vegaSpec = vegaLite.compile(parsedSpec).spec; |
| } |
| const view = new vega.View(vega.parse(vegaSpec), { renderer: 'none' }); |
| const svg = await view.toSVG(); |
| return svg; |
| }; |
|
|
| export const getCodeBlockContents = (content: string): object => { |
| |
| |
| content = removeAllDetails(content); |
|
|
| const codeBlockContents = content.match(/```[\s\S]*?```/g); |
|
|
| let codeBlocks = []; |
|
|
| |
| |
| |
| |
| |
| let htmlGroups: Array<{ html: string; css: string; js: string }> = []; |
|
|
| const initDefaultGroup = () => { |
| if (htmlGroups.length === 0) { |
| htmlGroups.push({ html: '', css: '', js: '' }); |
| } |
| }; |
|
|
| if (codeBlockContents) { |
| codeBlockContents.forEach((block) => { |
| const lang = block.split('\n')[0].replace('```', '').trim().toLowerCase(); |
| const code = block.replace(/```[\s\S]*?\n/, '').replace(/```$/, ''); |
| codeBlocks.push({ lang, code }); |
| }); |
|
|
| codeBlocks.forEach((block) => { |
| const { lang, code } = block; |
|
|
| if (lang === 'html') { |
| |
| htmlGroups.push({ html: code + '\n', css: '', js: '' }); |
| } else if (lang === 'css') { |
| initDefaultGroup(); |
| htmlGroups[htmlGroups.length - 1].css += code + '\n'; |
| } else if (lang === 'javascript' || lang === 'js') { |
| initDefaultGroup(); |
| htmlGroups[htmlGroups.length - 1].js += code + '\n'; |
| } |
| }); |
| } else { |
| |
| |
| content = removeAllDetails(content); |
|
|
| const inlineHtml = content.match(/<html>[\s\S]*?<\/html>/gi); |
| const inlineCss = content.match(/<style>[\s\S]*?<\/style>/gi); |
| const inlineJs = content.match(/<script>[\s\S]*?<\/script>/gi); |
|
|
| if (inlineHtml) { |
| inlineHtml.forEach((block) => { |
| const content = block.replace(/<\/?html>/gi, ''); |
| htmlGroups.push({ html: content + '\n', css: '', js: '' }); |
| }); |
| } |
| if (inlineCss) { |
| inlineCss.forEach((block) => { |
| const content = block.replace(/<\/?style>/gi, ''); |
| initDefaultGroup(); |
| htmlGroups[htmlGroups.length - 1].css += content + '\n'; |
| }); |
| } |
| if (inlineJs) { |
| inlineJs.forEach((block) => { |
| const content = block.replace(/<\/?script>/gi, ''); |
| initDefaultGroup(); |
| htmlGroups[htmlGroups.length - 1].js += content + '\n'; |
| }); |
| } |
| } |
|
|
| |
| const htmlContent = htmlGroups.map((g) => g.html).join(''); |
| const cssContent = htmlGroups.map((g) => g.css).join(''); |
| const jsContent = htmlGroups.map((g) => g.js).join(''); |
|
|
| return { |
| codeBlocks: codeBlocks, |
| html: htmlContent.trim(), |
| css: cssContent.trim(), |
| js: jsContent.trim(), |
| htmlGroups: htmlGroups |
| .filter((g) => g.html.trim() || g.css.trim() || g.js.trim()) |
| .map((g) => ({ |
| html: g.html.trim(), |
| css: g.css.trim(), |
| js: g.js.trim() |
| })) |
| }; |
| }; |
| export const parseFrontmatter = (content) => { |
| const match = content.match(/^---\s*\n([\s\S]*?)\n---/); |
| if (match) { |
| const frontmatter = {}; |
| match[1].split('\n').forEach((line) => { |
| const [key, ...value] = line.split(':'); |
| if (key && value) { |
| frontmatter[key.trim()] = value |
| .join(':') |
| .trim() |
| .replace(/^["']|["']$/g, ''); |
| } |
| }); |
| return frontmatter; |
| } |
| return {}; |
| }; |
|
|
| export const formatSkillName = (name) => { |
| return name.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export const displayFileHandler = ( |
| path: string, |
| stores: { showControls: Writable<boolean>; showFileNavPath: Writable<string | null> } |
| ) => { |
| if (path) { |
| stores.showControls.set(true); |
| stores.showFileNavPath.set(path); |
| } |
| }; |
|
|