|
|
|
@ -6,11 +6,13 @@ import { BaseNode } from "./BaseNode"; |
|
|
|
import { useCommentNavigation } from "@/hooks/useCommentNavigation"; |
|
|
|
import { useWorkflowStore } from "@/store/workflowStore"; |
|
|
|
import { ImageInputNodeData } from "@/types"; |
|
|
|
import { defaultNodeDimensions } from "@/store/utils/nodeDefaults"; |
|
|
|
import { useAdaptiveImageSrc } from "@/hooks/useAdaptiveImageSrc"; |
|
|
|
import { downloadMedia } from "@/utils/downloadMedia"; |
|
|
|
import { useShowHandleLabels } from "@/hooks/useShowHandleLabels"; |
|
|
|
import { HandleLabel } from "./HandleLabel"; |
|
|
|
import { readImageFile } from "@/utils/imageFile"; |
|
|
|
import type { LoadedImageFile } from "@/utils/imageFile"; |
|
|
|
|
|
|
|
type ImageInputNodeType = Node<ImageInputNodeData, "imageInput">; |
|
|
|
|
|
|
|
@ -19,24 +21,64 @@ export function ImageInputNode({ id, data, selected }: NodeProps<ImageInputNodeT |
|
|
|
const adaptiveImage = useAdaptiveImageSrc(nodeData.image, id); |
|
|
|
const commentNavigation = useCommentNavigation(id); |
|
|
|
const updateNodeData = useWorkflowStore((state) => state.updateNodeData); |
|
|
|
const addNode = useWorkflowStore((state) => state.addNode); |
|
|
|
const onConnect = useWorkflowStore((state) => state.onConnect); |
|
|
|
const nodes = useWorkflowStore((state) => state.nodes); |
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null); |
|
|
|
const imageToImageInputRef = useRef<HTMLInputElement>(null); |
|
|
|
const showLabels = useShowHandleLabels(selected); |
|
|
|
|
|
|
|
const writeLoadedImage = useCallback( |
|
|
|
(loaded: LoadedImageFile) => { |
|
|
|
updateNodeData(id, { |
|
|
|
image: loaded.image, |
|
|
|
imageRef: undefined, |
|
|
|
filename: loaded.filename, |
|
|
|
dimensions: loaded.dimensions, |
|
|
|
}); |
|
|
|
}, |
|
|
|
[id, updateNodeData] |
|
|
|
); |
|
|
|
|
|
|
|
const applyImageFile = useCallback( |
|
|
|
async (file: File) => { |
|
|
|
try { |
|
|
|
const loaded = await readImageFile(file); |
|
|
|
updateNodeData(id, { |
|
|
|
image: loaded.image, |
|
|
|
imageRef: undefined, |
|
|
|
filename: loaded.filename, |
|
|
|
dimensions: loaded.dimensions, |
|
|
|
writeLoadedImage(loaded); |
|
|
|
} catch (error) { |
|
|
|
alert(error instanceof Error ? error.message : "Failed to load image file."); |
|
|
|
} |
|
|
|
}, |
|
|
|
[writeLoadedImage] |
|
|
|
); |
|
|
|
|
|
|
|
const handleImageToImageFile = useCallback( |
|
|
|
async (file: File) => { |
|
|
|
try { |
|
|
|
const loaded = await readImageFile(file); |
|
|
|
writeLoadedImage(loaded); |
|
|
|
|
|
|
|
const currentNode = nodes.find((node) => node.id === id); |
|
|
|
const currentWidth = |
|
|
|
currentNode?.measured?.width ?? |
|
|
|
(typeof currentNode?.style?.width === "number" ? currentNode.style.width : defaultNodeDimensions.imageInput.width); |
|
|
|
const currentPosition = currentNode?.position ?? { x: 0, y: 0 }; |
|
|
|
const nextNodeId = addNode("nanoBanana", { |
|
|
|
x: currentPosition.x + currentWidth + 180, |
|
|
|
y: currentPosition.y, |
|
|
|
}); |
|
|
|
|
|
|
|
onConnect({ |
|
|
|
source: id, |
|
|
|
sourceHandle: "image", |
|
|
|
target: nextNodeId, |
|
|
|
targetHandle: "image", |
|
|
|
}); |
|
|
|
} catch (error) { |
|
|
|
alert(error instanceof Error ? error.message : "Failed to load image file."); |
|
|
|
} |
|
|
|
}, |
|
|
|
[id, updateNodeData] |
|
|
|
[addNode, id, nodes, onConnect, writeLoadedImage] |
|
|
|
); |
|
|
|
|
|
|
|
const handleFileChange = useCallback( |
|
|
|
@ -51,6 +93,18 @@ export function ImageInputNode({ id, data, selected }: NodeProps<ImageInputNodeT |
|
|
|
[applyImageFile] |
|
|
|
); |
|
|
|
|
|
|
|
const handleImageToImageChange = useCallback( |
|
|
|
(e: React.ChangeEvent<HTMLInputElement>) => { |
|
|
|
const input = e.currentTarget; |
|
|
|
const file = input.files?.[0]; |
|
|
|
if (!file) return; |
|
|
|
|
|
|
|
void handleImageToImageFile(file); |
|
|
|
input.value = ""; |
|
|
|
}, |
|
|
|
[handleImageToImageFile] |
|
|
|
); |
|
|
|
|
|
|
|
const handleDrop = useCallback( |
|
|
|
(e: React.DragEvent) => { |
|
|
|
e.preventDefault(); |
|
|
|
@ -93,6 +147,14 @@ export function ImageInputNode({ id, data, selected }: NodeProps<ImageInputNodeT |
|
|
|
onChange={handleFileChange} |
|
|
|
className="hidden" |
|
|
|
/> |
|
|
|
<input |
|
|
|
ref={imageToImageInputRef} |
|
|
|
type="file" |
|
|
|
accept="image/png,image/jpeg,image/webp" |
|
|
|
onChange={handleImageToImageChange} |
|
|
|
className="hidden" |
|
|
|
aria-label="Upload image for image to image" |
|
|
|
/> |
|
|
|
|
|
|
|
{nodeData.image ? ( |
|
|
|
<div className="relative group w-full h-full overflow-clip rounded-lg"> |
|
|
|
@ -131,10 +193,42 @@ export function ImageInputNode({ id, data, selected }: NodeProps<ImageInputNodeT |
|
|
|
onDragOver={handleDragOver} |
|
|
|
className={`w-full h-full bg-neutral-900/40 flex flex-col items-center justify-center transition-colors ${nodeData.isOptional ? "border-2 border-dashed border-neutral-600" : ""}`} |
|
|
|
> |
|
|
|
<svg className="w-8 h-8 text-neutral-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> |
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" /> |
|
|
|
<svg className="w-12 h-12 text-neutral-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.4}> |
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="m3 16 4.5-4.5a2.1 2.1 0 0 1 3 0L14 15m-1-1 1.5-1.5a2.1 2.1 0 0 1 3 0L21 16m-15 4h12a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3H6a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3Zm11-11.5h.01" /> |
|
|
|
</svg> |
|
|
|
<span className="text-xs text-neutral-500 mt-2">{nodeData.isOptional ? "Optional" : "Drop image"}</span> |
|
|
|
{nodeData.isOptional ? ( |
|
|
|
<span className="text-xs text-neutral-500 mt-2">Optional</span> |
|
|
|
) : ( |
|
|
|
<div className="mt-8 w-full max-w-[160px] self-start ml-7"> |
|
|
|
<div className="mb-3 text-sm text-neutral-400">尝试:</div> |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
className="nodrag nopan mb-4 flex items-center gap-2 text-sm font-semibold text-white transition-colors hover:text-blue-200 focus:outline-none focus:text-blue-200" |
|
|
|
onClick={(event) => { |
|
|
|
event.preventDefault(); |
|
|
|
event.stopPropagation(); |
|
|
|
imageToImageInputRef.current?.click(); |
|
|
|
}} |
|
|
|
> |
|
|
|
<svg className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}> |
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 16V5m0 0 4 4m-4-4-4 4M5 19h14" /> |
|
|
|
</svg> |
|
|
|
图生图 |
|
|
|
</button> |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
disabled |
|
|
|
aria-label="图片高清" |
|
|
|
title="图片高清功能待接入" |
|
|
|
className="nodrag nopan flex cursor-not-allowed items-center gap-2 text-sm font-semibold text-white/85" |
|
|
|
> |
|
|
|
<span className="flex h-4 min-w-4 items-center justify-center rounded border border-white/80 px-0.5 text-[10px] font-bold leading-none"> |
|
|
|
HD |
|
|
|
</span> |
|
|
|
图片高清 |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
)} |
|
|
|
|
|
|
|
|