52 lines
1.8 KiB
TypeScript
52 lines
1.8 KiB
TypeScript
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<T = Record<string, unknown>>(res: Response): Promise<T> {
|
||
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<void> {
|
||
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);
|
||
} |