mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
feat(studio): show prompt preview modal before copying
Clicking "Ask agent" now opens a modal that shows the full generated prompt so the user can read it before copying. The modal has a "Copy prompt" button that turns green on success. This replaces the silent clipboard copy that gave no visibility into what was copied.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { memo, useState, useCallback, useRef, useEffect } from "react";
|
import { memo, useState, useCallback, useRef, useEffect } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import { useBlockCatalog } from "../../hooks/useBlockCatalog";
|
import { useBlockCatalog } from "../../hooks/useBlockCatalog";
|
||||||
import {
|
import {
|
||||||
BLOCK_CATEGORIES,
|
BLOCK_CATEGORIES,
|
||||||
@@ -23,6 +24,7 @@ interface BlocksTabProps {
|
|||||||
export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }: BlocksTabProps) {
|
export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }: BlocksTabProps) {
|
||||||
const { loading, error, search, setSearch, category, setCategory, filteredBlocks } =
|
const { loading, error, search, setSearch, category, setCategory, filteredBlocks } =
|
||||||
useBlockCatalog();
|
useBlockCatalog();
|
||||||
|
const [promptModal, setPromptModal] = useState<{ title: string; prompt: string } | null>(null);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -118,6 +120,7 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }:
|
|||||||
posterUrl={block.preview?.poster}
|
posterUrl={block.preview?.poster}
|
||||||
videoUrl={block.preview?.video}
|
videoUrl={block.preview?.video}
|
||||||
onPreview={onPreviewBlock}
|
onPreview={onPreviewBlock}
|
||||||
|
onShowPrompt={setPromptModal}
|
||||||
onAdd={
|
onAdd={
|
||||||
block.category === "vfx" ||
|
block.category === "vfx" ||
|
||||||
block.category === "social" ||
|
block.category === "social" ||
|
||||||
@@ -131,6 +134,15 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{promptModal &&
|
||||||
|
createPortal(
|
||||||
|
<PromptPreviewModal
|
||||||
|
title={promptModal.title}
|
||||||
|
prompt={promptModal.prompt}
|
||||||
|
onClose={() => setPromptModal(null)}
|
||||||
|
/>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -290,11 +302,11 @@ function BlockCard({
|
|||||||
posterUrl?: string;
|
posterUrl?: string;
|
||||||
videoUrl?: string;
|
videoUrl?: string;
|
||||||
onAdd?: () => void;
|
onAdd?: () => void;
|
||||||
|
onShowPrompt?: (info: { title: string; prompt: string }) => void;
|
||||||
onPreview?: (preview: BlockPreviewInfo | null) => void;
|
onPreview?: (preview: BlockPreviewInfo | null) => void;
|
||||||
}) {
|
}) {
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const [adding, setAdding] = useState(false);
|
const [adding, setAdding] = useState(false);
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const colors = getCategoryColors(category);
|
const colors = getCategoryColors(category);
|
||||||
const needsWebGL = tags?.includes("html-in-canvas") || tags?.includes("webgl");
|
const needsWebGL = tags?.includes("html-in-canvas") || tags?.includes("webgl");
|
||||||
@@ -334,7 +346,7 @@ function BlockCard({
|
|||||||
|
|
||||||
const { activeCompPath, compositionDimensions } = useStudioContext();
|
const { activeCompPath, compositionDimensions } = useStudioContext();
|
||||||
|
|
||||||
const handleCopyPrompt = useCallback(
|
const handleShowPrompt = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const state = usePlayerStore.getState();
|
const state = usePlayerStore.getState();
|
||||||
@@ -352,9 +364,7 @@ function BlockCard({
|
|||||||
compositionDimensions: compositionDimensions ?? undefined,
|
compositionDimensions: compositionDimensions ?? undefined,
|
||||||
};
|
};
|
||||||
const prompt = buildAgentPrompt(title, name, description, category, blockType, context);
|
const prompt = buildAgentPrompt(title, name, description, category, blockType, context);
|
||||||
navigator.clipboard.writeText(prompt);
|
onShowPrompt?.({ title, prompt });
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 1500);
|
|
||||||
},
|
},
|
||||||
[title, name, description, category, blockType, activeCompPath, compositionDimensions],
|
[title, name, description, category, blockType, activeCompPath, compositionDimensions],
|
||||||
);
|
);
|
||||||
@@ -418,46 +428,26 @@ function BlockCard({
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCopyPrompt}
|
onClick={handleShowPrompt}
|
||||||
title="Copy a prompt to paste into your AI agent"
|
title="Generate a prompt to paste into your AI agent"
|
||||||
className={`flex items-center gap-1.5 px-3 ${onAdd ? "py-1" : "py-1.5"} rounded-md transition-colors ${
|
className={`flex items-center gap-1.5 px-3 ${onAdd ? "py-1" : "py-1.5"} rounded-md transition-colors ${
|
||||||
copied
|
onAdd
|
||||||
? "bg-emerald-500 text-white"
|
? "bg-white/15 text-white/90 hover:bg-white/25 text-[9px]"
|
||||||
: onAdd
|
: "bg-white text-black hover:bg-neutral-200 text-[10px] font-semibold"
|
||||||
? "bg-white/15 text-white/90 hover:bg-white/25"
|
}`}
|
||||||
: "bg-white text-black hover:bg-neutral-200"
|
|
||||||
} ${onAdd ? "text-[9px]" : "text-[10px] font-semibold"}`}
|
|
||||||
>
|
>
|
||||||
{copied ? (
|
<svg
|
||||||
<>
|
width={onAdd ? 9 : 11}
|
||||||
<svg
|
height={onAdd ? 9 : 11}
|
||||||
width={onAdd ? 9 : 11}
|
viewBox="0 0 24 24"
|
||||||
height={onAdd ? 9 : 11}
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
stroke="currentColor"
|
||||||
fill="none"
|
strokeWidth="2"
|
||||||
stroke="currentColor"
|
>
|
||||||
strokeWidth="2.5"
|
<rect x="9" y="9" width="13" height="13" rx="2" />
|
||||||
>
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||||
<path d="M20 6 9 17l-5-5" />
|
</svg>
|
||||||
</svg>
|
Ask agent
|
||||||
Copied!
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<svg
|
|
||||||
width={onAdd ? 9 : 11}
|
|
||||||
height={onAdd ? 9 : 11}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" />
|
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
||||||
</svg>
|
|
||||||
Ask agent
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -491,3 +481,78 @@ function BlockCard({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PromptPreviewModal({
|
||||||
|
title,
|
||||||
|
prompt,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
prompt: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleCopy = useCallback(() => {
|
||||||
|
navigator.clipboard.writeText(prompt);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1500);
|
||||||
|
}, [prompt]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-[520px] max-h-[80vh] flex flex-col rounded-2xl border border-neutral-800 bg-neutral-950 shadow-2xl"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between px-5 py-4 border-b border-neutral-800/60">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-neutral-200">Ask agent</h3>
|
||||||
|
<p className="text-xs text-neutral-500 mt-0.5">{title}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="p-1 rounded-md text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800/50"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto px-5 py-4">
|
||||||
|
<p className="text-[11px] text-neutral-500 mb-2">
|
||||||
|
Copy this prompt and paste it into your AI agent (Claude Code, Cursor, etc.)
|
||||||
|
</p>
|
||||||
|
<pre className="text-[11px] text-neutral-300 leading-relaxed whitespace-pre-wrap font-mono bg-neutral-900/60 rounded-lg p-3 border border-neutral-800 select-all">
|
||||||
|
{prompt}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between px-5 py-3 border-t border-neutral-800/60">
|
||||||
|
<span className="text-[11px] text-neutral-600">Paste into your agent after copying</span>
|
||||||
|
<button
|
||||||
|
className={`px-4 py-1.5 rounded-lg text-xs font-medium transition-colors ${
|
||||||
|
copied
|
||||||
|
? "bg-emerald-500 text-white"
|
||||||
|
: "bg-studio-accent/90 text-neutral-950 hover:bg-studio-accent"
|
||||||
|
}`}
|
||||||
|
onClick={handleCopy}
|
||||||
|
>
|
||||||
|
{copied ? "Copied!" : "Copy prompt"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user