import type { RequestMessage } from './client/api'; import { useEffect, useState } from 'react'; import { showToast } from './components/ui-lib'; import { ServiceProvider } from './constant'; import Locale from './locales'; // import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; import { fetch as tauriStreamFetch } from './utils/stream'; export function trimTopic(topic: string) { // Fix an issue where double quotes still show in the Indonesian language // This will remove the specified punctuation from the end of the string // and also trim quotes from both the start and end if they exist. return ( topic // fix for gemini .replace(/^["“”*]+|["“”*]+$/g, '') .replace(/[,。!?”“"、,.!?*]*$/, '') ); } export async function copyToClipboard(text: string) { try { if (window.__TAURI__) { window.__TAURI__.writeText(text); } else { await navigator.clipboard.writeText(text); } showToast(Locale.Copy.Success); } catch (error) { const textArea = document.createElement('textarea'); textArea.value = text; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); showToast(Locale.Copy.Success); } catch (error) { showToast(Locale.Copy.Failed); } document.body.removeChild(textArea); } } export async function downloadAs(text: string, filename: string) { if (window.__TAURI__) { const result = await window.__TAURI__.dialog.save({ defaultPath: `${filename}`, filters: [ { name: `${filename.split('.').pop()} files`, extensions: [`${filename.split('.').pop()}`], }, { name: 'All Files', extensions: ['*'], }, ], }); if (result !== null) { try { await window.__TAURI__.fs.writeTextFile(result, text); showToast(Locale.Download.Success); } catch (error) { showToast(Locale.Download.Failed); } } else { showToast(Locale.Download.Failed); } } else { const element = document.createElement('a'); element.setAttribute( 'href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`, ); element.setAttribute('download', filename); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); } } export function readFromFile() { return new Promise((res, rej) => { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'application/json'; fileInput.onchange = (event: any) => { const file = event.target.files[0]; const fileReader = new FileReader(); fileReader.onload = (e: any) => { res(e.target.result); }; fileReader.onerror = e => rej(e); fileReader.readAsText(file); }; fileInput.click(); }); } export function isIOS() { const userAgent = navigator.userAgent.toLowerCase(); return /iphone|ipad|ipod/.test(userAgent); } export function useWindowSize() { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight, }); useEffect(() => { const onResize = () => { setSize({ width: window.innerWidth, height: window.innerHeight, }); }; window.addEventListener('resize', onResize); return () => { window.removeEventListener('resize', onResize); }; }, []); return size; } export const MOBILE_MAX_WIDTH = 600; export function useMobileScreen() { const { width } = useWindowSize(); return width <= MOBILE_MAX_WIDTH; } export function isFirefox() { return ( typeof navigator !== 'undefined' && /firefox/i.test(navigator.userAgent) ); } export function selectOrCopy(el: HTMLElement, content: string) { const currentSelection = window.getSelection(); if (currentSelection?.type === 'Range') { return false; } copyToClipboard(content); return true; } function getDomContentWidth(dom: HTMLElement) { const style = window.getComputedStyle(dom); const paddingWidth = Number.parseFloat(style.paddingLeft) + Number.parseFloat(style.paddingRight); const width = dom.clientWidth - paddingWidth; return width; } function getOrCreateMeasureDom(id: string, init?: (dom: HTMLElement) => void) { let dom = document.getElementById(id); if (!dom) { dom = document.createElement('span'); dom.style.position = 'absolute'; dom.style.wordBreak = 'break-word'; dom.style.fontSize = '14px'; dom.style.transform = 'translateY(-200vh)'; dom.style.pointerEvents = 'none'; dom.style.opacity = '0'; dom.id = id; document.body.appendChild(dom); init?.(dom); } return dom!; } export function autoGrowTextArea(dom: HTMLTextAreaElement) { const measureDom = getOrCreateMeasureDom('__measure'); const singleLineDom = getOrCreateMeasureDom('__single_measure', (dom) => { dom.innerText = 'TEXT_FOR_MEASURE'; }); const width = getDomContentWidth(dom); measureDom.style.width = `${width}px`; measureDom.innerText = dom.value !== '' ? dom.value : '1'; measureDom.style.fontSize = dom.style.fontSize; measureDom.style.fontFamily = dom.style.fontFamily; const endWithEmptyLine = dom.value.endsWith('\n'); const height = Number.parseFloat(window.getComputedStyle(measureDom).height); const singleLineHeight = Number.parseFloat( window.getComputedStyle(singleLineDom).height, ); const rows = Math.round(height / singleLineHeight) + (endWithEmptyLine ? 1 : 0); return rows; } export function getCSSVar(varName: string) { return getComputedStyle(document.body).getPropertyValue(varName).trim(); } /** * Detects Macintosh */ export function isMacOS(): boolean { if (typeof window !== 'undefined') { const userAgent = window.navigator.userAgent.toLocaleLowerCase(); const macintosh = /iphone|ipad|ipod|macintosh/.test(userAgent); return !!macintosh; } return false; } export function getMessageTextContent(message: RequestMessage) { if (typeof message.content === 'string') { return message.content; } for (const c of message.content) { if (c.type === 'text') { return c.text ?? ''; } } return ''; } export function getMessageImages(message: RequestMessage): string[] { if (typeof message.content === 'string') { return []; } const urls: string[] = []; for (const c of message.content) { if (c.type === 'image_url') { urls.push(c.image_url?.url ?? ''); } } return urls; } export function isVisionModel(model: string) { // Note: This is a better way using the TypeScript feature instead of `&&` or `||` (ts v5.5.0-dev.20240314 I've been using) const excludeKeywords = ['claude-3-5-haiku-20241022']; const visionKeywords = [ 'vision', 'gpt-4o', 'claude-3', 'gemini-1.5', 'gemini-exp', 'learnlm', 'qwen-vl', 'qwen2-vl', ]; const isGpt4Turbo = model.includes('gpt-4-turbo') && !model.includes('preview'); return ( !excludeKeywords.some(keyword => model.includes(keyword)) && (visionKeywords.some(keyword => model.includes(keyword)) || isGpt4Turbo || isDalle3(model)) ); } export function isDalle3(model: string) { return model === 'dall-e-3'; } export function showPlugins(provider: ServiceProvider, model: string) { if ( provider === ServiceProvider.OpenAI || provider === ServiceProvider.Azure || provider === ServiceProvider.Moonshot || provider === ServiceProvider.ChatGLM ) { return true; } if (provider === ServiceProvider.Anthropic && !model.includes('claude-2')) { return true; } if (provider === ServiceProvider.Google && !model.includes('vision')) { return true; } return false; } export function fetch( url: string, options?: Record, ): Promise { if (window.__TAURI__) { return tauriStreamFetch(url, options); } return window.fetch(url, options); } export function adapter(config: Record) { const { baseURL, url, params, data: body, ...rest } = config; const path = baseURL ? `${baseURL}${url}` : url; const fetchUrl = params ? `${path}?${new URLSearchParams(params as any).toString()}` : path; return fetch(fetchUrl as string, { ...rest, body }).then((res) => { const { status, headers, statusText } = res; return res .text() .then((data: string) => ({ status, statusText, headers, data })); }); } export function safeLocalStorage(): { getItem: (key: string) => string | null; setItem: (key: string, value: string) => void; removeItem: (key: string) => void; clear: () => void; } { let storage: Storage | null; try { if (typeof window !== 'undefined' && window.localStorage) { storage = window.localStorage; } else { storage = null; } } catch (e) { console.error('localStorage is not available:', e); storage = null; } return { getItem(key: string): string | null { if (storage) { return storage.getItem(key); } else { console.warn( `Attempted to get item "${key}" from localStorage, but localStorage is not available.`, ); return null; } }, setItem(key: string, value: string): void { if (storage) { storage.setItem(key, value); } else { console.warn( `Attempted to set item "${key}" in localStorage, but localStorage is not available.`, ); } }, removeItem(key: string): void { if (storage) { storage.removeItem(key); } else { console.warn( `Attempted to remove item "${key}" from localStorage, but localStorage is not available.`, ); } }, clear(): void { if (storage) { storage.clear(); } else { console.warn( 'Attempted to clear localStorage, but localStorage is not available.', ); } }, }; } export function getOperationId(operation: { operationId?: string; method: string; path: string; }) { // pattern '^[a-zA-Z0-9_-]+$' return ( operation?.operationId || `${operation.method.toUpperCase()}${operation.path.replaceAll('/', '_')}` ); } export function clientUpdate() { // this a wild for updating client app return window.__TAURI__?.updater .checkUpdate() .then((updateResult: any) => { if (updateResult.shouldUpdate) { window.__TAURI__?.updater .installUpdate() .then((result: any) => { console.error('[Install Update result]', result); showToast(Locale.Settings.Update.Success); }) .catch((e: any) => { console.error('[Install Update Error]', e); showToast(Locale.Settings.Update.Failed); }); } }) .catch((e: any) => { console.error('[Check Update Error]', e); showToast(Locale.Settings.Update.Failed); }); } // https://gist.github.com/iwill/a83038623ba4fef6abb9efca87ae9ccb export function semverCompare(a: string, b: string) { if (a.startsWith(`${b}-`)) { return -1; } if (b.startsWith(`${a}-`)) { return 1; } return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper', }); }