Files
MindSift/frontend/src/components/VoiceInputButton.tsx
T
sandrodachandClaude Opus 5.5 83b3f27b3e
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m38s
feat(voice): microphone in the add-task and add-item rows; phone fixes in the column dialog
VoiceInputButton brings the Quick Dump microphone into other input rows (useVoiceInput, appends
to the text already there, hidden when voice input is off or unsupported): the add-task row above
the task list and the add-item field of lists (desktop row and phone sheet).

Column dialog on phones: the add-column buttons go below the field, and the Erledigt toggle of a
column shows only its tick, so the bin stays on screen. Docs and changelog; v0.32.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:17:49 +01:00

64 lines
2.5 KiB
TypeScript

import React, { useRef } from 'react';
import { Loader2, Mic, MicOff } from 'lucide-react';
import { useVoiceInput } from '../lib/useVoiceInput';
import { triggerHaptic } from '../lib/api';
import { VoiceWave } from './VoiceWave';
interface VoiceInputButtonProps {
// The field's text; dictated words are appended to what was there when the recording started
value: string;
onChange: (text: string) => void;
onError?: (message: string) => void;
// Field to focus once the text is final
focusRef?: React.RefObject<HTMLInputElement | HTMLTextAreaElement>;
className?: string;
}
// Microphone inside an input row, like the one in Quick Dump (Doku/ui-patterns.md). Hidden when voice
// input is off or the browser cannot do it (e.g. Firefox without an own Whisper server).
export const VoiceInputButton: React.FC<VoiceInputButtonProps> = ({ value, onChange, onError, focusRef, className = '' }) => {
const baseRef = useRef('');
const voice = useVoiceInput({
onText: (text, final) => {
const base = baseRef.current;
if (text) onChange(base ? `${base} ${text}` : text);
if (final) focusRef?.current?.focus();
},
onError,
});
if (!voice.available) return null;
const listening = voice.state === 'listening';
const transcribing = voice.state === 'transcribing';
return (
<span className={`inline-flex items-center gap-1 flex-shrink-0 ${className}`}>
{listening && voice.mode === 'server' && <VoiceWave getLevel={voice.level} className="h-3.5 text-accent" />}
<button
type="button"
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
if (voice.state === 'idle') {
baseRef.current = value.trim();
triggerHaptic(20);
}
voice.toggle();
}}
disabled={transcribing}
title={listening ? 'Aufnahme beenden' : transcribing ? 'Wird erkannt…' : 'Spracheingabe'}
aria-label={listening ? 'Aufnahme beenden' : transcribing ? 'Wird erkannt…' : 'Spracheingabe'}
className={`w-9 h-9 sm:w-7 sm:h-7 rounded-full flex items-center justify-center transition-colors ${
listening || transcribing ? 'bg-accent-dim text-accent' : 'text-muted hover:text-main hover:bg-panel-elevated'
}`}
>
{transcribing ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : listening ? (
<MicOff className="w-3.5 h-3.5" />
) : (
<Mic className="w-3.5 h-3.5" />
)}
</button>
</span>
);
};