fix: minor UI updates

This commit is contained in:
Xiaohan-Tian
2026-05-27 22:32:54 -07:00
parent 2140e944f2
commit e95ff72d2a
3 changed files with 156 additions and 12 deletions
+23
View File
@@ -153,6 +153,29 @@
gap: 16px; gap: 16px;
} }
.dialog-hint-card {
margin-top: 12px;
background-color: #252525;
border: 1px solid #3a3a3a;
border-radius: 6px;
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
.dialog-hint-card-title {
color: #e0e0e0;
font-size: 12px;
font-weight: 600;
}
.dialog-hint-card-text {
color: #999;
font-size: 11px;
line-height: 1.45;
}
.dialog-slider-group { .dialog-slider-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+93 -4
View File
@@ -1,8 +1,30 @@
import React from 'react'; import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { describe, expect, it } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
const { mockKgoneEnabled } = vi.hoisted(() => ({
mockKgoneEnabled: { value: false },
}));
vi.mock('../../core/config/ConfigManager', () => ({
ConfigManager: {
instance: () => ({
get: (key: string) => {
if (key === 'general.kgone.enabled') return mockKgoneEnabled.value;
return undefined;
},
}),
},
}));
import DialogProvider from './DialogProvider'; import DialogProvider from './DialogProvider';
import { showChoice, showChordDetectionOptions, showTempoApply, showTempoDetectionOptions } from '../../util/dialogUtil'; import {
showChoice,
showChordDetectionOptions,
showMidiChordDetectionOptions,
showTempoApply,
showTempoDetectionOptions,
} from '../../util/dialogUtil';
function finishDialogCloseAnimation() { function finishDialogCloseAnimation() {
const overlay = document.querySelector('.dialog-overlay'); const overlay = document.querySelector('.dialog-overlay');
@@ -11,6 +33,10 @@ function finishDialogCloseAnimation() {
} }
} }
beforeEach(() => {
mockKgoneEnabled.value = false;
});
describe('DialogProvider chord detection dialog', () => { describe('DialogProvider chord detection dialog', () => {
it('opens the chord detection modal with the expected defaults and resolves cancel to null', async () => { it('opens the chord detection modal with the expected defaults and resolves cancel to null', async () => {
let resolved: unknown = 'pending'; let resolved: unknown = 'pending';
@@ -36,6 +62,10 @@ describe('DialogProvider chord detection dialog', () => {
fireEvent.click(screen.getByRole('button', { name: 'Open' })); fireEvent.click(screen.getByRole('button', { name: 'Open' }));
expect(screen.getByText('Chord Detection')).toBeInTheDocument(); expect(screen.getByText('Chord Detection')).toBeInTheDocument();
expect(screen.getByText('Experimental Feature')).toBeInTheDocument();
expect(screen.getByText(/Chord analysis is still experimental\./)).toBeInTheDocument();
expect(screen.getByText('Recommended Source Material')).toBeInTheDocument();
expect(screen.getByText(/Vocal, Drums, Bass, and Others/)).toBeInTheDocument();
expect(screen.getByLabelText('Sensitivity')).toHaveValue('50'); expect(screen.getByLabelText('Sensitivity')).toHaveValue('50');
expect(screen.getByLabelText('Stability')).toHaveValue('50'); expect(screen.getByLabelText('Stability')).toHaveValue('50');
expect(screen.getByLabelText('No-Chord Threshold')).toHaveValue('0'); expect(screen.getByLabelText('No-Chord Threshold')).toHaveValue('0');
@@ -83,6 +113,61 @@ describe('DialogProvider chord detection dialog', () => {
enableSevenths: true, enableSevenths: true,
})); }));
}); });
it('shows K.G.One separator guidance when server integration is enabled', async () => {
mockKgoneEnabled.value = true;
render(
<DialogProvider>
<button
type="button"
onClick={async () => {
await showChordDetectionOptions('Tune chord detection settings before processing.', {
sensitivity: 50,
stability: 50,
noChordThreshold: 0,
enableSevenths: false,
});
}}
>
Open
</button>
</DialogProvider>,
);
fireEvent.click(screen.getByRole('button', { name: 'Open' }));
expect(screen.getByText(/Vocal, Drums, Bass, Guitar, Piano, and Others/)).toBeInTheDocument();
expect(screen.getByText(/Piano or Others stem/)).toBeInTheDocument();
});
});
describe('DialogProvider MIDI chord detection dialog', () => {
it('shows the experimental hint without audio stem guidance', async () => {
render(
<DialogProvider>
<button
type="button"
onClick={async () => {
await showMidiChordDetectionOptions('Tune MIDI chord detection settings before processing.', {
enableSevenths: false,
shortNoteSuppression: 'medium',
harmonicFocus: 'favor-sustained-notes',
});
}}
>
Open MIDI
</button>
</DialogProvider>,
);
fireEvent.click(screen.getByRole('button', { name: 'Open MIDI' }));
expect(screen.getByText('Chord Detection')).toBeInTheDocument();
expect(screen.getByText('Experimental Feature')).toBeInTheDocument();
expect(screen.getByText(/Voicing density, overlaps, and ornamental notes/)).toBeInTheDocument();
expect(screen.queryByText('Recommended Source Material')).not.toBeInTheDocument();
});
}); });
describe('DialogProvider tempo detection dialog', () => { describe('DialogProvider tempo detection dialog', () => {
@@ -108,8 +193,10 @@ describe('DialogProvider tempo detection dialog', () => {
fireEvent.click(screen.getByRole('button', { name: 'Open tempo' })); fireEvent.click(screen.getByRole('button', { name: 'Open tempo' }));
expect(screen.getByText('Tempo Detection')).toBeInTheDocument(); expect(screen.getByText('Tempo Detection')).toBeInTheDocument();
expect(screen.getByLabelText('Minimum BPM')).toHaveValue(80); expect(screen.getByText('Experimental Feature')).toBeInTheDocument();
expect(screen.getByLabelText('Maximum BPM')).toHaveValue(180); expect(screen.getByText(/Tempo analysis is still experimental\./)).toBeInTheDocument();
expect(screen.getByLabelText('Minimum BPM')).toHaveValue('80');
expect(screen.getByLabelText('Maximum BPM')).toHaveValue('180');
fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
finishDialogCloseAnimation(); finishDialogCloseAnimation();
@@ -139,6 +226,8 @@ describe('DialogProvider tempo detection dialog', () => {
fireEvent.click(screen.getByRole('button', { name: 'Open tempo' })); fireEvent.click(screen.getByRole('button', { name: 'Open tempo' }));
fireEvent.change(screen.getByLabelText('Minimum BPM'), { target: { value: '96' } }); fireEvent.change(screen.getByLabelText('Minimum BPM'), { target: { value: '96' } });
fireEvent.change(screen.getByLabelText('Maximum BPM'), { target: { value: '154' } }); fireEvent.change(screen.getByLabelText('Maximum BPM'), { target: { value: '154' } });
expect(screen.getByText('96')).toBeInTheDocument();
expect(screen.getByText('154')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Detect' })); fireEvent.click(screen.getByRole('button', { name: 'Detect' }));
finishDialogCloseAnimation(); finishDialogCloseAnimation();
+40 -8
View File
@@ -1,6 +1,7 @@
import React, { useState, useCallback, useRef } from 'react'; import React, { useState, useCallback, useRef } from 'react';
import './DialogProvider.css'; import './DialogProvider.css';
import { FaTimes } from 'react-icons/fa'; import { FaTimes } from 'react-icons/fa';
import { ConfigManager } from '../../core/config/ConfigManager';
import { registerDialogFns } from '../../util/dialogUtil'; import { registerDialogFns } from '../../util/dialogUtil';
import type { import type {
ChoiceOption, ChoiceOption,
@@ -43,6 +44,14 @@ const DEFAULT_TEMPO_DETECTION_OPTIONS: TempoDetectionOptionsResult = {
maxTempo: 180, maxTempo: 180,
}; };
const DETECTION_HINT_TITLE = 'Experimental Feature';
const CHORD_DETECTION_HINT_TEXT = 'Chord analysis is still experimental. Harmonic content, arrangement density, and transient-heavy material can affect accuracy. For more reliable results, start with the default settings, then refine sensitivity and stability until the detected harmony best matches the musical phrasing.';
const MIDI_CHORD_DETECTION_HINT_TEXT = 'Chord analysis is still experimental. Voicing density, overlaps, and ornamental notes can influence the result. For more reliable chord labels, begin with the default settings, then adjust note suppression and harmonic focus to match the musical role of the passage.';
const TEMPO_DETECTION_HINT_TEXT = 'Tempo analysis is still experimental. Rubato phrasing, sparse transients, and layered percussion can reduce accuracy. Start with the default BPM range, then narrow the analysis window to the most plausible tempo span for the material if the first pass is not musically convincing.';
const AUDIO_CHORD_SOURCE_HINT_TITLE = 'Recommended Source Material';
const AUDIO_CHORD_SOURCE_HINT_KGONE_TEXT = 'For the most dependable chord labels, analyze a stem with vocals and percussion reduced or removed. If K.G.One Music Studio server integration is available, run Separator with the "Vocal, Drums, Bass, Guitar, Piano, and Others" model and use the Piano or Others stem for analysis.';
const AUDIO_CHORD_SOURCE_HINT_LOCAL_TEXT = 'For the most dependable chord labels, analyze a stem with vocals and percussion reduced or removed. If you are using the local separator, choose the "Vocal, Drums, Bass, and Others" model and use the Others stem for analysis.';
const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [dialog, setDialog] = useState<DialogInfo | null>(null); const [dialog, setDialog] = useState<DialogInfo | null>(null);
const [isClosing, setIsClosing] = useState(false); const [isClosing, setIsClosing] = useState(false);
@@ -193,6 +202,7 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
const isTempoDetection = dialog.type === 'tempo-detection'; const isTempoDetection = dialog.type === 'tempo-detection';
const isTempoApply = dialog.type === 'tempo-apply'; const isTempoApply = dialog.type === 'tempo-apply';
const promptOptions = isPrompt ? (dialog.options as PromptOptions | undefined) : undefined; const promptOptions = isPrompt ? (dialog.options as PromptOptions | undefined) : undefined;
const isKGOneEnabled = (ConfigManager.instance().get('general.kgone.enabled') as boolean | undefined) ?? false;
const title = isAlert const title = isAlert
? 'Notice' ? 'Notice'
@@ -270,6 +280,14 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
setTempoDetectionOptions(current => ({ ...current, [key]: value })); setTempoDetectionOptions(current => ({ ...current, [key]: value }));
}; };
const detectionHintText = isChordDetection
? CHORD_DETECTION_HINT_TEXT
: isMidiChordDetection
? MIDI_CHORD_DETECTION_HINT_TEXT
: isTempoDetection
? TEMPO_DETECTION_HINT_TEXT
: null;
return ( return (
<> <>
{children} {children}
@@ -287,6 +305,12 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
</div> </div>
<div className="dialog-body"> <div className="dialog-body">
<p className="dialog-message">{dialog.message}</p> <p className="dialog-message">{dialog.message}</p>
{detectionHintText && (
<div className="dialog-hint-card">
<div className="dialog-hint-card-title">{DETECTION_HINT_TITLE}</div>
<div className="dialog-hint-card-text">{detectionHintText}</div>
</div>
)}
{isPrompt && ( {isPrompt && (
<input <input
className="dialog-input" className="dialog-input"
@@ -331,6 +355,12 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
)} )}
{isChordDetection && ( {isChordDetection && (
<div className="dialog-chord-detection-form"> <div className="dialog-chord-detection-form">
<div className="dialog-hint-card">
<div className="dialog-hint-card-title">{AUDIO_CHORD_SOURCE_HINT_TITLE}</div>
<div className="dialog-hint-card-text">
{isKGOneEnabled ? AUDIO_CHORD_SOURCE_HINT_KGONE_TEXT : AUDIO_CHORD_SOURCE_HINT_LOCAL_TEXT}
</div>
</div>
<div className="dialog-slider-group"> <div className="dialog-slider-group">
<div className="dialog-slider-header"> <div className="dialog-slider-header">
<label className="dialog-slider-label" htmlFor="dialog-chord-sensitivity">Sensitivity</label> <label className="dialog-slider-label" htmlFor="dialog-chord-sensitivity">Sensitivity</label>
@@ -439,13 +469,14 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
<div className="dialog-slider-group"> <div className="dialog-slider-group">
<div className="dialog-slider-header"> <div className="dialog-slider-header">
<label className="dialog-slider-label" htmlFor="dialog-tempo-min-tempo">Minimum BPM</label> <label className="dialog-slider-label" htmlFor="dialog-tempo-min-tempo">Minimum BPM</label>
<span className="dialog-slider-value">{tempoDetectionOptions.minTempo}</span>
</div> </div>
<input <input
id="dialog-tempo-min-tempo" id="dialog-tempo-min-tempo"
className="dialog-input" className="dialog-slider"
type="number" type="range"
min={40} min={60}
max={240} max={200}
step={1} step={1}
value={tempoDetectionOptions.minTempo} value={tempoDetectionOptions.minTempo}
onChange={(e) => updateTempoDetectionOption('minTempo', Number(e.target.value))} onChange={(e) => updateTempoDetectionOption('minTempo', Number(e.target.value))}
@@ -455,13 +486,14 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
<div className="dialog-slider-group"> <div className="dialog-slider-group">
<div className="dialog-slider-header"> <div className="dialog-slider-header">
<label className="dialog-slider-label" htmlFor="dialog-tempo-max-tempo">Maximum BPM</label> <label className="dialog-slider-label" htmlFor="dialog-tempo-max-tempo">Maximum BPM</label>
<span className="dialog-slider-value">{tempoDetectionOptions.maxTempo}</span>
</div> </div>
<input <input
id="dialog-tempo-max-tempo" id="dialog-tempo-max-tempo"
className="dialog-input" className="dialog-slider"
type="number" type="range"
min={40} min={60}
max={240} max={200}
step={1} step={1}
value={tempoDetectionOptions.maxTempo} value={tempoDetectionOptions.maxTempo}
onChange={(e) => updateTempoDetectionOption('maxTempo', Number(e.target.value))} onChange={(e) => updateTempoDetectionOption('maxTempo', Number(e.target.value))}