330 lines
14 KiB
Plaintext
330 lines
14 KiB
Plaintext
<%- include('partials/header') %>
|
|
|
|
<div class="container py-5">
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-8">
|
|
<div class="card shadow-lg border-0 chat">
|
|
<div class="card-header bg-primary text-white">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h3 class="mb-0"><i class="fas fa-comments me-2"></i>Chat with EduCat AI</h3>
|
|
<button type="button" id="clear-chat-btn" class="btn btn-outline-light btn-sm" title="Clear Chat History">
|
|
<i class="fas fa-trash-alt me-1"></i>Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div id="chat-container" class="p-4" style="height: 500px; overflow-y: auto;">
|
|
<!-- Initial bot message will be added here if no history exists -->
|
|
</div>
|
|
<div class="border-top bg-light p-3">
|
|
<div class="input-group">
|
|
<input type="text" id="chat-input" class="form-control border-0 bg-white shadow-sm" placeholder="Type your message here..." style="border-radius: 25px 0 0 25px; padding: 12px 20px;">
|
|
<button type="button" id="send-btn" class="btn btn-primary px-4 border-0" style="border-radius: 0 25px 25px 0;">
|
|
<i class="fas fa-paper-plane"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mt-4">
|
|
<div class="col-md-6">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body text-center">
|
|
<i class="fas fa-question-circle fa-2x text-primary mb-3"></i>
|
|
<h6>Ask Questions</h6>
|
|
<p class="text-muted small">Get answers about your study materials</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body text-center">
|
|
<i class="fas fa-graduation-cap fa-2x text-success mb-3"></i>
|
|
<h6>Study Help</h6>
|
|
<p class="text-muted small">Get study tips and techniques</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Get chat history from server-side rendered data
|
|
const serverChatHistory = <%- JSON.stringify(chatHistory || []) %>;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const chatContainer = document.getElementById('chat-container');
|
|
const chatInput = document.getElementById('chat-input');
|
|
const sendBtn = document.getElementById('send-btn');
|
|
const clearChatBtn = document.getElementById('clear-chat-btn');
|
|
|
|
// Load existing chat history from server
|
|
loadChatHistory();
|
|
|
|
sendBtn.addEventListener('click', sendMessage);
|
|
chatInput.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
sendMessage();
|
|
}
|
|
});
|
|
|
|
clearChatBtn.addEventListener('click', clearChatHistory);
|
|
|
|
function loadChatHistory() {
|
|
// Clear the container first
|
|
chatContainer.innerHTML = '';
|
|
|
|
if (serverChatHistory.length === 0) {
|
|
// Show initial welcome message if no history exists
|
|
addWelcomeMessage();
|
|
} else {
|
|
// Load all previous messages
|
|
serverChatHistory.forEach(conversation => {
|
|
addMessageToChat('user', conversation.human, false);
|
|
addMessageToChat('bot', conversation.ai, false);
|
|
});
|
|
}
|
|
|
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
}
|
|
|
|
function addWelcomeMessage() {
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.className = 'chat-message bot-message mb-3';
|
|
const welcomeText = `Hello! I'm **EduCat AI**, your study assistant. I can help you with:
|
|
|
|
• Questions about your notes and study materials
|
|
• Study techniques and academic strategies
|
|
• Explanations of complex concepts
|
|
• Creating study plans and schedules
|
|
|
|
How can I assist you today?`;
|
|
const formattedWelcome = formatMessage(welcomeText, true);
|
|
|
|
messageDiv.innerHTML = `
|
|
<div class="d-flex align-items-start">
|
|
<div class="avatar bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-3 flex-shrink-0" style="width: 45px; height: 45px; min-width: 45px;">
|
|
<i class="fas fa-cat" style="font-size: 1.2rem;"></i>
|
|
</div>
|
|
<div class="message-content flex-grow-1">
|
|
<div class="message-bubble bg-light p-3 rounded-3 shadow-sm border">
|
|
<div class="message-text">${formattedWelcome}</div>
|
|
</div>
|
|
<small class="text-muted d-block mt-1">Just now</small>
|
|
</div>
|
|
</div>
|
|
`;
|
|
chatContainer.appendChild(messageDiv);
|
|
}
|
|
|
|
function sendMessage() {
|
|
const message = chatInput.value.trim();
|
|
if (!message) return;
|
|
|
|
// Add user message to chat
|
|
addMessageToChat('user', message);
|
|
chatInput.value = '';
|
|
|
|
// Add typing indicator
|
|
addTypingIndicator();
|
|
|
|
// Send message to AI
|
|
sendToAI(message);
|
|
}
|
|
|
|
function formatMessage(text, isBot = false) {
|
|
if (!isBot) {
|
|
// For user messages, just escape HTML and preserve basic formatting
|
|
return text.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/\n/g, '<br>');
|
|
}
|
|
|
|
// For bot messages, apply rich formatting
|
|
let formatted = text
|
|
// Escape HTML first
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
|
|
// Convert markdown-style formatting
|
|
// Bold text **text** or __text__
|
|
formatted = formatted.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
formatted = formatted.replace(/__(.*?)__/g, '<strong>$1</strong>');
|
|
|
|
// Italic text *text* or _text_
|
|
formatted = formatted.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
|
|
formatted = formatted.replace(/(?<!_)_([^_]+)_(?!_)/g, '<em>$1</em>');
|
|
|
|
// Code blocks ```code```
|
|
formatted = formatted.replace(/```([\s\S]*?)```/g, '<pre class="code-block"><code>$1</code></pre>');
|
|
|
|
// Inline code `code`
|
|
formatted = formatted.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
|
|
|
|
// Headers
|
|
formatted = formatted.replace(/^### (.*$)/gm, '<h5 class="mt-3 mb-2">$1</h5>');
|
|
formatted = formatted.replace(/^## (.*$)/gm, '<h4 class="mt-3 mb-2">$1</h4>');
|
|
formatted = formatted.replace(/^# (.*$)/gm, '<h3 class="mt-3 mb-2">$1</h3>');
|
|
|
|
// Convert bullet points and numbered lists
|
|
// Handle bullet points: - item, * item, • item
|
|
formatted = formatted.replace(/^[\s]*[-*•]\s+(.+)$/gm, '<li class="bullet-item">$1</li>');
|
|
|
|
// Handle numbered lists: 1. item, 2. item, etc.
|
|
formatted = formatted.replace(/^[\s]*\d+\.\s+(.+)$/gm, '<li class="numbered-item">$1</li>');
|
|
|
|
// Wrap consecutive list items in proper list containers
|
|
formatted = formatted.replace(/(<li class="bullet-item">.*?<\/li>)(?:\s*<li class="bullet-item">.*?<\/li>)*/gs, function(match) {
|
|
return '<ul class="formatted-list">' + match + '</ul>';
|
|
});
|
|
|
|
formatted = formatted.replace(/(<li class="numbered-item">.*?<\/li>)(?:\s*<li class="numbered-item">.*?<\/li>)*/gs, function(match) {
|
|
return '<ol class="formatted-list">' + match.replace(/class="numbered-item"/g, 'class="list-item"') + '</ol>';
|
|
});
|
|
|
|
// Convert line breaks to <br> but not inside <pre> or list tags
|
|
let parts = formatted.split(/(<pre[\s\S]*?<\/pre>|<ul[\s\S]*?<\/ul>|<ol[\s\S]*?<\/ol>)/);
|
|
for (let i = 0; i < parts.length; i += 2) {
|
|
parts[i] = parts[i].replace(/\n\s*\n/g, '<br><br>').replace(/\n/g, '<br>');
|
|
}
|
|
formatted = parts.join('');
|
|
|
|
return formatted;
|
|
}
|
|
|
|
function addMessageToChat(sender, message, withScroll = true) {
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.className = `chat-message ${sender}-message mb-3`;
|
|
|
|
const time = new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|
const formattedMessage = formatMessage(message, sender === 'bot');
|
|
|
|
if (sender === 'user') {
|
|
messageDiv.innerHTML = `
|
|
<div class="d-flex align-items-start justify-content-end">
|
|
<div class="message-content me-3 flex-grow-1" style="max-width: 70%;">
|
|
<div class="message-bubble bg-primary text-white p-3 rounded-3 shadow-sm">
|
|
<div class="message-text">${formattedMessage}</div>
|
|
</div>
|
|
<small class="text-muted d-block text-end mt-1">${time}</small>
|
|
</div>
|
|
<div class="avatar bg-secondary text-white rounded-circle d-flex align-items-center justify-content-center flex-shrink-0" style="width: 45px; height: 45px; min-width: 45px;">
|
|
<i class="fas fa-user" style="font-size: 1.1rem;"></i>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
messageDiv.innerHTML = `
|
|
<div class="d-flex align-items-start">
|
|
<div class="avatar bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-3 flex-shrink-0" style="width: 45px; height: 45px; min-width: 45px;">
|
|
<i class="fas fa-cat" style="font-size: 1.2rem;"></i>
|
|
</div>
|
|
<div class="message-content flex-grow-1" style="max-width: 70%;">
|
|
<div class="message-bubble bg-light p-3 rounded-3 shadow-sm border">
|
|
<div class="message-text">${formattedMessage}</div>
|
|
</div>
|
|
<small class="text-muted d-block mt-1">${time}</small>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
chatContainer.appendChild(messageDiv);
|
|
if (withScroll) {
|
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
}
|
|
}
|
|
|
|
function addTypingIndicator() {
|
|
const typingDiv = document.createElement('div');
|
|
typingDiv.id = 'typing-indicator';
|
|
typingDiv.className = 'chat-message bot-message mb-3';
|
|
typingDiv.innerHTML = `
|
|
<div class="d-flex align-items-start">
|
|
<div class="avatar bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-3 flex-shrink-0" style="width: 45px; height: 45px; min-width: 45px;">
|
|
<i class="fas fa-cat" style="font-size: 1.2rem;"></i>
|
|
</div>
|
|
<div class="message-content flex-grow-1">
|
|
<div class="message-bubble bg-light p-3 rounded-3 shadow-sm border">
|
|
<div class="typing-dots">
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
chatContainer.appendChild(typingDiv);
|
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
}
|
|
|
|
function removeTypingIndicator() {
|
|
const typingIndicator = document.getElementById('typing-indicator');
|
|
if (typingIndicator) {
|
|
typingIndicator.remove();
|
|
}
|
|
}
|
|
|
|
async function sendToAI(message) {
|
|
try {
|
|
const response = await fetch('/api/chat', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
message: message
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
removeTypingIndicator();
|
|
|
|
if (result.success) {
|
|
addMessageToChat('bot', result.response);
|
|
} else {
|
|
addMessageToChat('bot', 'Sorry, I encountered an error. Please try again.');
|
|
}
|
|
} catch (error) {
|
|
removeTypingIndicator();
|
|
addMessageToChat('bot', 'Sorry, I\'m having trouble connecting right now. Please try again.');
|
|
}
|
|
}
|
|
|
|
async function clearChatHistory() {
|
|
if (!confirm('Are you sure you want to clear the chat history? This action cannot be undone.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/chat/history', {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
// Clear the chat container and show welcome message
|
|
chatContainer.innerHTML = '';
|
|
addWelcomeMessage();
|
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
} else {
|
|
alert('Failed to clear chat history. Please try again.');
|
|
}
|
|
} catch (error) {
|
|
alert('Error clearing chat history. Please try again.');
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<%- include('partials/footer') %>
|