import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } export const THREADS_MAX_CHARS = 500; /** 避免素材特殊字元讓 provider API 在組 request JSON 時解析失敗 */ export function sanitizePromptText(text: string | null | undefined): string { if (!text) return ""; return text .replace(/\r\n/g, "\n") .replace(/[\u2028\u2029]/g, "\n") .replace(/[\uD800-\uDFFF]/g, "") .replace(/\\(?!u[0-9a-fA-F]{4})/g, "\") .replace(/\\u(?![0-9a-fA-F]{4})/g, "u") .replace(/\\/g, "\") .replace(/\u0000/g, ""); } /** 安全解析 fetch 回應,避免空 body 造成 JSON.parse 崩潰。 */ export async function parseFetchJson>(res: Response): Promise { const text = await res.text(); if (!text.trim()) { throw new Error(`伺服器回應為空(${res.status})`); } try { return JSON.parse(text) as T; } catch { throw new Error(`伺服器回應格式錯誤(${res.status})`); } } export function humanDelay(minMs = 800, maxMs = 2000): Promise { const ms = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs; return new Promise((resolve) => setTimeout(resolve, ms)); } export function parseCount(text: string | null | undefined): number { if (!text) return 0; const cleaned = text.trim().toLowerCase().replace(/,/g, ""); const match = cleaned.match(/([\d.]+)\s*([km萬]?)/); if (!match) return parseInt(cleaned, 10) || 0; const value = parseFloat(match[1]); const suffix = match[2]; if (suffix === "k") return Math.round(value * 1000); if (suffix === "m") return Math.round(value * 1_000_000); if (suffix === "萬") return Math.round(value * 10_000); return Math.round(value); }