diff --git a/src/components/__tests__/GenerateAudioNode.test.tsx b/src/components/__tests__/GenerateAudioNode.test.tsx
index db137ff2..1e786124 100644
--- a/src/components/__tests__/GenerateAudioNode.test.tsx
+++ b/src/components/__tests__/GenerateAudioNode.test.tsx
@@ -303,37 +303,31 @@ describe("GenerateAudioNode", () => {
});
});
- describe("Regenerate Button", () => {
- it("should show regenerate button when audio is complete", () => {
+ describe("Run Button", () => {
+ it("should show run button in header", () => {
render(
-
+
);
- expect(screen.getByText("Regenerate")).toBeInTheDocument();
+ expect(screen.getByTitle("Run this node")).toBeInTheDocument();
});
- it("should call regenerateNode when regenerate button is clicked", () => {
+ it("should call regenerateNode when run button is clicked", () => {
render(
-
+
);
- const regenButton = screen.getByText("Regenerate");
- fireEvent.click(regenButton);
+ const runButton = screen.getByTitle("Run this node");
+ fireEvent.click(runButton);
expect(mockRegenerateNode).toHaveBeenCalledWith("test-audio-1");
});
- it("should disable regenerate button when workflow is running", () => {
+ it("should disable run button when workflow is running", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
const state = {
updateNodeData: mockUpdateNodeData,
@@ -355,15 +349,12 @@ describe("GenerateAudioNode", () => {
render(
-
+
);
- const regenButton = screen.getByText("Regenerate");
- expect(regenButton).toBeDisabled();
+ const runButton = screen.getByTitle("Run this node");
+ expect(runButton).toBeDisabled();
});
});
diff --git a/src/components/nodes/AudioInputNode.tsx b/src/components/nodes/AudioInputNode.tsx
index 327efbf4..cf8b6612 100644
--- a/src/components/nodes/AudioInputNode.tsx
+++ b/src/components/nodes/AudioInputNode.tsx
@@ -72,16 +72,15 @@ export function AudioInputNode({ id, data, selected }: NodeProps {
+ (ctx: CanvasRenderingContext2D, width: number, height: number, peaks: number[], progress?: number) => {
ctx.clearRect(0, 0, width, height);
const barCount = Math.min(peaks.length, width);
const barWidth = width / barCount;
const barGap = 1;
-
- ctx.fillStyle = "rgb(167, 139, 250)"; // violet-400
+ const progressX = progress !== undefined ? progress * width : -1;
for (let i = 0; i < barCount; i++) {
const peakIndex = Math.floor((i / barCount) * peaks.length);
@@ -90,8 +89,19 @@ export function AudioInputNode({ id, data, selected }: NodeProps 0) {
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.9)";
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.moveTo(progressX, 0);
+ ctx.lineTo(progressX, height);
+ ctx.stroke();
+ }
},
[]
);
@@ -124,7 +134,7 @@ export function AudioInputNode({ id, data, selected }: NodeProps {
if (!waveformData || !canvasRef.current) return;
@@ -136,20 +146,8 @@ export function AudioInputNode({ id, data, selected }: NodeProps 0 ? currentTime / nodeData.duration : undefined;
+ drawWaveform(ctx, width, height, waveformData.peaks, progress);
}, [isPlaying, currentTime, nodeData.duration, waveformData, drawWaveform]);
// Animation loop for smooth playback position updates
diff --git a/src/components/nodes/GenerateAudioNode.tsx b/src/components/nodes/GenerateAudioNode.tsx
index f6416ee1..f3bcaef9 100644
--- a/src/components/nodes/GenerateAudioNode.tsx
+++ b/src/components/nodes/GenerateAudioNode.tsx
@@ -26,6 +26,7 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps(null);
const canvasRef = useRef(null);
const waveformContainerRef = useRef(null);
+ const animationFrameRef = useRef(undefined);
// Get the current selected provider (default to fal)
const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal";
@@ -84,16 +85,15 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps {
+ (ctx: CanvasRenderingContext2D, width: number, height: number, peaks: number[], progress?: number) => {
ctx.clearRect(0, 0, width, height);
const barCount = Math.min(peaks.length, width);
const barWidth = width / barCount;
const barGap = 1;
-
- ctx.fillStyle = "rgb(167, 139, 250)"; // violet-400
+ const progressX = progress !== undefined ? progress * width : -1;
for (let i = 0; i < barCount; i++) {
const peakIndex = Math.floor((i / barCount) * peaks.length);
@@ -102,8 +102,19 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps 0) {
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.9)";
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.moveTo(progressX, 0);
+ ctx.lineTo(progressX, height);
+ ctx.stroke();
+ }
},
[]
);
@@ -136,7 +147,7 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps {
if (!waveformData || !canvasRef.current) return;
@@ -148,21 +159,28 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps 0 ? currentTime / nodeData.duration : undefined;
+ drawWaveform(ctx, width, height, waveformData.peaks, progress);
+ }, [isPlaying, currentTime, nodeData.duration, waveformData, drawWaveform]);
- ctx.strokeStyle = "rgba(255, 255, 255, 0.8)";
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(x, 0);
- ctx.lineTo(x, height);
- ctx.stroke();
+ // Animation loop for smooth playback position updates
+ useEffect(() => {
+ if (isPlaying && audioRef.current) {
+ const updatePosition = () => {
+ if (audioRef.current) {
+ setCurrentTime(audioRef.current.currentTime);
+ }
+ animationFrameRef.current = requestAnimationFrame(updatePosition);
+ };
+ animationFrameRef.current = requestAnimationFrame(updatePosition);
}
- }, [isPlaying, currentTime, nodeData.duration, waveformData, drawWaveform]);
+
+ return () => {
+ if (animationFrameRef.current) {
+ cancelAnimationFrame(animationFrameRef.current);
+ }
+ };
+ }, [isPlaying]);
const handleClearAudio = useCallback(() => {
if (audioRef.current) {
@@ -368,7 +386,10 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps updateNodeData(id, { customTitle: title || undefined })}
onCommentChange={(comment) => updateNodeData(id, { comment: comment || undefined })}
+ onRun={handleRegenerate}
selected={selected}
+ isExecuting={isRunning}
+ hasError={nodeData.status === "error"}
commentNavigation={commentNavigation ?? undefined}
minWidth={300}
minHeight={250}
@@ -496,18 +517,6 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps
)}
- {/* Regenerate button */}
- {nodeData.status === "complete" && nodeData.outputAudio && (
-
- )}
-
{/* Dynamic handles from schema */}
{dynamicHandles}