mirror of https://github.com/buster-so/buster.git
dictation tap in
This commit is contained in:
parent
92c526a3a5
commit
376703335a
|
@ -1,5 +1,4 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognition';
|
||||
import { Button } from '@/components/ui/buttons';
|
||||
import { ArrowUp, Magnifier, Sparkle2 } from '@/components/ui/icons';
|
||||
import Atom from '@/components/ui/icons/NucleoIconOutlined/atom';
|
||||
|
@ -16,6 +15,7 @@ import { AppTooltip } from '@/components/ui/tooltip';
|
|||
import { Text } from '@/components/ui/typography';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useMicrophonePermission } from './hooks/useMicrophonePermission';
|
||||
import { useSpeechRecognition } from '@/hooks/useSpeechRecognition';
|
||||
|
||||
export type BusterChatInputMode = 'auto' | 'research' | 'deep-research';
|
||||
|
||||
|
@ -42,21 +42,14 @@ export const BusterChatInputButtons = React.memo(
|
|||
onDictateListeningChange,
|
||||
}: BusterChatInputButtons) => {
|
||||
const hasGrantedPermissions = useMicrophonePermission();
|
||||
const { transcript, listening, browserSupportsSpeechRecognition } = useSpeechRecognition();
|
||||
const { transcript, listening, browserSupportsSpeechRecognition, onStartListening, onStopListening } =
|
||||
useSpeechRecognition();
|
||||
const hasValue = useMentionInputHasValue();
|
||||
const onChangeValue = useMentionInputSuggestionsOnChangeValue();
|
||||
const getValue = useMentionInputSuggestionsGetValue();
|
||||
|
||||
const disableSubmit = !hasValue;
|
||||
|
||||
const startListening = async () => {
|
||||
SpeechRecognition.startListening({ continuous: true });
|
||||
};
|
||||
|
||||
const stopListening = () => {
|
||||
SpeechRecognition.stopListening();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (listening && transcript) {
|
||||
onDictate?.(transcript);
|
||||
|
@ -87,7 +80,7 @@ export const BusterChatInputButtons = React.memo(
|
|||
rounding={'large'}
|
||||
variant={'ghost'}
|
||||
prefix={<Microphone />}
|
||||
onClick={listening ? stopListening : startListening}
|
||||
onClick={listening ? onStopListening : onStartListening}
|
||||
loading={false}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { useSpeechRecognition } from './useSpeechRecognition';
|
||||
|
||||
function SpeechRecognitionDemo() {
|
||||
const {
|
||||
onStartListening,
|
||||
onStopListening,
|
||||
listening,
|
||||
transcript,
|
||||
browserSupportsSpeechRecognition,
|
||||
error,
|
||||
} = useSpeechRecognition();
|
||||
|
||||
if (!browserSupportsSpeechRecognition) {
|
||||
return (
|
||||
<div style={{ padding: '2rem', fontFamily: 'sans-serif' }}>
|
||||
<h2>Speech Recognition Not Supported</h2>
|
||||
<p>Your browser does not support speech recognition.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '2rem', fontFamily: 'sans-serif' }}>
|
||||
<h2>Speech Recognition Demo</h2>
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onStartListening}
|
||||
disabled={listening}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
marginRight: '0.5rem',
|
||||
backgroundColor: listening ? '#ccc' : '#007bff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: listening ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
Start Listening
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onStopListening}
|
||||
disabled={!listening}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
backgroundColor: !listening ? '#ccc' : '#dc3545',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: !listening ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
Stop Listening
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<strong>Status:</strong>{' '}
|
||||
<span style={{ color: listening ? '#28a745' : '#6c757d' }}>
|
||||
{listening ? 'Listening...' : 'Not listening'}
|
||||
</span>
|
||||
</div>
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '1rem',
|
||||
padding: '0.75rem',
|
||||
backgroundColor: '#f8d7da',
|
||||
color: '#721c24',
|
||||
border: '1px solid #f5c6cb',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<strong>Error:</strong> {error}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<strong>Transcript:</strong>
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
padding: '1rem',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
minHeight: '100px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
}}
|
||||
>
|
||||
{transcript || 'No speech detected yet...'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: 'Hooks/useSpeechRecognition',
|
||||
component: SpeechRecognitionDemo,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies Meta<typeof SpeechRecognitionDemo>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
|
@ -0,0 +1,141 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
// Type definitions for Web Speech API
|
||||
interface SpeechRecognitionErrorEvent extends Event {
|
||||
error: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface SpeechRecognitionEvent extends Event {
|
||||
resultIndex: number;
|
||||
results: SpeechRecognitionResultList;
|
||||
}
|
||||
|
||||
interface SpeechRecognition extends EventTarget {
|
||||
continuous: boolean;
|
||||
interimResults: boolean;
|
||||
lang: string;
|
||||
onstart: ((this: SpeechRecognition, ev: Event) => void) | null;
|
||||
onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null;
|
||||
onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void) | null;
|
||||
onend: ((this: SpeechRecognition, ev: Event) => void) | null;
|
||||
start(): void;
|
||||
stop(): void;
|
||||
abort(): void;
|
||||
}
|
||||
|
||||
interface SpeechRecognitionConstructor {
|
||||
new (): SpeechRecognition;
|
||||
}
|
||||
|
||||
// Extend Window interface to include webkit speech recognition
|
||||
declare global {
|
||||
interface Window {
|
||||
SpeechRecognition: SpeechRecognitionConstructor;
|
||||
webkitSpeechRecognition: SpeechRecognitionConstructor;
|
||||
}
|
||||
}
|
||||
|
||||
interface UseSpeechRecognitionReturn {
|
||||
onStartListening: () => void;
|
||||
onStopListening: () => void;
|
||||
listening: boolean;
|
||||
transcript: string;
|
||||
browserSupportsSpeechRecognition: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function useSpeechRecognition(): UseSpeechRecognitionReturn {
|
||||
const [listening, setListening] = useState(false);
|
||||
const [transcript, setTranscript] = useState('');
|
||||
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Check browser support
|
||||
const browserSupportsSpeechRecognition =
|
||||
typeof window !== 'undefined' && (window.SpeechRecognition || window.webkitSpeechRecognition);
|
||||
|
||||
// Initialize speech recognition
|
||||
useEffect(() => {
|
||||
if (!browserSupportsSpeechRecognition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
const recognition = new SpeechRecognitionAPI();
|
||||
|
||||
recognition.continuous = true;
|
||||
recognition.interimResults = true;
|
||||
recognition.lang = 'en-US';
|
||||
|
||||
recognition.onstart = () => {
|
||||
setListening(true);
|
||||
};
|
||||
|
||||
recognition.onresult = (event: SpeechRecognitionEvent) => {
|
||||
let interimTranscript = '';
|
||||
let finalTranscript = '';
|
||||
|
||||
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||
const transcriptPiece = event.results[i][0].transcript;
|
||||
if (event.results[i].isFinal) {
|
||||
finalTranscript += transcriptPiece;
|
||||
} else {
|
||||
interimTranscript += transcriptPiece;
|
||||
}
|
||||
}
|
||||
|
||||
setTranscript(finalTranscript || interimTranscript);
|
||||
};
|
||||
|
||||
recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
|
||||
console.error('Speech recognition error:', event.error);
|
||||
if (event.error.includes('language-not-supported')) {
|
||||
setError('Browser does not support dictation');
|
||||
} else {
|
||||
setError(event.error);
|
||||
}
|
||||
|
||||
onStopListening();
|
||||
};
|
||||
|
||||
recognition.onend = () => {
|
||||
setListening(false);
|
||||
};
|
||||
|
||||
recognitionRef.current = recognition;
|
||||
|
||||
return () => {
|
||||
recognition.stop();
|
||||
};
|
||||
}, [browserSupportsSpeechRecognition]);
|
||||
|
||||
const onStartListening = useCallback(async () => {
|
||||
if (recognitionRef.current && !listening) {
|
||||
try {
|
||||
// Request microphone permission
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
setTranscript('');
|
||||
recognitionRef.current.start();
|
||||
} catch (error) {
|
||||
console.error('Microphone permission denied:', error);
|
||||
}
|
||||
}
|
||||
}, [listening]);
|
||||
|
||||
const onStopListening = useCallback(() => {
|
||||
if (recognitionRef.current) {
|
||||
recognitionRef.current.stop();
|
||||
setListening(false);
|
||||
}
|
||||
}, [listening]);
|
||||
|
||||
return {
|
||||
onStartListening,
|
||||
onStopListening,
|
||||
listening,
|
||||
error,
|
||||
transcript,
|
||||
browserSupportsSpeechRecognition: Boolean(browserSupportsSpeechRecognition),
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue