2
2
3
3
import { Textarea } from "@/components/ui/textarea" ;
4
4
import { Button } from "@/components/ui/button" ;
5
- import { Mic , Keyboard , ArrowUp , PauseIcon , PlayIcon } from "lucide-react" ;
5
+ import {
6
+ Mic ,
7
+ MicOff ,
8
+ Keyboard ,
9
+ ArrowUp ,
10
+ PauseIcon ,
11
+ PlayIcon ,
12
+ } from "lucide-react" ;
6
13
import Visualizer from "@/components/audio-viz" ;
7
14
import { SpeechStateManager } from "@/lib/speech-state" ;
8
15
import { useEffect , useState , useRef } from "react" ;
@@ -26,14 +33,18 @@ export const SpeechInput = ({
26
33
onTogglePause,
27
34
isPaused,
28
35
} : SpeechInputProps ) => {
29
- const [ activeInput , setActiveInput ] = useState < "record" | "text" | null > ( null ) ;
30
- const [ expandedInput , setExpandedInput ] = useState < "record" | "text" | null > ( null ) ;
36
+ const [ activeInput , setActiveInput ] = useState < "record" | "text" | null > (
37
+ null
38
+ ) ;
39
+ const [ expandedInput , setExpandedInput ] = useState < "record" | "text" | null > (
40
+ null
41
+ ) ;
31
42
const [ isSpeechActive , setIsSpeechActive ] = useState ( false ) ;
32
43
const textareaRef = useRef < HTMLTextAreaElement > ( null ) ;
33
44
34
45
useEffect ( ( ) => {
35
46
if ( ! speechState ) return ;
36
-
47
+
37
48
const intervalId = setInterval ( ( ) => {
38
49
setIsSpeechActive ( speechState . isActive ) ;
39
50
} , 250 ) ;
@@ -44,7 +55,7 @@ export const SpeechInput = ({
44
55
// Add click outside listener
45
56
const handleClickOutside = ( e : MouseEvent ) => {
46
57
const target = e . target as HTMLElement ;
47
- if ( ! target . closest ( ' .input-container' ) ) {
58
+ if ( ! target . closest ( " .input-container" ) ) {
48
59
setExpandedInput ( null ) ;
49
60
}
50
61
} ;
@@ -56,11 +67,11 @@ export const SpeechInput = ({
56
67
}
57
68
} ;
58
69
59
- document . addEventListener ( ' mousedown' , handleClickOutside ) ;
60
- document . addEventListener ( ' keydown' , handleEscape ) ;
70
+ document . addEventListener ( " mousedown" , handleClickOutside ) ;
71
+ document . addEventListener ( " keydown" , handleEscape ) ;
61
72
return ( ) => {
62
- document . removeEventListener ( ' mousedown' , handleClickOutside ) ;
63
- document . removeEventListener ( ' keydown' , handleEscape ) ;
73
+ document . removeEventListener ( " mousedown" , handleClickOutside ) ;
74
+ document . removeEventListener ( " keydown" , handleEscape ) ;
64
75
} ;
65
76
} , [ ] ) ;
66
77
@@ -79,46 +90,74 @@ export const SpeechInput = ({
79
90
< div className = "flex items-center space-x-2 input-container" >
80
91
< div
81
92
onClick = { ( ) => {
93
+ if ( ! speechState ?. isInitialized ) return ;
82
94
setActiveInput ( "record" ) ;
83
95
setExpandedInput ( "record" ) ;
84
96
} }
85
97
className = { `transition-all duration-300 ${
86
- expandedInput === null ? "flex-1 h-12" :
87
- expandedInput === "record" ? "flex-1 h-12" : "w-12 h-12"
88
- } `}
98
+ expandedInput === null
99
+ ? "flex-1 h-12"
100
+ : expandedInput === "record"
101
+ ? "flex-1 h-12"
102
+ : "w-12 h-12"
103
+ } ${ ! speechState ?. isInitialized ? "pointer-events-none opacity-75" : "cursor-pointer hover:opacity-80" } `}
89
104
>
90
- < div
91
- className = { `w-full h-full rounded-md relative hover:bg-accent hover:text-accent-foreground ${
92
- isSpeechActive ? "border-2 border-muted-foreground animate-pulse" : ""
105
+ < div
106
+ className = { `w-full h-full rounded-md relative ${
107
+ isSpeechActive
108
+ ? "border-2 border-muted-foreground animate-pulse"
109
+ : ""
110
+ } ${
111
+ speechState ?. isInitialized
112
+ ? "hover:bg-accent hover:text-accent-foreground"
113
+ : "bg-muted"
93
114
} `}
94
115
>
95
116
{ microphone && < Visualizer microphone = { microphone } /> }
96
117
< div className = "absolute inset-0 flex items-center justify-center gap-2" >
97
- < Mic className = { `w-6 h-6 ${ isPaused ? "text-muted-foreground" : "" } ` } />
118
+ { ! speechState ?. isInitialized ? (
119
+ < MicOff className = "w-6 h-6 text-muted-foreground" />
120
+ ) : (
121
+ < Mic
122
+ className = { `w-6 h-6 ${ isPaused ? "text-muted-foreground" : "" } ` }
123
+ />
124
+ ) }
98
125
{ expandedInput === "record" && (
99
- < span className = { `text-sm ${ isPaused ? "text-muted-foreground" : "" } ` } >
100
- { isPaused ? "Paused" : "Listening" }
126
+ < span
127
+ className = { `text-sm ${
128
+ isPaused || ! speechState ?. isInitialized
129
+ ? "text-muted-foreground"
130
+ : ""
131
+ } `}
132
+ >
133
+ { ! speechState ?. isInitialized
134
+ ? "Initializing..."
135
+ : isPaused
136
+ ? "Paused"
137
+ : "Listening" }
101
138
</ span >
102
139
) }
103
140
</ div >
104
141
105
- { ( expandedInput === "record" || activeInput === "record" ) && onTogglePause && (
106
- < Button
107
- variant = "ghost"
108
- size = "icon"
109
- className = "absolute left-1 top-1/2 -translate-y-1/2"
110
- onClick = { ( e ) => {
111
- e . stopPropagation ( ) ;
112
- onTogglePause ( ) ;
113
- } }
114
- >
115
- { isPaused ? (
116
- < PlayIcon className = "w-4 h-4" />
117
- ) : (
118
- < PauseIcon className = "w-4 h-4" />
119
- ) }
120
- </ Button >
121
- ) }
142
+ { ( expandedInput === "record" || activeInput === "record" ) &&
143
+ onTogglePause &&
144
+ speechState ?. isInitialized && (
145
+ < Button
146
+ variant = "ghost"
147
+ size = "icon"
148
+ className = "absolute left-1 top-1/2 -translate-y-1/2"
149
+ onClick = { ( e ) => {
150
+ e . stopPropagation ( ) ;
151
+ onTogglePause ( ) ;
152
+ } }
153
+ >
154
+ { isPaused ? (
155
+ < PlayIcon className = "w-4 h-4" />
156
+ ) : (
157
+ < PauseIcon className = "w-4 h-4" />
158
+ ) }
159
+ </ Button >
160
+ ) }
122
161
</ div >
123
162
</ div >
124
163
@@ -129,8 +168,11 @@ export const SpeechInput = ({
129
168
textareaRef . current ?. focus ( ) ;
130
169
} }
131
170
className = { `transition-all duration-300 relative ${
132
- expandedInput === null ? "flex-1 h-12" :
133
- expandedInput === "text" ? "flex-1 h-12" : "w-12 h-12"
171
+ expandedInput === null
172
+ ? "flex-1 h-12"
173
+ : expandedInput === "text"
174
+ ? "flex-1 h-12"
175
+ : "w-12 h-12"
134
176
} `}
135
177
>
136
178
{ expandedInput === "text" ? (
0 commit comments