Initial commit: OmniAI Web Frontend

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:38:01 +08:00
commit bedee3ba8d
183 changed files with 94805 additions and 0 deletions
@@ -0,0 +1,407 @@
import {
CameraOutlined,
ColumnWidthOutlined,
CustomerServiceOutlined,
DeleteOutlined,
DownloadOutlined,
EditOutlined,
FileImageOutlined,
FolderAddOutlined,
FontSizeOutlined,
LinkOutlined,
ScissorOutlined,
SwapOutlined,
} from "@ant-design/icons";
import { useCallback, useEffect, useRef, useState } from "react";
import { aiGenerationClient } from "../../api/aiGenerationClient";
import { waitForTask } from "../../api/taskSubscription";
import { getServerBaseUrl, isServerRequestError } from "../../api/serverConnection";
import { summarizeUrl, formatFileSize, fileToDataUrl } from "../../utils/toolPageUtils";
import TaskStatusBar from "../../components/TaskStatusBar";
import { addToolResultToAssetLibrary, saveToolResultToLocal } from "../workbench/toolResultActions";
import type { WebImageWorkbenchTool, WebViewKey } from "../../types";
interface WatermarkRemovalPageProps {
isAuthenticated?: boolean;
onOpenMore?: () => void;
onOpenImageTool?: (tool: WebImageWorkbenchTool) => void;
onSelectView?: (view: WebViewKey) => void;
}
function WatermarkRemovalPage({
isAuthenticated = false,
onOpenMore,
onOpenImageTool,
onSelectView,
}: WatermarkRemovalPageProps) {
const fileInputRef = useRef<HTMLInputElement>(null);
const pollRunRef = useRef(0);
const cancelRef = useRef(false);
const [sourceName, setSourceName] = useState("");
const [sourceFile, setSourceFile] = useState<File | null>(null);
const [sourceUrl, setSourceUrl] = useState("");
const [sourcePreview, setSourcePreview] = useState("");
const [resultPreview, setResultPreview] = useState("");
const [status, setStatus] = useState("上传含水印的图片,点击开始去水印");
const [activeTaskId, setActiveTaskId] = useState("");
const [taskProgress, setTaskProgress] = useState(0);
const [isProcessing, setIsProcessing] = useState(false);
const [isSavingAsset, setIsSavingAsset] = useState(false);
const [isDownloading, setIsDownloading] = useState(false);
const activeTaskIdRef = useRef(activeTaskId);
activeTaskIdRef.current = activeTaskId;
useEffect(() => {
return () => {
if (sourcePreview.startsWith("blob:")) URL.revokeObjectURL(sourcePreview);
};
}, [sourcePreview]);
useEffect(() => {
return () => {
pollRunRef.current += 1;
cancelRef.current = true;
if (activeTaskIdRef.current) {
aiGenerationClient.cancelTask(activeTaskIdRef.current).catch(() => {});
}
};
}, []);
const clearSource = () => {
if (sourcePreview.startsWith("blob:")) URL.revokeObjectURL(sourcePreview);
setSourceName("");
setSourceFile(null);
setSourceUrl("");
setSourcePreview("");
setResultPreview("");
setActiveTaskId("");
setTaskProgress(0);
setStatus("已清空素材");
};
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
if (sourcePreview.startsWith("blob:")) URL.revokeObjectURL(sourcePreview);
setSourceName(file.name);
setSourceFile(file);
setSourceUrl("");
setSourcePreview(URL.createObjectURL(file));
setResultPreview("");
setActiveTaskId("");
setTaskProgress(0);
setStatus(`已导入 ${file.name}`);
event.currentTarget.value = "";
};
const handleFileDrop = (event: React.DragEvent) => {
event.preventDefault();
const file = Array.from(event.dataTransfer.files).find((f) => f.type.startsWith("image/"));
if (!file) return;
if (sourcePreview.startsWith("blob:")) URL.revokeObjectURL(sourcePreview);
setSourceName(file.name);
setSourceFile(file);
setSourceUrl("");
setSourcePreview(URL.createObjectURL(file));
setResultPreview("");
setActiveTaskId("");
setTaskProgress(0);
setStatus(`已导入 ${file.name}`);
};
const handleImportUrl = () => {
const normalizedUrl = sourceUrl.trim();
if (!/^https?:\/\//i.test(normalizedUrl)) {
setStatus("请输入可访问的 HTTP/HTTPS 图片 URL");
return;
}
if (sourcePreview.startsWith("blob:")) URL.revokeObjectURL(sourcePreview);
setSourceFile(null);
setSourceName(summarizeUrl(normalizedUrl));
setSourcePreview(normalizedUrl);
setResultPreview("");
setActiveTaskId("");
setTaskProgress(0);
setStatus(`已导入 URL${summarizeUrl(normalizedUrl)}`);
};
const getSourceGenerationUrl = async () => {
if (!sourceFile) return sourcePreview;
setStatus(`正在上传 ${sourceFile.name}${formatFileSize(sourceFile.size)}`);
const uploaded = await aiGenerationClient.uploadAsset({
dataUrl: await fileToDataUrl(sourceFile),
name: sourceFile.name,
mimeType: sourceFile.type || "image/png",
});
return uploaded.signedUrl || uploaded.url;
};
const waitForTaskResult = async (taskId: string) => {
const runId = ++pollRunRef.current;
setActiveTaskId(taskId);
setTaskProgress(5);
await waitForTask(taskId, {
abortRef: cancelRef,
onProgress: (e) => {
if (pollRunRef.current !== runId) return;
const progress = Math.max(0, Math.min(100, Math.trunc(e.progress || 0)));
setTaskProgress(progress);
setStatus(`任务 ${taskId} ${e.status},进度 ${progress}%`);
if (e.status === "completed" && e.resultUrl) {
setResultPreview(e.resultUrl);
setStatus("去水印完成");
setTaskProgress(100);
}
},
});
if (cancelRef.current) throw new Error("已取消");
if (pollRunRef.current !== runId) throw new Error("任务已被替换");
};
const handleCancel = useCallback(() => {
cancelRef.current = true;
if (activeTaskId) {
aiGenerationClient.cancelTask(activeTaskId).catch(() => {});
}
setIsProcessing(false);
setStatus("已取消");
}, [activeTaskId]);
const handleDownload = async () => {
if (!resultPreview || isDownloading) return;
setIsDownloading(true);
try {
await saveToolResultToLocal({
url: resultPreview,
name: "watermark-removed",
type: "image",
taskId: activeTaskId || undefined,
tags: ["工具盒", "去水印", "生成图片"],
});
setStatus("下载完成");
} catch (error) {
setStatus(error instanceof Error ? error.message : "下载失败");
} finally {
setIsDownloading(false);
}
};
const handleSaveToAssets = async () => {
if (!resultPreview || isSavingAsset) return;
setIsSavingAsset(true);
try {
const status = await addToolResultToAssetLibrary({
url: resultPreview,
name: `watermark-removed-${Date.now()}.png`,
description: "工具盒去水印生成结果",
type: "image",
taskId: activeTaskId || undefined,
tags: ["工具盒", "去水印", "生成图片"],
});
setStatus(status === "server" ? "已加入资产库" : "已加入本地资产库");
} catch (error) {
setStatus(error instanceof Error ? error.message : "加入资产库失败");
} finally {
setIsSavingAsset(false);
}
};
const handleStart = async () => {
if (!isAuthenticated) {
setStatus("请先登录后再使用去水印功能");
return;
}
if (!sourcePreview) {
setStatus("请先上传需要去水印的图片");
return;
}
if (isProcessing) return;
setIsProcessing(true);
setResultPreview("");
setTaskProgress(0);
cancelRef.current = false;
try {
const generationUrl = await getSourceGenerationUrl();
setStatus(`素材已就绪,正在提交去水印任务`);
const result = await aiGenerationClient.createImageEditTask({
imageUrl: generationUrl,
function: "remove_watermark",
prompt: "去除图像中的文字",
n: 1,
});
await waitForTaskResult(result.taskId);
} catch (error) {
if (isServerRequestError(error) && error.status === 404) {
const baseUrl = getServerBaseUrl() || window.location.origin;
setStatus(`服务端 ${baseUrl} 还没有部署 /api/ai/image/edit 接口,请更新服务端`);
} else {
setStatus(error instanceof Error ? error.message : "去水印任务失败,请稍后重试");
}
} finally {
setIsProcessing(false);
}
};
return (
<section className="image-workbench-page watermark-removal-page">
<header className="image-workbench-topbar">
<button type="button" className="image-workbench-back-to-more" onClick={onOpenMore}>
</button>
<div className="image-workbench-tool-strip" aria-label="功能入口">
<button type="button" onClick={() => onOpenImageTool?.("workbench")}>
<EditOutlined />
</button>
<button type="button" onClick={() => onOpenImageTool?.("inpaint")}>
<ScissorOutlined />
</button>
<button type="button" onClick={() => onOpenImageTool?.("camera")}>
<CameraOutlined />
</button>
<button type="button" onClick={() => onSelectView?.("digitalHuman")}>
<CustomerServiceOutlined />
</button>
<button type="button" onClick={() => onSelectView?.("characterMix")}>
<SwapOutlined />
</button>
<button type="button" onClick={() => onSelectView?.("resolutionUpscale")}>
<ColumnWidthOutlined />
</button>
<button type="button" className="is-active" onClick={() => onSelectView?.("watermarkRemoval")}>
<DeleteOutlined />
</button>
<button type="button" onClick={() => onSelectView?.("subtitleRemoval")}>
<FontSizeOutlined />
</button>
</div>
</header>
<div className="image-workbench-subbar">
<button type="button" className="image-workbench-icon-btn" aria-label="返回工具盒" onClick={onOpenMore}>
<DeleteOutlined />
</button>
<strong></strong>
</div>
<main className="image-workbench-layout image-workbench-layout--camera watermark-removal-layout">
<aside className="image-workbench-panel image-workbench-panel--left">
<section className="image-workbench-control-card">
<div className="image-workbench-section-title">
<h3></h3>
<span>{sourcePreview ? "已上传" : "待上传"}</span>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/png,image/jpeg,image/webp"
onChange={handleFileChange}
/>
<div className="image-workbench-upload-shell" onDragOver={(e) => e.preventDefault()} onDrop={handleFileDrop}>
<button type="button" className="image-workbench-upload" onClick={() => fileInputRef.current?.click()}>
{sourcePreview ? <img src={sourcePreview} alt="" /> : <FileImageOutlined />}
<strong>{sourceName || "拖拽或选择图片"}</strong>
<span>PNG / JPG / WebP</span>
</button>
{sourcePreview ? (
<button type="button" className="image-workbench-upload-remove" aria-label="删除素材" onClick={clearSource}>
×
</button>
) : null}
</div>
<div className="image-workbench-url-row">
<label>
<LinkOutlined />
<input
value={sourceUrl}
placeholder="粘贴图片 URL"
onChange={(event) => setSourceUrl(event.target.value)}
/>
</label>
<button type="button" onClick={handleImportUrl}></button>
</div>
</section>
<section className="image-workbench-control-card">
<h3></h3>
<p className="watermark-removal-hint">
使 AI
</p>
</section>
<div className="image-workbench-actions">
<button type="button" className="image-workbench-primary" onClick={() => void handleStart()} disabled={isProcessing}>
<DeleteOutlined />
{isProcessing ? "处理中" : "开始去水印"}
</button>
{isProcessing && (
<button type="button" className="image-workbench-cancel" onClick={handleCancel} style={{ marginTop: 6 }}>
</button>
)}
</div>
</aside>
<section className="image-workbench-canvas image-workbench-canvas--camera watermark-removal-canvas" aria-label="去水印画布">
{sourcePreview ? (
<div className="watermark-removal-compare">
<div className="watermark-removal-compare__panel">
<span className="watermark-removal-compare__label"></span>
<img src={sourcePreview} alt="原图预览" />
</div>
<div className="watermark-removal-compare__panel">
<span className="watermark-removal-compare__label">{resultPreview ? "去水印结果" : "等待结果"}</span>
{resultPreview ? (
<>
<img src={resultPreview} alt="去水印结果" />
<div className="watermark-removal-compare__actions">
<button type="button" onClick={() => void handleSaveToAssets()} disabled={isSavingAsset}>
<FolderAddOutlined />
{isSavingAsset ? "加入中" : "加入资产库"}
</button>
<button type="button" onClick={() => void handleDownload()} disabled={isDownloading}>
<DownloadOutlined />
{isDownloading ? "保存中" : "保存本地"}
</button>
</div>
</>
) : (
<div className="watermark-removal-compare__placeholder">
<DeleteOutlined />
<span>{isProcessing ? "处理中..." : "点击开始去水印"}</span>
</div>
)}
</div>
</div>
) : (
<button
type="button"
className="image-workbench-empty image-workbench-empty--button"
onClick={() => fileInputRef.current?.click()}
onDragOver={(e) => e.preventDefault()}
onDrop={handleFileDrop}
>
<DeleteOutlined />
<strong></strong>
<span> PNG / JPG / WebP</span>
</button>
)}
</section>
</main>
<TaskStatusBar taskId={activeTaskId} status={status} progress={taskProgress} idleLabel="去水印" />
</section>
);
}
export default WatermarkRemovalPage;