haikal/templates/haikalbot/chatbot.html
Marwan Alwali 56cfbad80e update
2025-05-29 21:42:27 +03:00

521 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 title %}
{% block description %}
AI assistant
{% endblock description %}
{% block customCSS %}
<style>
.chart-container {
width: 100%;
margin-top: 1rem;
}
.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;
}
.character-count.warning {
color: #dc3545;
font-weight: bold;
}
</style>
{% endblock customCSS %}
{% block content %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<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;"></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")}}..." maxlength="400"></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>
// Global configuration
const MAX_MESSAGE_LENGTH = 400;
const WARNING_THRESHOLD = 350;
const isArabic = '{{ LANGUAGE_CODE }}' === 'ar';
// Chart rendering function
function renderInsightChart(labels, data, chartType = 'bar', title = 'Insight Chart') {
const canvasId = 'chart_' + Date.now();
const chartHtml = `<div class="chart-container"><canvas id="${canvasId}"></canvas></div>`;
$('#chatMessages').append(chartHtml);
new Chart(document.getElementById(canvasId), {
type: chartType,
data: {
labels: labels,
datasets: [{
label: title,
data: data,
borderWidth: 1,
backgroundColor: 'rgba(75, 192, 192, 0.5)',
borderColor: 'rgba(75, 192, 192, 1)',
}]
},
options: {
responsive: true,
plugins: {
legend: { display: true },
title: { display: true, text: title }
},
scales: {
y: { beginAtZero: true }
}
}
});
}
$(document).ready(function() {
// DOM elements
const messageInput = $('#messageInput');
const charCount = $('#charCount');
const sendMessageBtn = $('#sendMessageBtn');
const chatMessages = $('#chatMessages');
const clearChatBtn = $('#clearChatBtn');
const exportChatBtn = $('#exportChatBtn');
const suggestionChips = $('.suggestion-chip');
// Initialize character count
updateCharacterCount();
// Event handlers
messageInput.on('input', handleInput);
messageInput.on('keydown', handleKeyDown);
sendMessageBtn.on('click', sendMessage);
suggestionChips.on('click', handleSuggestionClick);
clearChatBtn.on('click', clearChat);
exportChatBtn.on('click', exportChat);
// Input handling
function handleInput() {
updateCharacterCount();
autoResizeTextarea();
toggleSendButton();
}
function handleKeyDown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (!sendMessageBtn.prop('disabled')) {
sendMessage();
}
}
}
function handleSuggestionClick() {
messageInput.val($(this).text().trim());
updateCharacterCount();
autoResizeTextarea();
sendMessageBtn.prop('disabled', false);
sendMessage();
}
// UI utilities
function updateCharacterCount() {
const currentLength = messageInput.val().length;
charCount.text(currentLength);
if (currentLength > WARNING_THRESHOLD) {
charCount.parent().addClass('warning');
} else {
charCount.parent().removeClass('warning');
}
}
function autoResizeTextarea() {
messageInput[0].style.height = 'auto';
messageInput[0].style.height = (messageInput[0].scrollHeight) + 'px';
}
function toggleSendButton() {
sendMessageBtn.prop('disabled', !messageInput.val().trim());
}
// Chat actions
function sendMessage() {
const message = messageInput.val().trim();
if (!message) return;
// Add user message to chat
addMessage(message, true);
// Clear input and reset UI
resetInputUI();
// 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) {
processBotResponse(response);
},
error: function(xhr, status, error) {
handleRequestError(error);
}
});
}
function resetInputUI() {
messageInput.val('').css('height', 'auto');
charCount.text('0').parent().removeClass('warning');
sendMessageBtn.prop('disabled', true);
}
function processBotResponse(response) {
hideTypingIndicator();
// Debug response structure
console.log("API Response:", response);
let botResponse = '';
// Check for direct response first
if (response.response) {
botResponse = response.response;
}
// Then check for insights data
else if (hasInsightsData(response)) {
botResponse = formatInsightsResponse(response);
}
// Fallback
else {
botResponse = isArabic ?
'عذرًا، لم أتمكن من معالجة طلبك. يبدو أن هيكل الاستجابة غير متوقع.' :
'Sorry, I couldn\'t process your request. The response structure appears unexpected.';
console.error("Unexpected response structure:", response);
}
addMessage(botResponse, false);
scrollToBottom();
}
function hasInsightsData(response) {
return response.insights || response['التحليلات'] ||
response.recommendations || response['التوصيات'];
}
function handleRequestError(error) {
hideTypingIndicator();
const errorMsg = isArabic ?
'عذرًا، حدث خطأ أثناء معالجة طلبك. يرجى المحاولة مرة أخرى.' :
'Sorry, an error occurred while processing your request. Please try again.';
addMessage(errorMsg, false);
console.error('API Error:', error);
}
// Chat management
function clearChat() {
if (confirm(isArabic ?
'هل أنت متأكد من أنك تريد مسح المحادثة؟' :
'Are you sure you want to clear the chat?')) {
const welcomeMessage = chatMessages.children().first();
chatMessages.empty().append(welcomeMessage);
}
}
function exportChat() {
let chatContent = '';
$('.message').each(function() {
const isUser = $(this).hasClass('user-message');
const sender = isUser ?
(isArabic ? 'أنت' : 'You') :
(isArabic ? 'المساعد الذكي' : 'AI Assistant');
const text = $(this).find('.chat-message').text().trim();
const time = $(this).find('.text-400').text().trim();
chatContent += `${sender} (${time}):\n${text}\n\n`;
});
downloadTextFile(chatContent, 'chat-export-' + new Date().toISOString().slice(0, 10) + '.txt');
}
function downloadTextFile(content, filename) {
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// Message display functions
function addMessage(text, isUser) {
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const messageClass = isUser ? 'user-message justify-content-between' : '';
const avatarHtml = getAvatarHtml(isUser);
const messageHtml = getMessageHtml(text, isUser, time);
const fullMessageHtml = `
<div class="message d-flex mb-3 ${messageClass}">
${avatarHtml}
${messageHtml}
</div>
`;
chatMessages.append(fullMessageHtml);
scrollToBottom();
}
function getAvatarHtml(isUser) {
if (isUser) {
return `
<div class="avatar avatar-l ms-3 order-1">
<div class="avatar-name rounded-circle">
<span><i class="fas fa-user"></i></span>
</div>
</div>
`;
}
return `
<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>
`;
}
function getMessageHtml(text, isUser, time) {
if (isUser) {
return `
<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="${isArabic ? 'نسخ' : 'Copy'}">
<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>
`;
}
const processedText = marked.parse(text);
return `
<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="${isArabic ? 'نسخ' : 'Copy'}">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="text-400 fs--2 text-end">
${time}
</div>
</div>
</div>
`;
}
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">${isArabic ? 'جاري الكتابة...' : 'Typing...'}</span>
</div>
</div>
`;
chatMessages.append(typingHtml);
scrollToBottom();
}
function hideTypingIndicator() {
$('#typingIndicator').remove();
}
function scrollToBottom() {
chatMessages.scrollTop(chatMessages[0].scrollHeight);
}
// Insights formatting
function formatInsightsResponse(response) {
console.log("Formatting insights response:", response);
let formattedResponse = '';
// Get data using both possible key formats
const insightsData = response.insights || response['التحليلات'] || [];
const recommendationsData = response.recommendations || response['التوصيات'] || [];
// Process insights
if (insightsData.length > 0) {
formattedResponse += isArabic ? '## نتائج التحليل\n\n' : '## Analysis Results\n\n';
insightsData.forEach(insight => {
if (insight.type) {
formattedResponse += `### ${insight.type}\n\n`;
}
if (insight.results && Array.isArray(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 field = getLocalizedValue(result, 'field', 'الحقل');
const statType = getLocalizedValue(result, 'statistic_type', 'نوع_الإحصاء');
formattedResponse += `- **${result.model || ''}**: ${statType} ${isArabic ? 'لـ' : 'of'} ${field} = ${result.value}\n`;
}
});
formattedResponse += '\n';
}
if (insight.relationships && Array.isArray(insight.relationships)) {
formattedResponse += isArabic ? '### العلاقات\n\n' : '### Relationships\n\n';
insight.relationships.forEach(rel => {
const from = getLocalizedValue(rel, 'from', 'من');
const to = getLocalizedValue(rel, 'to', 'إلى');
const type = getLocalizedValue(rel, 'type', 'نوع');
formattedResponse += `- ${from}${to} (${type})\n`;
});
formattedResponse += '\n';
}
});
}
// Process recommendations
if (recommendationsData.length > 0) {
formattedResponse += isArabic ? '## التوصيات\n\n' : '## Recommendations\n\n';
recommendationsData.forEach(rec => {
formattedResponse += `- ${rec}\n`;
});
}
return formattedResponse.trim() ||
(isArabic ? 'تم تحليل البيانات بنجاح ولكن لا توجد نتائج للعرض.' : 'Data analyzed successfully but no results to display.');
}
function getLocalizedValue(obj, englishKey, arabicKey) {
return isArabic ? (obj[arabicKey] || obj[englishKey] || '') : (obj[englishKey] || obj[arabicKey] || '');
}
$(document).on('click', '.copy-btn', function() {
const text = $(this).closest('.d-flex').find('.chat-message').text().trim();
navigator.clipboard.writeText(text).then(() => {
showCopySuccess($(this));
});
});
function showCopySuccess(button) {
const originalIcon = button.html();
button.html('<i class="fas fa-check"></i>');
setTimeout(() => {
button.html(originalIcon);
}, 1500);
}
scrollToBottom();
});
</script>
{% endblock content %}