Slide 20
Slide 20 text
import { useState, useRef, useEffect } from 'react'
import {
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が無関⼼な領域