Merge pull request #1965 from debpalash/fix/1856-dictation-step

fix(setup): stop the last onboarding step failing three times with no model
This commit is contained in:
Palash Debnath
2026-09-09 14:45:32 -07:00
committed by GitHub
3 changed files with 83 additions and 1 deletions
+1
View File
@@ -9,6 +9,7 @@ the frozen-backend fallback mirror it for their toolchains.
## [Unreleased]
**Highlights**
- The last onboarding step offers to install a speech-to-text model instead of failing three times when none is installed (#1856)
- A download that fails because the folder sits behind a mount point Windows will not cross now says so, and where to move it (#1957)
- A GPU that is merely short on free memory is no longer told to reinstall its drivers (#1812) — thanks @michaelhuamanflores!
- An error thrown by a browser extension is filtered on Safari and the macOS app too, not only on Chromium (#1901) — thanks @Chang-Jin-Lee!
+25 -1
View File
@@ -26,6 +26,8 @@ import { useTranslation } from 'react-i18next';
import { API, apiFetch } from '../api/client';
import { asrMissingPayload, toastAsrModelMissing } from '../utils/asrModelMissing';
import { useEffectiveDictationShortcut } from '../hooks/useEffectiveDictationShortcut';
import { useDictationReadiness } from '../hooks/useDictationReadiness';
import AsrModelChooser from './AsrModelChooser';
import { Button } from '../ui';
// Shared status-pill base; per-state color/bg/border appended below. The gruvbox
@@ -66,6 +68,12 @@ export default function DictationDemo({ embedded = false }) {
const [hotkeyState, setHotkeyState] = useState('unknown'); // unknown | registered | verified
const desktop = isTauri();
const { info: shortcut } = useEffectiveDictationShortcut(desktop);
// Transcribing a sample needs a speech-to-text model, and the mandatory-only
// install path ships none. Rendering the cards regardless meant the final
// onboarding step opened with one hard error per card and invited the user
// to press a hotkey that could not work (#1856). Probed for the same reason
// the demo already probes for its sample WAVs below.
const readiness = useDictationReadiness();
const [playingId, setPlayingId] = useState(null);
const [transcripts, setTranscripts] = useState({}); // {scriptId: {state, text, error}}
// null = probing, true/false once the demo assets are confirmed present.
@@ -222,7 +230,11 @@ export default function DictationDemo({ embedded = false }) {
// depend on the bundled WAVs, which installs don't always ship. Hiding
// the whole panel left the wizard's "Try dictation" act completely
// blank on every such install (#119/#124 follow-up, refined).
const showScripts = assetsAvailable !== false;
// `checking` still shows the cards: the probe resolves in well under a
// second and flashing the install panel first would be worse than a brief
// wait. Only a confirmed-missing model swaps them out.
const asrMissing = readiness.phase === 'missing';
const showScripts = assetsAvailable !== false && !asrMissing;
return (
<section
@@ -250,6 +262,18 @@ export default function DictationDemo({ embedded = false }) {
<audio ref={audioRef} onEnded={() => setPlayingId(null)} preload="none" />
{asrMissing && (
<div className="flex flex-col gap-2 rounded-[8px] border border-border bg-[rgba(0,0,0,0.15)] px-[12px] py-[10px]">
<p className="m-0 text-[11px] leading-[1.45] text-fg-muted">{t('asr_missing.message')}</p>
<AsrModelChooser
fallback={readiness.missing?.recommended}
onInstall={readiness.install}
onSelect={readiness.select}
disabled={readiness.phase === 'installing'}
/>
</div>
)}
{showScripts && (
<div className="dictation-demo__scripts grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-[10px]">
{SCRIPTS.map((s) => {
+57
View File
@@ -4,6 +4,24 @@ import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import i18n from '../i18n';
const { readiness, apiJson } = vi.hoisted(() => ({
readiness: {
phase: 'ready',
missing: null,
check: vi.fn().mockResolvedValue(true),
install: vi.fn(),
select: vi.fn(),
},
apiJson: vi.fn(),
}));
vi.mock('../hooks/useDictationReadiness', () => ({
useDictationReadiness: () => readiness,
}));
vi.mock('../api/client', async (importOriginal) => ({
...(await importOriginal()),
apiJson: (...a) => apiJson(...a),
}));
import DictationDemo from '../components/DictationDemo';
function withI18n(node) {
@@ -15,6 +33,8 @@ describe('DictationDemo', () => {
beforeEach(() => {
originalFetch = global.fetch;
readiness.phase = 'ready';
readiness.missing = null;
});
afterEach(() => {
@@ -92,4 +112,41 @@ describe('DictationDemo', () => {
const transcribeCall = calls.find((c) => String(c[0]).endsWith('/transcribe'));
expect(transcribeCall[1]?.method).toBe('POST');
});
// #1856: the mandatory-only install path ships no speech-to-text model, so
// the final onboarding step opened with one hard error per script card and
// told the user to press a hotkey that could not transcribe anything. The
// step now offers the models instead of failing three times.
it('offers the models instead of failing every card when none is installed', async () => {
readiness.phase = 'missing';
readiness.missing = { recommended: { repo_id: 'r/tiny', label: 'Tiny', size_gb: 0.1 } };
apiJson.mockResolvedValue({
models: [
{
id: 'sherpa-whisper-tiny',
repo_id: 'r/tiny',
label: 'Tiny',
tag: 'offline',
recommended: true,
size_gb: 0.1,
languages: '90+ languages',
installed: false,
},
],
});
render(withI18n(<DictationDemo />));
expect(await screen.findByTestId('asr-model-chooser')).toBeInTheDocument();
expect(screen.queryByText(/Schedule a meeting with Pat/)).not.toBeInTheDocument();
});
it('keeps the script cards while readiness is still being probed', () => {
readiness.phase = 'checking';
readiness.missing = null;
render(withI18n(<DictationDemo />));
// A sub-second probe must not flash the install panel over the cards.
expect(screen.getByText(/Schedule a meeting with Pat/)).toBeInTheDocument();
expect(screen.queryByTestId('asr-model-chooser')).not.toBeInTheDocument();
});
});