Skip to content

Commit 642214a

Browse files
committed
better input state
1 parent bfbde43 commit 642214a

File tree

3 files changed

+93
-37
lines changed

3 files changed

+93
-37
lines changed

next/components/assembly-view.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const AssemblyView: () => JSX.Element = () => {
6464

6565
const setupAudioProcessor = async () => {
6666
try {
67+
managerRef.current.isInitialized = false;
6768
const processor = await AudioProcessor.getInstance(microphone.stream);
6869
await processor.setupProcessor((blob: Blob) => {
6970
if (isConnected) {
@@ -77,6 +78,8 @@ const AssemblyView: () => JSX.Element = () => {
7778
processor.stop();
7879
},
7980
} as MediaRecorder; // Type assertion since we only use stop()
81+
82+
managerRef.current.isInitialized = true;
8083
} catch (error) {
8184
console.error("[AssemblyView] Failed to setup audio processor:", error);
8285
}
@@ -89,6 +92,7 @@ const AssemblyView: () => JSX.Element = () => {
8992
mainRecorder.current.stop();
9093
mainRecorder.current = null;
9194
}
95+
managerRef.current.isInitialized = false;
9296
};
9397
}, [microphone?.stream, isPaused, isConnected, sendAudio]);
9498

next/components/speech-input.tsx

+79-37
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
import { Textarea } from "@/components/ui/textarea";
44
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";
613
import Visualizer from "@/components/audio-viz";
714
import { SpeechStateManager } from "@/lib/speech-state";
815
import { useEffect, useState, useRef } from "react";
@@ -26,14 +33,18 @@ export const SpeechInput = ({
2633
onTogglePause,
2734
isPaused,
2835
}: 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+
);
3142
const [isSpeechActive, setIsSpeechActive] = useState(false);
3243
const textareaRef = useRef<HTMLTextAreaElement>(null);
3344

3445
useEffect(() => {
3546
if (!speechState) return;
36-
47+
3748
const intervalId = setInterval(() => {
3849
setIsSpeechActive(speechState.isActive);
3950
}, 250);
@@ -44,7 +55,7 @@ export const SpeechInput = ({
4455
// Add click outside listener
4556
const handleClickOutside = (e: MouseEvent) => {
4657
const target = e.target as HTMLElement;
47-
if (!target.closest('.input-container')) {
58+
if (!target.closest(".input-container")) {
4859
setExpandedInput(null);
4960
}
5061
};
@@ -56,11 +67,11 @@ export const SpeechInput = ({
5667
}
5768
};
5869

59-
document.addEventListener('mousedown', handleClickOutside);
60-
document.addEventListener('keydown', handleEscape);
70+
document.addEventListener("mousedown", handleClickOutside);
71+
document.addEventListener("keydown", handleEscape);
6172
return () => {
62-
document.removeEventListener('mousedown', handleClickOutside);
63-
document.removeEventListener('keydown', handleEscape);
73+
document.removeEventListener("mousedown", handleClickOutside);
74+
document.removeEventListener("keydown", handleEscape);
6475
};
6576
}, []);
6677

@@ -79,46 +90,74 @@ export const SpeechInput = ({
7990
<div className="flex items-center space-x-2 input-container">
8091
<div
8192
onClick={() => {
93+
if (!speechState?.isInitialized) return;
8294
setActiveInput("record");
8395
setExpandedInput("record");
8496
}}
8597
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"}`}
89104
>
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"
93114
}`}
94115
>
95116
{microphone && <Visualizer microphone={microphone} />}
96117
<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+
)}
98125
{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"}
101138
</span>
102139
)}
103140
</div>
104141

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+
)}
122161
</div>
123162
</div>
124163

@@ -129,8 +168,11 @@ export const SpeechInput = ({
129168
textareaRef.current?.focus();
130169
}}
131170
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"
134176
}`}
135177
>
136178
{expandedInput === "text" ? (

next/lib/speech-state.ts

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface SpeechSegment {
77

88
export class SpeechStateManager {
99
private isCollectingState: boolean;
10+
private isInitializedState: boolean;
1011
private segments: SpeechSegment[];
1112
private speechCollectedMs: number;
1213
private currentPartialText: string | null;
@@ -21,6 +22,7 @@ export class SpeechStateManager {
2122

2223
constructor(speechThresholdMs = 5000, gapThresholdMs = 5000) {
2324
this.isCollectingState = true;
25+
this.isInitializedState = false;
2426
this.segments = [];
2527
this.speechCollectedMs = 0;
2628
this.currentPartialText = null;
@@ -38,6 +40,14 @@ export class SpeechStateManager {
3840
return Date.now() < this.lastActivityMs;
3941
}
4042

43+
get isInitialized(): boolean {
44+
return this.isInitializedState;
45+
}
46+
47+
set isInitialized(value: boolean) {
48+
this.isInitializedState = value;
49+
}
50+
4151
/**
4252
* Main method to handle incoming transcript data.
4353
* @param text The transcript text

0 commit comments

Comments
 (0)