Box, VStack, Input, Button, Container, Text, Flex, Avatar, Spinner, useToast, IconButton, Switch, FormControl, FormLabel, useColorMode, useColorModeValue, } from '@chakra-ui/react' import { FaVolumeUp, FaMicrophone, FaStop, FaSun, FaMoon } from 'react-icons/fa' import AdvisorAvatar from './components/AdvisorAvatar' function App() { const { colorMode, toggleColorMode } = useColorMode(); const bg = useColorModeValue('gray.50', 'gray.900'); const containerBg = useColorModeValue('white', 'gray.800'); const messageBg = useColorModeValue('gray.100', 'gray.700'); const inputBg = useColorModeValue('white', 'gray.700'); const borderColor = useColorModeValue('gray.200', 'gray.600'); const textColor = useColorModeValue('black', 'white'); const initialMessage = { たの旅行プランをお手伝いする旅行アドバイザーです。国内外の旅行について、予算や期間、行きたい場所など、あなたの希望に合わせてアドバイスさせていただきます。気になることを話しかけてください。音声でのやり取りも可能です。", sender: 'ai' }; const [messages, setMessages] = useState([initialMessage]); const [inputMessage, setInputMessage] = useState('') const [isRecording, setIsRecording] = useState(false) const [recorder, setRecorder] = useState(null) const [advisorState, setAdvisorState] = useState('idle') const [autoContinue, setAutoContinue] = useState(false) const toast = useToast() const audioRef = useRef(null) const messagesEndRef = useRef(null) // アドバイザーの状態管理を簡略化 useEffect(() => { const lastMessage = messages[messages.length -1]; if (lastMessage?.sender === 'ai') { setAdvisorState('idle'); } }, [messages]); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; // メッセージが追加されたときに自動スクロール useEffect(() => { scrollToBottom(); }, [messages]); const formatConversationHistory = (messages) => { return messages .map(msg => `${msg.sender === 'user' ? 'クライアント' : 'エージェント'}: ${msg.text}`) .join('\n'); } const playAudio = (base64Audio) => { if (!base64Audio) return; setAdvisorState('talking'); const audio = new Audio(`data:audio/mp3;base64,${base64Audio}`); audio.addEventListener('ended', () => { setAdvisorState('idle'); // アドバイザーの発言が終わったら自動的に録音を開始 if (autoContinue) { startRecording(); } }); audio.play().catch(error => { console.error('Error playing audio:', error); setAdvisorState('idle'); toast({ title: 'Error', description: '音声の再生に失敗しました', status: 'error', duration: 3000, isClosable: true, }); }); } const startRecording = async () => { // すでに録音中の場合は開始しない if (isRecording) return; try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mediaRecorder = new MediaRecorder(stream); const audioChunks = []; mediaRecorder.addEventListener("dataavailable", event => { audioChunks.push(event.data); }); mediaRecorder.addEventListener("stop", async () => { const audioBlob = new Blob(audioChunks, { type: 'audio/mp3' }); await handleAudioUpload(audioBlob); }); mediaRecorder.start(); setRecorder(mediaRecorder); setIsRecording(true); toast({ title: '録音中', description: '話し終わったら停止ボタンを押してください', status: 'info', duration: null, isClosable: true, }); } catch (error) { console.error('Error starting recording:', error); toast({ title: 'Error', description: '録音の開始に失敗しました', status: 'error', duration: 3000, isClosable: true, }); } }; const stopRecording = () => { if (recorder && recorder.state === "recording") { recorder.stop(); recorder.stream.getTracks().forEach(track => track.stop()); setIsRecording(false); toast.closeAll(); // Close the recording toast } }; const handleAudioUpload = async (audioBlob) => { try { const formData = new FormData(); formData.append('audio', audioBlob); const response = await fetch('http://localhost:8000/api/speech-to-text', { method: 'POST', body: formData, }); if (!response.ok) { throw new Error('Failed to transcribe audio'); } const data = await response.json(); if (data.text.trim()) { // 音声認識結果をメッセージとして追加 const userMessage = { text: data.text, sender: 'user' }; const updatedMessages = [...messages, userMessage]; setMessages(updatedMessages); try { // バックエンドにメッセージを送信 const response = await fetch('http://localhost:8000/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message 無関⼼の⾕ (The Valley of Meh) 無関心の谷 AIは最初と最後に関⼼がある傾向をしめし、 ⾕に落ちた中盤の情報は無視する傾向がある AIが関⼼を⽰す領域 AIが無関⼼な領域