You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

334 lines
9.1 KiB

6 months ago
import { useEffect, useRef, useState } from 'react';
6 months ago
import './App.css'
6 months ago
import { sendChatMessage } from './util/chat';
6 months ago
import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognition';
import { textToSpeech } from './util/tts';
import { gsap } from "gsap";
import { SplitText } from 'gsap/SplitText';
6 months ago
import { invoke } from '@tauri-apps/api/core';
6 months ago
gsap.registerPlugin(SplitText);
6 months ago
const BASE_URL='http://localhost:3333';
function App() {
const [history, setHistory] = useState([]);
const [processing, setProcessing] = useState(false);
6 months ago
const [showProcessing, setShowProcessing] = useState(false);
6 months ago
const [prompt, setPrompt] = useState([]);
6 months ago
const refHistoryContainer= useRef(null);
const refPrompContainer= useRef(null);
6 months ago
const refInput=useRef(null);
6 months ago
6 months ago
const {
transcript,
finalTranscript,
listening,
resetTranscript,
6 months ago
browserSupportsSpeechRecognition,
isMicrophoneAvailable,
6 months ago
}=useSpeechRecognition();
function restart(){
console.log("Restarting...");
setHistory([]);
setPrompt([]);
refInput.current.value = '';
resetTranscript();
SpeechRecognition.stopListening();
// create start message
const startTime=Date.now();
setProcessing(true);
sendChatMessage([]).then(response => {
6 months ago
if (!response.ok) {
throw new Error('Network response was not ok');
}
6 months ago
let data=response;
console.log('get reply: ', data, new Date(Date.now()-startTime).toISOString().slice(11, 19));
// add to history
6 months ago
setHistory(() => [{
6 months ago
role: 'assistant',
content: data.output_text,
}]);
6 months ago
setPrompt(()=>[
6 months ago
data.prompt,
]);
6 months ago
6 months ago
// tts
console.log('create speech:', data.output_text);
textToSpeech(data.output_text).then(audioUrl => {
const audio = new Audio(audioUrl);
console.log('play audio...', new Date(Date.now()-startTime).toISOString().slice(11, 19));
audio.play().catch(error => {
console.error('Audio playback failed:', error);
});
}).catch(error => {
console.error('TTS error:', error);
6 months ago
});
6 months ago
setProcessing(false);
6 months ago
});
6 months ago
}
6 months ago
6 months ago
function toggleAudio() {
6 months ago
console.log("onclickAudio", listening, browserSupportsSpeechRecognition, isMicrophoneAvailable);
if(!browserSupportsSpeechRecognition) {
console.warn("Browser does not support speech recognition.");
return;
}
if(!isMicrophoneAvailable) {
console.warn("Microphone is not available.");
return;
}
6 months ago
if(!listening){
6 months ago
SpeechRecognition.startListening({ continuous: true, language: 'zh-TW' }).then(() => {
console.log("Speech recognition started.");
}).catch(error => {
console.error("Error starting speech recognition:", error);
});
6 months ago
6 months ago
}else{
SpeechRecognition.stopListening();
}
6 months ago
}
6 months ago
function onSubmit(event) {
6 months ago
event.preventDefault();
6 months ago
if(processing) {
console.warn("Already processing, ignoring submission.");
return;
}
setProcessing(true);
setShowProcessing(true);
6 months ago
const input = event.target.elements.input.value;
6 months ago
if(!input.trim()?.length) {
console.warn("Input is empty, ignoring submission.");
return;
}
6 months ago
6 months ago
const startTime=Date.now();
console.log("Submit reply:", input);
6 months ago
sendChatMessage([
...history,
{
role:'user',
content: input,
}
]).then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
6 months ago
setProcessing(false);
6 months ago
}
let data=response;
6 months ago
console.log('get reply: ', data, new Date(Date.now()-startTime).toISOString().slice(11, 19));
6 months ago
// add to history
6 months ago
6 months ago
setPrompt([
...prompt,
data.prompt,
]);
6 months ago
6 months ago
6 months ago
6 months ago
// tts
console.log('create speech:', data.output_text);
textToSpeech(data.output_text).then(audioUrl => {
const audio = new Audio(audioUrl);
console.log('play audio...', new Date(Date.now()-startTime).toISOString().slice(11, 19));
6 months ago
setShowProcessing(false);
setHistory(prev => [...prev, {
role: 'assistant',
content: data.output_text,
}]);
6 months ago
audio.play().catch(error => {
console.error('Audio playback failed:', error);
});
audio.addEventListener('ended',() => {
console.log('Audio playback ended');
setProcessing(()=>false);
});
}).catch(error => {
console.error('TTS error:', error);
setProcessing(()=>false);
});
6 months ago
});
6 months ago
// clear input
event.target.elements.input.value = '';
// setProcessing(()=>false);
setHistory(prev => [...prev, {
role: 'user',
content:input,
}]);
6 months ago
6 months ago
}
6 months ago
useEffect(()=>{
refHistoryContainer.current.scrollTop = refHistoryContainer.current.scrollHeight;
6 months ago
// Animate the history items
if(history.length === 0) return;
let last_item=document.querySelector('.last_history');
6 months ago
6 months ago
if(!last_item) return;
6 months ago
if(last_item.classList.contains('user')) return;
console.log('last_item', last_item);
6 months ago
let split=SplitText.create(last_item, {
type: "chars",
aria:'hidden'
});
console.log('split', split);
gsap.fromTo(split.chars, {
opacity: 0,
}, {
opacity: 1,
y: 0,
6 months ago
duration: 0.5,
6 months ago
ease: "steps(1)",
6 months ago
stagger: 0.1
6 months ago
});
6 months ago
},[history]);
useEffect(()=>{
refPrompContainer.current.scrollTop = refPrompContainer.current.scrollHeight;
},[prompt]);
6 months ago
useEffect(()=>{
if(listening){
refInput.current.value = transcript;
}
},[transcript]);
useEffect(()=>{
if(finalTranscript){
refInput.current.value = finalTranscript;
console.log('Final Transcript:', finalTranscript);
if(processing) return; // Prevent submission if already processing
// Submit the final transcript
onSubmit({
preventDefault: () => {},
target: {
elements: {
input: refInput.current
}
}
});
resetTranscript(); // Clear the transcript after submission
}
},[finalTranscript]);
6 months ago
useEffect(()=>{
console.log('window.SpeechRecognition=', window.SpeechRecognition || window.webkitSpeechRecognition);
// if (navigator.getUserMedia){
// navigator.getUserMedia({audio:true},
// function(stream) {
// // start_microphone(stream);
// console.log('Microphone access granted.');
// },
// function(e) {
// alert('Error capturing audio.');
// }
// );
// } else { alert('getUserMedia not supported in this browser.'); }
},[]);
6 months ago
6 months ago
return (
6 months ago
<main className='h-screen flex flex-col gap-8 justify-end p-8'>
<div ref={refPrompContainer} className='flex-1 flex flex-col gap-2 border-4 overflow-y-auto'>
6 months ago
{prompt?.length==0 ? (
<div className='p-2 border-b border-gray-200'>Promp will appear here...</div>
):(
prompt?.map((item, index) => (
6 months ago
<div key={index} className='p-2 border-b border-gray-500 bg-pink-200'>
6 months ago
<p className='text-lg'>{item}</p>
</div>
))
)}
</div>
6 months ago
<div ref={refHistoryContainer} className='flex-1 overflow-y-auto'>
<div className='flex flex-col justify-end gap-2'>
6 months ago
{history?.length==0 && !showProcessing? (
6 months ago
<div className='p-2'>History will appear here...</div>
):(
history.map((item, index) => (
<div key={index} className={`p-2 rounded border-4 ${item.role === 'user' ? 'bg-gray-100' : 'bg-yellow-100'}`}>
6 months ago
<p className={`text-lg whitespace-pre-wrap history_item ${index==history?.length-1 && item.role!='user' && 'last_history'}`}>{item.content}</p>
6 months ago
</div>
))
)}
6 months ago
{showProcessing && (
<div className='p-2 rounded border-4 bg-yellow-100'>
<span className='animate-pulse'>...</span>
</div>
)}
6 months ago
</div>
6 months ago
</div>
6 months ago
<div className='flex flex-col gap-2'>
<div className='flex flex-row justify-end gap-2 '>
<button className='self-end' onClick={restart}>Restart</button>
<span className='flex-1'></span>
<button className='' onClick={()=>{
refInput.current.value=''
resetTranscript();
}}>clear</button>
<button onClick={toggleAudio} className={`${listening? '!bg-red-200':'!bg-gray-200'}`}>{listening? 'AudioIn On':'AudioIn Off'}</button>
</div>
6 months ago
<form className='flex flex-col justify-center *:border-4 gap-4' onSubmit={onSubmit} autoComplete="off">
6 months ago
<textarea ref={refInput} id="input" name="input" required className='self-stretch p-2 resize-none' rows={3} autoComplete="off"/>
<button type="submit" className='uppercase' disabled={processing}>Send</button>
6 months ago
</form>
</div>
</main>
)
}
export default App