From 7304ae0c4b54e2ec5563b394935306464e358c08 Mon Sep 17 00:00:00 2001 From: NVWA Code Agent Date: Wed, 17 Dec 2025 08:24:57 +0000 Subject: [PATCH] application [wireframe-renderer-web] view page [pages/canvas] development --- .../src/pages/canvas.tsx | 546 +++++++++++++ database/migrations/0000_melted_blade.sql | 111 +++ database/migrations/meta/0000_snapshot.json | 717 ++++++++++++++++++ database/migrations/meta/_journal.json | 13 + 4 files changed, 1387 insertions(+) create mode 100644 apps/wireframe-renderer-web/src/pages/canvas.tsx create mode 100644 database/migrations/0000_melted_blade.sql create mode 100644 database/migrations/meta/0000_snapshot.json create mode 100644 database/migrations/meta/_journal.json diff --git a/apps/wireframe-renderer-web/src/pages/canvas.tsx b/apps/wireframe-renderer-web/src/pages/canvas.tsx new file mode 100644 index 0000000..ab882df --- /dev/null +++ b/apps/wireframe-renderer-web/src/pages/canvas.tsx @@ -0,0 +1,546 @@ +import { useState, useRef, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { Upload, Download, Paintbrush, Square, Circle, Minus, Type, Trash2, Play, Save, Folder } from "lucide-react"; +import { skill, entities, auth } from "@/lib/nvwa"; + +type Tool = "pen" | "rectangle" | "circle" | "line" | "text" | "eraser"; + +interface DrawingElement { + type: Tool; + x: number; + y: number; + width?: number; + height?: number; + x2?: number; + y2?: number; + text?: string; + color?: string; + strokeWidth?: number; +} + +export default function CanvasPage() { + const canvasRef = useRef(null); + const [tool, setTool] = useState("pen"); + const [isDrawing, setIsDrawing] = useState(false); + const [startPos, setStartPos] = useState({ x: 0, y: 0 }); + const [description, setDescription] = useState(""); + const [generatedImageUrl, setGeneratedImageUrl] = useState(null); + const [isGenerating, setIsGenerating] = useState(false); + const [elements, setElements] = useState([]); + const [currentProject, setCurrentProject] = useState(null); + const [currentWireframe, setCurrentWireframe] = useState(null); + const [projects, setProjects] = useState([]); + const [wireframeName, setWireframeName] = useState("Untitled Wireframe"); + const [isSaving, setIsSaving] = useState(false); + + // Canvas drawing functions + const startDrawing = (e: React.MouseEvent) => { + const canvas = canvasRef.current; + if (!canvas) return; + + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + setStartPos({ x, y }); + setIsDrawing(true); + + if (tool === "pen") { + // For pen, we add points continuously + const newElement: DrawingElement = { + type: "pen", + x, + y, + color: "#000000", + strokeWidth: 2 + }; + setElements(prev => [...prev, newElement]); + } + }; + + const draw = (e: React.MouseEvent) => { + if (!isDrawing || !canvasRef.current) return; + + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + if (tool === "pen") { + // Draw freehand line + ctx.strokeStyle = "#000000"; + ctx.lineWidth = 2; + ctx.lineCap = "round"; + ctx.beginPath(); + ctx.moveTo(startPos.x, startPos.y); + ctx.lineTo(x, y); + ctx.stroke(); + + // Update last element with current position + setElements(prev => { + const newElements = [...prev]; + const lastElement = newElements[newElements.length - 1]; + if (lastElement && lastElement.type === "pen") { + lastElement.x2 = x; + lastElement.y2 = y; + } + return newElements; + }); + setStartPos({ x, y }); + } else { + // Preview shape while drawing + redrawCanvas(); + ctx.strokeStyle = "#000000"; + ctx.lineWidth = 2; + + switch (tool) { + case "rectangle": + ctx.strokeRect(startPos.x, startPos.y, x - startPos.x, y - startPos.y); + break; + case "circle": + const radius = Math.sqrt(Math.pow(x - startPos.x, 2) + Math.pow(y - startPos.y, 2)); + ctx.beginPath(); + ctx.arc(startPos.x, startPos.y, radius, 0, 2 * Math.PI); + ctx.stroke(); + break; + case "line": + ctx.beginPath(); + ctx.moveTo(startPos.x, startPos.y); + ctx.lineTo(x, y); + ctx.stroke(); + break; + } + } + }; + + const stopDrawing = (e: React.MouseEvent) => { + if (!isDrawing || !canvasRef.current) return; + + const canvas = canvasRef.current; + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + if (tool !== "pen") { + const newElement: DrawingElement = { + type: tool, + x: startPos.x, + y: startPos.y, + x2: x, + y2: y, + color: "#000000", + strokeWidth: 2 + }; + setElements(prev => [...prev, newElement]); + } + + setIsDrawing(false); + redrawCanvas(); + }; + + const redrawCanvas = () => { + const canvas = canvasRef.current; + const ctx = canvas?.getContext("2d"); + if (!canvas || !ctx) return; + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Redraw all elements + elements.forEach(element => { + ctx.strokeStyle = element.color || "#000000"; + ctx.lineWidth = element.strokeWidth || 2; + + switch (element.type) { + case "rectangle": + if (element.x2 && element.y2) { + ctx.strokeRect(element.x, element.y, element.x2 - element.x, element.y2 - element.y); + } + break; + case "circle": + if (element.x2 && element.y2) { + const radius = Math.sqrt(Math.pow(element.x2 - element.x, 2) + Math.pow(element.y2 - element.y, 2)); + ctx.beginPath(); + ctx.arc(element.x, element.y, radius, 0, 2 * Math.PI); + ctx.stroke(); + } + break; + case "line": + if (element.x2 && element.y2) { + ctx.beginPath(); + ctx.moveTo(element.x, element.y); + ctx.lineTo(element.x2, element.y2); + ctx.stroke(); + } + break; + } + }); + }; + + useEffect(() => { + redrawCanvas(); + }, [elements]); + + useEffect(() => { + loadProjectsAndWireframe(); + }, []); + + const loadProjectsAndWireframe = async () => { + try { + const user = await auth.currentUser(); + if (!user) return; + + // Load projects + const { data: userProjects } = await entities + .from("nvwa_project") + .select("*") + .eq("user_id", user.id); + + if (userProjects && userProjects.length > 0) { + setProjects(userProjects); + // Use first project or create new one + const project = userProjects[0]; + setCurrentProject(project); + + // Load latest wireframe for this project + const { data: wireframes } = await entities + .from("nvwa_wireframe") + .select("*") + .eq("project_id", project.id) + .order("updated_at", { ascending: false }) + .limit(1); + + if (wireframes && wireframes.length > 0) { + const wireframe = wireframes[0]; + setCurrentWireframe(wireframe); + setWireframeName(wireframe.name); + setDescription(wireframe.description || ""); + + // Load canvas data + if (wireframe.canvas_data) { + setElements(wireframe.canvas_data); + } + } + } else { + // Create default project + const { data: newProject } = await entities + .from("nvwa_project") + .insert({ + name: "Default Project", + description: "Default wireframe project", + user_id: user.id + }) + .select() + .single(); + + if (newProject) { + setCurrentProject(newProject); + setProjects([newProject]); + } + } + } catch (error) { + console.error("Failed to load projects:", error); + } + }; + + const handleFileUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const img = new Image(); + img.onload = () => { + const canvas = canvasRef.current; + const ctx = canvas?.getContext("2d"); + if (canvas && ctx) { + // Resize canvas to fit image + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + } + }; + img.src = event.target?.result as string; + }; + reader.readAsDataURL(file); + }; + + const generateImage = async () => { + if (!canvasRef.current || !description.trim()) return; + + // Ensure wireframe is saved before generation + if (!currentWireframe) { + await saveWireframe(); + if (!currentWireframe) return; // Still no wireframe, maybe save failed + } + + setIsGenerating(true); + try { + // Convert canvas to base64 + const canvas = canvasRef.current; + const dataUrl = canvas.toDataURL("image/png"); + + // Use DoodlePainting skill + const result = await skill.execute("DoodlePainting", { + sketch: dataUrl, + prompt: description, + style: "modern", + structureStrength: 0.8 + }); + + if (result && result.imageUrls && result.imageUrls.length > 0) { + const generatedUrl = result.imageUrls[0]; + setGeneratedImageUrl(generatedUrl); + + // Save generation to database + await entities + .from("nvwa_generation") + .insert({ + wireframe_id: currentWireframe.id, + prompt: description, + status: "completed", + generated_image_url: generatedUrl + }); + } + } catch (error) { + console.error("Generation failed:", error); + alert("生成失败,请重试"); + + // Save failed generation + if (currentWireframe) { + await entities + .from("nvwa_generation") + .insert({ + wireframe_id: currentWireframe.id, + prompt: description, + status: "failed", + error_message: error instanceof Error ? error.message : "Unknown error" + }); + } + } finally { + setIsGenerating(false); + } + }; + + const downloadImage = () => { + if (!generatedImageUrl) return; + + const link = document.createElement("a"); + link.href = generatedImageUrl; + link.download = "wireframe-render.png"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const saveWireframe = async () => { + if (!currentProject) return; + + setIsSaving(true); + try { + const canvasData = elements; + + if (currentWireframe) { + // Update existing wireframe + await entities + .from("nvwa_wireframe") + .update({ + name: wireframeName, + canvas_data: canvasData, + description: description + }) + .eq("id", currentWireframe.id); + } else { + // Create new wireframe + const { data: newWireframe } = await entities + .from("nvwa_wireframe") + .insert({ + project_id: currentProject.id, + name: wireframeName, + canvas_data: canvasData, + description: description + }) + .select() + .single(); + + if (newWireframe) { + setCurrentWireframe(newWireframe); + } + } + } catch (error) { + console.error("Failed to save wireframe:", error); + alert("保存失败,请重试"); + } finally { + setIsSaving(false); + } + }; + + const clearCanvas = () => { + setElements([]); + const canvas = canvasRef.current; + const ctx = canvas?.getContext("2d"); + if (canvas && ctx) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + } + }; + + return ( +
+ {/* Toolbar */} +
+ + + + + + + + +
+ + {/* Canvas */} +
+ +
+ + {/* Sidebar */} +
+ + + Project & Wireframe + + +
+ + +
+
+ + setWireframeName(e.target.value)} + placeholder="Wireframe name" + /> +
+ +
+
+ + + + Description + + +