diff --git a/jam-ui/src/components/client/JKSessionJamTrackModal.js b/jam-ui/src/components/client/JKSessionJamTrackModal.js
new file mode 100644
index 000000000..b629cb90e
--- /dev/null
+++ b/jam-ui/src/components/client/JKSessionJamTrackModal.js
@@ -0,0 +1,199 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { Modal, ModalHeader, ModalBody, ModalFooter, Button, Table, Row, Col } from 'reactstrap';
+import JKJamTracksAutoComplete from '../jamtracks/JKJamTracksAutoComplete';
+import { autocompleteJamTracks, getPurchasedJamTracks } from '../../helpers/rest';
+
+const JKSessionJamTrackModal = ({ isOpen, toggle, onJamTrackSelect }) => {
+ const [inputValue, setInputValue] = useState('');
+ const [showDropdown, setShowDropdown] = useState(false);
+ const [jamTracks, setJamTracks] = useState([]);
+ const [selected, setSelected] = useState(null);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [loading, setLoading] = useState(false);
+ const page = useRef(1);
+ const PER_PAGE = 10;
+
+ // Update input value when selected changes (similar to JKJamTracksFilter)
+ useEffect(() => {
+ if (selected) {
+ const displayValue = selected.type === 'artist' ? selected.original_artist : selected.name;
+ setInputValue(displayValue);
+ }
+ }, [selected]);
+
+ const queryOptions = (selected) => {
+ const options = {
+ per_page: PER_PAGE,
+ page: page.current,
+ };
+
+ if (typeof selected === 'string') {
+ options.search = selected;
+ return options;
+ }
+
+ if (selected.type === 'artist') {
+ options.artist = selected.original_artist;
+ } else {
+ options.song = selected.name;
+ }
+
+ return options;
+ };
+
+ const handleOnSelect = async (selected) => {
+ page.current = 1;
+ setJamTracks([]);
+ setSearchTerm('');
+ setSelected(selected);
+ setShowDropdown(false); // Hide dropdown after selection
+ const params = queryOptions(selected);
+ await fetchJamTracks(params);
+ };
+
+ const handleOnEnter = async (queryStr) => {
+ page.current = 1;
+ setJamTracks([]);
+ setSelected(null);
+ setSearchTerm(queryStr);
+ const params = queryOptions(queryStr);
+ await fetchJamTracks(params);
+ };
+
+ const handleSearch = () => {
+ if (inputValue.trim()) {
+ handleOnEnter(inputValue.trim());
+ }
+ };
+
+ const fetchJamTracks = async (options) => {
+ try {
+ setLoading(true);
+ const resp = await getPurchasedJamTracks(options);
+ const data = await resp.json();
+ setJamTracks(prev => [...prev, ...data.jamtracks]);
+ page.current = page.current + 1;
+ } catch (error) {
+ console.error('Error fetching jam tracks:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Load purchased JamTracks when modal opens
+ useEffect(() => {
+ if (isOpen) {
+ loadPurchasedJamTracks();
+ } else {
+ // Reset state when modal closes
+ setJamTracks([]);
+ setInputValue('');
+ setSelected(null);
+ setSearchTerm('');
+ page.current = 1;
+ }
+ }, [isOpen]);
+
+ const loadPurchasedJamTracks = async () => {
+ try {
+ setLoading(true);
+ const resp = await getPurchasedJamTracks({ start: 0, search: '', append: false });
+ const data = await resp.json();
+ setJamTracks(data.jamtracks || []);
+ } catch (error) {
+ console.error('Error loading purchased jam tracks:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleShopJamTracks = () => {
+ // TODO: Implement shop functionality - could open external link or navigate to shop page
+ console.log('Shop JamTracks clicked');
+ window.open('/jamtracks', '_blank');
+ };
+
+ return (
+
+ Open JamTrack
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default JKSessionJamTrackModal;
diff --git a/jam-ui/src/components/client/JKSessionJamTrackStems.js b/jam-ui/src/components/client/JKSessionJamTrackStems.js
new file mode 100644
index 000000000..837ac6776
--- /dev/null
+++ b/jam-ui/src/components/client/JKSessionJamTrackStems.js
@@ -0,0 +1,70 @@
+import React, { useMemo } from 'react';
+import JKSessionAudioInputs from './JKSessionAudioInputs';
+import { getInstrumentIcon45 } from '../../helpers/utils';
+
+const JKSessionJamTrackStems = ({ jamTrackStems, mixerHelper }) => {
+ const stemTracks = useMemo(() => {
+ if (!jamTrackStems || jamTrackStems.length === 0) return [];
+
+ return jamTrackStems.map((stem, index) => {
+ // Create a track object similar to what JKSessionRemoteTracks creates
+ const track = {
+ // Use stem properties
+ id: stem.id,
+ part: stem.part,
+ instrument: stem.instrument,
+ track_type: stem.track_type,
+ position: stem.position,
+ // Add client_track_id for compatibility
+ client_track_id: `jamtrack-stem-${stem.id}`,
+ // Mock some properties that JKSessionMyTrack expects
+ instrument_name: stem.part || stem.instrument
+ };
+
+ // Create mixer data (placeholder for now)
+ const mixerData = {
+ mixer: null, // No actual mixer for jam track stems yet
+ hasMixer: false
+ };
+
+ const instrumentIcon = getInstrumentIcon45(stem.instrument) || '/assets/content/icon_instrument_guitar45.png';
+ const trackName = stem.part || stem.instrument || `Stem ${index + 1}`;
+
+ return {
+ track,
+ mixerFinder: [`jamtrack-stem-${stem.id}`, track, false],
+ mixers: mixerData,
+ hasMixer: false,
+ name: 'JamTrack',
+ trackName,
+ instrumentIcon,
+ photoUrl: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDUiIGhlaWdodD0iNDUiIHZpZXdCb3g9IjAgMCA0NSA0NSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ1IiBoZWlnaHQ9IjQ1IiBmaWxsPSJ0cmFuc3BhcmVudCIvPgo8L3N2Zz4=', // Transparent placeholder to maintain height
+ clientId: `jamtrack-stem-${stem.id}`,
+ connStatsClientId: `jamtrack-stem-${stem.id}`,
+ // Additional properties for JKSessionMyTrack
+ isJamTrackStem: true,
+ hideAvatar: true // Custom prop to hide avatar
+ };
+ });
+ }, [jamTrackStems]);
+
+ if (!stemTracks || stemTracks.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {stemTracks.map((track, index) => (
+
+ ))}
+
+ );
+};
+
+export default JKSessionJamTrackStems;
diff --git a/jam-ui/src/components/client/JKSessionMyTrack.js b/jam-ui/src/components/client/JKSessionMyTrack.js
index 598d4e7f6..6feb7fecf 100644
--- a/jam-ui/src/components/client/JKSessionMyTrack.js
+++ b/jam-ui/src/components/client/JKSessionMyTrack.js
@@ -12,7 +12,7 @@ import { getInstrumentName } from '../../helpers/utils';
import { ASSIGNMENT } from '../../helpers/globals';
import './JKSessionMyTrack.css';
-const JKSessionMyTrack = ({ track, mixers, hasMixer, name, trackName, instrumentIcon, photoUrl, clientId, connStatsClientId, mode, isChat = false, isRemote = false }) => {
+const JKSessionMyTrack = ({ track, mixers, hasMixer, name, trackName, instrumentIcon, photoUrl, clientId, connStatsClientId, mode, isChat = false, isRemote = false, hideAvatar = false }) => {
const mixerHelper = useMixersContext();
const jamClient = useJamClient();
const { convertPanToPercent } = usePanHelpers();
@@ -72,8 +72,11 @@ const JKSessionMyTrack = ({ track, mixers, hasMixer, name, trackName, instrument
window.open(`https://profile.jamkazam.com/user/${clientId}`, '_blank')}
+ style={{
+ cursor: hideAvatar ? 'default' : 'pointer',
+ visibility: hideAvatar ? 'hidden' : 'visible'
+ }}
+ onClick={hideAvatar ? undefined : () => window.open(`https://profile.jamkazam.com/user/${clientId}`, '_blank')}
>
diff --git a/jam-ui/src/components/client/JKSessionOpenMenu.js b/jam-ui/src/components/client/JKSessionOpenMenu.js
index f81c80ef8..e1d588443 100644
--- a/jam-ui/src/components/client/JKSessionOpenMenu.js
+++ b/jam-ui/src/components/client/JKSessionOpenMenu.js
@@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect, useContext } from 'react';
import { createPortal } from 'react-dom';
import { useJamClient } from '../../context/JamClientContext';
-const JKSessionOpenMenu = ({ onBackingTrackSelected }) => {
+const JKSessionOpenMenu = ({ onBackingTrackSelected, onJamTrackSelected, onMetronomeSelected }) => {
const [isOpen, setIsOpen] = useState(false);
const buttonRef = useRef(null);
const menuRef = useRef(null);
@@ -14,7 +14,11 @@ const JKSessionOpenMenu = ({ onBackingTrackSelected }) => {
console.log(`Selected: ${item}`);
setIsOpen(false);
- if (item === 'Backing Tracks') {
+ if (item === 'JamTracks') {
+ if (onJamTrackSelected) {
+ onJamTrackSelected();
+ }
+ } else if (item === 'Backing Tracks') {
try {
// Set up callback for when user selects a backing track file
window.JK = window.JK || {};
@@ -30,8 +34,15 @@ const JKSessionOpenMenu = ({ onBackingTrackSelected }) => {
} catch (error) {
console.error('Error opening backing track dialog:', error);
}
+ } else if (item === 'Metronome') {
+ try {
+ if (onMetronomeSelected) {
+ onMetronomeSelected();
+ }
+ } catch (error) {
+ console.error('Error opening metronome:', error);
+ }
}
- // TODO: Implement other menu item actions
};
// Close dropdown when clicking outside
diff --git a/jam-ui/src/components/client/JKSessionScreen.js b/jam-ui/src/components/client/JKSessionScreen.js
index 23b6163d6..be9760c26 100644
--- a/jam-ui/src/components/client/JKSessionScreen.js
+++ b/jam-ui/src/components/client/JKSessionScreen.js
@@ -19,7 +19,7 @@ import { useAuth } from '../../context/UserAuth';
import { dkeys } from '../../helpers/utils.js';
-import { getSessionHistory, getSession, joinSession as joinSessionRest, updateSessionSettings, getFriends, startRecording, stopRecording, submitSessionFeedback, getVideoConferencingRoomUrl } from '../../helpers/rest';
+import { getSessionHistory, getSession, joinSession as joinSessionRest, updateSessionSettings, getFriends, startRecording, stopRecording, submitSessionFeedback, getVideoConferencingRoomUrl, getJamTrack, closeJamTrack, openMetronome } from '../../helpers/rest';
import { CLIENT_ROLE, RECORD_TYPE_AUDIO, RECORD_TYPE_BOTH } from '../../helpers/globals';
import { MessageType } from '../../helpers/MessageFactory.js';
@@ -35,6 +35,8 @@ import JKSessionInviteModal from './JKSessionInviteModal.js';
import JKSessionVolumeModal from './JKSessionVolumeModal.js';
import JKSessionRecordingModal from './JKSessionRecordingModal.js';
import JKSessionLeaveModal from './JKSessionLeaveModal.js';
+import JKSessionJamTrackModal from './JKSessionJamTrackModal.js';
+import JKSessionJamTrackStems from './JKSessionJamTrackStems.js';
import JKSessionOpenMenu from './JKSessionOpenMenu.js';
import WindowPortal from '../common/WindowPortal.js';
import JKSessionBackingTrackPlayer from './JKSessionBackingTrackPlayer.js';
@@ -64,7 +66,7 @@ const JKSessionScreen = () => {
server,
registerMessageCallback } = useJamServerContext();
const { currentSession, setCurrentSession, currentSessionIdRef, setCurrentSessionId, inSession } = useCurrentSessionContext();
- const { globalObject } = useGlobalContext();
+ const { globalObject, metronomeState, closeMetronome, resetMetronome } = useGlobalContext();
const { getCurrentRecordingState, reset: resetRecordingState, currentlyRecording } = useRecordingHelpers();
const { SessionPageEnter } = useSessionUtils();
@@ -118,6 +120,13 @@ const JKSessionScreen = () => {
const [showBackingTrackPopup, setShowBackingTrackPopup] = useState(false);
const [backingTrackData, setBackingTrackData] = useState(null);
+ //state for jam track modal
+ const [showJamTrackModal, setShowJamTrackModal] = useState(false);
+
+ //state for selected jam track and stems
+ const [selectedJamTrack, setSelectedJamTrack] = useState(null);
+ const [jamTrackStems, setJamTrackStems] = useState([]);
+
useEffect(() => {
if (!isConnected || !jamClient) return;
console.debug("JKSessionScreen: -DEBUG- isConnected changed to true");
@@ -583,6 +592,12 @@ const JKSessionScreen = () => {
try {
setLeaveLoading(true);
+ // Close metronome if open before leaving
+ if (metronomeState.isOpen) {
+ console.log('Closing metronome before leaving session');
+ closeMetronome();
+ }
+
// Submit feedback to backend first
const clientId = server.clientId;
const backendDetails = jamClient.getAllClientsStateMap ? jamClient.getAllClientsStateMap() : {};
@@ -609,6 +624,17 @@ const JKSessionScreen = () => {
}
};
+ // Cleanup metronome state when leaving session
+ useEffect(() => {
+ return () => {
+ // Reset metronome state when component unmounts (session ends)
+ if (metronomeState.isOpen) {
+ console.log('Resetting metronome state on session cleanup');
+ resetMetronome();
+ }
+ };
+ }, [metronomeState.isOpen, resetMetronome]);
+
// Check if user can use video (subscription/permission check)
const canVideo = () => {
// This would need to be implemented based on user subscription logic
@@ -703,6 +729,131 @@ const JKSessionScreen = () => {
}
};
+ const handleJamTrackSelect = async (jamTrack) => {
+ console.log('Jam track selected:', jamTrack);
+ try {
+ // Fetch jam track details with stems
+ const response = await getJamTrack({ id: jamTrack.id });
+ const jamTrackData = await response.json();
+
+ console.log('Jam track data:', jamTrackData);
+
+ // Set the selected jam track and stems
+ setSelectedJamTrack(jamTrackData);
+ setJamTrackStems(jamTrackData.tracks || []);
+
+ toast.success(`Loaded JamTrack: ${jamTrackData.name}`);
+ } catch (error) {
+ console.error('Error loading jam track:', error);
+ toast.error('Failed to load JamTrack');
+ }
+ };
+
+ const handleJamTrackClose = async () => {
+ console.log('Closing jam track');
+ try {
+ // Call the close jam track API
+ await closeJamTrack({ id: currentSession.id });
+
+ // Clear the selected jam track and stems
+ setSelectedJamTrack(null);
+ setJamTrackStems([]);
+
+ toast.success('JamTrack closed successfully');
+ } catch (error) {
+ console.error('Error closing jam track:', error);
+ toast.error('Failed to close JamTrack');
+ }
+ };
+
+ const handleMetronomeSelected = async () => {
+ console.log('Opening metronome');
+ try {
+ // Check if currently recording - can't open metronome while recording
+ if (currentlyRecording) {
+ toast.warning("You can't open a metronome while recording.");
+ return;
+ }
+
+ // Check for unstable NTP clocks (like legacy implementation)
+ const unstableClocks = await checkUnstableClocks();
+ if (currentSession.participants && currentSession.participants.length > 1 && unstableClocks.length > 0) {
+ const names = unstableClocks.join(", ");
+ toast.warning(`Couldn't open metronome due to unstable clocks: ${names}`);
+ return;
+ }
+
+ // Track analytics (like legacy SessionStore)
+ if (window.stats && window.stats.write) {
+ const data = {
+ value: 1,
+ session_size: currentSession.participants?.length || 1,
+ user_id: currentUser?.id,
+ user_name: currentUser?.name
+ };
+ window.stats.write('web.metronome.open', data);
+ }
+
+ // Stop any current playback first (like legacy MixerStore)
+ await jamClient.SessionStopPlay();
+
+ // Open the metronome with default settings
+ const bpm = 120;
+ const sound = "Beep";
+ const meter = 1;
+ const mode = 0;
+
+ console.log(`Opening metronome with bpm: ${bpm}, sound: ${sound}, meter: ${meter}, mode: ${mode}`);
+
+ // Inform server about metronome opening (like legacy SessionStore)
+ await openMetronome({ id: currentSession.id });
+
+ // Start the metronome audio (backend will handle GUI via callback)
+
+ //alert('About to start metronome');
+ const result = await jamClient.SessionOpenMetronome(bpm, sound, meter, mode);
+ //alert('Metronome is started ' + JSON.stringify(result));
+
+ toast.success('Metronome opened successfully');
+ } catch (error) {
+ console.error('Error opening metronome:', error);
+ toast.error('Failed to open metronome');
+ }
+ };
+
+ const checkUnstableClocks = async () => {
+ try {
+ const unstable = [];
+
+ // Check current user's NTP stability
+ const myState = await jamClient.getMyNetworkState();
+ if (!myState.ntp_stable) {
+ unstable.push('this computer');
+ }
+
+ // Check other participants' NTP stability
+ if (currentSession.participants) {
+ for (const participant of currentSession.participants) {
+ if (participant.client_id !== server.clientId) {
+ try {
+ const peerState = await jamClient.getPeerState(participant.client_id);
+ if (!peerState.ntp_stable) {
+ unstable.push(participant.user.first_name + ' ' + participant.user.last_name);
+ }
+ } catch (error) {
+ // Ignore errors for individual peer checks
+ }
+ }
+ }
+ }
+
+ return unstable;
+ } catch (error) {
+ console.error('Error checking NTP stability:', error);
+ return [];
+ }
+ };
+
return (
{!isConnected && Connecting to backend...
}
@@ -721,7 +872,7 @@ const JKSessionScreen = () => {
-
+ setShowJamTrackModal(true)} onMetronomeSelected={handleMetronomeSelected} />
@@ -729,7 +880,7 @@ const JKSessionScreen = () => {
-
+
Audio Inputs
@@ -759,6 +910,33 @@ const JKSessionScreen = () => {
+ {/* JamTrack Section */}
+ {selectedJamTrack && jamTrackStems.length > 0 && (
+ <>
+
+
+ >
+ )}
@@ -989,6 +1167,12 @@ const JKSessionScreen = () => {
/>
)}
+
+ setShowJamTrackModal(!showJamTrackModal)}
+ onJamTrackSelect={handleJamTrackSelect}
+ />
)
}
diff --git a/jam-ui/src/components/dashboard/JKDashboardMain.js b/jam-ui/src/components/dashboard/JKDashboardMain.js
index 356825a0b..1ec166527 100644
--- a/jam-ui/src/components/dashboard/JKDashboardMain.js
+++ b/jam-ui/src/components/dashboard/JKDashboardMain.js
@@ -57,6 +57,7 @@ import JKPayPalConfirmation from '../shopping-cart/JKPayPalConfirmation';
import JKUnsubscribe from '../public/JKUnsubscribe';
import JKConfirmEmailChange from '../public/JKConfirmEmailChange';
+import JKPopupMediaControls from '../popups/JKPopupMediaControls';
//import loadable from '@loadable/component';
@@ -324,6 +325,7 @@ function JKDashboardMain() {
+
{/*Redirect*/}
diff --git a/jam-ui/src/components/popups/JKPopupMediaControls.js b/jam-ui/src/components/popups/JKPopupMediaControls.js
new file mode 100644
index 000000000..9275b3cb5
--- /dev/null
+++ b/jam-ui/src/components/popups/JKPopupMediaControls.js
@@ -0,0 +1,14 @@
+import React, { useEffect } from 'react'
+
+const JKPopupMediaControls = () => {
+ useEffect(() => {
+ console.log('JKPopupMediaControls mounted');
+ alert('JKPopupMediaControls mounted');
+ }, [])
+
+ return (
+ JKPopupMediaControls
+ )
+}
+
+export default JKPopupMediaControls
\ No newline at end of file
diff --git a/jam-ui/src/context/GlobalContext.js b/jam-ui/src/context/GlobalContext.js
index 77f7674a3..2d01f2e76 100644
--- a/jam-ui/src/context/GlobalContext.js
+++ b/jam-ui/src/context/GlobalContext.js
@@ -1,5 +1,5 @@
-import React, { createContext, useState } from 'react';
-
+import React, { createContext, useState, useCallback, useEffect } from 'react';
+import useMetronomeState from '../hooks/useMetronomeState';
// Create a global context
export const GlobalContext = createContext({});
@@ -31,12 +31,64 @@ export const GlobalProvider = ({ children }) => {
const [videoEnabled, setVideoEnabled] = useState(false);
+ // Metronome state management
+ const {
+ metronomeState,
+ updateMetronomeState,
+ openMetronome,
+ closeMetronome,
+ resetMetronome
+ } = useMetronomeState();
+
+ // Metronome callback handler - called by backend via execute_script
+ const handleMetronomeCallback2 = useCallback((args) => {
+ console.log('Metronome callback received:', args);
+ // Backend sends: { bpm, cricket, meter, playback, sound }
+ updateMetronomeState({
+ ...args,
+ isOpen: true // Backend callback means metronome is open
+ });
+ }, [updateMetronomeState]);
+
+ // Register the callback globally when component mounts
+ useEffect(() => {
+ if (!globalObject.JK) {
+ setGlobalObject(prev => ({
+ ...prev,
+ JK: {}
+ }));
+ }
+
+ // Ensure JK object exists on window
+ if (!window.JK) {
+ window.JK = {};
+ }
+
+ // Register the callback
+ window.JK.HandleMetronomeCallback2 = handleMetronomeCallback2;
+
+ // Also register on globalObject for consistency
+ setGlobalObject(prev => ({
+ ...prev,
+ JK: {
+ ...prev.JK,
+ HandleMetronomeCallback2: handleMetronomeCallback2
+ }
+ }));
+ }, [handleMetronomeCallback2]);
+
return (
{children}
diff --git a/jam-ui/src/context/JamClientContext.js b/jam-ui/src/context/JamClientContext.js
index f0e61b538..5f72ba702 100644
--- a/jam-ui/src/context/JamClientContext.js
+++ b/jam-ui/src/context/JamClientContext.js
@@ -1,4 +1,4 @@
-import React, { createContext, useContext, useRef } from 'react';
+import React, { createContext, useContext, useRef, useEffect } from 'react';
import JamClientProxy from '../jamClientProxy';
import { FakeJamClientProxy } from '../fakeJamClientProxy';
@@ -40,6 +40,15 @@ export const JamClientProvider = ({ children }) => {
const proxy = new JamClientProxy(app, console, globalObject);
proxyRef.current = proxy.init();
}
+
+ // Register metronome callback with jamClient when it's available
+ useEffect(() => {
+ if (proxyRef.current && proxyRef.current.setMetronomeOpenCallback) {
+ console.log('Registering metronome callback with jamClient');
+ proxyRef.current.setMetronomeOpenCallback("JK.HandleMetronomeCallback2");
+ }
+ }, [proxyRef.current]);
+
return (
{children}
diff --git a/jam-ui/src/helpers/rest.js b/jam-ui/src/helpers/rest.js
index 257490065..8d1fb20f4 100644
--- a/jam-ui/src/helpers/rest.js
+++ b/jam-ui/src/helpers/rest.js
@@ -918,3 +918,27 @@ export const getVideoConferencingRoomUrl = (musicSessionId) => {
.catch(error => reject(error));
});
}
+
+export const closeJamTrack = options => {
+ const { id, ...rest } = options;
+ return new Promise((resolve, reject) => {
+ apiFetch(`/sessions/${id}/jam_tracks/close`, {
+ method: 'POST',
+ body: JSON.stringify(rest)
+ })
+ .then(response => resolve(response))
+ .catch(error => reject(error));
+ });
+};
+
+export const openMetronome = options => {
+ const { id, ...rest } = options;
+ return new Promise((resolve, reject) => {
+ apiFetch(`/sessions/${id}/metronome/open`, {
+ method: 'POST',
+ body: JSON.stringify(rest)
+ })
+ .then(response => resolve(response))
+ .catch(error => reject(error));
+ });
+};
diff --git a/jam-ui/src/hooks/useMetronomeState.js b/jam-ui/src/hooks/useMetronomeState.js
new file mode 100644
index 000000000..f70bee7f2
--- /dev/null
+++ b/jam-ui/src/hooks/useMetronomeState.js
@@ -0,0 +1,72 @@
+import { useState, useCallback } from 'react';
+
+const METRO_SOUND_LOOKUP = {
+ 0: "BuiltIn",
+ 1: "SineWave",
+ 2: "Beep",
+ 3: "Click",
+ 4: "Kick",
+ 5: "Snare",
+ 6: "MetroFile"
+};
+
+const useMetronomeState = () => {
+ const [metronomeState, setMetronomeState] = useState({
+ isOpen: false,
+ bpm: 120,
+ cricket: false,
+ meter: 1,
+ playback: 1,
+ sound: 2,
+ soundName: "Beep"
+ });
+
+ const updateMetronomeState = useCallback((updates) => {
+ setMetronomeState(prev => {
+ const newState = { ...prev, ...updates };
+
+ // Update sound name based on sound number
+ if (updates.sound !== undefined) {
+ newState.soundName = METRO_SOUND_LOOKUP[updates.sound] || "Beep";
+ }
+
+ return newState;
+ });
+ }, []);
+
+ const openMetronome = useCallback((settings = {}) => {
+ updateMetronomeState({
+ isOpen: true,
+ ...settings
+ });
+ }, [updateMetronomeState]);
+
+ const closeMetronome = useCallback(() => {
+ setMetronomeState(prev => ({
+ ...prev,
+ isOpen: false
+ }));
+ }, []);
+
+ const resetMetronome = useCallback(() => {
+ setMetronomeState({
+ isOpen: false,
+ bpm: 120,
+ cricket: false,
+ meter: 1,
+ playback: 1,
+ sound: 2,
+ soundName: "Beep"
+ });
+ }, []);
+
+ return {
+ metronomeState,
+ updateMetronomeState,
+ openMetronome,
+ closeMetronome,
+ resetMetronome
+ };
+};
+
+export default useMetronomeState;
diff --git a/jam-ui/src/jamClientProxy.js b/jam-ui/src/jamClientProxy.js
index 1c2c08c39..4a4f530d9 100644
--- a/jam-ui/src/jamClientProxy.js
+++ b/jam-ui/src/jamClientProxy.js
@@ -504,6 +504,7 @@ class JamClientProxy {
case '3006': // execute_script
try {
// eslint-disable-next-line no-eval
+ console.log(`[jamClientProxy] execute_script: ${response['execute_script']}`);
eval(response['execute_script']);
} catch (error) {
this.logger.log(`[jamClientProxy] error: execute_script: ${response['execute_script']}`);