Files
cherry-studio/src/renderer/src/utils/index.ts
T
2024-11-01 17:43:21 +08:00

344 lines
8.5 KiB
TypeScript

import { FileType, Model } from '@renderer/types'
import imageCompression from 'browser-image-compression'
import html2canvas from 'html2canvas'
// @ts-ignore next-line`
import { v4 as uuidv4 } from 'uuid'
import { classNames } from './style'
export const runAsyncFunction = async (fn: () => void) => {
await fn()
}
/**
* 判断字符串是否是 json 字符串
* @param str 字符串
*/
export function isJSON(str: any): boolean {
if (typeof str !== 'string') {
return false
}
try {
return typeof JSON.parse(str) === 'object'
} catch (e) {
return false
}
}
export const delay = (seconds: number) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true)
}, seconds * 1000)
})
}
/**
* Waiting fn return true
**/
export const waitAsyncFunction = (fn: () => Promise<any>, interval = 200, stopTimeout = 60000) => {
let timeout = false
const timer = setTimeout(() => (timeout = true), stopTimeout)
return (async function check(): Promise<any> {
if (await fn()) {
clearTimeout(timer)
return Promise.resolve()
} else if (!timeout) {
return delay(interval / 1000).then(check)
} else {
return Promise.resolve()
}
})()
}
export const uuid = () => uuidv4()
export const convertToBase64 = (file: File): Promise<string | ArrayBuffer | null> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result)
reader.onerror = reject
reader.readAsDataURL(file)
})
}
export const compressImage = async (file: File) => {
return await imageCompression(file, {
maxSizeMB: 1,
maxWidthOrHeight: 300,
useWebWorker: false
})
}
// Converts 'gpt-3.5-turbo-16k-0613' to 'GPT-3.5-Turbo'
// Converts 'qwen2:1.5b' to 'QWEN2'
export const getDefaultGroupName = (id: string) => {
if (id.includes(':')) {
return id.split(':')[0].toUpperCase()
}
if (id.includes('-')) {
const parts = id.split('-')
return parts[0].toUpperCase() + '-' + parts[1].toUpperCase()
}
return id.toUpperCase()
}
export function droppableReorder<T>(list: T[], startIndex: number, endIndex: number, len = 1) {
const result = Array.from(list)
const removed = result.splice(startIndex, len)
result.splice(endIndex, 0, ...removed)
return result
}
export function firstLetter(str: string): string {
const match = str?.match(/\p{L}\p{M}*|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/u)
return match ? match[0] : ''
}
export function removeLeadingEmoji(str: string): string {
const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)+/u
return str.replace(emojiRegex, '').trim()
}
export function getLeadingEmoji(str: string): string {
const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)+/u
const match = str.match(emojiRegex)
return match ? match[0] : ''
}
export function isFreeModel(model: Model) {
return (model.id + model.name).toLocaleLowerCase().includes('free')
}
export async function isProduction() {
const { isPackaged } = await window.api.getAppInfo()
return isPackaged
}
export async function isDev() {
const isProd = await isProduction()
return !isProd
}
export function getErrorMessage(error: any) {
if (!error) {
return ''
}
if (typeof error === 'string') {
return error
}
if (error?.error) {
return getErrorMessage(error.error)
}
if (error?.message) {
return error.message
}
return ''
}
export function removeQuotes(str) {
return str.replace(/['"]+/g, '')
}
export function generateColorFromChar(char: string) {
// 使用字符的Unicode值作为随机种子
const seed = char.charCodeAt(0)
// 使用简单的线性同余生成器创建伪随机数
const a = 1664525
const c = 1013904223
const m = Math.pow(2, 32)
// 生成三个伪随机数作为RGB值
let r = (a * seed + c) % m
let g = (a * r + c) % m
let b = (a * g + c) % m
// 将伪随机数转换为0-255范围内的整数
r = Math.floor((r / m) * 256)
g = Math.floor((g / m) * 256)
b = Math.floor((b / m) * 256)
// 返回十六进制颜色字符串
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
}
export function getFirstCharacter(str) {
if (str.length === 0) return ''
// 使用 for...of 循环来获取第一个字符
for (const char of str) {
return char
}
}
/**
* is valid proxy url
* @param url proxy url
* @returns boolean
*/
export const isValidProxyUrl = (url: string) => {
return url.includes('://')
}
export function loadScript(url: string) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = url
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
}
export function convertMathFormula(input) {
// 使用正则表达式匹配并替换公式格式
return input.replaceAll(/\\\[/g, '$$$$').replaceAll(/\\\]/g, '$$$$')
}
export function getBriefInfo(text: string, maxLength: number = 50): string {
// 去除空行
const noEmptyLinesText = text.replace(/\n\s*\n/g, '\n')
// 检查文本是否超过最大长度
if (noEmptyLinesText.length <= maxLength) {
return noEmptyLinesText
}
// 找到最近的单词边界
let truncatedText = noEmptyLinesText.slice(0, maxLength)
const lastSpaceIndex = truncatedText.lastIndexOf(' ')
if (lastSpaceIndex !== -1) {
truncatedText = truncatedText.slice(0, lastSpaceIndex)
}
// 截取前面的内容,并在末尾添加 "..."
return truncatedText + '...'
}
export function removeTrailingDoubleSpaces(markdown: string): string {
// 使用正则表达式匹配末尾的两个空格,并替换为空字符串
return markdown.replace(/ {2}$/gm, '')
}
export function getFileDirectory(filePath: string) {
const parts = filePath.split('/')
const directory = parts.slice(0, -1).join('/')
return directory
}
export function getFileExtension(filePath: string) {
const parts = filePath.split('.')
const extension = parts.slice(-1)[0]
return '.' + extension
}
export async function captureDiv(divRef: React.RefObject<HTMLDivElement>) {
if (divRef.current) {
try {
const canvas = await html2canvas(divRef.current)
const imageData = canvas.toDataURL('image/png')
return imageData
} catch (error) {
console.error('Error capturing div:', error)
return Promise.reject()
}
}
return Promise.resolve(undefined)
}
export const captureScrollableDiv = async (divRef: React.RefObject<HTMLDivElement>) => {
if (divRef.current) {
try {
const div = divRef.current
// 保存原始样式
const originalStyle = {
height: div.style.height,
maxHeight: div.style.maxHeight,
overflow: div.style.overflow,
position: div.style.position
}
const originalScrollTop = div.scrollTop
// 修改样式以显示全部内容
div.style.height = 'auto'
div.style.maxHeight = 'none'
div.style.overflow = 'visible'
div.style.position = 'static'
// 捕获整个内容
const canvas = await html2canvas(div, {
scrollY: -window.scrollY,
windowHeight: document.documentElement.scrollHeight
})
// 恢复原始样式
div.style.height = originalStyle.height
div.style.maxHeight = originalStyle.maxHeight
div.style.overflow = originalStyle.overflow
div.style.position = originalStyle.position
const imageData = canvas.toDataURL('image/png')
// 恢复原始滚动位置
setTimeout(() => {
div.scrollTop = originalScrollTop
}, 0)
return imageData
} catch (error) {
console.error('Error capturing scrollable div:', error)
}
}
return Promise.resolve(undefined)
}
export function hasPath(url: string): boolean {
try {
const parsedUrl = new URL(url)
return parsedUrl.pathname !== '/' && parsedUrl.pathname !== ''
} catch (error) {
console.error('Invalid URL:', error)
return false
}
}
export function formatFileSize(file: FileType) {
const size = file.size
if (size > 1024 * 1024) {
return (size / 1024 / 1024).toFixed(1) + ' MB'
}
if (size > 1024) {
return (size / 1024).toFixed(0) + ' KB'
}
return (size / 1024).toFixed(2) + ' KB'
}
export function sortByEnglishFirst(a: string, b: string) {
const isAEnglish = /^[a-zA-Z]/.test(a)
const isBEnglish = /^[a-zA-Z]/.test(b)
if (isAEnglish && !isBEnglish) return -1
if (!isAEnglish && isBEnglish) return 1
return a.localeCompare(b)
}
export { classNames }