diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 501f1fecc..3e1ff4eca 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -458,16 +458,43 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { }; // 图片标注完成回调 - const handleAnnotateComplete = (annotatedImageUrl: string) => { + const handleAnnotateComplete = async (annotatedImageUrl: string) => { setAnnotatorOpen(false); + // 如果仍然是 base64 格式,先上传到服务器(ImageAnnotator 应该已经处理了,这里作为保护) + let finalUrl = annotatedImageUrl; + if (annotatedImageUrl.startsWith("data:")) { + try { + toast.info("正在上传图片..."); + const response = await fetch(annotatedImageUrl); + const blob = await response.blob(); + const formData = new FormData(); + formData.append("file", blob, `annotated-${Date.now()}.png`); + const uploadResponse = await fetch("/api/files/upload", { + method: "POST", + body: formData, + }); + if (uploadResponse.ok) { + const result = await uploadResponse.json(); + finalUrl = result.url; + } else { + toast.error("上传失败"); + return; + } + } catch (error) { + console.error("上传标注图片失败:", error); + toast.error("上传失败"); + return; + } + } + // 更新当前节点,标记为已标注图片 // 标注后的图片会带有视觉标记(箭头、圆圈、文字等) // 这些标记指示了用户希望 AI 重点关注/修改的区域 updateNode(node.id, { data: { ...data, - src: annotatedImageUrl, + src: finalUrl, hasAnnotations: true, // 🆕 标记为已标注 originalSrc: data.originalSrc || data.src, // 保留原始图片 prompt: data.prompt ? `${data.prompt} (已标注)` : '已标注图片', diff --git a/components/canvas/nodes/VideoNode.tsx b/components/canvas/nodes/VideoNode.tsx index 3271dfb0a..57eb50c1e 100644 --- a/components/canvas/nodes/VideoNode.tsx +++ b/components/canvas/nodes/VideoNode.tsx @@ -32,8 +32,8 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) { const updateNode = useCanvasStore((state) => state.updateNode); const deleteNode = useCanvasStore((state) => state.deleteNode); - // 处理视频上传 - const handleVideoUpload = (e: React.ChangeEvent) => { + // 处理视频上传 - 上传到服务器获取 URL + const handleVideoUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; @@ -43,14 +43,40 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) { return; } - const reader = new FileReader(); - reader.onload = (event) => { - const src = event.target?.result as string; - updateNode(node.id, { - data: { ...data, src, status: 'completed' }, + // 更新状态为上传中 + updateNode(node.id, { + data: { ...data, status: 'generating' }, + }); + + try { + // 上传视频到服务器 + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch('/api/video/upload', { + method: 'POST', + body: formData, }); - }; - reader.readAsDataURL(file); + + if (!response.ok) { + throw new Error('上传失败'); + } + + const result = await response.json(); + + // 使用服务器返回的 URL + updateNode(node.id, { + data: { ...data, src: result.url, status: 'completed' }, + }); + + toast.success('视频上传成功'); + } catch (error) { + console.error('视频上传失败:', error); + toast.error('视频上传失败,请重试'); + updateNode(node.id, { + data: { ...data, status: 'failed' }, + }); + } }; // 播放/暂停 diff --git a/components/multimodal-input.tsx b/components/multimodal-input.tsx index 9410f0f14..17ea135a0 100644 --- a/components/multimodal-input.tsx +++ b/components/multimodal-input.tsx @@ -797,13 +797,38 @@ function PureMultimodalInput({ }); toast.success(`已添加标注:${annotation.description}`); }} - onDrawingComplete={(annotatedImageUrl) => { + onDrawingComplete={async (annotatedImageUrl) => { // 将标注后的图片替换原图片(或添加为新附件) const fileName = `annotated-${Date.now()}.png`; + + // 如果仍然是 base64 格式,先上传到服务器 + let finalUrl = annotatedImageUrl; + if (annotatedImageUrl.startsWith("data:")) { + try { + toast.info("正在上传图片..."); + const response = await fetch(annotatedImageUrl); + const blob = await response.blob(); + const formData = new FormData(); + formData.append("file", blob, fileName); + const uploadResponse = await fetch("/api/files/upload", { + method: "POST", + body: formData, + }); + if (uploadResponse.ok) { + const result = await uploadResponse.json(); + finalUrl = result.url; + } + } catch (error) { + console.error("上传标注图片失败:", error); + toast.error("上传失败"); + return; + } + } + setAttachments((prev) => [ ...prev, { - url: annotatedImageUrl, + url: finalUrl, name: fileName, contentType: "image/png", }, diff --git a/components/top-navbar.tsx b/components/top-navbar.tsx index 1ada41943..78a07e264 100644 --- a/components/top-navbar.tsx +++ b/components/top-navbar.tsx @@ -15,6 +15,7 @@ import { } from "@/components/ui/dropdown-menu"; import { ImageAnnotator } from "@/components/image-annotator"; import { VideoPlayer } from "@/components/video-player"; +import { toast } from "sonner"; type RankingItem = { rank: number; @@ -130,10 +131,37 @@ export function TopNavbar({ }; // 处理图片编辑完成 - const handleImageEditorConfirm = (annotatedImageUrl: string) => { + const handleImageEditorConfirm = async (annotatedImageUrl: string) => { + // 如果仍然是 base64 格式,先上传到服务器(ImageAnnotator 应该已经处理了,这里作为保护) + let finalUrl = annotatedImageUrl; + if (annotatedImageUrl.startsWith("data:")) { + try { + toast.info("正在上传图片..."); + const response = await fetch(annotatedImageUrl); + const blob = await response.blob(); + const formData = new FormData(); + formData.append("file", blob, `edited-${Date.now()}.png`); + const uploadResponse = await fetch("/api/files/upload", { + method: "POST", + body: formData, + }); + if (uploadResponse.ok) { + const result = await uploadResponse.json(); + finalUrl = result.url; + } else { + toast.error("上传失败"); + return; + } + } catch (error) { + console.error("上传图片失败:", error); + toast.error("上传失败"); + return; + } + } + // 将编辑后的图片存储到 sessionStorage,以便在聊天页面中使用 try { - sessionStorage.setItem("pending-edited-image", annotatedImageUrl); + sessionStorage.setItem("pending-edited-image", finalUrl); } catch (error) { // sessionStorage不可用(如隐私模式),使用URL参数作为备选方案 console.warn("sessionStorage不可用,尝试使用URL参数:", error);