working with voice tts
This commit is contained in:
156
src/app/page.tsx
Normal file
156
src/app/page.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
// src/app/page.tsx
|
||||
"use client";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useChat } from "@/hooks/useChat";
|
||||
import { useWhisper } from "@/hooks/useWhisper";
|
||||
import { useVoiceRecorder } from "@/hooks/useVoiceRecorder";
|
||||
import { stopSpeaking } from "@/lib/tts";
|
||||
|
||||
export default function Home() {
|
||||
const [textInput, setTextInput] = useState("");
|
||||
const { messages, isLoading, sendMessage } = useChat();
|
||||
const { status: whisperStatus, modelMessage, transcribe } = useWhisper();
|
||||
const { isRecording, startRecording, stopRecording } = useVoiceRecorder();
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [messages]);
|
||||
|
||||
const handleTextSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!textInput.trim()) return;
|
||||
sendMessage(textInput, "text");
|
||||
setTextInput("");
|
||||
};
|
||||
|
||||
const handlePTTDown = async () => {
|
||||
if (whisperStatus !== "ready") return;
|
||||
stopSpeaking();
|
||||
await startRecording();
|
||||
};
|
||||
|
||||
const handlePTTUp = async () => {
|
||||
if (!isRecording) return;
|
||||
const audioData = await stopRecording();
|
||||
const text = await transcribe(audioData);
|
||||
if (text) sendMessage(text, "voice");
|
||||
};
|
||||
|
||||
const pttDisabled =
|
||||
whisperStatus !== "ready" || isLoading;
|
||||
|
||||
const pttLabel = () => {
|
||||
if (whisperStatus === "loading") return "⏳";
|
||||
if (whisperStatus === "transcribing") return "💬";
|
||||
if (isRecording) return "🔴";
|
||||
return "🎙";
|
||||
};
|
||||
|
||||
const statusLine = () => {
|
||||
if (whisperStatus === "loading") return modelMessage;
|
||||
if (whisperStatus === "transcribing") return "Transcribing on-device…";
|
||||
if (isRecording) return "Recording… release to send";
|
||||
if (whisperStatus === "ready") return "Hold to talk — Whisper ready ✓";
|
||||
return "Initialising Whisper…";
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex flex-col h-screen bg-gray-950 text-white">
|
||||
{/* Header */}
|
||||
<header className="flex items-center gap-3 px-6 py-4 border-b border-gray-800 bg-gray-900">
|
||||
<span className="text-2xl">🦞</span>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold tracking-tight">OpenClaw Voice</h1>
|
||||
<p className="text-xs text-gray-500">On-device Whisper · No API keys</p>
|
||||
</div>
|
||||
<span
|
||||
className={`ml-auto w-2 h-2 rounded-full ${
|
||||
whisperStatus === "ready" ? "bg-green-400" : "bg-yellow-400 animate-pulse"
|
||||
}`}
|
||||
/>
|
||||
</header>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto px-4 py-6 space-y-4">
|
||||
{messages.length === 0 && (
|
||||
<div className="text-center mt-20 space-y-2">
|
||||
<p className="text-gray-500 text-sm">
|
||||
{whisperStatus === "ready"
|
||||
? "Whisper loaded. Hold the button to talk or type below."
|
||||
: modelMessage || "Loading Whisper model…"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[75%] rounded-2xl px-4 py-3 text-sm leading-relaxed ${
|
||||
msg.role === "user"
|
||||
? "bg-indigo-600 text-white rounded-br-sm"
|
||||
: "bg-gray-800 text-gray-100 rounded-bl-sm"
|
||||
}`}
|
||||
>
|
||||
{msg.source === "voice" && (
|
||||
<span className="text-xs opacity-40 block mb-1">
|
||||
{msg.role === "user" ? "🎙 transcribed" : "🔊 spoken"}
|
||||
</span>
|
||||
)}
|
||||
{msg.content || <span className="opacity-40 animate-pulse">▍</span>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="border-t border-gray-800 bg-gray-900 px-4 py-4 space-y-3">
|
||||
{/* PTT Button */}
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
onMouseDown={handlePTTDown}
|
||||
onMouseUp={handlePTTUp}
|
||||
onTouchStart={(e) => { e.preventDefault(); handlePTTDown(); }}
|
||||
onTouchEnd={handlePTTUp}
|
||||
disabled={pttDisabled}
|
||||
className={`w-20 h-20 rounded-full text-3xl font-bold transition-all shadow-lg select-none
|
||||
${isRecording
|
||||
? "bg-red-500 scale-110 shadow-red-500/40 animate-pulse"
|
||||
: whisperStatus === "transcribing"
|
||||
? "bg-yellow-500 cursor-wait"
|
||||
: pttDisabled
|
||||
? "bg-gray-700 cursor-not-allowed opacity-50"
|
||||
: "bg-indigo-600 hover:bg-indigo-500 active:scale-95 cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
{pttLabel()}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-gray-500">{statusLine()}</p>
|
||||
|
||||
{/* Text Input */}
|
||||
<form onSubmit={handleTextSubmit} className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={textInput}
|
||||
onChange={(e) => setTextInput(e.target.value)}
|
||||
placeholder="Or type a message…"
|
||||
disabled={isLoading || isRecording}
|
||||
className="flex-1 bg-gray-800 rounded-xl px-4 py-2 text-sm outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || !textInput.trim()}
|
||||
className="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-40 px-4 py-2 rounded-xl text-sm font-medium transition"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user