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>
29 lines
1.3 KiB
TypeScript
29 lines
1.3 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { render, screen } from "@testing-library/react";
|
|
import { userEvent } from "@testing-library/user-event";
|
|
import { MicButton } from "../MicButton";
|
|
|
|
const props = { enabled: true, supported: true, state: "idle" as const, start: vi.fn(), stop: vi.fn() };
|
|
|
|
describe("MicButton", () => {
|
|
it("renders no shell until voice is enabled and confirmed available", () => {
|
|
const { rerender } = render(<MicButton {...props} enabled={false} />);
|
|
expect(document.querySelector(".btn-icon")).toBeNull();
|
|
expect(screen.queryByLabelText(/voice dictation/i)).toBeNull();
|
|
rerender(<MicButton {...props} supported={false} />);
|
|
expect(document.querySelector(".btn-icon")).toBeNull();
|
|
});
|
|
|
|
it("announces and toggles recording", async () => {
|
|
const user = userEvent.setup();
|
|
const start = vi.fn();
|
|
const stop = vi.fn();
|
|
const { rerender } = render(<MicButton {...props} start={start} stop={stop} />);
|
|
await user.click(screen.getByRole("button", { name: "Start voice dictation" }));
|
|
expect(start).toHaveBeenCalledOnce();
|
|
rerender(<MicButton {...props} state="listening" start={start} stop={stop} />);
|
|
await user.click(screen.getByRole("button", { name: "Stop voice dictation" }));
|
|
expect(stop).toHaveBeenCalledOnce();
|
|
});
|
|
});
|