Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/studio/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ export {
PlayerControls,
Timeline,
PreviewPanel,
AgentActivityTrack,
useTimelinePlayer,
usePlayerStore,
liveTime,
formatTime,
} from "./player";
export type { AgentActivity, TimelineElement, ActiveEdits } from "./player";
export type { TimelineElement, ZoomMode } from "./player";

// Editor
export { SourceEditor } from "./components/editor/SourceEditor";
Expand Down
64 changes: 3 additions & 61 deletions packages/studio/src/player/components/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ export const Timeline = memo(function Timeline({ onSeek, onDrillDown }: Timeline
const timelineReady = usePlayerStore((s) => s.timelineReady);
const selectedElementId = usePlayerStore((s) => s.selectedElementId);
const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
const activeEdits = usePlayerStore((s) => s.activeEdits);
const playheadRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [hoveredClip, setHoveredClip] = useState<string | null>(null);
Expand Down Expand Up @@ -348,8 +347,6 @@ export const Timeline = memo(function Timeline({ onSeek, onDrillDown }: Timeline
const isComposition = !!el.compositionSrc;
const clipKey = `${el.id}-${i}`;
const isHovered = hoveredClip === clipKey;
const activeEdit = activeEdits[el.id];
const isBeingEdited = !!activeEdit;

return (
<div
Expand All @@ -371,11 +368,9 @@ export const Timeline = memo(function Timeline({ onSeek, onDrillDown }: Timeline
: `1px solid rgba(255,255,255,${isHovered ? 0.3 : 0.15})`,
boxShadow: isSelected
? `0 0 0 1px ${style.clip}, 0 2px 8px rgba(0,0,0,0.4)`
: isBeingEdited
? `0 0 0 1px ${activeEdit.agentColor}80, 0 0 8px ${activeEdit.agentColor}40`
: isHovered
? "0 1px 4px rgba(0,0,0,0.3)"
: "none",
: isHovered
? "0 1px 4px rgba(0,0,0,0.3)"
: "none",
cursor: "pointer",
transition: "border-color 120ms, box-shadow 120ms, transform 80ms",
transform: isHovered && !isSelected ? "scaleY(1.04)" : "scaleY(1)",
Expand All @@ -400,59 +395,6 @@ export const Timeline = memo(function Timeline({ onSeek, onDrillDown }: Timeline
}
}}
>
{/* Agent ownership dot */}
{el.agentColor && (
<div
className="flex-shrink-0 w-1.5 h-1.5 rounded-full ml-1"
style={{ backgroundColor: el.agentColor }}
title={el.agentId ? `Agent: ${el.agentId}` : undefined}
/>
)}
{/* Editing glow pulse */}
{/* Agent editing indicator — cursor on the clip */}
{isBeingEdited && (
<>
<div
className="absolute inset-0 rounded-[5px] animate-pulse pointer-events-none"
style={{ boxShadow: `inset 0 0 0 1px ${activeEdit.agentColor}60` }}
/>
{/* Agent name badge above clip */}
<div
className="absolute pointer-events-none flex items-center gap-1"
style={{
top: -16,
left: 2,
zIndex: 30,
}}
>
{/* Mini cursor arrow */}
<svg
width="8"
height="10"
viewBox="0 0 12 16"
fill="none"
style={{ flexShrink: 0 }}
>
<path
d="M1 1L11 7L6 8L4 14L1 1Z"
fill={activeEdit.agentColor}
stroke="white"
strokeWidth="0.8"
/>
</svg>
<span
className="text-[8px] font-semibold px-1 py-px rounded whitespace-nowrap"
style={{
backgroundColor: activeEdit.agentColor,
color: "white",
boxShadow: `0 1px 4px ${activeEdit.agentColor}40`,
}}
>
{activeEdit.agentId}
</span>
</div>
</>
)}
<span
className="text-[10px] font-semibold truncate px-1.5 leading-none"
style={{ color: style.label }}
Expand Down
4 changes: 1 addition & 3 deletions packages/studio/src/player/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ export { Player } from "./components/Player";
export { PlayerControls } from "./components/PlayerControls";
export { Timeline } from "./components/Timeline";
export { PreviewPanel } from "./components/PreviewPanel";
export { AgentActivityTrack } from "./components/AgentActivityTrack";
export type { AgentActivity } from "./components/AgentActivityTrack";

// Hooks
export { useTimelinePlayer } from "./hooks/useTimelinePlayer";

// Store
export { usePlayerStore, liveTime } from "./store/playerStore";
export type { TimelineElement, ActiveEdits } from "./store/playerStore";
export type { TimelineElement, ZoomMode } from "./store/playerStore";

// Utils
export { formatTime } from "./lib/time";
60 changes: 44 additions & 16 deletions packages/studio/src/player/store/playerStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,9 @@ export interface TimelineElement {
volume?: number;
/** Path from data-composition-src — identifies sub-composition elements */
compositionSrc?: string;
/** Agent that created/last edited this element */
agentId?: string;
/** Agent's color for ownership visualization */
agentColor?: string;
}

/** Map of elementId → agentColor for clips currently being edited */
export interface ActiveEdits {
[elementId: string]: { agentId: string; agentColor: string };
}
export type ZoomMode = "fit" | "manual";

interface PlayerState {
isPlaying: boolean;
Expand All @@ -29,9 +22,15 @@ interface PlayerState {
timelineReady: boolean;
elements: TimelineElement[];
selectedElementId: string | null;
/** Clips currently being edited by agents — for glow animation */
activeEdits: ActiveEdits;
playbackRate: number;
/** Timeline zoom: 'fit' auto-scales to viewport, 'manual' uses pixelsPerSecond */
zoomMode: ZoomMode;
/** Pixels per second when in manual zoom mode */
pixelsPerSecond: number;
/** Edit range selection */
editRangeStart: number | null;
editRangeEnd: number | null;
editMode: boolean;

setIsPlaying: (playing: boolean) => void;
setCurrentTime: (time: number) => void;
Expand All @@ -40,8 +39,17 @@ interface PlayerState {
setTimelineReady: (ready: boolean) => void;
setElements: (elements: TimelineElement[]) => void;
setSelectedElementId: (id: string | null) => void;
setActiveEdits: (edits: ActiveEdits) => void;
setEditRange: (start: number | null, end: number | null) => void;
setEditMode: (active: boolean) => void;
updateElementStart: (elementId: string, newStart: number) => void;
updateElementDuration: (elementId: string, newDuration: number) => void;
updateElementTrack: (elementId: string, newTrack: number) => void;
updateElement: (
elementId: string,
updates: Partial<Pick<TimelineElement, "start" | "duration" | "track">>,
) => void;
setZoomMode: (mode: ZoomMode) => void;
setPixelsPerSecond: (pps: number) => void;
reset: () => void;
}

Expand All @@ -65,21 +73,42 @@ export const usePlayerStore = create<PlayerState>((set) => ({
timelineReady: false,
elements: [],
selectedElementId: null,
activeEdits: {},
playbackRate: 1,
zoomMode: "fit",
pixelsPerSecond: 100,
editRangeStart: null,
editRangeEnd: null,
editMode: false,

setIsPlaying: (playing) => set({ isPlaying: playing }),
setPlaybackRate: (rate) => set({ playbackRate: rate }),
setCurrentTime: (time) => set({ currentTime: time }),
setDuration: (duration) => set({ duration }),
setZoomMode: (mode) => set({ zoomMode: mode }),
setPixelsPerSecond: (pps) => set({ pixelsPerSecond: Math.max(10, pps) }),
setCurrentTime: (time) => set({ currentTime: Number.isFinite(time) ? time : 0 }),
setDuration: (duration) => set({ duration: Number.isFinite(duration) ? duration : 0 }),
setTimelineReady: (ready) => set({ timelineReady: ready }),
setElements: (elements) => set({ elements }),
setSelectedElementId: (id) => set({ selectedElementId: id }),
setActiveEdits: (edits) => set({ activeEdits: edits }),
setEditRange: (start, end) => set({ editRangeStart: start, editRangeEnd: end }),
setEditMode: (active) => set({ editMode: active, editRangeStart: null, editRangeEnd: null }),
updateElementStart: (elementId, newStart) =>
set((state) => ({
elements: state.elements.map((el) => (el.id === elementId ? { ...el, start: newStart } : el)),
})),
updateElementDuration: (elementId, newDuration) =>
set((state) => ({
elements: state.elements.map((el) =>
el.id === elementId ? { ...el, duration: newDuration } : el,
),
})),
updateElementTrack: (elementId, newTrack) =>
set((state) => ({
elements: state.elements.map((el) => (el.id === elementId ? { ...el, track: newTrack } : el)),
})),
updateElement: (elementId, updates) =>
set((state) => ({
elements: state.elements.map((el) => (el.id === elementId ? { ...el, ...updates } : el)),
})),
reset: () =>
set({
isPlaying: false,
Expand All @@ -88,6 +117,5 @@ export const usePlayerStore = create<PlayerState>((set) => ({
timelineReady: false,
elements: [],
selectedElementId: null,
activeEdits: {},
}),
}));
Loading