haikal/templates/haikalbot/chatbot.html
Marwan Alwali 250e0aa7bb update
2025-05-26 15:17:10 +03:00

458 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends 'base.html' %}
{% load i18n static %}
{% block title %}
{{ _("Haikalbot") }}
{% endblock %}
{% block description %}
AI assistant
{% endblock %}
{% block customCSS %}
<!-- No custom CSS as requested -->
{% endblock %}
{% block content %}
<style>
.chat-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.chat-textarea {
border-radius: 20px;
padding: 15px;
resize: none;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
border: 1px solid #dee2e6;
transition: all 0.3s ease;
}
.chat-textarea:focus {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
outline: none;
}
.send-button {
border-radius: 20px;
padding: 10px 25px;
font-weight: 500;
transition: all 0.3s ease;
}
.textarea-container {
position: relative;
}
.textarea-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
font-size: 0.8rem;
color: #6c757d;
}
</style>
<div class="card shadow-none mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center fw-bolder fs-3 d-inline-block">
<img class="d-dark-none" src="{% static 'images/favicons/haikalbot_v1.png' %}" alt="{% trans 'home' %}" width="32" />
<img class="d-light-none" src="{% static 'images/favicons/haikalbot_v2.png' %}" alt="{% trans 'home' %}" width="32" />
</div>
<div class="d-flex gap-3">
<span id="clearChatBtn" class="translate-middle-y cursor-pointer" title="{% if LANGUAGE_CODE == 'ar' %}مسح المحادثة{% else %}Clear Chat{% endif %}">
<i class="fas fa-trash-alt text-danger"></i>
</span>
<span id="exportChatBtn" class="translate-middle-y cursor-pointer" title="{% if LANGUAGE_CODE == 'ar' %}تصدير المحادثة{% else %}Export Chat{% endif %}">
<i class="fas fa-download text-success"></i>
</span>
</div>
</div>
<div class="card-body p-0">
<div id="chatMessages" class="overflow-auto p-3" style="height: 60vh;">
<!-- Chat messages will be appended here -->
</div>
<div class="bg-100 border-top p-3">
<div class="d-flex gap-2 flex-wrap mb-3" id="suggestionChips">
<button class="btn btn-sm btn-outline-primary suggestion-chip">
{{ _("How many cars are in inventory")}}?
</button>
<button class="btn btn-sm btn-outline-primary suggestion-chip">
{{ _("Show me sales analysis")}}
</button>
<button class="btn btn-sm btn-outline-primary suggestion-chip">
{{ _("What are the best-selling cars")}}?
</button>
</div>
<div class="chat-container">
<div class="textarea-container mb-3">
<label for="messageInput"></label>
<textarea class="form-control chat-textarea" id="messageInput" rows="3"
placeholder="{{ _("Type your message here")}}..."></textarea>
<div class="textarea-footer">
<div class="character-count">
<span id="charCount">0</span>/400
</div>
<span class="send-button position-absolute top-50 end-0 translate-middle-y cursor-pointer"
id="sendMessageBtn" disabled>
<i class="fas fa-paper-plane text-body"></i>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
$(document).ready(function() {
const messageInput = $('#messageInput');
const charCount = $('#charCount');
const sendMessageBtn = $('#sendMessageBtn');
const chatMessages = $('#chatMessages');
const clearChatBtn = $('#clearChatBtn');
const exportChatBtn = $('#exportChatBtn');
const suggestionChips = $('.suggestion-chip');
// Enable/disable send button based on input
messageInput.on('input', function() {
sendMessageBtn.prop('disabled', !messageInput.val().trim());
// Auto-resize textarea
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
});
// Send message on Enter key (but allow Shift+Enter for new line)
messageInput.on('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (!sendMessageBtn.prop('disabled')) {
sendMessage();
}
}
});
// Send message on button click
sendMessageBtn.on('click', sendMessage);
// Use suggestion chips
suggestionChips.on('click', function() {
messageInput.val($(this).text().trim());
sendMessageBtn.prop('disabled', false);
sendMessage();
});
// Clear chat
clearChatBtn.on('click', function() {
if (confirm('{% if LANGUAGE_CODE == "ar" %}هل أنت متأكد من أنك تريد مسح المحادثة؟{% else %}Are you sure you want to clear the chat?{% endif %}')) {
// Keep only the first welcome message
const welcomeMessage = chatMessages.children().first();
chatMessages.empty().append(welcomeMessage);
}
});
// Export chat
exportChatBtn.on('click', function() {
let chatContent = '';
$('.message').each(function() {
const isUser = $(this).hasClass('user-message');
const sender = isUser ? '{% if LANGUAGE_CODE == "ar" %}أنت{% else %}You{% endif %}' : '{% if LANGUAGE_CODE == "ar" %}المساعد الذكي{% else %}AI Assistant{% endif %}';
const text = $(this).find('.chat-message').text().trim();
const time = $(this).find('.text-400').text().trim();
chatContent += `${sender} (${time}):\n${text}\n\n`;
});
// Create and trigger download
const blob = new Blob([chatContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'chat-export-' + new Date().toISOString().slice(0, 10) + '.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
// Copy message text
$(document).on('click', '.copy-btn', function() {
const text = $(this).closest('.d-flex').find('.chat-message').text().trim();
navigator.clipboard.writeText(text).then(() => {
// Show temporary success indicator
const originalIcon = $(this).html();
$(this).html('<i class="fas fa-check"></i>');
setTimeout(() => {
$(this).html(originalIcon);
}, 1500);
});
});
// Function to send message
function sendMessage() {
const message = messageInput.val().trim();
if (!message) return;
// Add user message to chat
addMessage(message, true);
// Clear input and reset height
messageInput.val('').css('height', 'auto');
sendMessageBtn.prop('disabled', true);
// Show typing indicator
showTypingIndicator();
// Send to backend
$.ajax({
url: '{% url "haikalbot:haikalbot" %}',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
prompt: message,
language: '{{ LANGUAGE_CODE }}'
}),
headers: {
'X-CSRFToken': '{{ csrf_token }}'
},
success: function(response) {
// Hide typing indicator
hideTypingIndicator();
// Process response
let botResponse = '';
if (response.response) {
botResponse = response.response;
} else if (response.insights) {
// Format insights as a readable response
botResponse = formatInsightsResponse(response);
} else {
botResponse = '{% if LANGUAGE_CODE == "ar" %}عذرًا، لم أتمكن من معالجة طلبك.{% else %}Sorry, I couldn\'t process your request.{% endif %}';
}
// Add bot response to chat
addMessage(botResponse, false);
// Scroll to bottom
scrollToBottom();
},
error: function(xhr, status, error) {
// Hide typing indicator
hideTypingIndicator();
// Add error message
const errorMsg = '{% if LANGUAGE_CODE == "ar" %}عذرًا، حدث خطأ أثناء معالجة طلبك. يرجى المحاولة مرة أخرى.{% else %}Sorry, an error occurred while processing your request. Please try again.{% endif %}';
addMessage(errorMsg, false);
console.error('Error:', error);
}
});
}
// Function to add message to chat
function addMessage(text, isUser) {
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const messageClass = isUser ? 'user-message justify-content-end' : '';
let avatarHtml = '';
let messageHtml = '';
if (isUser) {
// User message
avatarHtml = `
<div class="avatar avatar-l ms-3 order-1">
<div class="avatar-name rounded-circle bg-primary text-white"><span><i class="fas fa-user"></i></span></div>
</div>
`;
// Process text (no markdown for user messages)
messageHtml = `
<div class="flex-1 order-0">
<div class="w-xxl-75 ms-auto">
<div class="d-flex hover-actions-trigger align-items-center">
<div class="hover-actions start-0 top-50 translate-middle-y">
<button class="btn btn-phoenix-secondary btn-icon fs--2 round-btn copy-btn" type="button" title="{% if LANGUAGE_CODE == 'ar' %}نسخ{% else %}Copy{% endif %}">
<i class="fas fa-copy"></i>
</button>
</div>
<div class="chat-message p-3 rounded-2">
${text}
</div>
</div>
<div class="text-400 fs--2">
${time}
</div>
</div>
</div>
`;
} else {
// Bot message
avatarHtml = `
<div class="me-3">
<div class="d-flex align-items-center fw-bolder fs-3 d-inline-block">
<img class="d-dark-none" src="{% static 'images/favicons/haikalbot_v1.png' %}" width="32" />
<img class="d-light-none" src="{% static 'images/favicons/haikalbot_v2.png' %}" width="32" />
</div>
</div>
`;
// Process markdown for bot messages
const processedText = marked.parse(text);
messageHtml = `
<div class="flex-1">
<div class="w-xxl-75">
<div class="d-flex hover-actions-trigger align-items-center">
<div class="chat-message bg-200 p-3 rounded-2">
${processedText}
</div>
<div class="hover-actions end-0 top-50 translate-middle-y">
<button class="btn btn-phoenix-secondary btn-icon fs--2 round-btn copy-btn" type="button" title="{{_("Copy")}}">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="text-400 fs--2 text-end">
${time}
</div>
</div>
</div>
`;
}
const fullMessageHtml = `
<div class="message d-flex mb-3 ${messageClass}">
${avatarHtml}
${messageHtml}
</div>
`;
chatMessages.append(fullMessageHtml);
scrollToBottom();
}
// Function to show typing indicator
function showTypingIndicator() {
const typingHtml = `
<div class="message d-flex mb-3" id="typingIndicator">
<div class="avatar avatar-l me-3">
<div class="avatar-name rounded-circle"><span><i class="fas fa-robot"></i></span></div>
</div>
<div class="flex-1 d-flex align-items-center">
<div class="spinner-border text-phoenix-secondary me-2" role="status"></div>
<span class="fs-9">{% if LANGUAGE_CODE == 'ar' %}جاري الكتابة...{% else %}Typing...{% endif %}</span>
</div>
</div>
`;
chatMessages.append(typingHtml);
scrollToBottom();
}
// Function to hide typing indicator
function hideTypingIndicator() {
$('#typingIndicator').remove();
}
// Function to scroll chat to bottom
function scrollToBottom() {
chatMessages.scrollTop(chatMessages[0].scrollHeight);
}
// Function to format insights response
function formatInsightsResponse(response) {
let formattedResponse = '';
const insightsKey = '{{ LANGUAGE_CODE }}' === 'ar' ? 'التحليلات' : 'insights';
const recsKey = '{{ LANGUAGE_CODE }}' === 'ar' ? 'التوصيات' : 'recommendations';
if (response[insightsKey] && response[insightsKey].length > 0) {
formattedResponse += '{{ LANGUAGE_CODE }}' === 'ar' ? '## نتائج التحليل\n\n' : '## Analysis Results\n\n';
response[insightsKey].forEach(insight => {
if (insight.type) {
formattedResponse += `### ${insight.type}\n\n`;
}
if (insight.results) {
insight.results.forEach(result => {
if (result.error) {
formattedResponse += `- **${result.model || ''}**: ${result.error}\n`;
} else if (result.count !== undefined) {
formattedResponse += `- **${result.model || ''}**: ${result.count}\n`;
} else if (result.value !== undefined) {
const fieldKey = '{{ LANGUAGE_CODE }}' === 'ar' ? 'الحقل' : 'field';
const statTypeKey = '{{ LANGUAGE_CODE }}' === 'ar' ? 'نوع_الإحصاء' : 'statistic_type';
formattedResponse += `- **${result.model || ''}**: ${result[statTypeKey]} of ${result[fieldKey]} = ${result.value}\n`;
}
});
formattedResponse += '\n';
}
if (insight.relationships) {
formattedResponse += '{{ LANGUAGE_CODE }}' === 'ar' ? ' العلاقات:\n\n' : ' Relationships:\n\n';
insight.relationships.forEach(rel => {
const fromKey = '{{ LANGUAGE_CODE }}' === 'ar' ? 'من' : 'from';
const toKey = '{{ LANGUAGE_CODE }}' === 'ar' ? 'إلى' : 'to';
const typeKey = '{{ LANGUAGE_CODE }}' === 'ar' ? 'نوع' : 'type';
formattedResponse += `- ${rel[fromKey]}${rel[toKey]} (${rel[typeKey]})\n`;
});
formattedResponse += '\n';
}
});
}
if (response[recsKey] && response[recsKey].length > 0) {
formattedResponse += '{{ LANGUAGE_CODE }}' === 'ar' ? ' التوصيات\n\n' : ' Recommendations\n\n';
response[recsKey].forEach(rec => {
formattedResponse += `- ${rec}\n`;
});
}
return formattedResponse || '{{ LANGUAGE_CODE }}' === 'ar' ? 'تم تحليل البيانات بنجاح.' : 'Data analyzed successfully.';
}
// Initialize
scrollToBottom();
});
messageInput.addEventListener('input', function() {
const currentLength = this.value.length;
charCount.textContent = currentLength;
// Optional: Add warning when approaching limit
if (currentLength > 350) {
charCount.style.color = 'red';
} else {
charCount.style.color = 'inherit';
}
});
</script>
{% endblock %}
{% block customJS %}
<!-- JS will be loaded from static file or added separately -->
{% endblock %}