import React, { Component } from 'react'; import Box from '@mui/material/Box'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import TextField from '@mui/material/TextField'; import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; import CloseIcon from '@mui/icons-material/Close'; import CircularProgress from '@mui/material/CircularProgress'; import Avatar from '@mui/material/Avatar'; import SmartToyIcon from '@mui/icons-material/SmartToy'; import PersonIcon from '@mui/icons-material/Person'; import MicIcon from '@mui/icons-material/Mic'; import StopIcon from '@mui/icons-material/Stop'; import PhotoCameraIcon from '@mui/icons-material/PhotoCamera'; import parse, { domToReact } from 'html-react-parser'; import { Link } from 'react-router-dom'; import { isUserLoggedIn } from './LoginComponent.js'; // Initialize window object for storing messages if (!window.chatMessages) { window.chatMessages = []; } class ChatAssistant extends Component { constructor(props) { super(props); const privacyConfirmed = sessionStorage.getItem('privacyConfirmed') === 'true'; this.state = { messages: window.chatMessages, inputValue: '', isTyping: false, isRecording: false, recordingTime: 0, mediaRecorder: null, audioChunks: [], aiThink: false, atDatabase: false, atWeb: false, privacyConfirmed: privacyConfirmed, isGuest: false }; this.messagesEndRef = React.createRef(); this.fileInputRef = React.createRef(); this.recordingTimer = null; } componentDidMount() { // Add socket listeners if socket is available and connected this.addSocketListeners(); const userStatus = isUserLoggedIn(); const isGuest = !userStatus.isLoggedIn; if (isGuest && !this.state.privacyConfirmed) { this.setState(prevState => { if (prevState.messages.find(msg => msg.id === 'privacy-prompt')) { return { isGuest: true }; } const privacyMessage = { id: 'privacy-prompt', sender: 'bot', text: 'Bitte bestätigen Sie, dass Sie die Datenschutzbestimmungen gelesen haben und damit einverstanden sind. ', }; const updatedMessages = [privacyMessage, ...prevState.messages]; window.chatMessages = updatedMessages; return { messages: updatedMessages, isGuest: true }; }); } else { this.setState({ isGuest }); } } componentDidUpdate(prevProps, prevState) { if (prevState.messages !== this.state.messages || prevState.isTyping !== this.state.isTyping) { this.scrollToBottom(); } } componentWillUnmount() { this.removeSocketListeners(); this.stopRecording(); if (this.recordingTimer) { clearInterval(this.recordingTimer); } } addSocketListeners = () => { this.removeSocketListeners(); window.socketManager.on('aiassyResponse', this.handleBotResponse); window.socketManager.on('aiassyStatus', this.handleStateResponse); } removeSocketListeners = () => { window.socketManager.off('aiassyResponse', this.handleBotResponse); window.socketManager.off('aiassyStatus', this.handleStateResponse); } handleBotResponse = (msgId,response) => { this.setState(prevState => { // Check if a message with this msgId already exists const existingMessageIndex = prevState.messages.findIndex(msg => msg.msgId === msgId); let updatedMessages; if (existingMessageIndex !== -1 && msgId) { // If message with this msgId exists, append the response updatedMessages = [...prevState.messages]; updatedMessages[existingMessageIndex] = { ...updatedMessages[existingMessageIndex], text: updatedMessages[existingMessageIndex].text + response.content }; } else { // Create a new message console.log('ChatAssistant: handleBotResponse', msgId, response); if(response && response.content) { const newBotMessage = { id: Date.now(), msgId: msgId, sender: 'bot', text: response.content, }; updatedMessages = [...prevState.messages, newBotMessage]; } } // Store in window object window.chatMessages = updatedMessages; return { messages: updatedMessages, isTyping: false }; }); } handleStateResponse = (msgId,response) => { if(response == 'think') this.setState({ aiThink: true }); if(response == 'nothink') this.setState({ aiThink: false }); if(response == 'database') this.setState({ atDatabase: true }); if(response == 'nodatabase') this.setState({ atDatabase: false }); if(response == 'web') this.setState({ atWeb: true }); if(response == 'noweb') this.setState({ atWeb: false }); } scrollToBottom = () => { this.messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); } handleInputChange = (event) => { this.setState({ inputValue: event.target.value }); } handleSendMessage = () => { const userMessage = this.state.inputValue.trim(); if (!userMessage) return; const newUserMessage = { id: Date.now(), sender: 'user', text: userMessage, }; // Update messages in component state this.setState(prevState => { const updatedMessages = [...prevState.messages, newUserMessage]; // Store in window object window.chatMessages = updatedMessages; return { messages: updatedMessages, inputValue: '', isTyping: true }; }, () => { // Emit message to socket server after state is updated if (userMessage.trim()) { window.socketManager.emit('aiassyMessage', userMessage); } }); } handleKeyDown = (event) => { if (event.key === 'Enter') { this.handleSendMessage(); } } startRecording = async () => { 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", () => { if (audioChunks.length > 0) { const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); this.sendAudioMessage(audioBlob); } // Stop all tracks on the stream to release the microphone stream.getTracks().forEach(track => track.stop()); }); // Start recording mediaRecorder.start(); // Set up timer - limit to 60 seconds this.recordingTimer = setInterval(() => { this.setState(prevState => { const newTime = prevState.recordingTime + 1; // Auto-stop after 10 seconds if (newTime >= 10) { this.stopRecording(); } return { recordingTime: newTime }; }); }, 1000); this.setState({ isRecording: true, mediaRecorder, audioChunks, recordingTime: 0 }); } catch (err) { console.error("Error accessing microphone:", err); alert("Could not access microphone. Please check your browser permissions."); } }; stopRecording = () => { const { mediaRecorder, isRecording } = this.state; if (this.recordingTimer) { clearInterval(this.recordingTimer); } if (mediaRecorder && isRecording) { mediaRecorder.stop(); this.setState({ isRecording: false, recordingTime: 0 }); } }; sendAudioMessage = async (audioBlob) => { // Create a URL for the audio blob const audioUrl = URL.createObjectURL(audioBlob); // Create a user message with audio content const newUserMessage = { id: Date.now(), sender: 'user', text: ``, isAudio: true }; // Update UI with the audio message this.setState(prevState => { const updatedMessages = [...prevState.messages, newUserMessage]; // Store in window object window.chatMessages = updatedMessages; return { messages: updatedMessages, isTyping: true }; }); // Convert audio to base64 for sending to server const reader = new FileReader(); reader.readAsDataURL(audioBlob); reader.onloadend = () => { const base64Audio = reader.result.split(',')[1]; // Send audio data to server window.socketManager.emit('aiassyAudioMessage', { audio: base64Audio, format: 'wav' }); }; }; handleImageUpload = () => { this.fileInputRef.current?.click(); }; handleFileChange = (event) => { const file = event.target.files[0]; if (file && file.type.startsWith('image/')) { this.resizeAndSendImage(file); } // Reset the file input event.target.value = ''; }; resizeAndSendImage = (file) => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); img.onload = () => { // Calculate new dimensions (max 450px width/height) const maxSize = 450; let { width, height } = img; if (width > height) { if (width > maxSize) { height *= maxSize / width; width = maxSize; } } else { if (height > maxSize) { width *= maxSize / height; height = maxSize; } } // Set canvas dimensions canvas.width = width; canvas.height = height; // Draw and compress image ctx.drawImage(img, 0, 0, width, height); // Convert to blob with compression canvas.toBlob((blob) => { this.sendImageMessage(blob); }, 'image/jpeg', 0.8); }; img.src = URL.createObjectURL(file); }; sendImageMessage = async (imageBlob) => { // Create a URL for the image blob const imageUrl = URL.createObjectURL(imageBlob); // Create a user message with image content const newUserMessage = { id: Date.now(), sender: 'user', text: `Uploaded image`, isImage: true }; // Update UI with the image message this.setState(prevState => { const updatedMessages = [...prevState.messages, newUserMessage]; // Store in window object window.chatMessages = updatedMessages; return { messages: updatedMessages, isTyping: true }; }); // Convert image to base64 for sending to server const reader = new FileReader(); reader.readAsDataURL(imageBlob); reader.onloadend = () => { const base64Image = reader.result.split(',')[1]; // Send image data to server window.socketManager.emit('aiassyPicMessage', { image: base64Image, format: 'jpeg' }); }; }; formatTime = (seconds) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs < 10 ? '0' : ''}${secs}`; }; handlePrivacyConfirm = () => { sessionStorage.setItem('privacyConfirmed', 'true'); this.setState(prevState => { const updatedMessages = prevState.messages.filter(msg => msg.id !== 'privacy-prompt'); window.chatMessages = updatedMessages; return { privacyConfirmed: true, messages: updatedMessages }; }); }; formatMarkdown = (text) => { // Replace code blocks with formatted HTML return text.replace(/```(.*?)\n([\s\S]*?)```/g, (match, language, code) => { return `
${code.trim()}
`; }); }; getParseOptions = () => ({ replace: (domNode) => { // Convert tags to React Router Links if (domNode.name === 'a' && domNode.attribs && domNode.attribs.href) { const href = domNode.attribs.href; // Only convert internal links (not external URLs) if (!href.startsWith('http://') && !href.startsWith('https://') && !href.startsWith('//')) { return ( {domToReact(domNode.children, this.getParseOptions())} ); } } // Style pre/code blocks if (domNode.name === 'pre' && domNode.attribs && domNode.attribs.class === 'code-block') { const language = domNode.attribs['data-language'] || ''; return (
            {language && 
{language}
} {domToReact(domNode.children, this.getParseOptions())}
); } if (domNode.name === 'button' && domNode.attribs && domNode.attribs['data-confirm-privacy']) { return ; } } }); render() { const { open, onClose } = this.props; const { messages, inputValue, isTyping, isRecording, recordingTime, isGuest, privacyConfirmed } = this.state; if (!open) { return null; } const inputsDisabled = isGuest && !privacyConfirmed; return ( Assistent 🧠 🛢 🌐 {messages &&messages.map((message) => ( {message.sender === 'bot' && ( )} {message.text ? parse(this.formatMarkdown(message.text), this.getParseOptions()) : ''} {message.sender === 'user' && ( )} ))} {isTyping && ( )}
{this.formatTime(recordingTime)} ) } }} /> {isRecording ? ( ) : ( )} ); } } export default ChatAssistant;