import { useState, useRef, useEffect } from "react"; import { useSearchParams } from "react-router-dom"; 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; } interface Project { id: number; name: string; description?: string; user_id: string; created_at: string; updated_at?: string; } interface Wireframe { id: number; project_id: number; name: string; description?: string; canvas_data?: DrawingElement[]; created_at: string; updated_at?: string; } export default function CanvasPage() { const [searchParams] = useSearchParams(); 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; const projectIdFromUrl = searchParams.get("projectId"); // Load projects const { data: userProjects } = await entities .from("nvwa_project") .select("*") .eq("user_id", user.id); if (userProjects && userProjects.length > 0) { setProjects(userProjects); // Use specified project or first one let project: Project; if (projectIdFromUrl) { const found = userProjects.find((p: Project) => p.id === parseInt(projectIdFromUrl)); if (found) { project = found; } else { project = userProjects[0]; } } else { 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 if no projects exist 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