From e95ff72d2a1ead85c99cc0e68367aa34f73234f6 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Wed, 27 May 2026 22:32:54 -0700 Subject: [PATCH] fix: minor UI updates --- src/components/common/DialogProvider.css | 23 +++++ src/components/common/DialogProvider.test.tsx | 97 ++++++++++++++++++- src/components/common/DialogProvider.tsx | 48 +++++++-- 3 files changed, 156 insertions(+), 12 deletions(-) diff --git a/src/components/common/DialogProvider.css b/src/components/common/DialogProvider.css index 1321ac1..903e79b 100644 --- a/src/components/common/DialogProvider.css +++ b/src/components/common/DialogProvider.css @@ -153,6 +153,29 @@ 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 { display: flex; flex-direction: column; diff --git a/src/components/common/DialogProvider.test.tsx b/src/components/common/DialogProvider.test.tsx index 8d7af6b..4e3a282 100644 --- a/src/components/common/DialogProvider.test.tsx +++ b/src/components/common/DialogProvider.test.tsx @@ -1,8 +1,30 @@ import React from '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 { showChoice, showChordDetectionOptions, showTempoApply, showTempoDetectionOptions } from '../../util/dialogUtil'; +import { + showChoice, + showChordDetectionOptions, + showMidiChordDetectionOptions, + showTempoApply, + showTempoDetectionOptions, +} from '../../util/dialogUtil'; function finishDialogCloseAnimation() { const overlay = document.querySelector('.dialog-overlay'); @@ -11,6 +33,10 @@ function finishDialogCloseAnimation() { } } +beforeEach(() => { + mockKgoneEnabled.value = false; +}); + describe('DialogProvider chord detection dialog', () => { it('opens the chord detection modal with the expected defaults and resolves cancel to null', async () => { let resolved: unknown = 'pending'; @@ -36,6 +62,10 @@ describe('DialogProvider chord detection dialog', () => { fireEvent.click(screen.getByRole('button', { name: 'Open' })); 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('Stability')).toHaveValue('50'); expect(screen.getByLabelText('No-Chord Threshold')).toHaveValue('0'); @@ -83,6 +113,61 @@ describe('DialogProvider chord detection dialog', () => { enableSevenths: true, })); }); + + it('shows K.G.One separator guidance when server integration is enabled', async () => { + mockKgoneEnabled.value = true; + + render( + + + , + ); + + 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( + + + , + ); + + 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', () => { @@ -108,8 +193,10 @@ describe('DialogProvider tempo detection dialog', () => { fireEvent.click(screen.getByRole('button', { name: 'Open tempo' })); expect(screen.getByText('Tempo Detection')).toBeInTheDocument(); - expect(screen.getByLabelText('Minimum BPM')).toHaveValue(80); - expect(screen.getByLabelText('Maximum BPM')).toHaveValue(180); + expect(screen.getByText('Experimental Feature')).toBeInTheDocument(); + 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' })); finishDialogCloseAnimation(); @@ -139,6 +226,8 @@ describe('DialogProvider tempo detection dialog', () => { fireEvent.click(screen.getByRole('button', { name: 'Open tempo' })); fireEvent.change(screen.getByLabelText('Minimum BPM'), { target: { value: '96' } }); 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' })); finishDialogCloseAnimation(); diff --git a/src/components/common/DialogProvider.tsx b/src/components/common/DialogProvider.tsx index 62e9ed2..28edc8f 100644 --- a/src/components/common/DialogProvider.tsx +++ b/src/components/common/DialogProvider.tsx @@ -1,6 +1,7 @@ import React, { useState, useCallback, useRef } from 'react'; import './DialogProvider.css'; import { FaTimes } from 'react-icons/fa'; +import { ConfigManager } from '../../core/config/ConfigManager'; import { registerDialogFns } from '../../util/dialogUtil'; import type { ChoiceOption, @@ -43,6 +44,14 @@ const DEFAULT_TEMPO_DETECTION_OPTIONS: TempoDetectionOptionsResult = { 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 [dialog, setDialog] = useState(null); const [isClosing, setIsClosing] = useState(false); @@ -193,6 +202,7 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) = const isTempoDetection = dialog.type === 'tempo-detection'; const isTempoApply = dialog.type === 'tempo-apply'; const promptOptions = isPrompt ? (dialog.options as PromptOptions | undefined) : undefined; + const isKGOneEnabled = (ConfigManager.instance().get('general.kgone.enabled') as boolean | undefined) ?? false; const title = isAlert ? 'Notice' @@ -270,6 +280,14 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) = 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 ( <> {children} @@ -287,6 +305,12 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =

{dialog.message}

+ {detectionHintText && ( +
+
{DETECTION_HINT_TITLE}
+
{detectionHintText}
+
+ )} {isPrompt && ( = ({ children }) = )} {isChordDetection && (
+
+
{AUDIO_CHORD_SOURCE_HINT_TITLE}
+
+ {isKGOneEnabled ? AUDIO_CHORD_SOURCE_HINT_KGONE_TEXT : AUDIO_CHORD_SOURCE_HINT_LOCAL_TEXT} +
+
@@ -439,13 +469,14 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
+ {tempoDetectionOptions.minTempo}
updateTempoDetectionOption('minTempo', Number(e.target.value))} @@ -455,13 +486,14 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
+ {tempoDetectionOptions.maxTempo}
updateTempoDetectionOption('maxTempo', Number(e.target.value))}