-
Notifications
You must be signed in to change notification settings - Fork 0
feat(workspace): start analysis from the first-run card #974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
822a445
a80d79e
5cd3e9f
2f222f4
53c42c6
ecd93ef
91036ea
2211502
fd33104
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,7 +46,8 @@ import { | |
| import { createTranslator, detectPreferredLocale, type TranslationKey } from "./i18n"; | ||
| import { ScoreView } from "./features/score/ScoreView"; | ||
| import { Workspace } from "./features/workspace/Workspace"; | ||
| import { EmptyState, ErrorState, LoadingState } from "./features/workspace/WorkspaceStates"; | ||
| import { EmptyState, ErrorState, FirstRunState, LoadingState } from "./features/workspace/WorkspaceStates"; | ||
| import { roleFocusForFirstRun, type FirstRunRoleId } from "./features/workspace/firstRunRoles"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { Input } from "@/components/ui/input"; | ||
| import { Progress } from "@/components/ui/progress"; | ||
|
|
@@ -258,6 +259,7 @@ export function App() { | |
| const [renderedProgressPercent, setRenderedProgressPercent] = useState<number | undefined>(undefined); | ||
| const [isStarting, setIsStarting] = useState(false); | ||
| const [selectedBootstrap, setSelectedBootstrap] = useState<ProjectBootstrapSummary | null>(null); | ||
| const [firstRunRoleId, setFirstRunRoleId] = useState<FirstRunRoleId>("whole-band"); | ||
| const [activeAnalysisBootstrap, setActiveAnalysisBootstrap] = useState<ProjectBootstrapSummary | null>(null); | ||
| const [selectionError, setSelectionError] = useState<string | null>(null); | ||
| const [selectionErrorSource, setSelectionErrorSource] = useState<"local" | "youtube" | null>(null); | ||
|
|
@@ -273,7 +275,7 @@ export function App() { | |
| sourceKind: "local_audio", | ||
| projectId: selectedBootstrap.projectId, | ||
| sourceLabel: selectedBootstrap.source.fileName, | ||
| roleFocus: defaultRequest.roleFocus | ||
| roleFocus: roleFocusForFirstRun(firstRunRoleId) | ||
| } | ||
| : defaultRequest; | ||
|
|
||
|
|
@@ -420,6 +422,7 @@ export function App() { | |
| const selection = await selectLocalAudioSource(); | ||
| if (selection.ok) { | ||
| setSelectedBootstrap(selection.bootstrap); | ||
| setFirstRunRoleId("whole-band"); | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -451,6 +454,7 @@ export function App() { | |
| const selection = await importYoutubeUrl(normalizedUrl); | ||
| if (selection.ok) { | ||
| setSelectedBootstrap(selection.bootstrap); | ||
| setFirstRunRoleId("whole-band"); | ||
| setYoutubeUrl(""); | ||
| } else { | ||
| setSelectionError(safeErrorDetail(selection.error.message, t("youtubeImportFailed"))); | ||
|
|
@@ -514,6 +518,22 @@ export function App() { | |
| if (jobResult) { | ||
| return <Workspace song={jobResult} sourceBootstrap={jobResultBootstrap} onSongUpdate={handleSongUpdate} />; | ||
| } | ||
| if (selectedBootstrap) { | ||
| return ( | ||
| <FirstRunState | ||
| fileName={selectedBootstrap.source.fileName} | ||
| selectedRoleId={firstRunRoleId} | ||
| onSelectRole={setFirstRunRoleId} | ||
| onStartAnalysis={() => { | ||
| void handleStartAnalysis(); | ||
| }} | ||
| onChooseDifferentFile={() => { | ||
| void handleChooseLocalAudio(); | ||
| }} | ||
|
Comment on lines
+530
to
+532
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Cancelling 'Choose a different file' discards the selected song Opening the file picker from the card's "Choose a different file" button and then cancelling runs Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Revalidated on current live heads. This finding is still valid on #974 exact |
||
| analysisDisabled={analysisInFlight || isStarting || isImporting} | ||
|
|
||
| /> | ||
| ); | ||
| } | ||
| return <EmptyState />; | ||
| }; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { EmptyState, FirstRunState, LoadingState } from "./WorkspaceStates"; | ||
|
|
||
| const originalLanguage = window.navigator.language; | ||
|
|
||
| function setNavigatorLanguage(language: string) { | ||
| Object.defineProperty(window.navigator, "language", { | ||
| configurable: true, | ||
| value: language | ||
| }); | ||
| } | ||
|
|
||
| describe("WorkspaceStates first-run card", () => { | ||
| afterEach(() => { | ||
| setNavigatorLanguage(originalLanguage); | ||
| }); | ||
|
|
||
| it("keeps the empty card text-only until a song is selected", () => { | ||
| render(<EmptyState />); | ||
| expect(screen.getByRole("heading", { name: "Ready to Analyze" })).toBeTruthy(); | ||
| expect(screen.queryByRole("button", { name: "Analyze this song" })).toBeNull(); | ||
| }); | ||
|
|
||
| it("names analyze as the next action after a song is selected", () => { | ||
| const onSelectRole = vi.fn(); | ||
| const onStartAnalysis = vi.fn(); | ||
| const onChooseDifferentFile = vi.fn(); | ||
| render( | ||
| <FirstRunState | ||
| fileName="rehearsal-take.wav" | ||
| selectedRoleId="whole-band" | ||
| onSelectRole={onSelectRole} | ||
| onStartAnalysis={onStartAnalysis} | ||
| onChooseDifferentFile={onChooseDifferentFile} | ||
| /> | ||
| ); | ||
|
|
||
| fireEvent.click(screen.getByRole("radio", { name: "Lead vocal" })); | ||
| fireEvent.click(screen.getByRole("button", { name: "Analyze this song" })); | ||
| fireEvent.click(screen.getByRole("button", { name: "Choose a different file" })); | ||
|
|
||
| expect(onSelectRole).toHaveBeenCalledWith("lead-vocal"); | ||
| expect(onStartAnalysis).toHaveBeenCalledTimes(1); | ||
| expect(onChooseDifferentFile).toHaveBeenCalledTimes(1); | ||
| expect(screen.queryByText("/Users/test/Music/rehearsal-take.wav")).toBeNull(); | ||
| }); | ||
|
|
||
| it("shows the selected song basename without exposing local path segments", () => { | ||
| render( | ||
| <FirstRunState | ||
| fileName="/Users/test/Music/late-night-set.wav" | ||
| selectedRoleId="bass-guitar" | ||
| onSelectRole={vi.fn()} | ||
| onStartAnalysis={vi.fn()} | ||
| onChooseDifferentFile={vi.fn()} | ||
| /> | ||
| ); | ||
|
|
||
| expect(screen.queryByText(/\/Users\/test/)).toBeNull(); | ||
| expect(screen.getByText("Selected song")).toBeTruthy(); | ||
| expect(screen.getByText("late-night-set.wav")).toBeTruthy(); | ||
| expect(document.querySelector('[data-selected-audio="late-night-set.wav"]')).toBeTruthy(); | ||
| }); | ||
|
|
||
| it("uses roving focus and arrow keys for the role radiogroup", () => { | ||
| const onSelectRole = vi.fn(); | ||
| render( | ||
| <FirstRunState | ||
| fileName="rehearsal-take.wav" | ||
| selectedRoleId="whole-band" | ||
| onSelectRole={onSelectRole} | ||
| onStartAnalysis={vi.fn()} | ||
| onChooseDifferentFile={vi.fn()} | ||
| /> | ||
| ); | ||
|
|
||
| const wholeBand = screen.getByRole("radio", { name: "Whole band" }); | ||
| const leadVocal = screen.getByRole("radio", { name: "Lead vocal" }); | ||
| expect(wholeBand).toHaveAttribute("tabindex", "0"); | ||
| expect(leadVocal).toHaveAttribute("tabindex", "-1"); | ||
|
|
||
| wholeBand.focus(); | ||
| fireEvent.keyDown(wholeBand, { key: "ArrowRight" }); | ||
|
|
||
| expect(onSelectRole).toHaveBeenCalledWith("lead-vocal"); | ||
| expect(document.activeElement).toBe(leadVocal); | ||
| }); | ||
|
|
||
| it("localizes the first-run next-action copy", () => { | ||
| setNavigatorLanguage("ko-KR"); | ||
| render( | ||
| <> | ||
| <EmptyState /> | ||
| <LoadingState /> | ||
| <FirstRunState | ||
| fileName="rehearsal-take.wav" | ||
| selectedRoleId="whole-band" | ||
| onSelectRole={vi.fn()} | ||
| onStartAnalysis={vi.fn()} | ||
| onChooseDifferentFile={vi.fn()} | ||
| /> | ||
| </> | ||
| ); | ||
|
|
||
| expect(screen.getByRole("heading", { name: "분석 준비 완료" })).toBeTruthy(); | ||
| expect(screen.getByRole("heading", { name: "오디오 분석 중" })).toBeTruthy(); | ||
| expect(screen.getByRole("heading", { name: "오늘 합주할 곡이 준비됐어요" })).toBeTruthy(); | ||
| expect(screen.getByRole("button", { name: "이 곡 분석하기" })).toBeTruthy(); | ||
| expect(screen.getByRole("button", { name: "다른 파일 선택" })).toBeTruthy(); | ||
| expect(screen.getByRole("radio", { name: "리드 보컬" })).toBeTruthy(); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.