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>
26 lines
1.8 KiB
TypeScript
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");
|
|
});
|
|
});
|