feat(studio): render queue, layout restructure, home page, hover preview#95
feat(studio): render queue, layout restructure, home page, hover preview#95miguel-heygen merged 8 commits intomainfrom
Conversation
2e4f5e5 to
dcf047b
Compare
c983e7e to
6bc905e
Compare
dcf047b to
efa7f60
Compare
b774491 to
fcea476
Compare
6bc905e to
27a5251
Compare
fcea476 to
441646a
Compare
vanceingalls
left a comment
There was a problem hiding this comment.
Overall: Big PR — layout restructure, render queue, home page, hover preview. The render queue hook is well-architected. The ExpandOnHover interaction is a nice touch. A few concerns:
useRenderQueue.ts — isRendering is stale
return {
jobs,
startRender,
deleteRender,
clearCompleted,
isRendering: activeJobRef.current !== null,
};activeJobRef.current is a ref, not state. When the render completes and activeJobRef.current is set to null, this return value won't trigger a re-render. Components reading isRendering will show the wrong state until something else causes a re-render. Use state instead of a ref for this value, or derive it from jobs: isRendering: jobs.some(j => j.status === "rendering").
useRenderQueue.ts — EventSource not cleaned up on unmount during active render
The cleanup effect:
useEffect(() => {
return () => {
eventSourceRef.current?.close();
};
}, []);This only runs on unmount. If the component re-mounts (e.g., navigating away and back), stale EventSource connections could accumulate. Also, if projectId changes mid-render, the old EventSource keeps running. Consider closing on projectId change too.
useRenderQueue.ts — deleteRender fires DELETE then removes from state regardless
const deleteRender = useCallback(async (jobId: string) => {
try {
await fetch(`/api/render/${jobId}`, { method: "DELETE" });
} catch {
// ignore
}
setJobs((prev) => prev.filter((j) => j.id !== jobId));
}, []);If the DELETE fails (network error, 404), the job disappears from the UI but still exists on the server. Next time loadRenders runs, it'll reappear. Either keep it in the UI on failure (show an error state), or don't call loadRenders after a failed delete.
ExpandedPreviewIframe — offset calculation uses stale refs
const offsetX = containerRef.current
? (containerRef.current.clientWidth - dims.w * scale) / 2
: 0;This reads from containerRef.current during render, which works but won't update reactively. The scale state updates from the ResizeObserver, but offsetX/offsetY are recalculated on every render, not just when the container resizes. Consider storing offset in state alongside scale inside the ResizeObserver callback.
ExpandedPreviewIframe — polling for __player with setInterval
const interval = setInterval(() => {
// ... 25 attempts at 200ms = 5 seconds max
}, 200);This is a lot of polling. Consider using a MutationObserver on the iframe or listening for a custom event from the runtime instead. If the composition takes >5s to load GSAP, the preview silently fails.
CompositionsTab.tsx — hover preview spawns iframe per card
When hovering a composition, a full iframe loads with the preview URL. If the user mouses across several cards quickly, multiple iframes spawn and load simultaneously. Consider debouncing the hover or reusing a single preview iframe that repositions.
App.tsx — PR adds motion dependency for ExpandOnHover
The motion package (motion/react, formerly framer-motion) is a significant dependency addition for a single hover animation. The expand-on-hover effect could likely be done with CSS transitions + a portal, avoiding the extra ~40KB. If motion is planned for more features, fine — but if it's just for this one interaction, it's heavy.
App.tsx — inline SVG logo repeated 3 times
The HyperFrames logo SVG appears inline in the favicon, the header, and the empty state. Extract to a shared component.
RenderQueueItem.tsx — formatTimeAgo doesn't update
The "just now" / "5m ago" text is calculated once on render and never updates. If the user leaves the renders tab open, timestamps go stale. Consider a small interval to refresh, or use relative timestamps that update on visibility change.
|
The isRendering stale ref is the only one that will cause a visible bug — the Export MP4 button will stay disabled (or enabled) at the wrong time because the ref change doesn't trigger a re-render. |
441646a to
d89c8f7
Compare
27a5251 to
036fe70
Compare
08fcb1f to
fa1ae02
Compare
f36ff01 to
d7543d0
Compare
0840b63 to
4252f7a
Compare
3221ba1 to
a0433ac
Compare
4252f7a to
717d292
Compare
a0433ac to
b549e32
Compare
717d292 to
f553966
Compare
b549e32 to
4ce235c
Compare
4567dad to
3ab9c2b
Compare
4ce235c to
dcf46dd
Compare
dcf46dd to
a6b1de5
Compare
3ab9c2b to
9ae239d
Compare
a6b1de5 to
7708567
Compare
…d tests - Add zoom state (zoomMode, pixelsPerSecond) to player store - Add updateElementDuration, updateElementTrack, updateElement actions - Remove agent activity tracking (activeEdits, agentId, agentColor) - Add comprehensive store tests (265 lines) - Add time utility tests
- TimelineClip: extracted clip rendering with drag, resize, and selection - CompositionThumbnail: renders composition preview as timeline thumbnail - VideoThumbnail: extracts and displays video frame thumbnails via canvas
…ayer - Timeline: refactored track rendering, zoom support, drag/resize - PlayerControls: updated layout, added playback rate selector - useTimelinePlayer: improved seeking, element discovery, composition support - Added Timeline tests (149 lines)
- NLELayout: add toolbar slot, composition breadcrumb, improved resizing - App: update toolbar wiring, remove split/delete, add edit modal - Remove AgentActivityTrack (unused) - Update vite config with improved dev server and build settings - Update package.json dependencies - Simplify htmlEditor.ts (keep parseStyleString, mergeStyleIntoTag, findElementBlock) - Update exports in index.ts
- NLELayout: add toolbar slot, composition breadcrumb, improved resizing - App: update toolbar wiring, remove split/delete, add edit modal - Remove AgentActivityTrack (unused) - Update vite config with improved dev server and build settings - Update package.json dependencies - Simplify htmlEditor.ts (keep parseStyleString, mergeStyleIntoTag, findElementBlock) - Update exports in index.ts
- Add TimelineToolbar with Edit button (replaces Split/Delete) - Add EditModal with time range selection, element list, prompt textarea - "Copy to Agent" copies structured context to clipboard - Wire toolbar and modal into App.tsx
Code quality improvements (desloppify): - Fix composition drill-down: patch compositionSrc from raw HTML map, handle -host suffix mismatch between DOM IDs and composition IDs - Reset duration/timeline state on drill-down navigation - Replace inline linter (~90 LOC) with server-side /api/lint endpoint using @hyperframes/core's canonical lintHyperframeHtml - Convert 13 useEffect(fn,[]) to useMountEffect(fn) — ban direct useEffect - Remove wrapPlayer pass-through (identical PlayerAPI/PlaybackAdapter) - Deduplicate useMountEffect to single canonical location - Replace formatTick duplicate with formatTime re-export - Move EditModal.tsx from components/timeline/ to player/components/ - Fix App.tsx barrel bypass — use player barrel imports - Fix fetch() during render → proper useEffect with race condition guard - Fix missing blur event listener cleanup (memory leak) - Fix WebM content-type in download, renders listing, delete endpoints - Fix render endpoint to read fps/quality/format from body - Convert FileTree from flat list to collapsible tree with sidebar layout Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

Summary
Test plan
vite buildexits cleanly (no hanging process from setInterval)🤖 Generated with Claude Code