"use client"; import { useState, useCallback, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Slider } from "@/components/ui/slider"; import { Label } from "@/components/ui/label"; import { Loader2, Download, Check, RotateCcw, Move3D, Sparkles } from "lucide-react"; import { toast } from "sonner"; import { QwenCameraWidget, type CameraState } from "./qwen-camera-widget"; import { cn } from "@/lib/utils"; interface AngleEditDialogProps { open: boolean; onOpenChange: (open: boolean) => void; imageUrl: string; onComplete?: (resultUrl: string) => void; } // 根据角度值生成 API 所需的字符串参数 function getAzimuthString(azimuth: number): string { const hAngle = azimuth % 360; if (hAngle < 22.5 || hAngle >= 337.5) return "front"; if (hAngle < 67.5) return "front-right"; if (hAngle < 112.5) return "right"; if (hAngle < 157.5) return "back-right"; if (hAngle < 202.5) return "back"; if (hAngle < 247.5) return "back-left"; if (hAngle < 292.5) return "left"; return "front-left"; } function getElevationString(elevation: number): string { if (elevation < -15) return "low"; if (elevation < 15) return "eye-level"; if (elevation < 45) return "elevated"; return "high"; } function getDistanceString(distance: number): string { if (distance < 3) return "wide"; if (distance < 7) return "medium"; return "close"; } export function AngleEditDialog({ open, onOpenChange, imageUrl, }: AngleEditDialogProps) { const [isProcessing, setIsProcessing] = useState(false); const [resultUrl, setResultUrl] = useState(null); const [loraScale, setLoraScale] = useState(0.9); const [cameraState, setCameraState] = useState({ azimuth: 45, elevation: 0, distance: 5, }); // 当对话框关闭时重置状态 useEffect(() => { if (!open) { setResultUrl(null); setIsProcessing(false); } }, [open]); // 处理相机状态变化 const handleCameraStateChange = useCallback((state: CameraState) => { setCameraState(state); // 角度变化时清除之前的结果 setResultUrl(null); }, []); // 生成新视角 const handleGenerate = useCallback(async () => { setIsProcessing(true); setResultUrl(null); try { const response = await fetch("/api/angle-edit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ image_url: imageUrl, azimuth: getAzimuthString(cameraState.azimuth), elevation: getElevationString(cameraState.elevation), distance: getDistanceString(cameraState.distance), lora_scale: loraScale, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "生成失败"); } const data = await response.json(); setResultUrl(data.image_url); toast.success("视角生成成功!"); } catch (error) { console.error("角度编辑失败:", error); toast.error(error instanceof Error ? error.message : "生成失败,请重试"); } finally { setIsProcessing(false); } }, [imageUrl, cameraState, loraScale]); // 下载结果图片 const handleDownload = useCallback(async () => { if (!resultUrl) return; try { const response = await fetch(resultUrl); const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `angle-edit-${cameraState.azimuth}-${cameraState.elevation}-${Date.now()}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast.success("图片已下载"); } catch { toast.error("下载失败"); } }, [resultUrl, cameraState]); // 使用此图片(直接发送到聊天) const handleUseImage = useCallback(() => { if (resultUrl) { // 关闭对话框,图片已经通过 API 保存到数据库 onOpenChange(false); toast.success("图片已添加到对话"); } }, [resultUrl, onOpenChange]); // 重置 const handleReset = useCallback(() => { setResultUrl(null); setCameraState({ azimuth: 45, elevation: 0, distance: 5, }); setLoraScale(0.9); }, []); return (
多角度编辑 拖拽 3D 控制器中的彩色手柄调整相机角度,AI 将生成对应视角的图像
{/* 左侧:3D 控制器 (占 5 列) */}
{/* LoRA 强度控制 */}
{(loraScale * 100).toFixed(0)}%
setLoraScale(v)} min={0.5} max={1.0} step={0.05} className="py-2" />

强度越高角度变化越明显,越低则保留更多原图特征。建议 85-95%。

{/* 右侧:图片预览 (占 7 列) */}
{/* 原图 */}
原图
原图
{/* 结果图 */}
生成结果
{resultUrl && ( 完成 )}
{isProcessing ? (

正在生成新视角...

预计需要 10-30 秒

) : resultUrl ? ( 生成结果 ) : (

准备就绪

拖拽左侧彩色手柄调整角度后点击生成

)}
{/* 底部操作栏 */}
{resultUrl ? ( <> ) : ( )}
); } export default AngleEditDialog;