Add shared voice dictation support across chat, planning, and task-creation composers. - Add browser speech-recognition and composer insertion hooks. - Provide a reusable mic button with accessible recording states. - Integrate and test dictation across all supported composer surfaces. - Document the feature and add a package changeset. Files changed: .changeset/fn-8573-voice-dictation-mic.md | 7 + docs/dashboard-guide.md | 6 + packages/dashboard/app/components/ChatView.tsx | 44 +++- .../dashboard/app/components/ComposeChatPanel.tsx | 8 +- packages/dashboard/app/components/MicButton.css | 5 + packages/dashboard/app/components/MicButton.tsx | 20 ++ .../dashboard/app/components/PlanningModeModal.tsx | 82 ++++-- .../dashboard/app/components/QuickEntryBox.tsx | 4 + .../app/components/StandardChatSurface.tsx | 108 +++++--- packages/dashboard/app/components/TaskChatTab.tsx | 4 + packages/dashboard/app/components/TaskComments.tsx | 66 ++--- packages/dashboard/app/components/TaskForm.tsx | 18 +- .../app/components/TaskPlannerChatTab.tsx | 7 + .../app/components/__tests__/MicButton.test.tsx | 28 +++ .../components/__tests__/QuickEntryBox.test.tsx | 5 + .../app/components/__tests__/TaskForm.test.tsx | 5 + .../app/components/__tests__/insertAtCaret.test.ts | 25 ++ .../__tests__/voice-dictation-composers.test.tsx | 230 +++++++++++++++++ packages/dashboard/app/components/insertAtCaret.ts | 20 ++ .../hooks/__tests__/useComposerDictation.test.tsx | 58 +++++ .../app/hooks/__tests__/useVoiceDictation.test.tsx | 158 ++++++++++++ .../dashboard/app/hooks/useComposerDictation.ts | 112 +++++++++ .../dashboard/app/hooks/useVoiceDictation.ts | 277 +++++++++++++++++++++ 23 files changed, 1204 insertions(+), 93 deletions(-) Fusion-Task-Id: FN-8573 Fusion-Task-Lineage: 905ae982-3e63-494f-aba8-e3665f00cdda Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
21 lines
1.6 KiB
TypeScript
21 lines
1.6 KiB
TypeScript
import { Mic, MicOff } from "lucide-react";
|
|
import { useEffect, useRef, useState, type ComponentProps } from "react";
|
|
import "./MicButton.css";
|
|
|
|
type MicButtonProps = Pick<ComponentProps<"button">, "disabled"> & { enabled: boolean; supported: boolean; state: "idle" | "listening" | "transcribing" | "error"; error?: string; start: () => void | Promise<void>; stop: () => void; };
|
|
/** FNXC:VoiceInput 2026-07-24-03:10: Never leave an empty composer shell: availability is unproven until the hook confirms it. */
|
|
export function MicButton({ enabled, supported, state, error, start, stop, disabled }: MicButtonProps) {
|
|
const priorState = useRef(state);
|
|
const [announcement, setAnnouncement] = useState("");
|
|
useEffect(() => {
|
|
if (priorState.current !== state) {
|
|
setAnnouncement(error ?? (state === "error" ? "Voice dictation error" : state === "listening" ? "Voice dictation started" : state === "idle" ? "Voice dictation stopped" : "Voice dictation transcribing"));
|
|
priorState.current = state;
|
|
}
|
|
}, [error, state]);
|
|
if (!enabled || !supported) return null;
|
|
const active = state === "listening" || state === "transcribing";
|
|
const label = state === "error" ? "Voice dictation error" : active ? "Stop voice dictation" : "Start voice dictation";
|
|
return <><button type="button" className={`btn btn-icon mic-button mic-button--${state}`} disabled={disabled} aria-label={label} onClick={() => { if (active) stop(); else void start(); }}>{state === "error" ? <MicOff aria-hidden="true" /> : <Mic aria-hidden="true" />}</button><span className="sr-only" aria-live="polite">{announcement}</span></>;
|
|
}
|