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
23 changes: 2 additions & 21 deletions ui/src/constants/timing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,8 @@
//
// SPDX-License-Identifier: MPL-2.0

/**
* Central timing constants for throttled server updates.
*
* These values balance responsiveness against network/server load.
* They apply to all controls that send parameter changes to the server
* during continuous interactions (slider drags, compositor layer edits, etc.).
*/

/**
* Throttle interval for parameter updates sent to the server via WebSocket
* during continuous interactions (slider drags, compositor opacity/rotation, etc.).
*
* 33ms ≈ 30 updates/sec — perceptually smooth for slider-driven changes
* while leaving headroom for network RTT and server processing.
*/
// 33ms ≈ 30 updates/sec for slider-driven parameter changes.
export const PARAM_THROTTLE_MS = 33;

/**
* Debounce delay for text input controls on node cards.
*
* 300ms gives the user time to finish typing before sending the value
* to the server, avoiding excessive partial updates.
*/
// Debounce delay for text input controls on node cards.
export const TEXT_DEBOUNCE_MS = 300;
26 changes: 1 addition & 25 deletions ui/src/hooks/useConvertViewState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,72 +8,52 @@ import type { ConversionStatus } from '@/components/converter/ConversionProgress
import type { OutputMode } from '@/services/converter';
import type { SamplePipeline } from '@/types/generated/api-types';

