feat: 添加去水印功能 - 图片工具栏新增去水印按钮,系统设置新增PicWish API配置
This commit is contained in:
parent
ff359fe03b
commit
f2d7aff7c6
@ -447,6 +447,11 @@ export function AdminSettingsClient() {
|
|||||||
yScale: "1.5", // Y轴放大倍数
|
yScale: "1.5", // Y轴放大倍数
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 去水印API配置状态
|
||||||
|
const [watermarkRemoveConfig, setWatermarkRemoveConfig] = useState({
|
||||||
|
apiKey: "", // PicWish API密钥
|
||||||
|
});
|
||||||
|
|
||||||
// 角度编辑API配置状态
|
// 角度编辑API配置状态
|
||||||
const [angleEditConfig, setAngleEditConfig] = useState({
|
const [angleEditConfig, setAngleEditConfig] = useState({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -531,6 +536,19 @@ export function AdminSettingsClient() {
|
|||||||
}
|
}
|
||||||
}, [extendConfigData]);
|
}, [extendConfigData]);
|
||||||
|
|
||||||
|
// 加载去水印API配置
|
||||||
|
const { data: watermarkRemoveConfigData } = useSWR("/api/admin/settings?key=watermark_remove_config", fetcher);
|
||||||
|
useEffect(() => {
|
||||||
|
if (watermarkRemoveConfigData?.setting?.value) {
|
||||||
|
const config = watermarkRemoveConfigData.setting.value as {
|
||||||
|
apiKey?: string;
|
||||||
|
};
|
||||||
|
setWatermarkRemoveConfig({
|
||||||
|
apiKey: config.apiKey || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [watermarkRemoveConfigData]);
|
||||||
|
|
||||||
// 加载角度编辑API配置
|
// 加载角度编辑API配置
|
||||||
const { data: angleEditConfigData } = useSWR("/api/admin/settings?key=angle_edit_config", fetcher);
|
const { data: angleEditConfigData } = useSWR("/api/admin/settings?key=angle_edit_config", fetcher);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -1013,6 +1031,32 @@ export function AdminSettingsClient() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- 去水印API配置逻辑 ---
|
||||||
|
const handleSaveWatermarkRemoveConfig = async () => {
|
||||||
|
try {
|
||||||
|
if (!watermarkRemoveConfig.apiKey.trim()) {
|
||||||
|
toast.error("请输入PicWish API密钥");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch("/api/admin/settings", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
key: "watermark_remove_config",
|
||||||
|
value: {
|
||||||
|
apiKey: watermarkRemoveConfig.apiKey.trim(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error("保存失败");
|
||||||
|
toast.success("去水印API配置保存成功");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("保存去水印API配置失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- 角度编辑API配置逻辑 ---
|
// --- 角度编辑API配置逻辑 ---
|
||||||
const handleSaveAngleEditConfig = async () => {
|
const handleSaveAngleEditConfig = async () => {
|
||||||
try {
|
try {
|
||||||
@ -4559,6 +4603,49 @@ export function AdminSettingsClient() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* 去水印API配置 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z" />
|
||||||
|
<line x1="4" y1="4" x2="20" y2="20" />
|
||||||
|
</svg>
|
||||||
|
去水印API配置
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
配置PicWish去水印API,用于自动去除图片中的水印
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6 pt-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="watermarkRemoveApiKey">API密钥 *</Label>
|
||||||
|
<Input
|
||||||
|
id="watermarkRemoveApiKey"
|
||||||
|
type="password"
|
||||||
|
value={watermarkRemoveConfig.apiKey}
|
||||||
|
onChange={(e) => setWatermarkRemoveConfig({ ...watermarkRemoveConfig, apiKey: e.target.value })}
|
||||||
|
placeholder="输入PicWish API密钥"
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
在 PicWish 开发者平台获取API密钥
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border border-purple-200 bg-purple-50 p-4 space-y-2">
|
||||||
|
<div className="text-sm font-medium text-purple-900">PicWish API 说明</div>
|
||||||
|
<div className="text-xs text-purple-800 space-y-1">
|
||||||
|
<p>• 使用 PicWish 云服务进行智能去水印</p>
|
||||||
|
<p>• 支持各种类型的水印去除(文字、图片水印)</p>
|
||||||
|
<p>• API文档:https://picwish.com/developer</p>
|
||||||
|
<p>• 处理时间:约10-30秒/张</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleSaveWatermarkRemoveConfig}>
|
||||||
|
<Save className="mr-2 h-4 w-4" /> 保存配置
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* 角度编辑API配置 */}
|
{/* 角度编辑API配置 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
219
app/api/watermark-remove/route.ts
Normal file
219
app/api/watermark-remove/route.ts
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/app/(auth)/auth";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
|
import { systemSettings } from "@/lib/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
|
|
||||||
|
// 获取去水印API配置
|
||||||
|
async function getWatermarkRemoveConfig() {
|
||||||
|
try {
|
||||||
|
const setting = await db
|
||||||
|
.select()
|
||||||
|
.from(systemSettings)
|
||||||
|
.where(eq(systemSettings.key, "watermark_remove_config"))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (setting.length > 0 && setting[0].value) {
|
||||||
|
return setting[0].value as { apiKey: string };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[WatermarkRemove] 获取配置失败:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮询获取处理结果
|
||||||
|
async function pollForResult(taskId: string, apiKey: string): Promise<{ success: boolean; imageUrl?: string; error?: string }> {
|
||||||
|
const maxAttempts = 30; // 最多轮询30次(30秒)
|
||||||
|
const pollInterval = 1000; // 每次间隔1秒
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://techsz.aoscdn.com/api/tasks/visual/external/watermark-remove/${taskId}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-API-KEY": apiKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error(`[WatermarkRemove] 轮询失败 (attempt ${attempt + 1}):`, errorText);
|
||||||
|
// 继续轮询
|
||||||
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log(`[WatermarkRemove] 轮询结果 (attempt ${attempt + 1}):`, JSON.stringify(result).substring(0, 200));
|
||||||
|
|
||||||
|
// 检查任务状态
|
||||||
|
if (result.data?.state === 1) {
|
||||||
|
// 任务完成
|
||||||
|
const imageUrl = result.data?.image;
|
||||||
|
if (imageUrl) {
|
||||||
|
return { success: true, imageUrl };
|
||||||
|
}
|
||||||
|
return { success: false, error: "处理完成但未返回图片URL" };
|
||||||
|
} else if (result.data?.state === -1) {
|
||||||
|
// 任务失败
|
||||||
|
return { success: false, error: result.data?.error || "去水印处理失败" };
|
||||||
|
}
|
||||||
|
// state === 0 表示处理中,继续轮询
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[WatermarkRemove] 轮询错误 (attempt ${attempt + 1}):`, error);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: "处理超时,请稍后重试" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// 验证用户登录
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: "请先登录" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// 获取请求参数
|
||||||
|
const body = await request.json();
|
||||||
|
const { image_url, image_base64 } = body;
|
||||||
|
|
||||||
|
if (!image_url && !image_base64) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "请提供图片URL或Base64数据" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取API配置
|
||||||
|
const config = await getWatermarkRemoveConfig();
|
||||||
|
if (!config?.apiKey) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "去水印服务未配置,请联系管理员在系统设置中配置API密钥" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果传入的是base64,需要先上传到S3获取URL
|
||||||
|
let imageUrl = image_url;
|
||||||
|
if (image_base64 && !image_url) {
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.from(image_base64, "base64");
|
||||||
|
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
||||||
|
const filename = `watermark-temp-${Date.now()}.jpeg`;
|
||||||
|
const uploadResult = await uploadFile(filename, arrayBuffer);
|
||||||
|
imageUrl = uploadResult.url;
|
||||||
|
console.log("[WatermarkRemove] Base64图片已上传到S3:", imageUrl);
|
||||||
|
} catch (uploadError) {
|
||||||
|
console.error("[WatermarkRemove] 上传图片失败:", uploadError);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "上传图片失败" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[WatermarkRemove] 开始处理图片:", imageUrl);
|
||||||
|
|
||||||
|
// 创建去水印任务
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("sync", "0");
|
||||||
|
formData.append("url", imageUrl);
|
||||||
|
|
||||||
|
const createResponse = await fetch(
|
||||||
|
"https://techsz.aoscdn.com/api/tasks/visual/external/watermark-remove",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"X-API-KEY": config.apiKey,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!createResponse.ok) {
|
||||||
|
const errorText = await createResponse.text();
|
||||||
|
console.error("[WatermarkRemove] 创建任务失败:", errorText);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "创建去水印任务失败" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createResult = await createResponse.json();
|
||||||
|
console.log("[WatermarkRemove] 创建任务结果:", JSON.stringify(createResult).substring(0, 200));
|
||||||
|
|
||||||
|
const taskId = createResult.data?.task_id;
|
||||||
|
if (!taskId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "未获取到任务ID" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮询获取结果
|
||||||
|
const pollResult = await pollForResult(taskId, config.apiKey);
|
||||||
|
|
||||||
|
if (!pollResult.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: pollResult.error || "去水印处理失败" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载处理后的图片并上传到S3
|
||||||
|
const processedImageUrl = pollResult.imageUrl!;
|
||||||
|
console.log("[WatermarkRemove] 处理完成,结果URL:", processedImageUrl);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 下载处理后的图片
|
||||||
|
const imageResponse = await fetch(processedImageUrl);
|
||||||
|
if (!imageResponse.ok) {
|
||||||
|
throw new Error("下载处理后的图片失败");
|
||||||
|
}
|
||||||
|
const imageBuffer = await imageResponse.arrayBuffer();
|
||||||
|
|
||||||
|
// 上传到S3
|
||||||
|
const filename = `watermark-removed-${Date.now()}.png`;
|
||||||
|
const uploadResult = await uploadFile(filename, imageBuffer);
|
||||||
|
|
||||||
|
const processingTime = (Date.now() - startTime) / 1000;
|
||||||
|
console.log(`[WatermarkRemove] 处理完成,耗时: ${processingTime}s`);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
original_url: uploadResult.url,
|
||||||
|
thumbnail_url: uploadResult.url,
|
||||||
|
processing_time: processingTime,
|
||||||
|
});
|
||||||
|
} catch (uploadError) {
|
||||||
|
console.error("[WatermarkRemove] 上传结果失败:", uploadError);
|
||||||
|
// 如果上传失败,直接返回原始URL
|
||||||
|
const processingTime = (Date.now() - startTime) / 1000;
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
original_url: processedImageUrl,
|
||||||
|
thumbnail_url: processedImageUrl,
|
||||||
|
processing_time: processingTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[WatermarkRemove] 处理错误:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error instanceof Error ? error.message : "去水印处理失败" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -123,6 +123,7 @@ export function Chat({
|
|||||||
const [isProcessingRmbg, setIsProcessingRmbg] = useState(false);
|
const [isProcessingRmbg, setIsProcessingRmbg] = useState(false);
|
||||||
const [isProcessingExtend, setIsProcessingExtend] = useState(false);
|
const [isProcessingExtend, setIsProcessingExtend] = useState(false);
|
||||||
const [isProcessingAngleEdit, setIsProcessingAngleEdit] = useState(false);
|
const [isProcessingAngleEdit, setIsProcessingAngleEdit] = useState(false);
|
||||||
|
const [isProcessingWatermarkRemove, setIsProcessingWatermarkRemove] = useState(false);
|
||||||
const currentModelIdRef = useRef(currentModelId);
|
const currentModelIdRef = useRef(currentModelId);
|
||||||
|
|
||||||
// 🆕 视频参数状态
|
// 🆕 视频参数状态
|
||||||
@ -889,6 +890,110 @@ export function Chat({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理图片去水印
|
||||||
|
const handleImageWatermarkRemove = async (imageUrl: string) => {
|
||||||
|
// 防止重复点击
|
||||||
|
if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend || isProcessingWatermarkRemove) {
|
||||||
|
toast.info("正在处理中,请稍候...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessingWatermarkRemove(true);
|
||||||
|
const messageId = generateUUID();
|
||||||
|
|
||||||
|
// 创建临时加载消息
|
||||||
|
const loadingMessage: ChatMessage = {
|
||||||
|
id: messageId,
|
||||||
|
role: "assistant",
|
||||||
|
parts: [{ type: "text", text: "正在去水印..." }],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
} as ChatMessage;
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, loadingMessage]);
|
||||||
|
|
||||||
|
// 延迟滚动到底部,确保DOM已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
const messagesContainer = document.querySelector('[role="log"]');
|
||||||
|
if (messagesContainer) {
|
||||||
|
messagesContainer.scrollTo({
|
||||||
|
top: messagesContainer.scrollHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用去水印API
|
||||||
|
const watermarkRemoveResponse = await fetch("/api/watermark-remove", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
image_url: imageUrl,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!watermarkRemoveResponse.ok) {
|
||||||
|
const error = await watermarkRemoveResponse.json();
|
||||||
|
throw new Error(error.error || "去水印处理失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await watermarkRemoveResponse.json();
|
||||||
|
|
||||||
|
if (result.success && result.original_url) {
|
||||||
|
// 构造图片Markdown(使用与Gemini生成相同的格式)
|
||||||
|
const imageMarkdown = ``;
|
||||||
|
|
||||||
|
// 创建最终消息(前端显示用)
|
||||||
|
const finalMessage: ChatMessage = {
|
||||||
|
id: messageId,
|
||||||
|
role: "assistant",
|
||||||
|
parts: [{ type: "text", text: imageMarkdown }],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
} as ChatMessage;
|
||||||
|
|
||||||
|
setMessages((prev) => {
|
||||||
|
const filtered = prev.filter((msg) => msg.id !== messageId);
|
||||||
|
return [...filtered, finalMessage];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存消息到数据库
|
||||||
|
try {
|
||||||
|
const saveResponse = await fetch("/api/messages", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
chatId: id,
|
||||||
|
message: {
|
||||||
|
id: messageId,
|
||||||
|
role: "assistant",
|
||||||
|
parts: [{ type: "text", text: imageMarkdown }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!saveResponse.ok) {
|
||||||
|
console.error("[Chat] Failed to save message:", await saveResponse.text());
|
||||||
|
}
|
||||||
|
} catch (saveError) {
|
||||||
|
console.error("[Chat] Failed to save message:", saveError);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(`去水印完成!处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`);
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || "去水印处理失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[去水印] 错误:", error);
|
||||||
|
toast.error(error instanceof Error ? error.message : "去水印失败,请稍后重试");
|
||||||
|
// 移除加载消息
|
||||||
|
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||||
|
} finally {
|
||||||
|
setIsProcessingWatermarkRemove(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 处理图像拓展
|
// 处理图像拓展
|
||||||
const handleImageExtend = async (imageUrl: string) => {
|
const handleImageExtend = async (imageUrl: string) => {
|
||||||
// 防止重复点击
|
// 防止重复点击
|
||||||
@ -1157,10 +1262,12 @@ export function Chat({
|
|||||||
onImageRmbg={handleImageRmbg}
|
onImageRmbg={handleImageRmbg}
|
||||||
onImageExtend={handleImageExtend}
|
onImageExtend={handleImageExtend}
|
||||||
onImageAngleEdit={handleImageAngleEdit}
|
onImageAngleEdit={handleImageAngleEdit}
|
||||||
|
onImageWatermarkRemove={handleImageWatermarkRemove}
|
||||||
isProcessingEnhance={isProcessingEnhance}
|
isProcessingEnhance={isProcessingEnhance}
|
||||||
isProcessingRmbg={isProcessingRmbg}
|
isProcessingRmbg={isProcessingRmbg}
|
||||||
isProcessingExtend={isProcessingExtend}
|
isProcessingExtend={isProcessingExtend}
|
||||||
isProcessingAngleEdit={isProcessingAngleEdit}
|
isProcessingAngleEdit={isProcessingAngleEdit}
|
||||||
|
isProcessingWatermarkRemove={isProcessingWatermarkRemove}
|
||||||
pendingGeneration={pendingGeneration}
|
pendingGeneration={pendingGeneration}
|
||||||
imageCount={imageParams?.imageCount || 1}
|
imageCount={imageParams?.imageCount || 1}
|
||||||
isGeneratingImageFlag={isGeneratingImageFlag}
|
isGeneratingImageFlag={isGeneratingImageFlag}
|
||||||
|
|||||||
@ -13,10 +13,12 @@ type ResponseProps = ComponentProps<typeof Streamdown> & {
|
|||||||
onImageRmbg?: (imageUrl: string) => void;
|
onImageRmbg?: (imageUrl: string) => void;
|
||||||
onImageExtend?: (imageUrl: string) => void;
|
onImageExtend?: (imageUrl: string) => void;
|
||||||
onImageAngleEdit?: (imageUrl: string) => void;
|
onImageAngleEdit?: (imageUrl: string) => void;
|
||||||
|
onImageWatermarkRemove?: (imageUrl: string) => void;
|
||||||
isProcessingEnhance?: boolean;
|
isProcessingEnhance?: boolean;
|
||||||
isProcessingRmbg?: boolean;
|
isProcessingRmbg?: boolean;
|
||||||
isProcessingExtend?: boolean;
|
isProcessingExtend?: boolean;
|
||||||
isProcessingAngleEdit?: boolean;
|
isProcessingAngleEdit?: boolean;
|
||||||
|
isProcessingWatermarkRemove?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 注意:移除了 memo 的自定义比较函数,确保 isProcessing* 状态变化时组件能正确重新渲染
|
// 注意:移除了 memo 的自定义比较函数,确保 isProcessing* 状态变化时组件能正确重新渲染
|
||||||
@ -29,10 +31,12 @@ export const Response = ({
|
|||||||
onImageRmbg,
|
onImageRmbg,
|
||||||
onImageExtend,
|
onImageExtend,
|
||||||
onImageAngleEdit,
|
onImageAngleEdit,
|
||||||
|
onImageWatermarkRemove,
|
||||||
isProcessingEnhance,
|
isProcessingEnhance,
|
||||||
isProcessingRmbg,
|
isProcessingRmbg,
|
||||||
isProcessingExtend,
|
isProcessingExtend,
|
||||||
isProcessingAngleEdit,
|
isProcessingAngleEdit,
|
||||||
|
isProcessingWatermarkRemove,
|
||||||
...props
|
...props
|
||||||
}: ResponseProps) => (
|
}: ResponseProps) => (
|
||||||
<Streamdown
|
<Streamdown
|
||||||
@ -46,7 +50,7 @@ export const Response = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// 使用 key 强制在处理状态变化时重新渲染组件
|
// 使用 key 强制在处理状态变化时重新渲染组件
|
||||||
const processingKey = `${isProcessingEnhance}-${isProcessingRmbg}-${isProcessingExtend}-${isProcessingAngleEdit}`;
|
const processingKey = `${isProcessingEnhance}-${isProcessingRmbg}-${isProcessingExtend}-${isProcessingAngleEdit}-${isProcessingWatermarkRemove}`;
|
||||||
return <ImagePreview
|
return <ImagePreview
|
||||||
key={`${src}-${processingKey}`}
|
key={`${src}-${processingKey}`}
|
||||||
src={src}
|
src={src}
|
||||||
@ -57,10 +61,12 @@ export const Response = ({
|
|||||||
onImageRmbg={onImageRmbg}
|
onImageRmbg={onImageRmbg}
|
||||||
onImageExtend={onImageExtend}
|
onImageExtend={onImageExtend}
|
||||||
onImageAngleEdit={onImageAngleEdit}
|
onImageAngleEdit={onImageAngleEdit}
|
||||||
|
onImageWatermarkRemove={onImageWatermarkRemove}
|
||||||
isProcessingEnhance={isProcessingEnhance}
|
isProcessingEnhance={isProcessingEnhance}
|
||||||
isProcessingRmbg={isProcessingRmbg}
|
isProcessingRmbg={isProcessingRmbg}
|
||||||
isProcessingExtend={isProcessingExtend}
|
isProcessingExtend={isProcessingExtend}
|
||||||
isProcessingAngleEdit={isProcessingAngleEdit}
|
isProcessingAngleEdit={isProcessingAngleEdit}
|
||||||
|
isProcessingWatermarkRemove={isProcessingWatermarkRemove}
|
||||||
/>;
|
/>;
|
||||||
},
|
},
|
||||||
video: ({ src, alt }: { src?: string; alt?: string }) => {
|
video: ({ src, alt }: { src?: string; alt?: string }) => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { X, Download, Edit, Target, Paintbrush, Sparkles, Scissors, Expand, Maximize2, Move3D } from "lucide-react";
|
import { X, Download, Edit, Target, Paintbrush, Sparkles, Scissors, Expand, Maximize2, Move3D, Droplet } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ImageAnnotator } from "./image-annotator";
|
import { ImageAnnotator } from "./image-annotator";
|
||||||
import { AngleEditDialog } from "./angle-edit-dialog";
|
import { AngleEditDialog } from "./angle-edit-dialog";
|
||||||
@ -16,10 +16,12 @@ export function ImagePreview({
|
|||||||
onImageRmbg,
|
onImageRmbg,
|
||||||
onImageExtend,
|
onImageExtend,
|
||||||
onImageAngleEdit,
|
onImageAngleEdit,
|
||||||
|
onImageWatermarkRemove,
|
||||||
isProcessingEnhance,
|
isProcessingEnhance,
|
||||||
isProcessingRmbg,
|
isProcessingRmbg,
|
||||||
isProcessingExtend,
|
isProcessingExtend,
|
||||||
isProcessingAngleEdit,
|
isProcessingAngleEdit,
|
||||||
|
isProcessingWatermarkRemove,
|
||||||
imageIndex
|
imageIndex
|
||||||
}: {
|
}: {
|
||||||
src: string;
|
src: string;
|
||||||
@ -35,10 +37,12 @@ export function ImagePreview({
|
|||||||
onImageRmbg?: (imageUrl: string) => void;
|
onImageRmbg?: (imageUrl: string) => void;
|
||||||
onImageExtend?: (imageUrl: string) => void;
|
onImageExtend?: (imageUrl: string) => void;
|
||||||
onImageAngleEdit?: (imageUrl: string) => void;
|
onImageAngleEdit?: (imageUrl: string) => void;
|
||||||
|
onImageWatermarkRemove?: (imageUrl: string) => void;
|
||||||
isProcessingEnhance?: boolean;
|
isProcessingEnhance?: boolean;
|
||||||
isProcessingRmbg?: boolean;
|
isProcessingRmbg?: boolean;
|
||||||
isProcessingExtend?: boolean;
|
isProcessingExtend?: boolean;
|
||||||
isProcessingAngleEdit?: boolean;
|
isProcessingAngleEdit?: boolean;
|
||||||
|
isProcessingWatermarkRemove?: boolean;
|
||||||
imageIndex?: number;
|
imageIndex?: number;
|
||||||
}) {
|
}) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@ -55,9 +59,10 @@ export function ImagePreview({
|
|||||||
isProcessingRmbg,
|
isProcessingRmbg,
|
||||||
isProcessingExtend,
|
isProcessingExtend,
|
||||||
isProcessingAngleEdit,
|
isProcessingAngleEdit,
|
||||||
|
isProcessingWatermarkRemove,
|
||||||
src: src?.substring(0, 50)
|
src: src?.substring(0, 50)
|
||||||
});
|
});
|
||||||
}, [isProcessingEnhance, isProcessingRmbg, isProcessingExtend, isProcessingAngleEdit, src]);
|
}, [isProcessingEnhance, isProcessingRmbg, isProcessingExtend, isProcessingAngleEdit, isProcessingWatermarkRemove, src]);
|
||||||
|
|
||||||
// 解析 alt 文本,提取原图 URL 和宽度
|
// 解析 alt 文本,提取原图 URL 和宽度
|
||||||
// 格式优先级:
|
// 格式优先级:
|
||||||
@ -326,11 +331,11 @@ export function ImagePreview({
|
|||||||
className="flex items-center gap-1 rounded-full bg-black/70 hover:bg-black/80 px-2 py-1 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
className="flex items-center gap-1 rounded-full bg-black/70 hover:bg-black/80 px-2 py-1 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend && !isProcessingAngleEdit) {
|
if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend && !isProcessingAngleEdit && !isProcessingWatermarkRemove) {
|
||||||
setShowAngleEditDialog(true);
|
setShowAngleEditDialog(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend || isProcessingAngleEdit}
|
disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend || isProcessingAngleEdit || isProcessingWatermarkRemove}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="角度调整"
|
aria-label="角度调整"
|
||||||
title="角度调整"
|
title="角度调整"
|
||||||
@ -339,6 +344,24 @@ export function ImagePreview({
|
|||||||
<span className="text-[10px] font-medium">角度调整</span>
|
<span className="text-[10px] font-medium">角度调整</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{onImageWatermarkRemove && (
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-1 rounded-full bg-black/70 hover:bg-black/80 px-2 py-1 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend && !isProcessingAngleEdit && !isProcessingWatermarkRemove) {
|
||||||
|
onImageWatermarkRemove(original);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend || isProcessingAngleEdit || isProcessingWatermarkRemove}
|
||||||
|
type="button"
|
||||||
|
aria-label="去水印"
|
||||||
|
title="去水印"
|
||||||
|
>
|
||||||
|
<Droplet size={12} />
|
||||||
|
<span className="text-[10px] font-medium">去水印</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1 rounded-full bg-black/70 hover:bg-black/80 px-2 py-1 text-white transition-colors shadow-lg whitespace-nowrap"
|
className="flex items-center gap-1 rounded-full bg-black/70 hover:bg-black/80 px-2 py-1 text-white transition-colors shadow-lg whitespace-nowrap"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@ -152,10 +152,12 @@ const PurePreviewMessage = ({
|
|||||||
onImageRmbg,
|
onImageRmbg,
|
||||||
onImageExtend,
|
onImageExtend,
|
||||||
onImageAngleEdit,
|
onImageAngleEdit,
|
||||||
|
onImageWatermarkRemove,
|
||||||
isProcessingEnhance,
|
isProcessingEnhance,
|
||||||
isProcessingRmbg,
|
isProcessingRmbg,
|
||||||
isProcessingExtend,
|
isProcessingExtend,
|
||||||
isProcessingAngleEdit,
|
isProcessingAngleEdit,
|
||||||
|
isProcessingWatermarkRemove,
|
||||||
}: {
|
}: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
message: ChatMessage;
|
message: ChatMessage;
|
||||||
@ -172,10 +174,12 @@ const PurePreviewMessage = ({
|
|||||||
onImageRmbg?: (imageUrl: string) => void;
|
onImageRmbg?: (imageUrl: string) => void;
|
||||||
onImageExtend?: (imageUrl: string) => void;
|
onImageExtend?: (imageUrl: string) => void;
|
||||||
onImageAngleEdit?: (imageUrl: string) => void;
|
onImageAngleEdit?: (imageUrl: string) => void;
|
||||||
|
onImageWatermarkRemove?: (imageUrl: string) => void;
|
||||||
isProcessingEnhance?: boolean;
|
isProcessingEnhance?: boolean;
|
||||||
isProcessingRmbg?: boolean;
|
isProcessingRmbg?: boolean;
|
||||||
isProcessingExtend?: boolean;
|
isProcessingExtend?: boolean;
|
||||||
isProcessingAngleEdit?: boolean;
|
isProcessingAngleEdit?: boolean;
|
||||||
|
isProcessingWatermarkRemove?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const [mode, setMode] = useState<"view" | "edit">("view");
|
const [mode, setMode] = useState<"view" | "edit">("view");
|
||||||
|
|
||||||
@ -312,10 +316,12 @@ const PurePreviewMessage = ({
|
|||||||
onImageRmbg={onImageRmbg}
|
onImageRmbg={onImageRmbg}
|
||||||
onImageExtend={onImageExtend}
|
onImageExtend={onImageExtend}
|
||||||
onImageAngleEdit={onImageAngleEdit}
|
onImageAngleEdit={onImageAngleEdit}
|
||||||
|
onImageWatermarkRemove={onImageWatermarkRemove}
|
||||||
isProcessingEnhance={isProcessingEnhance}
|
isProcessingEnhance={isProcessingEnhance}
|
||||||
isProcessingRmbg={isProcessingRmbg}
|
isProcessingRmbg={isProcessingRmbg}
|
||||||
isProcessingExtend={isProcessingExtend}
|
isProcessingExtend={isProcessingExtend}
|
||||||
isProcessingAngleEdit={isProcessingAngleEdit}
|
isProcessingAngleEdit={isProcessingAngleEdit}
|
||||||
|
isProcessingWatermarkRemove={isProcessingWatermarkRemove}
|
||||||
>
|
>
|
||||||
{sanitizeText(part.content)}
|
{sanitizeText(part.content)}
|
||||||
</Response>
|
</Response>
|
||||||
|
|||||||
@ -42,10 +42,12 @@ type MessagesProps = {
|
|||||||
onImageRmbg?: (imageUrl: string) => void;
|
onImageRmbg?: (imageUrl: string) => void;
|
||||||
onImageExtend?: (imageUrl: string) => void;
|
onImageExtend?: (imageUrl: string) => void;
|
||||||
onImageAngleEdit?: (imageUrl: string) => void;
|
onImageAngleEdit?: (imageUrl: string) => void;
|
||||||
|
onImageWatermarkRemove?: (imageUrl: string) => void;
|
||||||
isProcessingEnhance?: boolean;
|
isProcessingEnhance?: boolean;
|
||||||
isProcessingRmbg?: boolean;
|
isProcessingRmbg?: boolean;
|
||||||
isProcessingExtend?: boolean;
|
isProcessingExtend?: boolean;
|
||||||
isProcessingAngleEdit?: boolean;
|
isProcessingAngleEdit?: boolean;
|
||||||
|
isProcessingWatermarkRemove?: boolean;
|
||||||
pendingGeneration?: PendingGeneration | null; // 🆕 持久化的生成状态
|
pendingGeneration?: PendingGeneration | null; // 🆕 持久化的生成状态
|
||||||
imageCount?: number; // 🆕 用户选择的图片数量
|
imageCount?: number; // 🆕 用户选择的图片数量
|
||||||
isGeneratingImageFlag?: boolean; // 🆕 正在生成图片的标志(来自 Chat 组件)
|
isGeneratingImageFlag?: boolean; // 🆕 正在生成图片的标志(来自 Chat 组件)
|
||||||
@ -67,10 +69,12 @@ function PureMessages({
|
|||||||
onImageRmbg,
|
onImageRmbg,
|
||||||
onImageExtend,
|
onImageExtend,
|
||||||
onImageAngleEdit,
|
onImageAngleEdit,
|
||||||
|
onImageWatermarkRemove,
|
||||||
isProcessingEnhance = false,
|
isProcessingEnhance = false,
|
||||||
isProcessingRmbg = false,
|
isProcessingRmbg = false,
|
||||||
isProcessingExtend = false,
|
isProcessingExtend = false,
|
||||||
isProcessingAngleEdit = false,
|
isProcessingAngleEdit = false,
|
||||||
|
isProcessingWatermarkRemove = false,
|
||||||
pendingGeneration,
|
pendingGeneration,
|
||||||
imageCount = 1,
|
imageCount = 1,
|
||||||
isGeneratingImageFlag = false,
|
isGeneratingImageFlag = false,
|
||||||
@ -402,10 +406,12 @@ function PureMessages({
|
|||||||
onImageRmbg={onImageRmbg}
|
onImageRmbg={onImageRmbg}
|
||||||
onImageExtend={onImageExtend}
|
onImageExtend={onImageExtend}
|
||||||
onImageAngleEdit={onImageAngleEdit}
|
onImageAngleEdit={onImageAngleEdit}
|
||||||
|
onImageWatermarkRemove={onImageWatermarkRemove}
|
||||||
isProcessingEnhance={isProcessingEnhance}
|
isProcessingEnhance={isProcessingEnhance}
|
||||||
isProcessingRmbg={isProcessingRmbg}
|
isProcessingRmbg={isProcessingRmbg}
|
||||||
isProcessingExtend={isProcessingExtend}
|
isProcessingExtend={isProcessingExtend}
|
||||||
isProcessingAngleEdit={isProcessingAngleEdit}
|
isProcessingAngleEdit={isProcessingAngleEdit}
|
||||||
|
isProcessingWatermarkRemove={isProcessingWatermarkRemove}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user