"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}
);
}