@hyperframes/studio 0.6.31 → 0.6.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-CSG9kRJg.js +138 -0
- package/dist/assets/index-SKRp8mGz.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +52 -1
- package/src/components/StudioLeftSidebar.tsx +4 -0
- package/src/components/StudioPreviewArea.tsx +29 -1
- package/src/components/StudioRightPanel.tsx +46 -37
- package/src/components/TimelineToolbar.tsx +62 -55
- package/src/components/editor/PropertyPanel.tsx +1 -1
- package/src/components/editor/manualEditingAvailability.ts +10 -1
- package/src/components/nle/NLELayout.tsx +35 -4
- package/src/components/nle/NLEPreview.test.ts +3 -11
- package/src/components/nle/NLEPreview.tsx +20 -41
- package/src/components/nle/usePreviewBlockDrop.ts +109 -0
- package/src/components/sidebar/BlocksTab.tsx +321 -122
- package/src/components/sidebar/LeftSidebar.tsx +58 -41
- package/src/components/ui/Tooltip.tsx +63 -0
- package/src/components/ui/index.ts +1 -0
- package/src/hooks/useBlockCatalog.ts +5 -1
- package/src/hooks/useDomEditSession.ts +19 -1
- package/src/hooks/useFileManager.ts +3 -0
- package/src/main.tsx +6 -0
- package/src/player/components/PlayerControls.tsx +253 -234
- package/src/player/lib/playbackAdapter.test.ts +3 -3
- package/src/player/lib/playbackAdapter.ts +3 -1
- package/src/utils/blockInstaller.ts +65 -32
- package/src/utils/timelineAssetDrop.test.ts +2 -1
- package/src/utils/timelineAssetDrop.ts +8 -1
- package/dist/assets/index-BWBj8I6Q.css +0 -1
- package/dist/assets/index-Do0kAMcy.js +0 -115
|
@@ -6,16 +6,25 @@ import {
|
|
|
6
6
|
getCategoryColors,
|
|
7
7
|
type BlockCategory,
|
|
8
8
|
} from "../../utils/blockCategories";
|
|
9
|
-
import {
|
|
9
|
+
import { usePlayerStore } from "../../player";
|
|
10
|
+
import { formatTime } from "../../player/lib/time";
|
|
11
|
+
import { useStudioContext } from "../../contexts/StudioContext";
|
|
12
|
+
export interface BlockPreviewInfo {
|
|
13
|
+
videoUrl?: string;
|
|
14
|
+
posterUrl?: string;
|
|
15
|
+
title: string;
|
|
16
|
+
}
|
|
10
17
|
|
|
11
18
|
interface BlocksTabProps {
|
|
12
|
-
onAddBlock
|
|
19
|
+
onAddBlock?: (blockName: string) => void;
|
|
20
|
+
onPreviewBlock?: (preview: BlockPreviewInfo | null) => void;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
// fallow-ignore-next-line complexity
|
|
16
|
-
export const BlocksTab = memo(function BlocksTab({ onAddBlock }: BlocksTabProps) {
|
|
24
|
+
export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }: BlocksTabProps) {
|
|
17
25
|
const { loading, error, search, setSearch, category, setCategory, filteredBlocks } =
|
|
18
26
|
useBlockCatalog();
|
|
27
|
+
const [promptModal, setPromptModal] = useState<{ title: string; prompt: string } | null>(null);
|
|
19
28
|
|
|
20
29
|
if (loading) {
|
|
21
30
|
return (
|
|
@@ -56,7 +65,7 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock }: BlocksTabProps)
|
|
|
56
65
|
type="text"
|
|
57
66
|
value={search}
|
|
58
67
|
onChange={(e) => setSearch(e.target.value)}
|
|
59
|
-
placeholder="Search
|
|
68
|
+
placeholder="Search by name, category, or tag…"
|
|
60
69
|
className="w-full bg-neutral-900 border border-neutral-800 rounded-md pl-7 pr-2 py-1.5 text-[11px] text-neutral-200 placeholder:text-neutral-600 focus:outline-none focus:border-neutral-700 transition-colors"
|
|
61
70
|
/>
|
|
62
71
|
</div>
|
|
@@ -98,28 +107,42 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock }: BlocksTabProps)
|
|
|
98
107
|
>
|
|
99
108
|
{filteredBlocks.map((block) => {
|
|
100
109
|
const dur = "duration" in block ? (block.duration as number) : undefined;
|
|
101
|
-
const dims =
|
|
102
|
-
"dimensions" in block
|
|
103
|
-
? (block.dimensions as { width: number; height: number })
|
|
104
|
-
: undefined;
|
|
105
110
|
return (
|
|
106
111
|
<BlockCard
|
|
107
112
|
key={block.name}
|
|
108
113
|
name={block.name}
|
|
109
114
|
title={block.title}
|
|
115
|
+
description={block.description}
|
|
116
|
+
blockType={block.type}
|
|
110
117
|
duration={dur}
|
|
111
118
|
category={block.category}
|
|
112
119
|
tags={block.tags}
|
|
113
120
|
posterUrl={block.preview?.poster}
|
|
114
121
|
videoUrl={block.preview?.video}
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
onPreview={onPreviewBlock}
|
|
123
|
+
onShowPrompt={setPromptModal}
|
|
124
|
+
onAdd={
|
|
125
|
+
block.category === "vfx" ||
|
|
126
|
+
block.category === "social" ||
|
|
127
|
+
block.category === "scenes"
|
|
128
|
+
? () => onAddBlock?.(block.name)
|
|
129
|
+
: undefined
|
|
130
|
+
}
|
|
117
131
|
/>
|
|
118
132
|
);
|
|
119
133
|
})}
|
|
120
134
|
</div>
|
|
121
135
|
)}
|
|
122
136
|
</div>
|
|
137
|
+
{promptModal &&
|
|
138
|
+
createPortal(
|
|
139
|
+
<PromptPreviewModal
|
|
140
|
+
title={promptModal.title}
|
|
141
|
+
prompt={promptModal.prompt}
|
|
142
|
+
onClose={() => setPromptModal(null)}
|
|
143
|
+
/>,
|
|
144
|
+
document.body,
|
|
145
|
+
)}
|
|
123
146
|
</div>
|
|
124
147
|
);
|
|
125
148
|
});
|
|
@@ -153,77 +176,168 @@ function CategoryPill({
|
|
|
153
176
|
);
|
|
154
177
|
}
|
|
155
178
|
|
|
179
|
+
interface CompositionContext {
|
|
180
|
+
currentTime: number;
|
|
181
|
+
activeCompPath: string | null;
|
|
182
|
+
elements: Array<{
|
|
183
|
+
id: string;
|
|
184
|
+
start: number;
|
|
185
|
+
duration: number;
|
|
186
|
+
track: number;
|
|
187
|
+
label?: string;
|
|
188
|
+
compositionSrc?: string;
|
|
189
|
+
}>;
|
|
190
|
+
compositionDimensions?: { width: number; height: number };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function formatCompositionContext(ctx: CompositionContext): string {
|
|
194
|
+
const lines: string[] = [
|
|
195
|
+
`Playback time: ${formatTime(ctx.currentTime)}`,
|
|
196
|
+
`Active composition: ${ctx.activeCompPath || "index.html"}`,
|
|
197
|
+
];
|
|
198
|
+
if (ctx.compositionDimensions) {
|
|
199
|
+
lines.push(
|
|
200
|
+
`Dimensions: ${ctx.compositionDimensions.width}x${ctx.compositionDimensions.height}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const visibleNow = ctx.elements.filter(
|
|
204
|
+
(el) => ctx.currentTime >= el.start && ctx.currentTime < el.start + el.duration,
|
|
205
|
+
);
|
|
206
|
+
if (visibleNow.length > 0) {
|
|
207
|
+
lines.push(
|
|
208
|
+
"",
|
|
209
|
+
`Elements visible at ${formatTime(ctx.currentTime)}:`,
|
|
210
|
+
...visibleNow.map(
|
|
211
|
+
(el) =>
|
|
212
|
+
`- ${el.label || el.id} (track ${el.track}, ${formatTime(el.start)}–${formatTime(el.start + el.duration)}${el.compositionSrc ? `, src: ${el.compositionSrc}` : ""})`,
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const maxZ = ctx.elements.length > 0 ? Math.max(...ctx.elements.map((_, i) => i + 1)) : 0;
|
|
217
|
+
lines.push("", `Highest track index: ${maxZ}`);
|
|
218
|
+
return lines.join("\n");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildAgentPrompt(
|
|
222
|
+
title: string,
|
|
223
|
+
name: string,
|
|
224
|
+
description: string,
|
|
225
|
+
category: BlockCategory,
|
|
226
|
+
blockType: string,
|
|
227
|
+
context: CompositionContext,
|
|
228
|
+
): string {
|
|
229
|
+
const isComponent = blockType === "hyperframes:component";
|
|
230
|
+
const kind = isComponent ? "component" : "block";
|
|
231
|
+
const compositionInfo = formatCompositionContext(context);
|
|
232
|
+
|
|
233
|
+
const categoryPrompts: Record<string, string> = {
|
|
234
|
+
captions: [
|
|
235
|
+
`Using /hyperframes, add the "${title}" caption style (registry: ${name}) to my composition.`,
|
|
236
|
+
`${description}`,
|
|
237
|
+
`Transcribe the audio with /hyperframes-media, then wire the transcript into this caption component. Match the font colors and animation timing to my composition's design tokens. Place it as an overlay above the main content with the highest z-index.`,
|
|
238
|
+
].join("\n\n"),
|
|
239
|
+
vfx: [
|
|
240
|
+
`Using /hyperframes, add the "${title}" VFX (registry: ${name}) as a full-screen overlay on my composition.`,
|
|
241
|
+
`${description}`,
|
|
242
|
+
`This is a WebGL effect that requires chrome://flags/#html-in-canvas. Layer it on top of all content, adjust the shader uniforms and color palette to complement my scene, and set the duration to match the composition length.`,
|
|
243
|
+
].join("\n\n"),
|
|
244
|
+
transitions: [
|
|
245
|
+
`Using /hyperframes, add the "${title}" transition (registry: ${name}) between my scenes.`,
|
|
246
|
+
`${description}`,
|
|
247
|
+
`Place this transition at the cut point between the current scene and the next. Set the duration to 0.5–1s, position it at the scene boundary on the timeline, and make sure the z-index is above both scenes. Adjust colors to match my palette.`,
|
|
248
|
+
].join("\n\n"),
|
|
249
|
+
effects: [
|
|
250
|
+
`Using /hyperframes, add the "${title}" effect (registry: ${name}) as an overlay on my composition.`,
|
|
251
|
+
`${description}`,
|
|
252
|
+
`Layer this on top of the current content. Adjust the opacity, colors, and animation timing to enhance the scene without overwhelming the main content.`,
|
|
253
|
+
].join("\n\n"),
|
|
254
|
+
social: [
|
|
255
|
+
`Using /hyperframes, add the "${title}" template (registry: ${name}) to my composition.`,
|
|
256
|
+
`${description}`,
|
|
257
|
+
`Replace the placeholder text, handle, and avatar with my actual content. Match the typography and colors to my brand. Adjust timing so the elements animate in sync with the voiceover.`,
|
|
258
|
+
].join("\n\n"),
|
|
259
|
+
data: [
|
|
260
|
+
`Using /hyperframes, add the "${title}" visualization (registry: ${name}) to my composition.`,
|
|
261
|
+
`${description}`,
|
|
262
|
+
`Replace the placeholder data with my actual values and labels. Adjust the color scale, animation stagger timing, and typography to match my composition's design system. Size it to fit the current viewport.`,
|
|
263
|
+
].join("\n\n"),
|
|
264
|
+
scenes: [
|
|
265
|
+
`Using /hyperframes, add the "${title}" scene (registry: ${name}) to my composition.`,
|
|
266
|
+
`${description}`,
|
|
267
|
+
`Replace all placeholder text, images, and content with my actual material. Match fonts, colors, and layout to my existing design tokens. Set the timeline position and duration to fit the narrative flow.`,
|
|
268
|
+
].join("\n\n"),
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const instruction =
|
|
272
|
+
categoryPrompts[category] ??
|
|
273
|
+
[
|
|
274
|
+
`Using /hyperframes, add the "${title}" ${kind} (registry: ${name}) to my composition.`,
|
|
275
|
+
`${description}`,
|
|
276
|
+
`Customize it to match my composition's design and timeline.`,
|
|
277
|
+
].join("\n\n");
|
|
278
|
+
|
|
279
|
+
return [instruction, "", "## Current composition state", "", compositionInfo].join("\n");
|
|
280
|
+
}
|
|
281
|
+
|
|
156
282
|
function BlockCard({
|
|
157
283
|
name,
|
|
158
284
|
title,
|
|
285
|
+
description,
|
|
286
|
+
blockType,
|
|
159
287
|
duration,
|
|
160
288
|
category,
|
|
161
289
|
tags,
|
|
162
290
|
posterUrl,
|
|
163
291
|
videoUrl,
|
|
164
|
-
dimensions,
|
|
165
292
|
onAdd,
|
|
293
|
+
onShowPrompt,
|
|
294
|
+
onPreview,
|
|
166
295
|
}: {
|
|
167
296
|
name: string;
|
|
168
297
|
title: string;
|
|
298
|
+
description: string;
|
|
299
|
+
blockType: string;
|
|
169
300
|
duration?: number;
|
|
170
301
|
category: BlockCategory;
|
|
171
302
|
tags?: string[];
|
|
172
303
|
posterUrl?: string;
|
|
173
304
|
videoUrl?: string;
|
|
174
|
-
|
|
175
|
-
|
|
305
|
+
onAdd?: () => void;
|
|
306
|
+
onShowPrompt?: (info: { title: string; prompt: string }) => void;
|
|
307
|
+
onPreview?: (preview: BlockPreviewInfo | null) => void;
|
|
176
308
|
}) {
|
|
177
309
|
const [hovered, setHovered] = useState(false);
|
|
178
310
|
const [adding, setAdding] = useState(false);
|
|
179
311
|
const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
180
|
-
const leaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
181
|
-
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
182
312
|
const colors = getCategoryColors(category);
|
|
183
313
|
const needsWebGL = tags?.includes("html-in-canvas") || tags?.includes("webgl");
|
|
184
314
|
|
|
185
|
-
const cancelLeave = useCallback(() => {
|
|
186
|
-
if (leaveTimer.current) {
|
|
187
|
-
clearTimeout(leaveTimer.current);
|
|
188
|
-
leaveTimer.current = null;
|
|
189
|
-
}
|
|
190
|
-
}, []);
|
|
191
|
-
|
|
192
315
|
const handleEnter = useCallback(() => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (hoverTimer.current) {
|
|
199
|
-
clearTimeout(hoverTimer.current);
|
|
200
|
-
hoverTimer.current = null;
|
|
201
|
-
}
|
|
202
|
-
cancelLeave();
|
|
203
|
-
setHovered(false);
|
|
204
|
-
}, [cancelLeave]);
|
|
316
|
+
hoverTimer.current = setTimeout(() => {
|
|
317
|
+
setHovered(true);
|
|
318
|
+
onPreview?.({ videoUrl, posterUrl, title });
|
|
319
|
+
}, 300);
|
|
320
|
+
}, [onPreview, videoUrl, posterUrl, title]);
|
|
205
321
|
|
|
206
322
|
const handleLeave = useCallback(() => {
|
|
207
323
|
if (hoverTimer.current) {
|
|
208
324
|
clearTimeout(hoverTimer.current);
|
|
209
325
|
hoverTimer.current = null;
|
|
210
326
|
}
|
|
211
|
-
|
|
212
|
-
|
|
327
|
+
setHovered(false);
|
|
328
|
+
onPreview?.(null);
|
|
329
|
+
}, [onPreview]);
|
|
213
330
|
|
|
214
331
|
useEffect(() => {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (e.key === "Escape") dismiss();
|
|
332
|
+
return () => {
|
|
333
|
+
if (hoverTimer.current) clearTimeout(hoverTimer.current);
|
|
218
334
|
};
|
|
219
|
-
|
|
220
|
-
return () => window.removeEventListener("keydown", onKey);
|
|
221
|
-
}, [hovered, dismiss]);
|
|
335
|
+
}, []);
|
|
222
336
|
|
|
223
337
|
const handleAdd = useCallback(
|
|
224
338
|
(e: React.MouseEvent) => {
|
|
225
339
|
e.stopPropagation();
|
|
226
|
-
if (adding) return;
|
|
340
|
+
if (adding || !onAdd) return;
|
|
227
341
|
setAdding(true);
|
|
228
342
|
onAdd();
|
|
229
343
|
setTimeout(() => setAdding(false), 1000);
|
|
@@ -231,12 +345,38 @@ function BlockCard({
|
|
|
231
345
|
[onAdd, adding],
|
|
232
346
|
);
|
|
233
347
|
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
348
|
+
const { activeCompPath, compositionDimensions } = useStudioContext();
|
|
349
|
+
|
|
350
|
+
const handleShowPrompt = useCallback(
|
|
351
|
+
(e: React.MouseEvent) => {
|
|
352
|
+
e.stopPropagation();
|
|
353
|
+
const state = usePlayerStore.getState();
|
|
354
|
+
const context: CompositionContext = {
|
|
355
|
+
currentTime: state.currentTime,
|
|
356
|
+
activeCompPath,
|
|
357
|
+
elements: state.elements.map((el) => ({
|
|
358
|
+
id: el.id,
|
|
359
|
+
start: el.start,
|
|
360
|
+
duration: el.duration,
|
|
361
|
+
track: el.track,
|
|
362
|
+
label: el.label,
|
|
363
|
+
compositionSrc: el.compositionSrc,
|
|
364
|
+
})),
|
|
365
|
+
compositionDimensions: compositionDimensions ?? undefined,
|
|
366
|
+
};
|
|
367
|
+
const prompt = buildAgentPrompt(title, name, description, category, blockType, context);
|
|
368
|
+
onShowPrompt?.({ title, prompt });
|
|
238
369
|
},
|
|
239
|
-
[
|
|
370
|
+
[
|
|
371
|
+
title,
|
|
372
|
+
name,
|
|
373
|
+
description,
|
|
374
|
+
category,
|
|
375
|
+
blockType,
|
|
376
|
+
activeCompPath,
|
|
377
|
+
compositionDimensions,
|
|
378
|
+
onShowPrompt,
|
|
379
|
+
],
|
|
240
380
|
);
|
|
241
381
|
|
|
242
382
|
return (
|
|
@@ -244,14 +384,11 @@ function BlockCard({
|
|
|
244
384
|
className="group/card rounded-md overflow-hidden cursor-pointer transition-colors bg-neutral-900 hover:bg-neutral-800"
|
|
245
385
|
onPointerEnter={handleEnter}
|
|
246
386
|
onPointerLeave={handleLeave}
|
|
247
|
-
draggable
|
|
248
|
-
onDragStart={handleDragStart}
|
|
249
387
|
>
|
|
250
388
|
{/* Thumbnail */}
|
|
251
389
|
<div className="aspect-video w-full overflow-hidden relative">
|
|
252
390
|
{hovered && videoUrl ? (
|
|
253
391
|
<video
|
|
254
|
-
ref={videoRef}
|
|
255
392
|
src={videoUrl}
|
|
256
393
|
autoPlay
|
|
257
394
|
muted
|
|
@@ -277,14 +414,52 @@ function BlockCard({
|
|
|
277
414
|
</div>
|
|
278
415
|
)}
|
|
279
416
|
|
|
280
|
-
{/*
|
|
281
|
-
<
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
417
|
+
{/* Action overlay */}
|
|
418
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center gap-1.5 bg-black/60 opacity-0 group-hover/card:opacity-100 transition-opacity">
|
|
419
|
+
{onAdd && (
|
|
420
|
+
<button
|
|
421
|
+
type="button"
|
|
422
|
+
onClick={handleAdd}
|
|
423
|
+
title="Add to composition at current time"
|
|
424
|
+
className="flex items-center gap-1 px-3 py-1.5 rounded-md bg-white text-black text-[10px] font-semibold hover:bg-neutral-200 transition-colors"
|
|
425
|
+
>
|
|
426
|
+
<svg
|
|
427
|
+
width="10"
|
|
428
|
+
height="10"
|
|
429
|
+
viewBox="0 0 24 24"
|
|
430
|
+
fill="none"
|
|
431
|
+
stroke="currentColor"
|
|
432
|
+
strokeWidth="2.5"
|
|
433
|
+
>
|
|
434
|
+
<path d="M12 5v14M5 12h14" />
|
|
435
|
+
</svg>
|
|
436
|
+
{adding ? "Added!" : "Add"}
|
|
437
|
+
</button>
|
|
438
|
+
)}
|
|
439
|
+
<button
|
|
440
|
+
type="button"
|
|
441
|
+
onClick={handleShowPrompt}
|
|
442
|
+
title="Generate a prompt to paste into your AI agent"
|
|
443
|
+
className={`flex items-center gap-1.5 px-3 ${onAdd ? "py-1" : "py-1.5"} rounded-md transition-colors ${
|
|
444
|
+
onAdd
|
|
445
|
+
? "bg-white/15 text-white/90 hover:bg-white/25 text-[9px]"
|
|
446
|
+
: "bg-white text-black hover:bg-neutral-200 text-[10px] font-semibold"
|
|
447
|
+
}`}
|
|
448
|
+
>
|
|
449
|
+
<svg
|
|
450
|
+
width={onAdd ? 9 : 11}
|
|
451
|
+
height={onAdd ? 9 : 11}
|
|
452
|
+
viewBox="0 0 24 24"
|
|
453
|
+
fill="none"
|
|
454
|
+
stroke="currentColor"
|
|
455
|
+
strokeWidth="2"
|
|
456
|
+
>
|
|
457
|
+
<rect x="9" y="9" width="13" height="13" rx="2" />
|
|
458
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
459
|
+
</svg>
|
|
460
|
+
Ask agent
|
|
461
|
+
</button>
|
|
462
|
+
</div>
|
|
288
463
|
|
|
289
464
|
{/* Badges */}
|
|
290
465
|
<div className="absolute top-1 right-1 flex items-center gap-0.5 pointer-events-none">
|
|
@@ -313,72 +488,96 @@ function BlockCard({
|
|
|
313
488
|
</span>
|
|
314
489
|
</div>
|
|
315
490
|
</div>
|
|
491
|
+
</div>
|
|
492
|
+
);
|
|
493
|
+
}
|
|
316
494
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
495
|
+
function PromptPreviewModal({
|
|
496
|
+
title,
|
|
497
|
+
prompt,
|
|
498
|
+
onClose,
|
|
499
|
+
}: {
|
|
500
|
+
title: string;
|
|
501
|
+
prompt: string;
|
|
502
|
+
onClose: () => void;
|
|
503
|
+
}) {
|
|
504
|
+
const [value, setValue] = useState(prompt);
|
|
505
|
+
const [copied, setCopied] = useState(false);
|
|
506
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
507
|
+
|
|
508
|
+
useEffect(() => {
|
|
509
|
+
requestAnimationFrame(() => textareaRef.current?.focus());
|
|
510
|
+
}, []);
|
|
511
|
+
|
|
512
|
+
const handleCopy = useCallback(() => {
|
|
513
|
+
navigator.clipboard.writeText(value);
|
|
514
|
+
setCopied(true);
|
|
515
|
+
setTimeout(() => setCopied(false), 1500);
|
|
516
|
+
}, [value]);
|
|
517
|
+
|
|
518
|
+
return (
|
|
519
|
+
<div
|
|
520
|
+
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
521
|
+
onClick={onClose}
|
|
522
|
+
>
|
|
523
|
+
<div
|
|
524
|
+
className="w-[560px] max-h-[80vh] flex flex-col rounded-2xl border border-neutral-800 bg-neutral-950 shadow-2xl"
|
|
525
|
+
onClick={(e) => e.stopPropagation()}
|
|
526
|
+
>
|
|
527
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-neutral-800/60">
|
|
528
|
+
<div>
|
|
529
|
+
<h3 className="text-sm font-medium text-neutral-200">Ask agent</h3>
|
|
530
|
+
<p className="text-xs text-neutral-500 mt-0.5">{title}</p>
|
|
531
|
+
</div>
|
|
532
|
+
<button
|
|
533
|
+
className="p-1 rounded-md text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800/50"
|
|
534
|
+
onClick={onClose}
|
|
326
535
|
>
|
|
327
|
-
<
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
536
|
+
<svg
|
|
537
|
+
width="14"
|
|
538
|
+
height="14"
|
|
539
|
+
viewBox="0 0 24 24"
|
|
540
|
+
fill="none"
|
|
541
|
+
stroke="currentColor"
|
|
542
|
+
strokeWidth="2"
|
|
543
|
+
strokeLinecap="round"
|
|
332
544
|
>
|
|
333
|
-
<
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
<span className={`w-2 h-2 rounded-full ${colors.dot}`} />
|
|
370
|
-
<span className={`text-[11px] ${colors.text}`}>
|
|
371
|
-
{BLOCK_CATEGORIES.find((c) => c.id === category)?.label}
|
|
372
|
-
</span>
|
|
373
|
-
{duration != null && (
|
|
374
|
-
<span className="text-[11px] text-neutral-500">{duration}s</span>
|
|
375
|
-
)}
|
|
376
|
-
</div>
|
|
377
|
-
</div>
|
|
378
|
-
</div>
|
|
379
|
-
</div>,
|
|
380
|
-
document.body,
|
|
381
|
-
)}
|
|
545
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
546
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
547
|
+
</svg>
|
|
548
|
+
</button>
|
|
549
|
+
</div>
|
|
550
|
+
<div className="flex-1 overflow-y-auto px-5 py-4">
|
|
551
|
+
<p className="text-[11px] text-neutral-500 mb-2">
|
|
552
|
+
Edit the prompt below, then copy and paste into your AI agent
|
|
553
|
+
</p>
|
|
554
|
+
<textarea
|
|
555
|
+
ref={textareaRef}
|
|
556
|
+
value={value}
|
|
557
|
+
onChange={(e) => setValue(e.target.value)}
|
|
558
|
+
onKeyDown={(e) => {
|
|
559
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleCopy();
|
|
560
|
+
if (e.key === "Escape") onClose();
|
|
561
|
+
}}
|
|
562
|
+
className="w-full min-h-[240px] text-[11px] text-neutral-200 leading-relaxed font-mono bg-neutral-900/60 rounded-lg p-3 border border-neutral-800 resize-y focus:outline-none focus:border-studio-accent/60 focus:ring-1 focus:ring-studio-accent/30"
|
|
563
|
+
/>
|
|
564
|
+
</div>
|
|
565
|
+
<div className="flex items-center justify-between px-5 py-3 border-t border-neutral-800/60">
|
|
566
|
+
<span className="text-[11px] text-neutral-600">
|
|
567
|
+
{navigator.platform.includes("Mac") ? "⌘" : "Ctrl"}+Enter to copy
|
|
568
|
+
</span>
|
|
569
|
+
<button
|
|
570
|
+
className={`px-4 py-1.5 rounded-lg text-xs font-medium transition-colors ${
|
|
571
|
+
copied
|
|
572
|
+
? "bg-emerald-500 text-white"
|
|
573
|
+
: "bg-studio-accent/90 text-neutral-950 hover:bg-studio-accent"
|
|
574
|
+
}`}
|
|
575
|
+
onClick={handleCopy}
|
|
576
|
+
>
|
|
577
|
+
{copied ? "Copied!" : "Copy prompt"}
|
|
578
|
+
</button>
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
382
581
|
</div>
|
|
383
582
|
);
|
|
384
583
|
}
|