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: ``,
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 (