Skip to content

Commit 3ed424c

Browse files
committed
feat(dictation): allow cancel while transcription is processing
1 parent da5624b commit 3ed424c

7 files changed

Lines changed: 114 additions & 19 deletions

File tree

src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ function MainApp() {
195195
dictationHint,
196196
dictationReady,
197197
handleToggleDictation,
198+
cancelDictation,
198199
clearDictationTranscript,
199200
clearDictationError,
200201
clearDictationHint,
@@ -2435,6 +2436,7 @@ function MainApp() {
24352436
dictationState,
24362437
dictationLevel,
24372438
onToggleDictation: handleToggleDictation,
2439+
onCancelDictation: cancelDictation,
24382440
dictationTranscript,
24392441
onDictationTranscriptHandled: (id) => {
24402442
clearDictationTranscript(id);
@@ -2540,6 +2542,7 @@ function MainApp() {
25402542
dictationState={dictationState}
25412543
dictationLevel={dictationLevel}
25422544
onToggleDictation={handleToggleDictation}
2545+
onCancelDictation={cancelDictation}
25432546
onOpenDictationSettings={() => openSettings("dictation")}
25442547
dictationError={dictationError}
25452548
onDismissDictationError={clearDictationError}

src/features/composer/components/Composer.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ type ComposerProps = {
106106
dictationState?: "idle" | "listening" | "processing";
107107
dictationLevel?: number;
108108
onToggleDictation?: () => void;
109+
onCancelDictation?: () => void;
109110
onOpenDictationSettings?: () => void;
110111
dictationTranscript?: DictationTranscript | null;
111112
onDictationTranscriptHandled?: (id: string) => void;
@@ -214,6 +215,7 @@ export const Composer = memo(function Composer({
214215
dictationState = "idle",
215216
dictationLevel = 0,
216217
onToggleDictation,
218+
onCancelDictation,
217219
onOpenDictationSettings,
218220
dictationTranscript = null,
219221
onDictationTranscriptHandled,
@@ -695,6 +697,7 @@ export const Composer = memo(function Composer({
695697
dictationState={dictationState}
696698
dictationLevel={dictationLevel}
697699
onToggleDictation={onToggleDictation}
700+
onCancelDictation={onCancelDictation}
698701
onOpenDictationSettings={onOpenDictationSettings}
699702
dictationError={dictationError}
700703
onDismissDictationError={onDismissDictationError}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/** @vitest-environment jsdom */
2+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3+
import { createRef } from "react";
4+
import { afterEach, describe, expect, it, vi } from "vitest";
5+
import { ComposerInput } from "./ComposerInput";
6+
7+
vi.mock("../../../services/dragDrop", () => ({
8+
subscribeWindowDragDrop: vi.fn(() => () => {}),
9+
}));
10+
11+
vi.mock("@tauri-apps/api/core", () => ({
12+
convertFileSrc: (path: string) => `tauri://${path}`,
13+
}));
14+
15+
afterEach(() => {
16+
cleanup();
17+
});
18+
19+
describe("ComposerInput dictation controls", () => {
20+
it("uses the mic control to cancel transcription while processing", () => {
21+
const onToggleDictation = vi.fn();
22+
const onCancelDictation = vi.fn();
23+
const onOpenDictationSettings = vi.fn();
24+
render(
25+
<ComposerInput
26+
text=""
27+
disabled={false}
28+
sendLabel="Send"
29+
canStop={false}
30+
canSend={false}
31+
isProcessing={false}
32+
onStop={() => {}}
33+
onSend={() => {}}
34+
dictationState="processing"
35+
dictationEnabled={true}
36+
onToggleDictation={onToggleDictation}
37+
onCancelDictation={onCancelDictation}
38+
onOpenDictationSettings={onOpenDictationSettings}
39+
onTextChange={() => {}}
40+
onSelectionChange={() => {}}
41+
onKeyDown={() => {}}
42+
textareaRef={createRef<HTMLTextAreaElement>()}
43+
suggestionsOpen={false}
44+
suggestions={[]}
45+
highlightIndex={0}
46+
onHighlightIndex={() => {}}
47+
onSelectSuggestion={() => {}}
48+
/>,
49+
);
50+
51+
const cancelButton = screen.getByRole("button", {
52+
name: "Cancel transcription",
53+
});
54+
fireEvent.click(cancelButton);
55+
56+
expect(onCancelDictation).toHaveBeenCalledTimes(1);
57+
expect(onToggleDictation).not.toHaveBeenCalled();
58+
expect(onOpenDictationSettings).not.toHaveBeenCalled();
59+
});
60+
});

src/features/composer/components/ComposerInput.tsx

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import ChevronDown from "lucide-react/dist/esm/icons/chevron-down";
1212
import ChevronUp from "lucide-react/dist/esm/icons/chevron-up";
1313
import Mic from "lucide-react/dist/esm/icons/mic";
1414
import Square from "lucide-react/dist/esm/icons/square";
15+
import X from "lucide-react/dist/esm/icons/x";
1516
import Brain from "lucide-react/dist/esm/icons/brain";
1617
import GitFork from "lucide-react/dist/esm/icons/git-fork";
1718
import PlusCircle from "lucide-react/dist/esm/icons/plus-circle";
@@ -46,6 +47,7 @@ type ComposerInputProps = {
4647
dictationLevel?: number;
4748
dictationEnabled?: boolean;
4849
onToggleDictation?: () => void;
50+
onCancelDictation?: () => void;
4951
onOpenDictationSettings?: () => void;
5052
dictationError?: string | null;
5153
onDismissDictationError?: () => void;
@@ -148,6 +150,7 @@ export function ComposerInput({
148150
dictationLevel = 0,
149151
dictationEnabled = false,
150152
onToggleDictation,
153+
onCancelDictation,
151154
onOpenDictationSettings,
152155
dictationError = null,
153156
onDismissDictationError,
@@ -331,27 +334,39 @@ export function ComposerInput({
331334
}
332335
}, [canStop, onSend, onStop]);
333336
const isDictating = dictationState === "listening";
337+
const isDictationProcessing = dictationState === "processing";
334338
const isDictationBusy = dictationState !== "idle";
335339
const allowOpenDictationSettings = Boolean(
336-
onOpenDictationSettings && !dictationEnabled && !disabled,
340+
onOpenDictationSettings && !dictationEnabled && !disabled && !isDictationProcessing,
337341
);
338342
const micDisabled =
339-
disabled || dictationState === "processing" || !dictationEnabled || !onToggleDictation;
343+
disabled ||
344+
(!allowOpenDictationSettings &&
345+
(isDictationProcessing
346+
? !onCancelDictation
347+
: !dictationEnabled || !onToggleDictation));
340348
const micAriaLabel = allowOpenDictationSettings
341349
? "Open dictation settings"
342-
: dictationState === "processing"
343-
? "Dictation processing"
350+
: isDictationProcessing
351+
? "Cancel transcription"
344352
: isDictating
345353
? "Stop dictation"
346354
: "Start dictation";
347355
const micTitle = allowOpenDictationSettings
348356
? "Dictation disabled. Open settings"
349-
: dictationState === "processing"
350-
? "Processing dictation"
357+
: isDictationProcessing
358+
? "Cancel transcription"
351359
: isDictating
352360
? "Stop dictation"
353361
: "Start dictation";
354362
const handleMicClick = useCallback(() => {
363+
if (isDictationProcessing) {
364+
if (disabled || !onCancelDictation) {
365+
return;
366+
}
367+
onCancelDictation();
368+
return;
369+
}
355370
if (allowOpenDictationSettings) {
356371
onOpenDictationSettings?.();
357372
return;
@@ -361,7 +376,10 @@ export function ComposerInput({
361376
}
362377
onToggleDictation();
363378
}, [
379+
disabled,
380+
isDictationProcessing,
364381
allowOpenDictationSettings,
382+
onCancelDictation,
365383
micDisabled,
366384
onOpenDictationSettings,
367385
onToggleDictation,
@@ -478,15 +496,19 @@ export function ComposerInput({
478496
{isExpanded ? "Collapse input" : "Expand input"}
479497
</PopoverMenuItem>
480498
)}
481-
{(onToggleDictation || onOpenDictationSettings) && (
499+
{(onToggleDictation || onOpenDictationSettings || onCancelDictation) && (
482500
<PopoverMenuItem
483501
onClick={handleMobileDictationClick}
484-
disabled={
485-
disabled ||
486-
dictationState === "processing" ||
487-
(!onToggleDictation && !allowOpenDictationSettings)
502+
disabled={micDisabled}
503+
icon={
504+
isDictationProcessing ? (
505+
<X size={14} />
506+
) : isDictating ? (
507+
<Square size={14} />
508+
) : (
509+
<Mic size={14} />
510+
)
488511
}
489-
icon={isDictating ? <Square size={14} /> : <Mic size={14} />}
490512
>
491513
{micAriaLabel}
492514
</PopoverMenuItem>
@@ -685,19 +707,21 @@ export function ComposerInput({
685707
<button
686708
className={`composer-action composer-action--mic${
687709
isDictationBusy ? " is-active" : ""
688-
}${dictationState === "processing" ? " is-processing" : ""}${
710+
}${isDictationProcessing ? " is-processing is-stop" : ""}${
689711
micDisabled ? " is-disabled" : ""
690712
}`}
691713
onClick={handleMicClick}
692-
disabled={
693-
disabled ||
694-
dictationState === "processing" ||
695-
(!onToggleDictation && !allowOpenDictationSettings)
696-
}
714+
disabled={micDisabled}
697715
aria-label={micAriaLabel}
698716
title={micTitle}
699717
>
700-
{isDictating ? <Square aria-hidden /> : <Mic aria-hidden />}
718+
{isDictationProcessing ? (
719+
<X aria-hidden />
720+
) : isDictating ? (
721+
<Square aria-hidden />
722+
) : (
723+
<Mic aria-hidden />
724+
)}
701725
</button>
702726
<button
703727
className={`composer-action${canStop ? " is-stop" : " is-send"}${

src/features/layout/hooks/layoutNodes/buildPrimaryNodes.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export function buildPrimaryNodes(options: LayoutNodesOptions): PrimaryLayoutNod
178178
dictationState={options.dictationState}
179179
dictationLevel={options.dictationLevel}
180180
onToggleDictation={options.onToggleDictation}
181+
onCancelDictation={options.onCancelDictation}
181182
onOpenDictationSettings={options.onOpenDictationSettings}
182183
dictationTranscript={options.dictationTranscript}
183184
onDictationTranscriptHandled={options.onDictationTranscriptHandled}

src/features/layout/hooks/layoutNodes/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ export type LayoutNodesOptions = {
467467
dictationState: DictationSessionState;
468468
dictationLevel: number;
469469
onToggleDictation: () => void;
470+
onCancelDictation?: () => void;
470471
dictationTranscript: DictationTranscript | null;
471472
onDictationTranscriptHandled: (id: string) => void;
472473
dictationError: string | null;

src/features/workspaces/components/WorkspaceHome.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ type WorkspaceHomeProps = {
7676
dictationState: DictationSessionState;
7777
dictationLevel: number;
7878
onToggleDictation: () => void;
79+
onCancelDictation?: () => void;
7980
onOpenDictationSettings: () => void;
8081
dictationError: string | null;
8182
onDismissDictationError: () => void;
@@ -138,6 +139,7 @@ export function WorkspaceHome({
138139
dictationState,
139140
dictationLevel,
140141
onToggleDictation,
142+
onCancelDictation,
141143
onOpenDictationSettings,
142144
dictationError,
143145
onDismissDictationError,
@@ -386,6 +388,7 @@ export function WorkspaceHome({
386388
dictationLevel={dictationLevel}
387389
dictationEnabled={dictationEnabled}
388390
onToggleDictation={onToggleDictation}
391+
onCancelDictation={onCancelDictation}
389392
onOpenDictationSettings={onOpenDictationSettings}
390393
dictationError={dictationError}
391394
onDismissDictationError={onDismissDictationError}

0 commit comments

Comments
 (0)