Files
fusion/packages/dashboard/app/components/__tests__/insertAtCaret.test.ts
gsxdsm 32adc0a153 FN-8573: add voice dictation controls to composers
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>
2026-07-25 05:15:02 -07:00

26 lines
1.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { computeInsertion, replaceRange } from "../insertAtCaret";
describe("dictation insertion", () => {
it("inserts at empty, start, middle, and end", () => {
expect(computeInsertion({ value: "", selectionStart: 0, selectionEnd: 0, insertText: "hello" })).toMatchObject({ nextValue: "hello", nextCaret: 5 });
expect(computeInsertion({ value: "world", selectionStart: 0, selectionEnd: 0, insertText: "hello " }).nextValue).toBe("hello world");
expect(computeInsertion({ value: "ab", selectionStart: 1, selectionEnd: 1, insertText: "X" }).nextValue).toBe("aXb");
expect(computeInsertion({ value: "a", selectionStart: 1, selectionEnd: 1, insertText: "b" }).nextValue).toBe("ab");
});
it("replaces active selections and repeated previews", () => {
const first = computeInsertion({ value: "before after", selectionStart: 7, selectionEnd: 12, insertText: "one" });
const second = replaceRange({ value: first.nextValue, anchor: first.anchor, nextText: "two words" });
const final = replaceRange({ value: second.nextValue, anchor: second.anchor, nextText: "final" });
expect(first.nextValue).toBe("before one");
expect(second.nextValue).toBe("before two words");
expect(final.nextValue).toBe("before final");
expect(final.anchor).toEqual({ start: 7, end: 12 });
});
it("preserves intervening text when its caller shifts an anchor for a prior edit", () => {
const first = computeInsertion({ value: "after", selectionStart: 0, selectionEnd: 0, insertText: "partial" });
const shifted = { start: first.anchor.start + 7, end: first.anchor.end + 7 };
expect(replaceRange({ value: "prefix partialafter", anchor: shifted, nextText: "final" }).nextValue).toBe("prefix finalafter");
});
});