458 lines
18 KiB
HTML
458 lines
18 KiB
HTML
{% 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 %}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|