mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
* feat(studio): runtime hooks — global time compiler + keyframe runtime Add the runtime bridge layer: global time compilation (tween % → clip %), soft reload after mutations, runtime keyframe preview, and keyframe commit helper. * feat(studio): runtime hooks — global time compiler + keyframe runtime Add the runtime bridge layer: global time compilation (tween % → clip %), soft reload after mutations, runtime keyframe preview, and keyframe commit helper. * feat(studio): keyframe cache + commit hooks Add hooks for keyframe cache population (tween → clip-relative %), mutation dispatch, keyframe snapping, and audio beat detection. * feat(studio): timeline UI — dopesheet diamonds + keyboard nav Add dopesheet strip with diamond keyframe indicators, timeline property rows, keyboard navigation (J/Shift+J/Delete/K), and feature gate (STUDIO_KEYFRAMES_ENABLED defaults to false). * feat(studio): design panel — arc controls + ease curve + stagger Add arc path controls (curviness slider, auto-rotate), motion path SVG overlay, ease curve visualization, stagger controls, and expanded animation card. Includes border-radius editor dependency from #1217. * feat(studio): gesture recording core Add gesture recording engine with RAF sampling, modifier key property mapping (Shift→rotationXY, Alt→rotation, Cmd→opacity), Ramer-Douglas-Peucker simplification, and ghost trail SVG overlay. * fix(studio): keyframe drag + recording bug bash 21 fixes: capture GSAP base at drag start, translate:none before gsap.set, skip reapplyPathOffsets for GSAP elements, clamp recording seek, _auto flag for 100% keyframes, overlay flash fix, block edits during recording. * feat(studio): keyframe integration wiring + docs Wire App.tsx recording orchestration, TimelineToolbar K/R buttons, PropertyPanel per-property diamonds, shortcuts panel, toast notifications, and keyframes guide documentation. All gated on STUDIO_KEYFRAMES_ENABLED (default false).
161 lines
5.1 KiB
TypeScript
161 lines
5.1 KiB
TypeScript
import { useState, useCallback, useRef, useEffect } from "react";
|
|
import { copyTextToClipboard } from "../utils/clipboard";
|
|
import { readTagSnippetByTarget } from "../utils/sourcePatcher";
|
|
import { toProjectAbsolutePath, type AgentModalAnchorPoint } from "../utils/studioHelpers";
|
|
import { buildElementAgentPrompt, type DomEditSelection } from "../components/editor/domEditing";
|
|
import { usePlayerStore } from "../player";
|
|
|
|
// ── Types ──
|
|
|
|
export interface UseAskAgentModalParams {
|
|
projectId: string | null;
|
|
activeCompPath: string | null;
|
|
projectDir: string | null;
|
|
projectIdRef: React.MutableRefObject<string | null>;
|
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
domEditSelectionRef: React.MutableRefObject<DomEditSelection | null>;
|
|
domEditSelection: DomEditSelection | null;
|
|
}
|
|
|
|
// ── Hook ──
|
|
|
|
export function useAskAgentModal({
|
|
activeCompPath,
|
|
projectDir,
|
|
projectIdRef,
|
|
showToast,
|
|
domEditSelectionRef,
|
|
domEditSelection,
|
|
}: UseAskAgentModalParams) {
|
|
// ── State ──
|
|
|
|
const [agentPromptTagSnippet, setAgentPromptTagSnippet] = useState<string | undefined>();
|
|
const [agentPromptSelectionContext, setAgentPromptSelectionContext] = useState<
|
|
string | undefined
|
|
>();
|
|
const [agentModalAnchorPoint, setAgentModalAnchorPoint] = useState<AgentModalAnchorPoint | null>(
|
|
null,
|
|
);
|
|
const [copiedAgentPrompt, setCopiedAgentPrompt] = useState(false);
|
|
const [agentModalOpen, setAgentModalOpen] = useState(false);
|
|
|
|
// ── Refs ──
|
|
|
|
const copiedAgentTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
// ── Callbacks ──
|
|
|
|
const preloadAgentPromptSnippet = useCallback(
|
|
async (selection: DomEditSelection) => {
|
|
const pid = projectIdRef.current;
|
|
if (!pid) return;
|
|
|
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
try {
|
|
const response = await fetch(
|
|
`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
|
|
);
|
|
if (!response.ok) return;
|
|
|
|
const data = (await response.json()) as { content?: string };
|
|
const html = data.content;
|
|
const tagSnippet =
|
|
typeof html === "string" ? readTagSnippetByTarget(html, selection) : undefined;
|
|
|
|
setAgentPromptTagSnippet((current) => {
|
|
if (domEditSelectionRef.current !== selection) return current;
|
|
return tagSnippet;
|
|
});
|
|
} catch {
|
|
// Runtime outerHTML is still available as a synchronous copy fallback.
|
|
}
|
|
},
|
|
[activeCompPath, domEditSelectionRef, projectIdRef],
|
|
);
|
|
|
|
const handleAskAgent = useCallback(() => {
|
|
if (!domEditSelection) return;
|
|
setAgentPromptTagSnippet(undefined);
|
|
setAgentPromptSelectionContext(undefined);
|
|
setAgentModalAnchorPoint(null);
|
|
void preloadAgentPromptSnippet(domEditSelection);
|
|
setAgentModalOpen(true);
|
|
}, [domEditSelection, preloadAgentPromptSnippet]);
|
|
|
|
const handleAgentModalSubmit = useCallback(
|
|
async (userInstruction: string) => {
|
|
if (!domEditSelection) return;
|
|
|
|
const targetPath = domEditSelection.sourceFile || activeCompPath || "index.html";
|
|
const tagSnippet = agentPromptTagSnippet ?? domEditSelection.element.outerHTML;
|
|
const prompt = buildElementAgentPrompt({
|
|
selection: domEditSelection,
|
|
currentTime: usePlayerStore.getState().currentTime,
|
|
tagSnippet,
|
|
selectionContext: agentPromptSelectionContext,
|
|
userInstruction,
|
|
sourceFilePath: toProjectAbsolutePath(projectDir, targetPath),
|
|
});
|
|
|
|
const copied = await copyTextToClipboard(prompt);
|
|
if (!copied) {
|
|
showToast("Could not copy prompt to clipboard.", "error");
|
|
return;
|
|
}
|
|
|
|
setAgentModalOpen(false);
|
|
setAgentPromptSelectionContext(undefined);
|
|
setAgentModalAnchorPoint(null);
|
|
if (copiedAgentTimerRef.current) clearTimeout(copiedAgentTimerRef.current);
|
|
setCopiedAgentPrompt(true);
|
|
copiedAgentTimerRef.current = setTimeout(() => setCopiedAgentPrompt(false), 1600);
|
|
},
|
|
[
|
|
activeCompPath,
|
|
agentPromptSelectionContext,
|
|
agentPromptTagSnippet,
|
|
domEditSelection,
|
|
projectDir,
|
|
showToast,
|
|
],
|
|
);
|
|
|
|
// ── Effects ──
|
|
|
|
// Clear agent-prompt state when selection changes
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
useEffect(() => {
|
|
setAgentPromptTagSnippet(undefined);
|
|
setAgentPromptSelectionContext(undefined);
|
|
setAgentModalAnchorPoint(null);
|
|
setCopiedAgentPrompt(false);
|
|
}, [domEditSelection]);
|
|
|
|
// Cleanup copiedAgentTimerRef
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
useEffect(
|
|
() => () => {
|
|
if (copiedAgentTimerRef.current) clearTimeout(copiedAgentTimerRef.current);
|
|
},
|
|
[],
|
|
);
|
|
|
|
return {
|
|
// State
|
|
agentModalOpen,
|
|
agentModalAnchorPoint,
|
|
copiedAgentPrompt,
|
|
agentPromptSelectionContext,
|
|
|
|
// Setters (consumed by handlePreviewCanvasMouseDown and other callers)
|
|
setAgentModalOpen,
|
|
setAgentPromptSelectionContext,
|
|
setAgentModalAnchorPoint,
|
|
|
|
// Callbacks
|
|
preloadAgentPromptSnippet,
|
|
handleAskAgent,
|
|
handleAgentModalSubmit,
|
|
};
|
|
}
|