Webapp: copy paste image references into promptbox

This commit is contained in:
bflatastic
2026-07-25 09:10:01 +07:00
parent a49426b77d
commit 9fbbfb5877
2 changed files with 59 additions and 16 deletions
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { twMerge } from "tailwind-merge";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faImages, faMusic, faVideo } from "@fortawesome/pro-solid-svg-icons";
@@ -51,9 +51,10 @@ const listKinds = (labels: string[]) =>
: `${labels.slice(0, -1).join(", ")} or ${labels[labels.length - 1]}`;
/**
* Makes the prompt box a drop target for reference media. Files are routed
* by MIME type; kinds the current model doesn't take are rejected with a
* toast instead of silently vanishing.
* Makes the prompt box a drop target for reference media, and catches
* clipboard pastes of files anywhere on the page (focus-independent). Files
* are routed by MIME type; kinds the current model doesn't take are
* rejected with a toast instead of silently vanishing.
*/
export function usePromptBoxDrop({
acceptsImages,
@@ -134,15 +135,7 @@ export function usePromptBoxDrop({
}
};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
if (!enabled || !e.dataTransfer.types.includes("Files")) return;
e.preventDefault();
e.stopPropagation();
setDragState("idle");
const files = Array.from(e.dataTransfer.files);
if (files.length === 0) return;
const routeFiles = (files: File[]) => {
const accepted: DroppedFiles = { images: [], videos: [], audios: [] };
const rejectedKinds = new Set<DropKind>();
let unknownCount = 0;
@@ -175,11 +168,47 @@ export function usePromptBoxDrop({
);
} else if (unknownCount > 0 && !anyAccepted) {
toast.error(
`Only ${listKinds(acceptedKindLabels(acceptsImages, acceptsVideos, acceptsAudio))} files can be dropped here`,
`Only ${listKinds(acceptedKindLabels(acceptsImages, acceptsVideos, acceptsAudio))} files can be added here`,
);
}
};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
if (!enabled || !e.dataTransfer.types.includes("Files")) return;
e.preventDefault();
e.stopPropagation();
setDragState("idle");
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) routeFiles(files);
};
// Paste-to-add: a copied image (or media file) pasted anywhere on the
// page lands in the deck, whether or not the textarea is focused. The
// listener re-reads the freshest router through a ref so it can stay
// subscribed once.
const routeFilesRef = useRef(routeFiles);
useEffect(() => {
routeFilesRef.current = routeFiles;
});
useEffect(() => {
if (!enabled) return;
const handlePaste = (e: ClipboardEvent) => {
const files = Array.from(e.clipboardData?.files ?? []);
// Plain text pastes carry no files — leave them entirely alone.
if (files.length === 0) return;
// Guard against double-adding if more than one box ever mounts.
const marked = e as ClipboardEvent & { promptBoxHandled?: boolean };
if (marked.promptBoxHandled) return;
marked.promptBoxHandled = true;
// No preventDefault: any text alongside the file still pastes into
// whichever field is focused.
routeFilesRef.current(files);
};
window.addEventListener("paste", handlePaste);
return () => window.removeEventListener("paste", handlePaste);
}, [enabled]);
return {
dragState,
dropZoneProps: {
@@ -572,6 +572,20 @@ export default function CreateVideo() {
activeCharacters,
]);
// Seedance 2.0 models support @-mention references in the prompt — once at
// least one ref is attached, surface that in the placeholder.
const isSeedance2Model = (selectedModel?.model ?? "").startsWith(
"seedance_2p0",
);
const hasUploadedRefs =
referenceImages.length > 0 ||
referenceVideos.length > 0 ||
referenceAudios.length > 0;
const promptPlaceholder =
isSeedance2Model && isReferenceMode && hasUploadedRefs
? "Describe your video. Use @Image1, @Video1... to reference your uploads"
: "Describe the video you want to generate...";
const modelItems = useMemo(
() => buildModelPopoverItems(apiModels, selectedModel?.model ?? ""),
[apiModels, selectedModel?.model],
@@ -1287,7 +1301,7 @@ export default function CreateVideo() {
onSubmit={handleGenerate}
isSubmitting={isGenerating}
credits={estimatedCredits}
placeholder="Describe the video you want to generate..."
placeholder={promptPlaceholder}
mentionItems={mentionItems.length > 0 ? mentionItems : undefined}
autoAdvance={loggedIn && !!prompt.trim() && !isGenerating && !needsImage}
banner={
@@ -1559,7 +1573,7 @@ export default function CreateVideo() {
isSubmitting={isGenerating}
credits={estimatedCredits}
maxPromptLength={maxPromptLength}
placeholder="Describe the video you want to generate..."
placeholder={promptPlaceholder}
supportsImagePrompts={supportsImagePrompts}
maxImagePromptCount={
isReferenceMode ? (selectedModel?.image_references_max ?? 3) : 1