533 lines
22 KiB
HTML
533 lines
22 KiB
HTML
{% 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-phoenix-primary suggestion-chip">{{ _("How many cars are in inventory") }}?</button>
|
||
<button class="btn btn-sm btn-phoenix-primary suggestion-chip">{{ _("Show me sales analysis") }}</button>
|
||
<button class="btn btn-sm btn-phoenix-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 %}
|