/**
* Custom hook to manage ConvertView state
* Groups related state together to reduce component complexity
*
* Note: Setters from useState are guaranteed to be stable by React
* and will never change between renders, so they're safe to use in
* dependency arrays without causing unnecessary re-renders.
*/
/** Grouped state for the ConvertView component. */
export function useConvertViewState() {
// Sample templates state
const [samples, setSamples] = useState<SamplePipeline[]>([]);
const [samplesLoading, setSamplesLoading] = useState<boolean>(true);
const [samplesError, setSamplesError] = useState<string | null>(null);

// Input mode and selection state
const [inputMode, setInputMode] = useState<'upload' | 'asset'>('upload');
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [selectedAssetId, setSelectedAssetId] = useState<string>('');

// Pipeline state
const [pipelineYaml, setPipelineYaml] = useState<string>('');
const [selectedTemplateId, setSelectedTemplateId] = useState<string>('');
const [textInput, setTextInput] = useState<string>('');

// Conversion state
const [conversionStatus, setConversionStatus] = useState<ConversionStatus>('idle');
const [conversionMessage, setConversionMessage] = useState<string>('');
const [outputMode, setOutputMode] = useState<OutputMode>('playback');
const [abortController, setAbortController] = useState<AbortController | null>(null);

// Output state
const [mediaUrl, setMediaUrl] = useState<string | null>(null);
const [mediaContentType, setMediaContentType] = useState<string | null>(null);
const [mediaStream, setMediaStream] = useState<ReadableStream<Uint8Array> | null>(null);
const [useStreaming, setUseStreaming] = useState<boolean>(false);
const [streamKey, setStreamKey] = useState<number>(0);

// UI state
const [showTechnicalDetails, setShowTechnicalDetails] = useState<boolean>(false);

return {
// Sample templates
samples,
setSamples,
samplesLoading,
setSamplesLoading,
samplesError,
setSamplesError,

// Input mode and selection
inputMode,
setInputMode,
selectedFile,
setSelectedFile,
selectedAssetId,
setSelectedAssetId,

// Pipeline
pipelineYaml,
setPipelineYaml,
selectedTemplateId,
setSelectedTemplateId,
textInput,
setTextInput,

// Conversion
conversionStatus,
setConversionStatus,
conversionMessage,
Expand All @@ -82,8 +62,6 @@ export function useConvertViewState() {
setOutputMode,
abortController,
setAbortController,

// Output
mediaUrl,
setMediaUrl,
mediaContentType,
Expand All @@ -94,8 +72,6 @@ export function useConvertViewState() {
setUseStreaming,
streamKey,
setStreamKey,

// UI
showTechnicalDetails,
setShowTechnicalDetails,
};
Expand Down
52 changes: 10 additions & 42 deletions ui/src/hooks/useDesignViewModals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

import { useState, useCallback } from 'react';

/**
* Custom hook to manage modal states in DesignView
*/
/** Grouped modal open/close state for the DesignView component. */
export function useDesignViewModals() {
const [showClearModal, setShowClearModal] = useState(false);
const [showSaveModal, setShowSaveModal] = useState(false);
Expand All @@ -19,58 +17,28 @@ export function useDesignViewModals() {
description: string;
} | null>(null);

const handleOpenClearModal = useCallback(() => {
setShowClearModal(true);
}, []);

const handleCloseClearModal = useCallback(() => {
setShowClearModal(false);
}, []);

const handleOpenSaveModal = useCallback(() => {
setShowSaveModal(true);
}, []);

const handleCloseSaveModal = useCallback(() => {
setShowSaveModal(false);
}, []);

const handleOpenCreateModal = useCallback(() => {
setShowCreateModal(true);
}, []);

const handleCloseCreateModal = useCallback(() => {
setShowCreateModal(false);
}, []);

const handleOpenLoadSampleModal = useCallback(() => {
setShowLoadSampleModal(true);
}, []);

const handleOpenClearModal = useCallback(() => setShowClearModal(true), []);
const handleCloseClearModal = useCallback(() => setShowClearModal(false), []);
const handleOpenSaveModal = useCallback(() => setShowSaveModal(true), []);
const handleCloseSaveModal = useCallback(() => setShowSaveModal(false), []);
const handleOpenCreateModal = useCallback(() => setShowCreateModal(true), []);
const handleCloseCreateModal = useCallback(() => setShowCreateModal(false), []);
const handleOpenLoadSampleModal = useCallback(() => setShowLoadSampleModal(true), []);
const handleCloseLoadSampleModal = useCallback(() => {
setShowLoadSampleModal(false);
setPendingSample(null);
}, []);

const handleOpenSaveFragmentModal = useCallback(() => {
setShowSaveFragmentModal(true);
}, []);

const handleCloseSaveFragmentModal = useCallback(() => {
setShowSaveFragmentModal(false);
}, []);
const handleOpenSaveFragmentModal = useCallback(() => setShowSaveFragmentModal(true), []);
const handleCloseSaveFragmentModal = useCallback(() => setShowSaveFragmentModal(false), []);
Comment on lines +20 to +32
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: useDesignViewModals callback reformatting is logically identical

In ui/src/hooks/useDesignViewModals.ts, eight callbacks were reformatted from multi-line useCallback(() => { setX(true); }, []) to single-line useCallback(() => setX(true), []). The logic is identical — arrow functions with a single expression implicitly return the result of setState, which returns void in both forms. The one callback that remained multi-line (handleCloseLoadSampleModal) is the only one with two statements, correctly preserving its block form.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground


return {
// State
showClearModal,
showSaveModal,
showCreateModal,
showLoadSampleModal,
showSaveFragmentModal,
pendingSample,
setPendingSample,

// Handlers
handleOpenClearModal,
handleCloseClearModal,
handleOpenSaveModal,
Expand Down
49 changes: 3 additions & 46 deletions ui/src/hooks/useNumericSlider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
//
// SPDX-License-Identifier: MPL-2.0

/**
* Shared hook for numeric slider controls with throttled updates.
* Handles local state, store synchronization, drag tracking, and throttled updates.
*/
/** Shared hook for numeric slider controls with throttled updates. */

import { useAtomValue } from 'jotai/react';
import { throttle } from 'lodash-es';
Expand All @@ -19,56 +16,28 @@ export interface UseNumericSliderOptions {
nodeId: string;
sessionId?: string;
paramKey: string;
/**
* Dot-notation path for reading/writing nested params.
* Defaults to `paramKey` when omitted.
*/
/** Dot-notation path for reading/writing nested params. Defaults to `paramKey`. */
path?: string;
min: number;
max: number;
step: number;
defaultValue: number;
propValue?: number;
onParamChange?: (nodeId: string, paramName: string, value: unknown) => void;
/**
* Transform the value before sending it to onParamChange.
* For example, use Math.round for integer types.
*/
transformValue?: (value: number) => number;
/**
* Throttle delay in milliseconds (default: 100ms)
*/
throttleMs?: number;
}

export interface UseNumericSliderResult {
/**
* The current local value to display in the slider
*/
localValue: number;
/**
* Handler for slider onChange event
*/
handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
/**
* Handler for slider onPointerDown event
*/
handlePointerDown: (event: React.PointerEvent<HTMLInputElement>) => void;
/**
* Handler for slider onPointerUp event
*/
handlePointerUp: (event: React.PointerEvent<HTMLInputElement>) => void;
/**
* Whether the slider should be disabled (no onParamChange provided)
*/
disabled: boolean;
}

const clampValue = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);

/**
* Custom hook for managing numeric slider state with throttled updates
*/
export const useNumericSlider = (options: UseNumericSliderOptions): UseNumericSliderResult => {
const {
nodeId,
Expand All @@ -87,13 +56,11 @@ export const useNumericSlider = (options: UseNumericSliderOptions): UseNumericSl

const effectivePath = pathOverride ?? paramKey;

// Get stored value from Jotai per-node atom (fine-grained reactivity).
// Use readByPath so nested paths (e.g. "properties.score") are resolved.
// Read from Jotai per-node atom; readByPath resolves nested paths (e.g. "properties.score").
const paramsKey = sessionId ? `${sessionId}\0${nodeId}` : nodeId;
const nodeParams = useAtomValue(nodeParamsAtom(paramsKey));
const storedValue = readByPath(nodeParams, effectivePath) as number | undefined;

// Determine effective value: stored > prop > default
const effectiveValue = (() => {
if (typeof storedValue === 'number' && Number.isFinite(storedValue)) {
return clampValue(storedValue, min, max);
Expand All @@ -104,19 +71,14 @@ export const useNumericSlider = (options: UseNumericSliderOptions): UseNumericSl
return clampValue(defaultValue, min, max);
})();

// Local state for immediate UI feedback
const [localValue, setLocalValue] = useState(effectiveValue);

// Refs for tracking drag state and local value
const isDraggingRef = useRef(false);
const localValueRef = useRef(localValue);

// Keep localValueRef in sync
useEffect(() => {
localValueRef.current = localValue;
}, [localValue]);

// Sync local value with effective value when not dragging
useEffect(() => {
if (isDraggingRef.current) {
return;
Expand All @@ -127,7 +89,6 @@ export const useNumericSlider = (options: UseNumericSliderOptions): UseNumericSl
}
}, [effectiveValue, step]);

// Create throttled update function
const throttledChange = useMemo(() => {
if (!onParamChange) {
return null;
Expand All @@ -142,30 +103,26 @@ export const useNumericSlider = (options: UseNumericSliderOptions): UseNumericSl
);
}, [nodeId, onParamChange, effectivePath, transformValue, throttleMs]);

// Cancel throttled function on unmount
useEffect(
() => () => {
throttledChange?.cancel();
},
[throttledChange]
);

// Handler for slider value change
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const raw = Number.parseFloat(event.target.value);
const clamped = clampValue(Number.isFinite(raw) ? raw : min, min, max);
setLocalValue(clamped);
throttledChange?.(clamped);
};

// Handler for pointer down (start dragging)
const handlePointerDown = (event: React.PointerEvent<HTMLInputElement>) => {
isDraggingRef.current = true;
event.stopPropagation();
event.currentTarget.setPointerCapture?.(event.pointerId);
};

// Handler for pointer up (stop dragging)
const handlePointerUp = (event: React.PointerEvent<HTMLInputElement>) => {
isDraggingRef.current = false;
event.stopPropagation();
Expand Down
20 changes: 1 addition & 19 deletions ui/src/hooks/usePermissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@

import { usePermissionStore, getCurrentPermissions } from '../stores/permissionStore';

/**
* Hook to access user permissions throughout the UI
*/
export function usePermissions() {
const { role, permissions, isLoading } = usePermissionStore();
const currentPerms = permissions || getCurrentPermissions();
Expand All @@ -16,39 +13,24 @@ export function usePermissions() {
isLoading,
permissions: currentPerms,

// Convenience flags for common permission checks
can: {
// Session operations
createSession: currentPerms.createSessions,
destroySession: currentPerms.destroySessions,
listSessions: currentPerms.listSessions,
modifySession: currentPerms.modifySessions,

// Node operations
tuneNodes: currentPerms.tuneNodes,
listNodes: currentPerms.listNodes,

// Plugin operations
loadPlugin: currentPerms.loadPlugins,
deletePlugin: currentPerms.deletePlugins,

// Asset operations
uploadAsset: currentPerms.uploadAssets,
deleteAsset: currentPerms.deleteAssets,

// Advanced
accessAllSessions: currentPerms.accessAllSessions,

// Composite permissions
enterStaging: currentPerms.modifySessions,
saveTemplate: currentPerms.createSessions, // Saving templates requires creating sessions
saveTemplate: currentPerms.createSessions,
commitBatchChanges: currentPerms.modifySessions,
},

// Helper to check if user is admin
isAdmin: () => role === 'admin',

// Helper to check if user has basic access
hasAccess: () => currentPerms.listSessions || currentPerms.listNodes,
};
}
Loading
Loading