fix: few minor UI and behavior adjustments

This commit is contained in:
Xiaohan-Tian
2026-05-11 23:23:11 -07:00
parent f57dd7c1da
commit 64dda5bb01
6 changed files with 81 additions and 26 deletions
+48 -21
View File
@@ -329,7 +329,7 @@ const ClipTab: React.FC<ClipTabProps> = ({ bpm, keySignature }) => {
setGenStatus('polling'); setGenStatus('polling');
setGenHint('Generating clip...'); setGenHint('Generating clip...');
while (true) { while (true) {
if (signal.aborted) return; if (signal.aborted) return;
@@ -655,7 +655,7 @@ const FullSongTab: React.FC = () => {
type ResultItem = { progress: number; stage: string; status: number }; type ResultItem = { progress: number; stage: string; status: number };
type PollResponse = { data: Array<{ status: number; result: string }>; code: number }; type PollResponse = { data: Array<{ status: number; result: string }>; code: number };
while (true) { while (true) {
if (signal.aborted) return; if (signal.aborted) return;
@@ -905,6 +905,19 @@ const SeparatorTab: React.FC<{ mode: KGOneMode }> = ({ mode }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// Reset separator results whenever the active project changes
useEffect(() => {
stemAudioUrls.forEach(s => URL.revokeObjectURL(s.url));
setStemAudioUrls([]);
setGenStatus('idle');
setGenHint('');
setErrorMsg('');
setIsImporting(false);
setImportError('');
originalRegionRef.current = null;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectName]);
useEffect(() => { useEffect(() => {
setModel(availableSeparatorModels[0].value); setModel(availableSeparatorModels[0].value);
}, [availableSeparatorModels]); }, [availableSeparatorModels]);
@@ -1116,7 +1129,7 @@ const SeparatorTab: React.FC<{ mode: KGOneMode }> = ({ mode }) => {
let files: string[] = []; let files: string[] = [];
while (true) { while (true) {
if (signal.aborted) return; if (signal.aborted) return;
@@ -1363,7 +1376,8 @@ const SeparatorTab: React.FC<{ mode: KGOneMode }> = ({ mode }) => {
<div className="kgone-local-mode-text"> <div className="kgone-local-mode-text">
Only Vocal and Instrument (Medium Accuracy) is available while not integrated with K.G.One Music Studio server. Only Vocal and Instrument (Medium Accuracy) is available while not integrated with K.G.One Music Studio server.
Processing in local may take long time depending on your hardware. When fallback to CPU happens, the webpage may Processing in local may take long time depending on your hardware. When fallback to CPU happens, the webpage may
temporarily hang with little or no UI response until processing advances. temporarily hang with little or no UI response until processing advances.{' '}
<a href="https://github.com/KGAudioLab/K.G.One" target="_blank" rel="noopener noreferrer">Learn more about K.G.One Music Studio server integration.</a>
</div> </div>
<div className="kgone-runtime-row"> <div className="kgone-runtime-row">
<div className="kgone-provider-chip">Provider: {localProviderLabel}</div> <div className="kgone-provider-chip">Provider: {localProviderLabel}</div>
@@ -1740,7 +1754,7 @@ const RemixTab: React.FC = () => {
type ResultItem = { progress: number; stage: string; status: number }; type ResultItem = { progress: number; stage: string; status: number };
type PollResponse = { data: Array<{ status: number; result: string }>; code: number }; type PollResponse = { data: Array<{ status: number; result: string }>; code: number };
while (true) { while (true) {
if (signal.aborted) return; if (signal.aborted) return;
@@ -2021,7 +2035,7 @@ const RemixTab: React.FC = () => {
const RepaintTab: React.FC = () => { const RepaintTab: React.FC = () => {
const { selectedRegionIds, projectName, bpm, timeSignature, maxBars, const { selectedRegionIds, projectName, bpm, timeSignature, maxBars,
isLooping, loopingRange, refreshProjectState } = useProjectStore(); isLooping, loopingRange, refreshProjectState } = useProjectStore();
// Form state (mirrors FullSongTab) // Form state (mirrors FullSongTab)
const [caption, setCaption] = useState(''); const [caption, setCaption] = useState('');
@@ -2258,7 +2272,7 @@ const RepaintTab: React.FC = () => {
type ResultItem = { progress: number; stage: string; status: number }; type ResultItem = { progress: number; stage: string; status: number };
type PollResponse = { data: Array<{ status: number; result: string }>; code: number }; type PollResponse = { data: Array<{ status: number; result: string }>; code: number };
while (true) { while (true) {
if (signal.aborted) return; if (signal.aborted) return;
@@ -2573,24 +2587,37 @@ const KGOnePanel: React.FC<KGOnePanelProps> = ({ isVisible }) => {
return ( return (
<div className={`kgone-panel${isVisible ? '' : ' is-hidden'}`}> <div className={`kgone-panel${isVisible ? '' : ' is-hidden'}`}>
<div className="kgone-panel-header"> <div className="kgone-panel-header">
<h3>K.G.One Music Generator</h3> <h3>{mode === 'local-separator' ? 'Music Generator' : 'K.G.One Music Generator'}</h3>
</div> </div>
<div className="kgone-tabs"> <div className="kgone-tabs">
{/* Clip tab temporarily disabled, will enable in the future */} {/* Clip tab temporarily disabled, will enable in the future */}
{KGONE_TABS.map(tab => ( {KGONE_TABS.map(tab => {
<button const isDisabled = disabledTabs.has(tab);
key={tab} const button = (
className={`kgone-tab${activeTab === tab ? ' active' : ''}${disabledTabs.has(tab) ? ' is-disabled' : ''}`} <button
onClick={() => { key={tab}
if (disabledTabs.has(tab)) return; className={`kgone-tab${activeTab === tab ? ' active' : ''}${isDisabled ? ' is-disabled' : ''}`}
setActiveTab(tab); onClick={() => {
}} if (isDisabled) return;
disabled={disabledTabs.has(tab)} setActiveTab(tab);
> }}
{formatKGOneTabLabel(tab)} disabled={isDisabled}
</button> style={isDisabled ? { width: '100%' } : undefined}
))} >
{formatKGOneTabLabel(tab)}
</button>
);
return isDisabled ? (
<span
key={tab}
title="Requires K.G.One Music Studio server integration"
style={{ flex: 1 }}
>
{button}
</span>
) : button;
})}
</div> </div>
<div className="kgone-panel-body"> <div className="kgone-panel-body">
+24
View File
@@ -122,6 +122,30 @@ const Toolbar: React.FC = () => {
return; return;
} }
// Untitled Project is ephemeral — no existing save to preserve, so rename directly
if (savedProjectName === RESERVED_PROJECT_NAME) {
const storage = KGProjectStorage.getInstance();
const exists = await storage.exists(newName);
if (exists) {
const confirmed = await showConfirm(
`Project "${newName}" already exists. Do you want to overwrite it?`
);
if (!confirmed) return;
setProjectName(newName);
await saveProject(newName, savedProjectName, setStatus, (finalName) => {
setSavedProjectName(finalName);
if (finalName !== newName) setProjectName(finalName);
}, true /* forceOverwrite */);
return;
}
setProjectName(newName);
await saveProject(newName, savedProjectName, setStatus, (finalName) => {
setSavedProjectName(finalName);
if (finalName !== newName) setProjectName(finalName);
});
return;
}
// Ask whether the user wants to rename or save as a copy // Ask whether the user wants to rename or save as a copy
const choice = await showChoice( const choice = await showChoice(
"Would you like to rename this project, or save it as a new copy?", "Would you like to rename this project, or save it as a new copy?",
@@ -591,7 +591,7 @@ const GeneralSettings: React.FC = () => {
disabled={kgoneServerManaged} disabled={kgoneServerManaged}
/> />
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}> <div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
Base URL of a running K.G.One server. Used for full-song generation, clip generation, and stem separation. Base URL of a running K.G.One Music Studio server. Used for full-song generation, clip generation, and stem separation.
</div> </div>
</div> </div>
</div> </div>
+4 -2
View File
@@ -45,6 +45,7 @@ interface RegionItemProps {
audioBuffer?: AudioBuffer; audioBuffer?: AudioBuffer;
previewWaveformPeaks?: AudioRecordingPeak[]; previewWaveformPeaks?: AudioRecordingPeak[];
isPreview?: boolean; isPreview?: boolean;
isAudioRegion?: boolean;
} }
const RegionItem: React.FC<RegionItemProps> = ({ const RegionItem: React.FC<RegionItemProps> = ({
@@ -71,6 +72,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
audioBuffer, audioBuffer,
previewWaveformPeaks, previewWaveformPeaks,
isPreview = false, isPreview = false,
isAudioRegion = false,
}) => { }) => {
// Get selection state and time signature from store // Get selection state and time signature from store
const { selectedRegionIds, timeSignature, bpm } = useProjectStore(); const { selectedRegionIds, timeSignature, bpm } = useProjectStore();
@@ -652,7 +654,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
return ( return (
<div <div
key={id} key={id}
className={`track-region ${isDragging ? 'dragging' : ''} ${isSelected ? (isPrimarySelected ? 'selected' : 'selected-secondary') : ''} ${audioRegion ? 'audio-region' : ''}`} className={`track-region ${isDragging ? 'dragging' : ''} ${isSelected ? (isPrimarySelected ? 'selected' : 'selected-secondary') : ''} ${(audioRegion || isAudioRegion) ? 'audio-region' : ''}`}
style={{ ...style, cursor: isPreview ? 'default' : cursor, ...(isFineDragging ? { transform: `translateX(${fineTranslateX}px)`, zIndex: 100 } : {}) }} style={{ ...style, cursor: isPreview ? 'default' : cursor, ...(isFineDragging ? { transform: `translateX(${fineTranslateX}px)`, zIndex: 100 } : {}) }}
onMouseMove={isPreview ? undefined : handleMouseMove} onMouseMove={isPreview ? undefined : handleMouseMove}
onMouseLeave={isPreview ? undefined : handleMouseLeave} onMouseLeave={isPreview ? undefined : handleMouseLeave}
@@ -666,7 +668,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
<div className="region-header"> <div className="region-header">
{name} {name}
</div> </div>
<div className={`region-content${audioRegion ? ' audio-region-content' : ''}`} ref={regionContentRef}> <div className={`region-content${(audioRegion || isAudioRegion) ? ' audio-region-content' : ''}`} ref={regionContentRef}>
{!isPreview && <div className="region-left-buttons"> {!isPreview && <div className="region-left-buttons">
{!audioRegion && ( {!audioRegion && (
<button <button
+1
View File
@@ -654,6 +654,7 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
trackIndex={index} trackIndex={index}
previewWaveformPeaks={recordingAudioPreviewPeaks} previewWaveformPeaks={recordingAudioPreviewPeaks}
isPreview isPreview
isAudioRegion
/> />
)} )}
{isAutomationActive && activeTrackAutomationType && ( {isAutomationActive && activeTrackAutomationType && (
+3 -2
View File
@@ -4,6 +4,7 @@ import { KGMidiTrack } from './track/KGMidiTrack';
import { KGAudioTrack } from './track/KGAudioTrack'; import { KGAudioTrack } from './track/KGAudioTrack';
import { type TimeSignature, WithDefault } from '../types/projectTypes'; import { type TimeSignature, WithDefault } from '../types/projectTypes';
import { TIME_CONSTANTS, KEY_SIGNATURE_MAP } from '../constants/coreConstants'; import { TIME_CONSTANTS, KEY_SIGNATURE_MAP } from '../constants/coreConstants';
import { RESERVED_PROJECT_NAME } from '../util/projectNameUtil';
// Type for valid key signatures // Type for valid key signatures
export type KeySignature = keyof typeof KEY_SIGNATURE_MAP; export type KeySignature = keyof typeof KEY_SIGNATURE_MAP;
@@ -14,7 +15,7 @@ export type KeySignature = keyof typeof KEY_SIGNATURE_MAP;
*/ */
export class KGProject { export class KGProject {
@Expose() @Expose()
private name: string = "Untitled Project"; private name: string = RESERVED_PROJECT_NAME;
@Expose() @Expose()
private maxBars: number = 32; private maxBars: number = 32;
@@ -68,7 +69,7 @@ export class KGProject {
private tracks: KGTrack[] = []; private tracks: KGTrack[] = [];
// Constructor // Constructor
constructor(name: string = "Untitled Project", maxBars: number = 32, currentBars: number = 0, bpm: number = 125, timeSignature: TimeSignature = { numerator: 4, denominator: 4 }, keySignature: KeySignature = "C major", selectedMode: string = "ionian", isLooping: boolean = false, loopingRange: [number, number] = [0, 0], barWidthMultiplier: number = 1, tracks: KGTrack[] = [], projectStructureVersion: number = KGProject.CURRENT_PROJECT_STRUCTURE_VERSION) { constructor(name: string = RESERVED_PROJECT_NAME, maxBars: number = 32, currentBars: number = 0, bpm: number = 125, timeSignature: TimeSignature = { numerator: 4, denominator: 4 }, keySignature: KeySignature = "C major", selectedMode: string = "ionian", isLooping: boolean = false, loopingRange: [number, number] = [0, 0], barWidthMultiplier: number = 1, tracks: KGTrack[] = [], projectStructureVersion: number = KGProject.CURRENT_PROJECT_STRUCTURE_VERSION) {
this.name = name; this.name = name;
this.maxBars = maxBars; this.maxBars = maxBars;
this.currentBars = currentBars; this.currentBars = currentBars;