import { REFERENCE_IMAGE_COMPRESS_THRESHOLD, REFERENCE_IMAGE_MAX_DIMENSION, REFERENCE_IMAGE_INITIAL_QUALITY, REFERENCE_IMAGE_MIN_QUALITY, type WorkbenchMode, type ReferenceKind, type ReferenceItem, } from "./workbenchConstants"; import { resolvePreUploadedUrl } from "../../api/referenceUploadService"; export function getRatioOptionClassName(value: string) { return `ai-workbench-ratio-option__preview--${value.replace(":", "-")}`; } export function getSettingsGridColumnsClassName(columns: 2 | 3 | 4 = 3) { return `ai-workbench-settings-panel__grid--cols-${columns}`; } export function getReferenceAccept(mode: WorkbenchMode, videoFrameMode?: string) { if (mode === "chat") return ".docx,.txt,.md,.xlsx,.xls,.png,.jpg,.jpeg,.gif,.webp"; if (mode === "image") return "image/*"; if (videoFrameMode === "start-end") return "image/*"; return "image/*,video/mp4,video/quicktime,video/webm,video/x-msvideo,.mp4,.mov,.webm,.avi,audio/mpeg,audio/mp3,audio/wav,audio/x-wav,.mp3,.wav"; } export function getReferenceUploadLabel(mode: WorkbenchMode) { if (mode === "video") return "参考内容"; if (mode === "image") return "参考图"; return "附件"; } export function getReferenceLimit(mode: WorkbenchMode, videoFrameMode?: string) { if (mode === "video" && videoFrameMode === "start-end") return 2; if (mode === "video") return 12; if (mode === "image") return 9; return 4; } export function getReferenceKindLabel(kind: ReferenceKind) { if (kind === "image") return "图片"; if (kind === "video") return "视频"; if (kind === "audio") return "音频"; return "附件"; } export function getReferenceEmptyCopy(mode: WorkbenchMode) { if (mode === "video") return "上传最多12个参考素材,首尾帧模式仅保留2张图片,输入文字或 @ 引用内容,自由组合图、文、音、视频多元素"; if (mode === "image") return "最多上传9张参考图,输入文字或 @ 引用内容,控制角色、风格和构图"; return "上传附件后可用 @ 引用,帮助 Agent 读取上下文"; } export function hexToRgbTriplet(hex: string) { const normalized = hex.replace("#", ""); const full = normalized.length === 3 ? normalized .split("") .map((char) => `${char}${char}`) .join("") : normalized; const value = Number.parseInt(full, 16); const r = (value >> 16) & 255; const g = (value >> 8) & 255; const b = value & 255; return `${r}, ${g}, ${b}`; } export function inferReferenceKind(file: File, mode: WorkbenchMode): ReferenceKind { if (file.type.startsWith("image/")) return "image"; if (file.type.startsWith("video/")) return "video"; if (file.type.startsWith("audio/")) return "audio"; return mode === "chat" ? "file" : "image"; } export function disposeReferencePreview(item: Pick) { if (item.previewUrl) URL.revokeObjectURL(item.previewUrl); } export function fileToDataUrl(file: File) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === "string") { resolve(reader.result); } else { reject(new Error("Unable to read reference file")); } }; reader.onerror = () => reject(reader.error || new Error("Unable to read reference file")); reader.readAsDataURL(file); }); } export function bytesToHex(buffer: ArrayBuffer) { return Array.from(new Uint8Array(buffer)) .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); } export async function buildReferenceFingerprint(file: File, kind: ReferenceKind) { if (kind === "image" && window.crypto?.subtle) { const digest = await window.crypto.subtle.digest("SHA-256", await file.arrayBuffer()); return `image:${bytesToHex(digest)}`; } return `${kind}:${file.name}:${file.size}:${file.lastModified}:${file.type}`; } export function canCompressReferenceImage(file: File) { return ( file.size > REFERENCE_IMAGE_COMPRESS_THRESHOLD && file.type.startsWith("image/") && !/svg|gif/i.test(file.type) ); } function canvasToBlob(canvas: HTMLCanvasElement, type: string, quality: number) { return new Promise((resolve) => { canvas.toBlob(resolve, type, quality); }); } function getCompressedImageName(fileName: string) { const baseName = fileName.replace(/\.[^.]+$/, ""); return `${baseName || "reference"}.jpg`; } export async function compressReferenceImageIfNeeded(file: File) { if (!canCompressReferenceImage(file)) { return { file, compressed: false }; } try { const bitmap = await createImageBitmap(file); const scale = Math.min(1, REFERENCE_IMAGE_MAX_DIMENSION / Math.max(bitmap.width, bitmap.height)); let width = Math.max(1, Math.round(bitmap.width * scale)); let height = Math.max(1, Math.round(bitmap.height * scale)); const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); if (!context) { bitmap.close(); return { file, compressed: false }; } const render = () => { canvas.width = width; canvas.height = height; context.fillStyle = "#ffffff"; context.fillRect(0, 0, width, height); context.drawImage(bitmap, 0, 0, width, height); }; const encode = async () => { let quality = REFERENCE_IMAGE_INITIAL_QUALITY; let nextBlob = await canvasToBlob(canvas, "image/jpeg", quality); while (nextBlob && nextBlob.size > REFERENCE_IMAGE_COMPRESS_THRESHOLD && quality > REFERENCE_IMAGE_MIN_QUALITY) { quality = Math.max(REFERENCE_IMAGE_MIN_QUALITY, quality - 0.08); nextBlob = await canvasToBlob(canvas, "image/jpeg", quality); } return nextBlob; }; render(); let blob = await encode(); while (blob && blob.size > REFERENCE_IMAGE_COMPRESS_THRESHOLD && Math.max(width, height) > 960) { width = Math.max(1, Math.round(width * 0.82)); height = Math.max(1, Math.round(height * 0.82)); render(); blob = await encode(); } bitmap.close(); if (!blob || blob.size >= file.size) { return { file, compressed: false }; } return { file: new File([blob], getCompressedImageName(file.name), { type: "image/jpeg", lastModified: file.lastModified, }), compressed: true, }; } catch { return { file, compressed: false }; } } export function buildReferenceToken(kind: ReferenceKind, index: number) { if (kind === "image") return `@图片${index}`; if (kind === "video") return `@视频${index}`; if (kind === "audio") return `@音频${index}`; return `@附件${index}`; } export async function resolveReferenceUrls(items: ReferenceItem[]): Promise { const tasks = items.map(async (item) => { if (item.remoteUrl) return item.remoteUrl; if (!item.file) { if (item.previewUrl && /^https?:\/\//i.test(item.previewUrl)) { return item.previewUrl; } return null; } const url = await resolvePreUploadedUrl(item.file, item.name, item.fingerprint); if (url) { item.remoteUrl = url; return url; } return null; }); const results = await Promise.all(tasks); return results.filter((url): url is string => url !== null); }