"use client"; import { useEffect, useMemo, useState, type ReactNode } from "react"; import { Modal, Segmented, Tooltip } from "antd"; import { Camera, Download, FolderPlus, Image as ImageIcon, Info, Lock, LockOpen, Maximize2, MessageSquare, Minus, Music2, Pencil, Plus, RefreshCw, Scissors, Settings2, Trash2, Upload, Video } from "lucide-react"; import { canvasThemes } from "@/lib/canvas-theme"; import { formatBytes, getDataUrlByteSize } from "@/lib/image-utils"; import { useThemeStore } from "@/stores/use-theme-store"; import { CanvasNodeType, type CanvasNodeData, type ViewportTransform } from "../types"; type CanvasNodeHoverToolbarProps = { node: CanvasNodeData | null; viewport: ViewportTransform; onKeep: (nodeId: string) => void; onLeave: () => void; onInfo: (node: CanvasNodeData) => void; onEditText: (node: CanvasNodeData) => void; onDecreaseFont: (node: CanvasNodeData) => void; onIncreaseFont: (node: CanvasNodeData) => void; onToggleDialog: (node: CanvasNodeData) => void; onGenerateImage: (node: CanvasNodeData) => void; onUpload: (node: CanvasNodeData) => void; onDownload: (node: CanvasNodeData) => void; onSaveAsset: (node: CanvasNodeData) => void; onCrop: (node: CanvasNodeData) => void; onAngle: (node: CanvasNodeData) => void; onViewImage: (node: CanvasNodeData) => void; onRetry: (node: CanvasNodeData) => void; onToggleFreeResize: (node: CanvasNodeData) => void; onDelete: (node: CanvasNodeData) => void; }; export function CanvasNodeHoverToolbar({ node, viewport, onKeep, onLeave, onInfo, onEditText, onDecreaseFont, onIncreaseFont, onToggleDialog, onGenerateImage, onUpload, onDownload, onSaveAsset, onCrop, onAngle, onViewImage, onRetry, onToggleFreeResize, onDelete, }: CanvasNodeHoverToolbarProps) { if (!node) return null; const left = viewport.x + (node.position.x + node.width / 2) * viewport.k; const top = viewport.y + node.position.y * viewport.k - 14; const isImage = node.type === CanvasNodeType.Image; const isVideo = node.type === CanvasNodeType.Video; const isAudio = node.type === CanvasNodeType.Audio; const hasImage = isImage && Boolean(node.metadata?.content); const hasVideo = isVideo && Boolean(node.metadata?.content); const hasAudio = isAudio && Boolean(node.metadata?.content); const isText = node.type === CanvasNodeType.Text; const isConfig = node.type === CanvasNodeType.Config; const canOpenDialog = isText || hasImage || isVideo; const canRetry = node.metadata?.status === "error"; const hasSpecificTools = canRetry || isText || isImage || isVideo || isAudio || isConfig; return (
onKeep(node.id)} onMouseLeave={onLeave} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} > } onClick={() => onInfo(node)} /> } onClick={() => onDelete(node)} danger /> {hasSpecificTools ? : null} {canRetry ? } onClick={() => onRetry(node)} /> : null} {hasImage || hasVideo || isText ? } onClick={() => onSaveAsset(node)} /> : null} {hasImage || hasVideo || hasAudio ? } onClick={() => onDownload(node)} /> : null} {canOpenDialog ? } onClick={() => onToggleDialog(node)} /> : null} {isText ? } onClick={() => onEditText(node)} /> : null} {isText ? } onClick={() => onGenerateImage(node)} /> : null} {isConfig ? } onClick={() => onInfo(node)} /> : null} {isText ? } onClick={() => onDecreaseFont(node)} /> : null} {isText ? } onClick={() => onIncreaseFont(node)} /> : null} {isImage ? } onClick={() => onUpload(node)} /> : null} {isVideo ? } onClick={() => onUpload(node)} /> : null} {isAudio ? } onClick={() => onUpload(node)} /> : null} {hasImage ? ( : } onClick={() => onToggleFreeResize(node)} active={node.metadata?.freeResize} /> ) : null} {hasImage ? } onClick={() => onCrop(node)} /> : null} {hasImage ? } onClick={() => onAngle(node)} /> : null} {hasImage ? } onClick={() => onViewImage(node)} /> : null}
); } export function CanvasNodeInfoModal({ node, open, onClose }: { node: CanvasNodeData | null; open: boolean; onClose: () => void }) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const [view, setView] = useState<"info" | "json">("info"); const imageBytes = node?.type === CanvasNodeType.Image && node.metadata?.content ? getDataUrlByteSize(node.metadata.content) : 0; const batchCount = node?.type === CanvasNodeType.Image ? node.metadata?.batchChildIds?.length || 0 : 0; const json = useMemo(() => { if (!node) return ""; return JSON.stringify( node, (key, value) => { if (key === "title") return undefined; if (key === "content" && typeof value === "string" && value.startsWith("data:image/")) { return "[base64 image]"; } return value; }, 2, ); }, [node]); useEffect(() => { if (open) setView("info"); }, [node?.id, open]); const title = (
节点信息 setView(value as "info" | "json")} options={[ { label: "信息", value: "info" }, { label: "JSON", value: "json" }, ]} />
); return ( {node ? (
{view === "info" ? (
{batchCount > 1 ? : null} {node.metadata?.prompt ? : null} {imageBytes ? : null} {node.metadata?.errorDetails ? (
{node.metadata.errorDetails}
) : null}
) : (
                            {json}
                        
)}
) : null}
); } function ToolbarAction({ title, label, icon, onClick, hint, active = false, danger = false }: { title: string; label: string; icon: ReactNode; onClick?: () => void; hint?: string; active?: boolean; danger?: boolean }) { return ( ); } function IconAction({ title, icon, onClick }: { title: string; icon: ReactNode; onClick: () => void }) { return ( ); } function ToolbarDivider() { return ; } function InfoRow({ label, value }: { label: string; value: ReactNode }) { return (
{label} {value}
); }