| import { v4 as uuidv4 } from 'uuid'; |
| import sha256 from 'js-sha256'; |
| 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'; |
|
|
| |
| |
| |
|
|
| 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) => { |
| 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 { |
| |
| 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) { |
| const doc = new DOMParser().parseFromString(html, 'text/html'); |
| return doc.documentElement.textContent; |
| } |
|
|
| 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 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; |
| }; |
|
|
| 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; |
|
|
| |
|
|
| 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; |
| } |
|
|
| canvas.width = width; |
| canvas.height = height; |
|
|
| const context = canvas.getContext('2d'); |
| context.drawImage(img, 0, 0, width, height); |
|
|
| |
| const mimeType = imageUrl.match(/^data:([^;]+);/)?.[1]; |
| const compressedUrl = canvas.toDataURL(mimeType); |
| resolve(compressedUrl); |
| }; |
| 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 textArea = document.createElement('textarea'); |
| textArea.value = text; |
|
|
| |
| 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; |
| } |
|
|
| 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 ('mapping' in _chats[0]) { |
| 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 convertOpenAIMessages = (convo) => { |
| |
| const mapping = convo['mapping']; |
| const messages = []; |
| let currentId = ''; |
| let lastId = null; |
|
|
| 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 new_chat = { |
| id: message_id, |
| parentId: lastId, |
| childrenIds: message['children'] || [], |
| role: message['message']?.['author']?.['role'] !== 'user' ? 'assistant' : 'user', |
| content: |
| message['message']?.['content']?.['parts']?.[0] || |
| message['message']?.['content']?.['text'] || |
| '', |
| model: 'gpt-3.5-turbo', |
| done: true, |
| context: null |
| }; |
| messages.push(new_chat); |
| lastId = currentId; |
| } |
| } catch (error) { |
| console.log('Error with', message, '\nError:', error); |
| } |
| } |
|
|
| const history: Record<PropertyKey, (typeof messages)[number]> = {}; |
| messages.forEach((obj) => (history[obj.id] = obj)); |
|
|
| const chat = { |
| history: { |
| currentId: currentId, |
| messages: history |
| }, |
| models: ['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; |
| } |
|
|
| |
| const lastMessage = messages[messages.length - 1]; |
| if (lastMessage.childrenIds.length !== 0) { |
| return false; |
| } |
|
|
| |
| const firstMessage = messages[0]; |
| if (firstMessage.parentId !== null) { |
| 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; |
| for (const convo of _chats) { |
| const chat = convertOpenAIMessages(convo); |
|
|
| if (validateChat(chat)) { |
| chats.push({ |
| id: convo['id'], |
| user_id: '', |
| title: convo['title'], |
| chat: chat, |
| timestamp: convo['create_time'] |
| }); |
| } else { |
| failed++; |
| } |
| } |
| console.log(failed, 'Conversations could not be imported'); |
| 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 = /[\uD800-\uDBFF][\uDC00-\uDFFF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g; |
|
|
| |
| 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; |
| }); |
| }; |
|
|
| export const removeAllDetails = (content) => { |
| return replaceOutsideCode(content, (segment) => { |
| return segment.replace(/<details[^>]*>.*?<\/details>/gis, ''); |
| }); |
| }; |
|
|
| 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]; |
| } |
|
|
| if (attributes.result) { |
| content = content.replace(match, unescapeHtml(attributes.result)); |
| } |
| } |
| } |
|
|
| 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') => { |
| 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(' '); |
| }; |
|
|
| 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 date.toLocaleString('default', { month: 'long' }); |
| } 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) => { |
| if (messageId === null) { |
| return []; |
| } |
|
|
| const message = history.messages[messageId]; |
| if (message === undefined) { |
| return []; |
| } |
| if (message?.parentId) { |
| return [...createMessagesList(history, message.parentId), message]; |
| } else { |
| return [message]; |
| } |
| }; |
|
|
| 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; |
| } |
| return schemaObj; |
| } |
|
|
| |
| return {}; |
| } |
|
|
| |
| export const convertOpenApiToToolPayload = (openApiSpec) => { |
| const toolPayload = []; |
|
|
| |
| if (!openApiSpec || !openApiSpec.paths) { |
| return toolPayload; |
| } |
|
|
| for (const [path, methods] of Object.entries(openApiSpec.paths)) { |
| for (const [method, operation] of Object.entries(methods)) { |
| if (operation?.operationId) { |
| const tool = { |
| name: operation.operationId, |
| description: operation.description || operation.summary || 'No description available.', |
| parameters: { |
| type: 'object', |
| properties: {}, |
| required: [] |
| } |
| }; |
|
|
| |
| if (operation.parameters) { |
| operation.parameters.forEach((param) => { |
| const paramName = param?.name; |
| if (!paramName) return; |
| 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.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; |
| } |
| } |
| } |
|
|
| 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 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; |
| }; |
|
|
| 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) => { |
| |
| 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; |
| }; |
|
|
| export const renderMermaidDiagram = async (mermaid, code: string) => { |
| const parseResult = await mermaid.parse(code, { suppressErrors: false }); |
| if (parseResult) { |
| const { svg } = await mermaid.render(`mermaid-${uuidv4()}`, code); |
| return svg; |
| } |
| return ''; |
| }; |
|
|
| 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 => { |
| const codeBlockContents = content.match(/```[\s\S]*?```/g); |
|
|
| let codeBlocks = []; |
|
|
| let htmlContent = ''; |
| let cssContent = ''; |
| let jsContent = ''; |
|
|
| 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') { |
| htmlContent += code + '\n'; |
| } else if (lang === 'css') { |
| cssContent += code + '\n'; |
| } else if (lang === 'javascript' || lang === 'js') { |
| jsContent += 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, ''); |
| htmlContent += content + '\n'; |
| }); |
| } |
| if (inlineCss) { |
| inlineCss.forEach((block) => { |
| const content = block.replace(/<\/?style>/gi, ''); |
| cssContent += content + '\n'; |
| }); |
| } |
| if (inlineJs) { |
| inlineJs.forEach((block) => { |
| const content = block.replace(/<\/?script>/gi, ''); |
| jsContent += content + '\n'; |
| }); |
| } |
| } |
|
|
| return { |
| codeBlocks: codeBlocks, |
| html: htmlContent.trim(), |
| css: cssContent.trim(), |
| js: jsContent.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()); |
| }; |
|
|