Files
hyperframes/packages/studio/src/hooks/useAskAgentModal.ts
T
Miguel Ángel a468550f82 feat(studio): keyframe system — parser, runtime, timeline UI, design panel, gesture recording (#1311)
* 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).
2026-06-09 18:30:23 -04:00

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,
};
}