Merge pull request #1 from KGAudioLab/fix/2025-08-12-misc
Fix/2025 08 12 misc
This commit is contained in:
@@ -1 +1,7 @@
|
|||||||
When making chord progression for existing melody, you should be very careful to make sure the chords can match the notes in the melody at any given time, they should sound harmony.
|
When making chord progression for existing melody, you should be very careful to make sure the chords can match the notes in the melody at any given time, they should sound harmony.
|
||||||
|
|
||||||
|
DO NOT OMIT ANYTHING WHEN ADDING NOTES!
|
||||||
|
THINGS LIKE BELOW SHOULD NEVER HAPPEN!!!
|
||||||
|
```
|
||||||
|
<!-- [Additional 56 hi-hat notes omitted for brevity - 64 total] -->
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
### No Region Selected
|
### No MIDI Region Selected
|
||||||
|
|
||||||
To proceed, please select a MIDI region in the track view or open a region in the piano roll.
|
Please select a MIDI region in your track to continue.
|
||||||
|
|
||||||
Tips:
|
The K.G.Studio Musician Assistant Agent can only add, remove, or edit notes within the boundaries of the selected region. Ensure your region's start and end beats covers the musical section you want to work on. The agent will focus its edits within this region, though it may reference music outside the selection if needed for context.
|
||||||
- Double-click in a track to create a new MIDI region, then select it.
|
|
||||||
- Click an existing region to select it.
|
|
||||||
- Use the Piano button to open the active region in the piano roll.
|
|
||||||
|
|
||||||
After selecting a region, try your request again.
|
When the agent is not working well, try to reduce the region size to limit the scope of the agent's work.
|
||||||
|
|
||||||
|
**Region Operations:**
|
||||||
|
- Double-click in a track to create and select a new MIDI region.
|
||||||
|
- Click on an existing region to select it.
|
||||||
|
- Use the Piano button to open and work with the currently active region in the piano roll.
|
||||||
|
|
||||||
|
After selecting a region, please try your request again.
|
||||||
@@ -222,6 +222,7 @@ You have access to two tools for working with the current music region: **remove
|
|||||||
|
|
||||||
## Important Considerations
|
## Important Considerations
|
||||||
|
|
||||||
|
- **Do not omit notes**: It is important that when adding notes, you must explicitly output every note that should be added — do not omit, summarize, or replace them with comments like “...”. Even if the pattern is repetitive, list all notes in full detail in the correct order. NEVER OMIT ANY NOTES IN THE XML BECAUSE OF REPETITION!!
|
||||||
- **Reading Music**: You should NEVER ask the user to manually provide you music pieces BEFORE invoking the read_music tool. Always use the read_music tool to get the music pieces first.
|
- **Reading Music**: You should NEVER ask the user to manually provide you music pieces BEFORE invoking the read_music tool. Always use the read_music tool to get the music pieces first.
|
||||||
- **Music Validation**: Always validate your musical choices:
|
- **Music Validation**: Always validate your musical choices:
|
||||||
- Ensure pitches are within reasonable ranges for the current instrument
|
- Ensure pitches are within reasonable ranges for the current instrument
|
||||||
|
|||||||
+3
-4
@@ -61,15 +61,14 @@ function App() {
|
|||||||
|
|
||||||
{/* Main Display Area containing MainContent, ChatBox, and Settings */}
|
{/* Main Display Area containing MainContent, ChatBox, and Settings */}
|
||||||
<div className="main-display-area">
|
<div className="main-display-area">
|
||||||
{showSettings ? (
|
{showSettings && <SettingsPanel onClose={() => setShowSettings(false)} />}
|
||||||
<SettingsPanel onClose={() => setShowSettings(false)} />
|
{!showSettings && (
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
{showInstrumentSelection && <InstrumentSelection />}
|
{showInstrumentSelection && <InstrumentSelection />}
|
||||||
<MainContent />
|
<MainContent />
|
||||||
{showChatBox && <ChatBox />}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<ChatBox isVisible={showChatBox && !showSettings} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Track Control */}
|
{/* Track Control */}
|
||||||
|
|||||||
@@ -10,36 +10,38 @@ import { URL_CONSTANTS } from '../../constants/coreConstants';
|
|||||||
export class OpenAIProvider extends LLMProvider {
|
export class OpenAIProvider extends LLMProvider {
|
||||||
readonly name = 'OpenAI';
|
readonly name = 'OpenAI';
|
||||||
|
|
||||||
private apiKey: string;
|
|
||||||
private model: string;
|
|
||||||
private flexMode: boolean = false;
|
|
||||||
private baseURL: string;
|
|
||||||
private isCompatibleProvider: boolean;
|
|
||||||
private apiEndpoint: string;
|
|
||||||
private isOllamaFormat: boolean | null = null; // Detected at runtime
|
private isOllamaFormat: boolean | null = null; // Detected at runtime
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current configuration values from ConfigManager
|
||||||
|
*/
|
||||||
|
private getCurrentConfig() {
|
||||||
const configManager = ConfigManager.instance();
|
const configManager = ConfigManager.instance();
|
||||||
const llmProvider = configManager.get('general.llm_provider') as string;
|
const llmProvider = configManager.get('general.llm_provider') as string;
|
||||||
this.isCompatibleProvider = llmProvider === 'openai_compatible';
|
const isCompatibleProvider = llmProvider === 'openai_compatible';
|
||||||
|
|
||||||
// Set API key, model, base URL, and endpoint based on provider type
|
if (isCompatibleProvider) {
|
||||||
if (this.isCompatibleProvider) {
|
const apiKey = configManager.get('general.openai_compatible.api_key') as string;
|
||||||
this.apiKey = configManager.get('general.openai_compatible.api_key') as string;
|
const model = configManager.get('general.openai_compatible.model') as string;
|
||||||
this.model = configManager.get('general.openai_compatible.model') as string;
|
const baseURL = configManager.get('general.openai_compatible.base_url') as string;
|
||||||
this.baseURL = configManager.get('general.openai_compatible.base_url') as string;
|
|
||||||
// For compatible providers, use the full URL as provided (assume it includes the endpoint)
|
// For compatible providers, use the full URL as provided (assume it includes the endpoint)
|
||||||
// Common patterns: http://localhost:11434/api/chat (Ollama), https://api.openrouter.ai/v1 (OpenRouter)
|
// Common patterns: http://localhost:11434/api/chat (Ollama), https://api.openrouter.ai/v1 (OpenRouter)
|
||||||
this.apiEndpoint = this.baseURL;
|
const apiEndpoint = baseURL;
|
||||||
this.flexMode = false; // Not applicable to compatible providers
|
const flexMode = false; // Not applicable to compatible providers
|
||||||
|
|
||||||
|
return { apiKey, model, baseURL, apiEndpoint, flexMode, isCompatibleProvider };
|
||||||
} else {
|
} else {
|
||||||
this.apiKey = configManager.get('general.openai.api_key') as string;
|
const apiKey = configManager.get('general.openai.api_key') as string;
|
||||||
this.model = configManager.get('general.openai.model') as string;
|
const model = configManager.get('general.openai.model') as string;
|
||||||
this.flexMode = (configManager.get('general.openai.flex') as boolean) === true;
|
const flexMode = (configManager.get('general.openai.flex') as boolean) === true;
|
||||||
this.baseURL = URL_CONSTANTS.DEFAULT_OPENAI_BASE_URL;
|
const baseURL = URL_CONSTANTS.DEFAULT_OPENAI_BASE_URL;
|
||||||
this.apiEndpoint = `${this.baseURL}/chat/completions`;
|
const apiEndpoint = `${baseURL}/chat/completions`;
|
||||||
|
|
||||||
|
return { apiKey, model, baseURL, apiEndpoint, flexMode, isCompatibleProvider };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +129,9 @@ export class OpenAIProvider extends LLMProvider {
|
|||||||
systemPrompt?: string,
|
systemPrompt?: string,
|
||||||
tools?: Record<string, unknown>[]
|
tools?: Record<string, unknown>[]
|
||||||
): AsyncIterableIterator<StreamChunk> {
|
): AsyncIterableIterator<StreamChunk> {
|
||||||
|
// Get fresh config values
|
||||||
|
const config = this.getCurrentConfig();
|
||||||
|
|
||||||
// Build OpenAI messages array with role preservation
|
// Build OpenAI messages array with role preservation
|
||||||
const openAIMessages: Array<{ role: string; content: string }> = [];
|
const openAIMessages: Array<{ role: string; content: string }> = [];
|
||||||
|
|
||||||
@@ -141,15 +146,15 @@ export class OpenAIProvider extends LLMProvider {
|
|||||||
content: msg.content
|
content: msg.content
|
||||||
})));
|
})));
|
||||||
|
|
||||||
const response = await fetch(this.apiEndpoint, {
|
const response = await fetch(config.apiEndpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${this.apiKey}`,
|
'Authorization': `Bearer ${config.apiKey}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: this.model,
|
model: config.model,
|
||||||
...(this.flexMode && !this.isCompatibleProvider ? { service_tier: 'flex' } : {}),
|
...(config.flexMode && !config.isCompatibleProvider ? { service_tier: 'flex' } : {}),
|
||||||
messages: openAIMessages,
|
messages: openAIMessages,
|
||||||
stream: true,
|
stream: true,
|
||||||
tools: tools || undefined
|
tools: tools || undefined
|
||||||
@@ -332,6 +337,9 @@ export class OpenAIProvider extends LLMProvider {
|
|||||||
systemPrompt?: string,
|
systemPrompt?: string,
|
||||||
tools?: Record<string, unknown>[]
|
tools?: Record<string, unknown>[]
|
||||||
): Promise<LLMResponse> {
|
): Promise<LLMResponse> {
|
||||||
|
// Get fresh config values
|
||||||
|
const config = this.getCurrentConfig();
|
||||||
|
|
||||||
// Build OpenAI messages array with role preservation
|
// Build OpenAI messages array with role preservation
|
||||||
const openAIMessages: Array<{ role: string; content: string }> = [];
|
const openAIMessages: Array<{ role: string; content: string }> = [];
|
||||||
|
|
||||||
@@ -346,15 +354,15 @@ export class OpenAIProvider extends LLMProvider {
|
|||||||
content: msg.content
|
content: msg.content
|
||||||
})));
|
})));
|
||||||
|
|
||||||
const response = await fetch(this.apiEndpoint, {
|
const response = await fetch(config.apiEndpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${this.apiKey}`,
|
'Authorization': `Bearer ${config.apiKey}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: this.model,
|
model: config.model,
|
||||||
...(this.flexMode && !this.isCompatibleProvider ? { service_tier: 'flex' } : {}),
|
...(config.flexMode && !config.isCompatibleProvider ? { service_tier: 'flex' } : {}),
|
||||||
messages: openAIMessages,
|
messages: openAIMessages,
|
||||||
stream: false,
|
stream: false,
|
||||||
tools: tools || undefined
|
tools: tools || undefined
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export class AttemptCompletionTool extends BaseTool {
|
|||||||
agentState.setIsWorkingOnTask(false);
|
agentState.setIsWorkingOnTask(false);
|
||||||
|
|
||||||
return this.createSuccessResult(
|
return this.createSuccessResult(
|
||||||
`Task completed: ${comment}. Agent task status updated to not working.`
|
`Task completed: ${comment}. `
|
||||||
);
|
);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import { KGMidiTrack } from '../../core/track/KGMidiTrack';
|
|||||||
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
||||||
import { convertRegionToABCNotation } from '../../util/abcNotationUtil';
|
import { convertRegionToABCNotation } from '../../util/abcNotationUtil';
|
||||||
import { KEY_SIGNATURE_MAP } from '../../constants/coreConstants';
|
import { KEY_SIGNATURE_MAP } from '../../constants/coreConstants';
|
||||||
import { useProjectStore } from '../../stores/projectStore';
|
import { FLUIDR3_INSTRUMENT_MAP } from '../../constants/generalMidiConstants';
|
||||||
import { KGRegion } from '../../core/region/KGRegion';
|
|
||||||
import { KGCore } from '../../core/KGCore';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tool for reading music content from the project
|
* Tool for reading music content from the project
|
||||||
@@ -34,6 +32,25 @@ export class ReadMusicTool extends BaseTool {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the display name for percussion instruments, or null if not percussion
|
||||||
|
*/
|
||||||
|
private getPercussionDisplayName(track: KGMidiTrack): string | null {
|
||||||
|
try {
|
||||||
|
const instrument = track.getInstrument();
|
||||||
|
const instrumentInfo = FLUIDR3_INSTRUMENT_MAP[instrument];
|
||||||
|
|
||||||
|
if (instrumentInfo && instrumentInfo.group === 'PERCUSSION_KIT') {
|
||||||
|
return instrumentInfo.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting percussion display name:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async execute(params: Record<string, unknown>): Promise<ToolResult> {
|
async execute(params: Record<string, unknown>): Promise<ToolResult> {
|
||||||
try {
|
try {
|
||||||
// Validate parameters
|
// Validate parameters
|
||||||
@@ -104,65 +121,43 @@ export class ReadMusicTool extends BaseTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get KGCore instance
|
* Find tracks that should be skipped because they have no musical content
|
||||||
|
* (no regions or regions with no notes)
|
||||||
*/
|
*/
|
||||||
private getKGCore(): KGCore {
|
private findTracksToSkip(tracks: KGMidiTrack[]): KGMidiTrack[] {
|
||||||
return KGCore.instance();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the track that contains the active piano roll region or first selected region
|
|
||||||
*/
|
|
||||||
private findTrackToSkip(tracks: KGMidiTrack[]): KGMidiTrack | null {
|
|
||||||
try {
|
try {
|
||||||
const store = useProjectStore.getState();
|
const tracksToSkip: KGMidiTrack[] = [];
|
||||||
const core = this.getKGCore();
|
|
||||||
|
|
||||||
// First check for active piano roll region
|
for (const track of tracks) {
|
||||||
if (store.activeRegionId) {
|
const regions = track.getRegions();
|
||||||
const activeRegion = this.findRegionById(store.activeRegionId, tracks);
|
|
||||||
if (activeRegion) {
|
// Skip tracks with no regions
|
||||||
const track = this.findTrackByRegion(activeRegion, tracks);
|
if (regions.length === 0) {
|
||||||
return track;
|
tracksToSkip.push(track);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all regions in this track are empty (have no notes)
|
||||||
|
const hasAnyNotes = regions.some(region => {
|
||||||
|
if (region.getCurrentType() === 'KGMidiRegion') {
|
||||||
|
return (region as KGMidiRegion).getNotes().length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Skip tracks where no regions have notes
|
||||||
|
if (!hasAnyNotes) {
|
||||||
|
tracksToSkip.push(track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then check for selected regions
|
return tracksToSkip;
|
||||||
const selectedItems = core.getSelectedItems();
|
|
||||||
const selectedRegion = selectedItems.find((item: unknown) => item instanceof KGRegion) as KGRegion;
|
|
||||||
|
|
||||||
if (selectedRegion) {
|
|
||||||
const track = this.findTrackByRegion(selectedRegion, tracks);
|
|
||||||
return track;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error finding track to skip:', error);
|
console.error('Error finding tracks to skip:', error);
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a region by ID across all tracks
|
|
||||||
*/
|
|
||||||
private findRegionById(regionId: string, tracks: KGMidiTrack[]): KGRegion | null {
|
|
||||||
for (const track of tracks) {
|
|
||||||
const regions = track.getRegions();
|
|
||||||
const region = regions.find(r => r.getId() === regionId);
|
|
||||||
if (region) {
|
|
||||||
return region;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find track that contains the given region
|
|
||||||
*/
|
|
||||||
private findTrackByRegion(region: KGRegion, tracks: KGMidiTrack[]): KGMidiTrack | null {
|
|
||||||
return tracks.find(track => track.getRegions().includes(region)) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate ABC notation for all tracks
|
* Generate ABC notation for all tracks
|
||||||
@@ -174,9 +169,8 @@ export class ReadMusicTool extends BaseTool {
|
|||||||
return 'No MIDI tracks found in the project.';
|
return 'No MIDI tracks found in the project.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the track to skip (unless it's the first track)
|
// Find tracks to skip (tracks with no content)
|
||||||
const trackToSkip = this.findTrackToSkip(midiTracks);
|
const tracksToSkip = this.findTracksToSkip(midiTracks);
|
||||||
const firstTrack = midiTracks[0]; // The melody track
|
|
||||||
|
|
||||||
// Get project settings for proper notation
|
// Get project settings for proper notation
|
||||||
const project = this.getCurrentProject();
|
const project = this.getCurrentProject();
|
||||||
@@ -187,15 +181,29 @@ export class ReadMusicTool extends BaseTool {
|
|||||||
let output = `All Tracks (beats ${startBeat}-${endBeat || 'end'}):\n\n`;
|
let output = `All Tracks (beats ${startBeat}-${endBeat || 'end'}):\n\n`;
|
||||||
|
|
||||||
midiTracks.forEach((track, index) => {
|
midiTracks.forEach((track, index) => {
|
||||||
// Skip this track if it's the track to skip AND it's not the first track (melody)
|
// Skip tracks that have no musical content
|
||||||
if (trackToSkip && track === trackToSkip && track !== firstTrack) {
|
if (tracksToSkip.includes(track)) {
|
||||||
return; // Skip this track
|
return; // Skip this track
|
||||||
}
|
}
|
||||||
const trackNumber = index + 1;
|
const trackNumber = index + 1;
|
||||||
const trackName = track.getName() || `Track ${trackNumber}`;
|
const trackName = track.getName() || `Track ${trackNumber}`;
|
||||||
|
|
||||||
// hardcode the 1st track to be the melody, other track names are the same as the original track names
|
// Check if this track uses a percussion instrument
|
||||||
output += `Track ${trackNumber} - ${trackNumber === 1 ? 'Melody' : trackName}:\n`;
|
const percussionDisplayName = this.getPercussionDisplayName(track);
|
||||||
|
|
||||||
|
let displayTrackName: string;
|
||||||
|
if (percussionDisplayName) {
|
||||||
|
// Use percussion instrument display name for all percussion tracks
|
||||||
|
displayTrackName = percussionDisplayName;
|
||||||
|
} else if (trackNumber === 1) {
|
||||||
|
// Use "Melody" for the first non-percussion track
|
||||||
|
displayTrackName = 'Melody';
|
||||||
|
} else {
|
||||||
|
// Use original track name for other non-percussion tracks
|
||||||
|
displayTrackName = trackName;
|
||||||
|
}
|
||||||
|
|
||||||
|
output += `Track ${trackNumber} - ${displayTrackName}:\n`;
|
||||||
|
|
||||||
// Get all regions from the track and convert each one
|
// Get all regions from the track and convert each one
|
||||||
const regions = track.getRegions().filter(region => region instanceof KGMidiRegion) as KGMidiRegion[];
|
const regions = track.getRegions().filter(region => region instanceof KGMidiRegion) as KGMidiRegion[];
|
||||||
|
|||||||
+35
-16
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useRef, useEffect, memo } from 'react';
|
import React, { useState, useRef, useEffect, memo, useCallback } from 'react';
|
||||||
import { FaPlus, FaBan } from 'react-icons/fa';
|
import { FaPlus, FaBan } from 'react-icons/fa';
|
||||||
import { UserMessage, AssistantMessage } from './chat';
|
import { UserMessage, AssistantMessage } from './chat';
|
||||||
import { AgentCore } from '../agent/core/AgentCore';
|
import { AgentCore } from '../agent/core/AgentCore';
|
||||||
@@ -44,7 +44,11 @@ const createLLMProvider = (): LLMProvider => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChatBox: React.FC = () => {
|
interface ChatBoxProps {
|
||||||
|
isVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
@@ -62,6 +66,33 @@ const ChatBox: React.FC = () => {
|
|||||||
// Track if this is the first message (for system prompt logging)
|
// Track if this is the first message (for system prompt logging)
|
||||||
const [isFirstMessage, setIsFirstMessage] = useState(true);
|
const [isFirstMessage, setIsFirstMessage] = useState(true);
|
||||||
|
|
||||||
|
const generateMessageId = (): string => {
|
||||||
|
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearChatUI = useCallback(async () => {
|
||||||
|
// Clear UI state
|
||||||
|
setMessages([]);
|
||||||
|
|
||||||
|
// Reset first message flag so system prompt will be logged again
|
||||||
|
setIsFirstMessage(true);
|
||||||
|
|
||||||
|
// Auto-show welcome message after clearing (like on app startup)
|
||||||
|
try {
|
||||||
|
const result = await processUserMessage('/welcome');
|
||||||
|
if (result.pseudoAssistantResponse) {
|
||||||
|
const pseudoId = generateMessageId();
|
||||||
|
setMessages(prev => [...prev, {
|
||||||
|
id: pseudoId,
|
||||||
|
role: 'assistant',
|
||||||
|
content: result.pseudoAssistantResponse!,
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore errors, just don't show welcome if it fails
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Initialize AgentCore with configured provider and register clear UI callback
|
// Initialize AgentCore with configured provider and register clear UI callback
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeProvider = async () => {
|
const initializeProvider = async () => {
|
||||||
@@ -103,11 +134,7 @@ const ChatBox: React.FC = () => {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, [clearChatUI]);
|
||||||
|
|
||||||
const generateMessageId = (): string => {
|
|
||||||
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAbort = () => {
|
const handleAbort = () => {
|
||||||
if (abortController) {
|
if (abortController) {
|
||||||
@@ -128,14 +155,6 @@ const ChatBox: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearChatUI = () => {
|
|
||||||
// Clear UI state
|
|
||||||
setMessages([]);
|
|
||||||
|
|
||||||
// Reset first message flag so system prompt will be logged again
|
|
||||||
setIsFirstMessage(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearCommand = () => {
|
const handleClearCommand = () => {
|
||||||
const { setStatus } = useProjectStore.getState();
|
const { setStatus } = useProjectStore.getState();
|
||||||
clearChatHistoryAndUI(setStatus);
|
clearChatHistoryAndUI(setStatus);
|
||||||
@@ -519,7 +538,7 @@ const ChatBox: React.FC = () => {
|
|||||||
}, [isProcessing, isExecutingTools]);
|
}, [isProcessing, isExecutingTools]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chatbox">
|
<div className="chatbox" style={{ display: isVisible ? 'flex' : 'none' }}>
|
||||||
<div className="chatbox-header">
|
<div className="chatbox-header">
|
||||||
<h3>K.G.Studio Musician Assistant</h3>
|
<h3>K.G.Studio Musician Assistant</h3>
|
||||||
<div className="chatbox-actions">
|
<div className="chatbox-actions">
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import { KGMidiTrack, type InstrumentType } from '../core/track/KGMidiTrack';
|
|||||||
const InstrumentSelection: React.FC = () => {
|
const InstrumentSelection: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
tracks,
|
tracks,
|
||||||
instrumentSelectionTrackId,
|
selectedTrackId,
|
||||||
closeInstrumentSelection,
|
closeInstrumentSelection,
|
||||||
setTrackInstrument
|
setTrackInstrument
|
||||||
} = useProjectStore();
|
} = useProjectStore();
|
||||||
|
|
||||||
const targetTrack = useMemo(() => {
|
const targetTrack = useMemo(() => {
|
||||||
return tracks.find(t => t.getId().toString() === instrumentSelectionTrackId) || null;
|
return tracks.find(t => t.getId().toString() === selectedTrackId) || null;
|
||||||
}, [tracks, instrumentSelectionTrackId]);
|
}, [tracks, selectedTrackId]);
|
||||||
|
|
||||||
const currentInstrumentKey: InstrumentType = (targetTrack && targetTrack instanceof KGMidiTrack)
|
const currentInstrumentKey: InstrumentType = (targetTrack && targetTrack instanceof KGMidiTrack)
|
||||||
? (targetTrack.getInstrument() as InstrumentType)
|
? (targetTrack.getInstrument() as InstrumentType)
|
||||||
@@ -27,7 +27,7 @@ const InstrumentSelection: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Sync when the target track or its instrument changes
|
// Sync when the target track or its instrument changes
|
||||||
setSelectedGroupKey(currentInstrumentDef?.group || 'PIANO_AND_KEYBOARDS');
|
setSelectedGroupKey(currentInstrumentDef?.group || 'PIANO_AND_KEYBOARDS');
|
||||||
}, [instrumentSelectionTrackId, currentInstrumentKey, currentInstrumentDef]);
|
}, [selectedTrackId, currentInstrumentKey, currentInstrumentDef]);
|
||||||
|
|
||||||
const groups = useMemo(() => Object.entries(INSTRUMENT_GROUPS) as Array<[string, string]>, []);
|
const groups = useMemo(() => Object.entries(INSTRUMENT_GROUPS) as Array<[string, string]>, []);
|
||||||
|
|
||||||
@@ -42,8 +42,9 @@ const InstrumentSelection: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectInstrument = async (instrumentKey: string) => {
|
const handleSelectInstrument = async (instrumentKey: string) => {
|
||||||
const instrument = instrumentKey as InstrumentType;
|
// If no valid target track, ignore user interaction
|
||||||
if (!targetTrack || !(targetTrack instanceof KGMidiTrack)) return;
|
if (!targetTrack || !(targetTrack instanceof KGMidiTrack)) return;
|
||||||
|
const instrument = instrumentKey as InstrumentType;
|
||||||
try {
|
try {
|
||||||
await setTrackInstrument(targetTrack.getId(), instrument);
|
await setTrackInstrument(targetTrack.getId(), instrument);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -53,25 +54,26 @@ const InstrumentSelection: React.FC = () => {
|
|||||||
|
|
||||||
const previewImage = FLUIDR3_INSTRUMENT_MAP[currentInstrumentKey]?.image || 'piano.png';
|
const previewImage = FLUIDR3_INSTRUMENT_MAP[currentInstrumentKey]?.image || 'piano.png';
|
||||||
const previewAlt = FLUIDR3_INSTRUMENT_MAP[currentInstrumentKey]?.displayName || currentInstrumentKey;
|
const previewAlt = FLUIDR3_INSTRUMENT_MAP[currentInstrumentKey]?.displayName || currentInstrumentKey;
|
||||||
|
const hasTargetTrack = !!targetTrack;
|
||||||
if (!targetTrack) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="instrument-selection">
|
<div className="instrument-selection">
|
||||||
<div className="instrument-selection-header">
|
<div className="instrument-selection-header">
|
||||||
<h3>{`${previewAlt.toString()}`}</h3>
|
<h3>{hasTargetTrack ? `${previewAlt.toString()}` : ''}</h3>
|
||||||
<button className="instrument-selection-close-btn" onClick={closeInstrumentSelection}>✕</button>
|
<button className="instrument-selection-close-btn" onClick={closeInstrumentSelection}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="instrument-selection-top">
|
<div className="instrument-selection-top">
|
||||||
<div className="instrument-preview">
|
{hasTargetTrack && (
|
||||||
<img
|
<div className="instrument-preview">
|
||||||
src={`${import.meta.env.BASE_URL}resources/instruments/${previewImage}`}
|
<img
|
||||||
alt={previewAlt.toString()}
|
src={`${import.meta.env.BASE_URL}resources/instruments/${previewImage}`}
|
||||||
width={256}
|
alt={previewAlt.toString()}
|
||||||
height={256}
|
width={256}
|
||||||
/>
|
height={256}
|
||||||
</div>
|
/>
|
||||||
<div className="instrument-name-overlay">{targetTrack.getName()}</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="instrument-name-overlay">{hasTargetTrack ? targetTrack.getName() : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="instrument-selection-bottom">
|
<div className="instrument-selection-bottom">
|
||||||
<div className="instrument-groups">
|
<div className="instrument-groups">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { KGMainContentState } from '../../core/state/KGMainContentState';
|
|||||||
import { isModifierKeyPressed } from '../../util/osUtil';
|
import { isModifierKeyPressed } from '../../util/osUtil';
|
||||||
import { CreateRegionCommand, ResizeRegionCommand, MoveRegionCommand } from '../../core/commands';
|
import { CreateRegionCommand, ResizeRegionCommand, MoveRegionCommand } from '../../core/commands';
|
||||||
import { KGCore } from '../../core/KGCore';
|
import { KGCore } from '../../core/KGCore';
|
||||||
|
import { generateNewRegionName } from '../../util/miscUtil';
|
||||||
|
|
||||||
interface TrackGridPanelProps {
|
interface TrackGridPanelProps {
|
||||||
tracks: KGTrack[];
|
tracks: KGTrack[];
|
||||||
@@ -71,6 +72,25 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
|||||||
// Get beats per bar from the time signature
|
// Get beats per bar from the time signature
|
||||||
const beatsPerBar = timeSignature.numerator;
|
const beatsPerBar = timeSignature.numerator;
|
||||||
|
|
||||||
|
// Check for overlapping regions before creating a new one
|
||||||
|
const newRegionStartBeat = (barNumber - 1) * beatsPerBar;
|
||||||
|
const newRegionEndBeat = newRegionStartBeat + beatsPerBar - 1;
|
||||||
|
|
||||||
|
const existingRegions = track.getRegions();
|
||||||
|
const hasOverlap = existingRegions.some(region => {
|
||||||
|
const existingStart = region.getStartFromBeat();
|
||||||
|
const existingEnd = existingStart + region.getLength() - 1;
|
||||||
|
return newRegionStartBeat <= existingEnd && newRegionEndBeat >= existingStart;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasOverlap) {
|
||||||
|
if (DEBUG_MODE.TRACK_GRID_PANEL) {
|
||||||
|
console.log(`Cannot create region at bar ${barNumber}: overlaps with existing region`);
|
||||||
|
}
|
||||||
|
alert('Cannot create region: overlaps with existing region');
|
||||||
|
return; // Don't create the region if it overlaps
|
||||||
|
}
|
||||||
|
|
||||||
// Create and execute the region creation command
|
// Create and execute the region creation command
|
||||||
const command = CreateRegionCommand.fromBarCoordinates(
|
const command = CreateRegionCommand.fromBarCoordinates(
|
||||||
trackId,
|
trackId,
|
||||||
@@ -78,7 +98,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
|||||||
barNumber,
|
barNumber,
|
||||||
1, // Default to 1 bar length
|
1, // Default to 1 bar length
|
||||||
beatsPerBar,
|
beatsPerBar,
|
||||||
`${track.getName()} Region`
|
generateNewRegionName(trackId)
|
||||||
);
|
);
|
||||||
|
|
||||||
KGCore.instance().executeCommand(command);
|
KGCore.instance().executeCommand(command);
|
||||||
|
|||||||
@@ -193,8 +193,8 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Select this track as active when opening instrument panel
|
// Select this track as active when opening instrument panel
|
||||||
setSelectedTrack(track.getId().toString());
|
setSelectedTrack(track.getId().toString());
|
||||||
// Toggle global InstrumentSelection panel for this track
|
// Toggle global InstrumentSelection panel (it follows selectedTrackId)
|
||||||
toggleInstrumentSelectionForTrack(track.getId().toString());
|
toggleInstrumentSelectionForTrack();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle settings button click
|
// Handle settings button click
|
||||||
@@ -214,9 +214,9 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear selection if this track is selected
|
// Clear selection if this track is selected
|
||||||
if (selectedTrackId === track.getId().toString()) {
|
// if (selectedTrackId === track.getId().toString()) {
|
||||||
setSelectedTrack(null);
|
// setSelectedTrack(null);
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Delete the track using the command system
|
// Delete the track using the command system
|
||||||
await removeTrack(track.getId());
|
await removeTrack(track.getId());
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ export class KGAudioBus {
|
|||||||
this.instrument = newInstrument;
|
this.instrument = newInstrument;
|
||||||
|
|
||||||
// Restore volume settings
|
// Restore volume settings
|
||||||
// this.updateSamplerVolume();
|
this.updateSamplerVolume();
|
||||||
|
|
||||||
console.log(`Instrument changed successfully to ${newInstrument}`);
|
console.log(`Instrument changed successfully to ${newInstrument}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { KGCommand } from '../KGCommand';
|
|||||||
import { KGCore } from '../../KGCore';
|
import { KGCore } from '../../KGCore';
|
||||||
import { KGMidiTrack, type InstrumentType } from '../../track/KGMidiTrack';
|
import { KGMidiTrack, type InstrumentType } from '../../track/KGMidiTrack';
|
||||||
import { KGAudioInterface } from '../../audio-interface/KGAudioInterface';
|
import { KGAudioInterface } from '../../audio-interface/KGAudioInterface';
|
||||||
|
import { generateNewTrackName } from '../../../util/miscUtil';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Command to add a new track to the project
|
* Command to add a new track to the project
|
||||||
@@ -28,7 +29,7 @@ export class AddTrackCommand extends KGCommand {
|
|||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.trackName = trackName || `Track ${this.trackId}`;
|
this.trackName = trackName || generateNewTrackName();
|
||||||
this.instrument = instrument;
|
this.instrument = instrument;
|
||||||
|
|
||||||
// Track index will be set during execution
|
// Track index will be set during execution
|
||||||
|
|||||||
+46
-18
@@ -57,7 +57,7 @@ interface ProjectState {
|
|||||||
|
|
||||||
// Instrument selection panel state
|
// Instrument selection panel state
|
||||||
showInstrumentSelection: boolean;
|
showInstrumentSelection: boolean;
|
||||||
instrumentSelectionTrackId: string | null;
|
// instrumentSelectionTrackId removed; panel now follows selectedTrackId
|
||||||
|
|
||||||
// Settings state
|
// Settings state
|
||||||
showSettings: boolean;
|
showSettings: boolean;
|
||||||
@@ -105,8 +105,8 @@ interface ProjectState {
|
|||||||
toggleChatBox: () => void;
|
toggleChatBox: () => void;
|
||||||
|
|
||||||
// Instrument selection panel actions
|
// Instrument selection panel actions
|
||||||
openInstrumentSelectionForTrack: (trackId: string) => void;
|
openInstrumentSelectionForTrack: () => void;
|
||||||
toggleInstrumentSelectionForTrack: (trackId: string) => void;
|
toggleInstrumentSelectionForTrack: () => void;
|
||||||
closeInstrumentSelection: () => void;
|
closeInstrumentSelection: () => void;
|
||||||
|
|
||||||
// Settings actions
|
// Settings actions
|
||||||
@@ -200,7 +200,6 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
// Also auto-select it and open instrument selection panel
|
// Also auto-select it and open instrument selection panel
|
||||||
let initialSelectedTrackId: string | null = null;
|
let initialSelectedTrackId: string | null = null;
|
||||||
let initialShowInstrumentSelection = false;
|
let initialShowInstrumentSelection = false;
|
||||||
let initialInstrumentSelectionTrackId: string | null = null;
|
|
||||||
try {
|
try {
|
||||||
const project = KGCore.instance().getCurrentProject();
|
const project = KGCore.instance().getCurrentProject();
|
||||||
if (project.getTracks().length === 0) {
|
if (project.getTracks().length === 0) {
|
||||||
@@ -209,7 +208,6 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
const createdId = String(addDefaultTrackCommand.getTrackId());
|
const createdId = String(addDefaultTrackCommand.getTrackId());
|
||||||
initialSelectedTrackId = createdId;
|
initialSelectedTrackId = createdId;
|
||||||
initialShowInstrumentSelection = true;
|
initialShowInstrumentSelection = true;
|
||||||
initialInstrumentSelectionTrackId = createdId;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating default track on startup:', error);
|
console.error('Error creating default track on startup:', error);
|
||||||
@@ -242,7 +240,6 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
|
|
||||||
// Initial Instrument Selection panel state
|
// Initial Instrument Selection panel state
|
||||||
showInstrumentSelection: initialShowInstrumentSelection,
|
showInstrumentSelection: initialShowInstrumentSelection,
|
||||||
instrumentSelectionTrackId: initialInstrumentSelectionTrackId,
|
|
||||||
|
|
||||||
// Initial Settings state
|
// Initial Settings state
|
||||||
showSettings: false,
|
showSettings: false,
|
||||||
@@ -280,6 +277,13 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
const project = KGCore.instance().getCurrentProject();
|
const project = KGCore.instance().getCurrentProject();
|
||||||
set({ tracks: [...project.getTracks()] as KGTrack[] });
|
set({ tracks: [...project.getTracks()] as KGTrack[] });
|
||||||
|
|
||||||
|
// Auto-select the newly created track and open instrument selection panel
|
||||||
|
const newTrackId = command.getTrackId().toString();
|
||||||
|
set({
|
||||||
|
selectedTrackId: newTrackId,
|
||||||
|
showInstrumentSelection: true,
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`Added track ${command.getTrackId()}`);
|
console.log(`Added track ${command.getTrackId()}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding track:', error);
|
console.error('Error adding track:', error);
|
||||||
@@ -289,13 +293,43 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
|
|
||||||
removeTrack: async (id: number) => {
|
removeTrack: async (id: number) => {
|
||||||
try {
|
try {
|
||||||
|
// Get the current tracks and find the index of the track being deleted
|
||||||
|
const currentTracks = KGCore.instance().getCurrentProject().getTracks();
|
||||||
|
const deletedTrackIndex = currentTracks.findIndex(track => track.getId() === id);
|
||||||
|
const { selectedTrackId } = get();
|
||||||
|
const isCurrentTrackSelected = selectedTrackId === id.toString();
|
||||||
|
|
||||||
// Create and execute the remove track command
|
// Create and execute the remove track command
|
||||||
const command = new RemoveTrackCommand(id);
|
const command = new RemoveTrackCommand(id);
|
||||||
KGCore.instance().executeCommand(command);
|
KGCore.instance().executeCommand(command);
|
||||||
|
|
||||||
// Update the store state with a new array reference to trigger re-render
|
// Update the store state with a new array reference to trigger re-render
|
||||||
const project = KGCore.instance().getCurrentProject();
|
const project = KGCore.instance().getCurrentProject();
|
||||||
set({ tracks: [...project.getTracks()] as KGTrack[] });
|
const remainingTracks = [...project.getTracks()] as KGTrack[];
|
||||||
|
set({ tracks: remainingTracks });
|
||||||
|
|
||||||
|
// Auto-select another track if any remain
|
||||||
|
if (remainingTracks.length > 0) {
|
||||||
|
// Prefer previous track, fallback to next track
|
||||||
|
const newSelectedIndex = deletedTrackIndex > 0
|
||||||
|
? deletedTrackIndex - 1 // Select previous track
|
||||||
|
: 0; // Select first remaining track (was next)
|
||||||
|
|
||||||
|
const newSelectedTrack = remainingTracks[newSelectedIndex];
|
||||||
|
const newSelectedTrackId = newSelectedTrack.getId().toString();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
set({
|
||||||
|
selectedTrackId: isCurrentTrackSelected ? newSelectedTrackId : selectedTrackId,
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
// No tracks left, clear selection and close instrument panel
|
||||||
|
set({
|
||||||
|
selectedTrackId: null,
|
||||||
|
showInstrumentSelection: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Removed track ${id}`);
|
console.log(`Removed track ${id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -492,7 +526,6 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
set({
|
set({
|
||||||
selectedTrackId: firstTrackIdStr,
|
selectedTrackId: firstTrackIdStr,
|
||||||
showInstrumentSelection: true,
|
showInstrumentSelection: true,
|
||||||
instrumentSelectionTrackId: firstTrackIdStr
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,13 +636,8 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setSelectedTrack: (trackId: string | null) => {
|
setSelectedTrack: (trackId: string | null) => {
|
||||||
const { showInstrumentSelection } = get();
|
|
||||||
// Update selected track id
|
// Update selected track id
|
||||||
set({ selectedTrackId: trackId });
|
set({ selectedTrackId: trackId });
|
||||||
// If instrument panel is open and a track is selected, retarget the panel
|
|
||||||
if (showInstrumentSelection && trackId) {
|
|
||||||
set({ instrumentSelectionTrackId: trackId });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Piano roll actions
|
// Piano roll actions
|
||||||
@@ -647,14 +675,14 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Instrument selection panel actions
|
// Instrument selection panel actions
|
||||||
openInstrumentSelectionForTrack: (trackId: string) => {
|
openInstrumentSelectionForTrack: () => {
|
||||||
set({ showInstrumentSelection: true, instrumentSelectionTrackId: trackId });
|
set({ showInstrumentSelection: true });
|
||||||
},
|
},
|
||||||
toggleInstrumentSelectionForTrack: (trackId: string) => {
|
toggleInstrumentSelectionForTrack: () => {
|
||||||
set({ showInstrumentSelection: true, instrumentSelectionTrackId: trackId });
|
set({ showInstrumentSelection: true });
|
||||||
},
|
},
|
||||||
closeInstrumentSelection: () => {
|
closeInstrumentSelection: () => {
|
||||||
set({ showInstrumentSelection: false, instrumentSelectionTrackId: null });
|
set({ showInstrumentSelection: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
// Settings action implementations
|
// Settings action implementations
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ export const clearChatHistoryWithStatus = (setStatus?: (message: string) => void
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Global callback for clearing UI state
|
// Global callback for clearing UI state
|
||||||
let globalClearChatUI: (() => void) | null = null;
|
let globalClearChatUI: (() => void | Promise<void>) | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a callback to clear chat UI state
|
* Register a callback to clear chat UI state
|
||||||
* This allows external components to clear the ChatBox UI
|
* This allows external components to clear the ChatBox UI
|
||||||
*/
|
*/
|
||||||
export const registerClearChatUICallback = (callback: () => void) => {
|
export const registerClearChatUICallback = (callback: () => void | Promise<void>) => {
|
||||||
globalClearChatUI = callback;
|
globalClearChatUI = callback;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
* Miscellaneous utility functions
|
* Miscellaneous utility functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { KGCore } from '../core/KGCore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a unique ID with a consistent format
|
* Generates a unique ID with a consistent format
|
||||||
* @param prefix - The prefix for the ID (typically class name like 'KGMidiNote')
|
* @param prefix - The prefix for the ID (typically class name like 'KGMidiNote')
|
||||||
@@ -12,4 +14,56 @@ export const generateUniqueId = (prefix: string): string => {
|
|||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const randomString = Math.random().toString(36).substring(2, 11); // 9 character random string
|
const randomString = Math.random().toString(36).substring(2, 11); // 9 character random string
|
||||||
return `${prefix}_${timestamp}_${randomString}`;
|
return `${prefix}_${timestamp}_${randomString}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new sequential track name that doesn't conflict with existing tracks
|
||||||
|
* @returns A track name in format "Track {number}" where number is the next available sequential number
|
||||||
|
* @example generateNewTrackName() -> 'Track 1' (if no tracks exist)
|
||||||
|
* @example generateNewTrackName() -> 'Track 3' (if 'Track 1' and 'Track 2' already exist)
|
||||||
|
*/
|
||||||
|
export const generateNewTrackName = (): string => {
|
||||||
|
const currentProject = KGCore.instance().getCurrentProject();
|
||||||
|
const existingTracks = currentProject.getTracks();
|
||||||
|
const existingNames = existingTracks.map(track => track.getName());
|
||||||
|
|
||||||
|
let i = 1;
|
||||||
|
while (true) {
|
||||||
|
const candidateName = `Track ${i}`;
|
||||||
|
if (!existingNames.includes(candidateName)) {
|
||||||
|
return candidateName;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new sequential region name that doesn't conflict with existing regions on the same track
|
||||||
|
* @param trackId - The ID of the track where the region will be created
|
||||||
|
* @returns A region name in format "{trackName} Region {number}" where number is the next available sequential number
|
||||||
|
* @example generateNewRegionName('1') -> 'Piano Region 1' (if no regions exist on track)
|
||||||
|
* @example generateNewRegionName('1') -> 'Piano Region 3' (if 'Piano Region 1' and 'Piano Region 2' already exist)
|
||||||
|
*/
|
||||||
|
export const generateNewRegionName = (trackId: string): string => {
|
||||||
|
const currentProject = KGCore.instance().getCurrentProject();
|
||||||
|
const tracks = currentProject.getTracks();
|
||||||
|
const targetTrack = tracks.find(track => track.getId().toString() === trackId);
|
||||||
|
|
||||||
|
if (!targetTrack) {
|
||||||
|
// Fallback if track not found
|
||||||
|
return 'Region 1';
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackName = targetTrack.getName();
|
||||||
|
const existingRegions = targetTrack.getRegions();
|
||||||
|
const existingNames = existingRegions.map(region => region.getName());
|
||||||
|
|
||||||
|
let i = 1;
|
||||||
|
while (true) {
|
||||||
|
const candidateName = `${trackName} Region ${i}`;
|
||||||
|
if (!existingNames.includes(candidateName)) {
|
||||||
|
return candidateName;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user