update
This commit is contained in:
parent
250e0aa7bb
commit
d26f777c73
@ -1,49 +1,31 @@
|
||||
from django.db.models import Avg, Sum, Max, Min, ForeignKey, OneToOneField
|
||||
import inspect
|
||||
import hashlib
|
||||
from django.db import models
|
||||
from django.db.models import Avg, Sum, Max, Min, ForeignKey, OneToOneField, Count
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
def _localized_keys(language):
|
||||
"""
|
||||
Get localized key names based on language.
|
||||
|
||||
:param language: Language code ('en' or 'ar')
|
||||
:type language: str
|
||||
:return: Dictionary of localized keys
|
||||
:rtype: dict
|
||||
"""
|
||||
if language == 'ar':
|
||||
return {
|
||||
'type': 'نوع', 'model': 'النموذج', 'count': 'العدد', 'filters': 'الفلاتر_المطبقة',
|
||||
'error': 'خطأ', 'chart_type': 'نوع_الرسم_البياني', 'labels': 'التسميات', 'data': 'البيانات',
|
||||
'visualization_data': 'بيانات_الرسم_البياني', 'field': 'الحقل', 'value': 'القيمة',
|
||||
'statistic_type': 'نوع_الإحصاء', 'results': 'النتائج', 'title': 'العنوان'
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'type': 'type', 'model': 'model', 'count': 'count', 'filters': 'filters_applied',
|
||||
'error': 'error', 'chart_type': 'chart_type', 'labels': 'labels', 'data': 'data',
|
||||
'visualization_data': 'visualization_data', 'field': 'field', 'value': 'value',
|
||||
'statistic_type': 'statistic_type', 'results': 'results', 'title': 'title'
|
||||
}
|
||||
return {
|
||||
'type': 'نوع' if language == 'ar' else 'type',
|
||||
'model': 'النموذج' if language == 'ar' else 'model',
|
||||
'count': 'العدد' if language == 'ar' else 'count',
|
||||
'filters': 'الفلاتر_المطبقة' if language == 'ar' else 'filters_applied',
|
||||
'error': 'خطأ' if language == 'ar' else 'error',
|
||||
'chart_type': 'نوع_الرسم_البياني' if language == 'ar' else 'chart_type',
|
||||
'labels': 'التسميات' if language == 'ar' else 'labels',
|
||||
'data': 'البيانات' if language == 'ar' else 'data',
|
||||
'visualization_data': 'بيانات_الرسم_البياني' if language == 'ar' else 'visualization_data',
|
||||
'field': 'الحقل' if language == 'ar' else 'field',
|
||||
'value': 'القيمة' if language == 'ar' else 'value',
|
||||
'statistic_type': 'نوع_الإحصاء' if language == 'ar' else 'statistic_type',
|
||||
'results': 'النتائج' if language == 'ar' else 'results',
|
||||
'title': 'العنوان' if language == 'ar' else 'title',
|
||||
}
|
||||
|
||||
|
||||
def generate_count_insight(models, query_params, dealer_id=None, language='en'):
|
||||
"""
|
||||
Generate count insights for the specified models.
|
||||
|
||||
:param models: List of models to analyze
|
||||
:type models: list
|
||||
:param query_params: Query parameters for filtering
|
||||
:type query_params: dict
|
||||
:param dealer_id: Dealer ID for filtering
|
||||
:type dealer_id: int
|
||||
:param language: Language code ('en' or 'ar')
|
||||
:type language: str
|
||||
:return: Count insights
|
||||
:rtype: dict
|
||||
"""
|
||||
keys = _localized_keys(language)
|
||||
results = []
|
||||
|
||||
@ -59,7 +41,9 @@ def generate_count_insight(models, query_params, dealer_id=None, language='en'):
|
||||
|
||||
filters = {}
|
||||
for key, value in query_params.items():
|
||||
if key not in ['field', 'operation'] and hasattr(model, key):
|
||||
if key in ['field', 'operation']:
|
||||
continue
|
||||
if hasattr(model, key):
|
||||
try:
|
||||
field = model._meta.get_field(key)
|
||||
if isinstance(field, models.IntegerField):
|
||||
@ -76,13 +60,12 @@ def generate_count_insight(models, query_params, dealer_id=None, language='en'):
|
||||
results.append({
|
||||
keys['model']: model.__name__,
|
||||
keys['count']: queryset.count(),
|
||||
keys['filters']: filters
|
||||
keys['filters']: filters,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
results.append({
|
||||
keys['model']: model.__name__,
|
||||
keys['error']: str(e)
|
||||
keys['error']: str(e),
|
||||
})
|
||||
|
||||
return {
|
||||
@ -91,30 +74,17 @@ def generate_count_insight(models, query_params, dealer_id=None, language='en'):
|
||||
keys['visualization_data']: {
|
||||
keys['chart_type']: 'bar',
|
||||
keys['labels']: [r[keys['model']] for r in results if keys['count'] in r],
|
||||
keys['data']: [r[keys['count']] for r in results if keys['count'] in r]
|
||||
keys['data']: [r[keys['count']] for r in results if keys['count'] in r],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def generate_statistics_insight(models, query_params, dealer_id=None, language='en'):
|
||||
"""
|
||||
Generate statistical insights for the specified models.
|
||||
|
||||
:param models: List of models to analyze
|
||||
:type models: list
|
||||
:param query_params: Query parameters for filtering
|
||||
:type query_params: dict
|
||||
:param dealer_id: Dealer ID for filtering
|
||||
:type dealer_id: int
|
||||
:param language: Language code ('en' or 'ar')
|
||||
:type language: str
|
||||
:return: Statistical insights
|
||||
:rtype: dict
|
||||
"""
|
||||
keys = _localized_keys(language)
|
||||
results = []
|
||||
field = query_params.get('field')
|
||||
operation = query_params.get('operation', 'average')
|
||||
stat_map = {'average': Avg, 'sum': Sum, 'max': Max, 'min': Min}
|
||||
|
||||
for model in models:
|
||||
try:
|
||||
@ -128,37 +98,28 @@ def generate_statistics_insight(models, query_params, dealer_id=None, language='
|
||||
elif hasattr(model, 'dealer'):
|
||||
queryset = queryset.filter(dealer=dealer_id)
|
||||
|
||||
filters = {}
|
||||
for k, v in query_params.items():
|
||||
if k not in ['field', 'operation'] and hasattr(model, k):
|
||||
filters[k] = v
|
||||
filters = {
|
||||
k: v for k, v in query_params.items()
|
||||
if k not in ['field', 'operation'] and hasattr(model, k)
|
||||
}
|
||||
|
||||
if filters:
|
||||
queryset = queryset.filter(**filters)
|
||||
|
||||
stat_map = {
|
||||
'average': Avg,
|
||||
'sum': Sum,
|
||||
'max': Max,
|
||||
'min': Min
|
||||
}
|
||||
|
||||
if operation in stat_map:
|
||||
agg = queryset.aggregate(val=stat_map[operation](field))['val']
|
||||
value = agg
|
||||
else:
|
||||
value = queryset.count()
|
||||
value = queryset.aggregate(val=stat_map.get(operation, Count)(field))['val']
|
||||
|
||||
results.append({
|
||||
keys['model']: model.__name__,
|
||||
keys['field']: field,
|
||||
keys['statistic_type']: operation,
|
||||
keys['value']: value,
|
||||
keys['filters']: filters
|
||||
keys['filters']: filters,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
results.append({keys['model']: model.__name__, keys['error']: str(e)})
|
||||
results.append({
|
||||
keys['model']: model.__name__,
|
||||
keys['error']: str(e),
|
||||
})
|
||||
|
||||
return {
|
||||
keys['type']: keys['type'] + '_analysis',
|
||||
@ -173,18 +134,6 @@ def generate_statistics_insight(models, query_params, dealer_id=None, language='
|
||||
|
||||
|
||||
def generate_recommendations(model_classes, analysis_type, language='en'):
|
||||
"""
|
||||
Generate recommendations based on model analysis.
|
||||
|
||||
:param model_classes: List of models to analyze
|
||||
:type model_classes: list
|
||||
:param analysis_type: Type of analysis
|
||||
:type analysis_type: str
|
||||
:param language: Language code ('en' or 'ar')
|
||||
:type language: str
|
||||
:return: List of recommendations
|
||||
:rtype: list
|
||||
"""
|
||||
recs = []
|
||||
for model in model_classes:
|
||||
for field in model._meta.fields:
|
||||
@ -198,34 +147,20 @@ def generate_recommendations(model_classes, analysis_type, language='en'):
|
||||
|
||||
|
||||
def generate_model_insight(model, dealer_id=None, language='en'):
|
||||
"""
|
||||
Generate insights for a specific model.
|
||||
|
||||
:param model: Model to analyze
|
||||
:type model: Model class
|
||||
:param dealer_id: Dealer ID for filtering
|
||||
:type dealer_id: int
|
||||
:param language: Language code ('en' or 'ar')
|
||||
:type language: str
|
||||
:return: Model insights
|
||||
:rtype: dict
|
||||
"""
|
||||
keys = _localized_keys(language)
|
||||
fields_info = [
|
||||
{
|
||||
'name': f.name,
|
||||
'type': f.__class__.__name__,
|
||||
'null': f.null,
|
||||
'blank': f.blank,
|
||||
'unique': f.unique,
|
||||
'pk': f.primary_key
|
||||
} for f in model._meta.fields
|
||||
]
|
||||
fields_info = [{
|
||||
'name': f.name,
|
||||
'type': f.__class__.__name__,
|
||||
'null': f.null,
|
||||
'blank': f.blank,
|
||||
'unique': f.unique,
|
||||
'pk': f.primary_key
|
||||
} for f in model._meta.fields]
|
||||
|
||||
try:
|
||||
qs = model.objects.all()
|
||||
if dealer_id:
|
||||
if hasattr(model, 'dealer_id'):
|
||||
if hasattr(model, 'dealer'):
|
||||
qs = qs.filter(dealer_id=dealer_id)
|
||||
elif hasattr(model, 'dealer'):
|
||||
qs = qs.filter(dealer=dealer_id)
|
||||
@ -242,20 +177,6 @@ def generate_model_insight(model, dealer_id=None, language='en'):
|
||||
|
||||
|
||||
def generate_relationship_insight(models, query_params=None, dealer_id=None, language='en'):
|
||||
"""
|
||||
Generate relationship insights between models.
|
||||
|
||||
:param models: List of models to analyze
|
||||
:type models: list
|
||||
:param query_params: Query parameters (unused)
|
||||
:type query_params: dict
|
||||
:param dealer_id: Dealer ID (unused)
|
||||
:type dealer_id: int
|
||||
:param language: Language code ('en' or 'ar')
|
||||
:type language: str
|
||||
:return: Relationship insights
|
||||
:rtype: dict
|
||||
"""
|
||||
from_ = "من" if language == 'ar' else "from"
|
||||
to_ = "إلى" if language == 'ar' else "to"
|
||||
rel_type = "نوع" if language == 'ar' else "type"
|
||||
@ -267,7 +188,7 @@ def generate_relationship_insight(models, query_params=None, dealer_id=None, lan
|
||||
relationships.append({
|
||||
from_: model.__name__,
|
||||
to_: field.related_model.__name__,
|
||||
rel_type: field.__class__.__name__
|
||||
rel_type: field.__class__.__name__,
|
||||
})
|
||||
for field in model._meta.many_to_many:
|
||||
relationships.append({
|
||||
@ -283,37 +204,24 @@ def generate_relationship_insight(models, query_params=None, dealer_id=None, lan
|
||||
|
||||
|
||||
def generate_performance_insight(models, query_params=None, dealer_id=None, language='en'):
|
||||
"""
|
||||
Generate performance insights for models.
|
||||
|
||||
:param models: List of models to analyze
|
||||
:type models: list
|
||||
:param query_params: Query parameters (unused)
|
||||
:type query_params: dict
|
||||
:param dealer_id: Dealer ID (unused)
|
||||
:type dealer_id: int
|
||||
:param language: Language code ('en' or 'ar')
|
||||
:type language: str
|
||||
:return: Performance insights
|
||||
:rtype: dict
|
||||
"""
|
||||
issues = []
|
||||
|
||||
for model in models:
|
||||
for field in model._meta.fields:
|
||||
if isinstance(field, ForeignKey) and not field.db_index:
|
||||
issues.append({
|
||||
'model': model.__name__,
|
||||
# 'model': model.__name__,
|
||||
'field': field.name,
|
||||
'issue': 'Missing index on ForeignKey'
|
||||
})
|
||||
if isinstance(field, models.CharField) and not field.db_index and field.name in ['name', 'title']:
|
||||
issues.append({
|
||||
'model': model.__name__,
|
||||
'field': field.name,
|
||||
'issue': 'Unindexed CharField used in filtering'
|
||||
})
|
||||
# if isinstance(field, models.CharField) and not field.db_index and field.name in ['name', 'title']:
|
||||
# issues.append({
|
||||
# 'model': model.__name__,
|
||||
# 'field': field.name,
|
||||
# 'issue': 'Unindexed CharField used in filtering'
|
||||
# })
|
||||
|
||||
return {
|
||||
'type': 'تحليل_الأداء' if language == 'ar' else 'performance_analysis',
|
||||
'issues': issues
|
||||
}
|
||||
}
|
||||
@ -1,93 +1,53 @@
|
||||
import hashlib
|
||||
import logging
|
||||
from django.utils import timezone
|
||||
from django.db import models
|
||||
from ..models import AnalysisCache
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CacheService:
|
||||
"""
|
||||
Service for handling analysis result caching operations.
|
||||
|
||||
This service provides methods for generating cache keys, retrieving
|
||||
cached results, and storing new results in the cache.
|
||||
"""
|
||||
|
||||
def generate_hash(self, prompt, dealer_id, language):
|
||||
"""
|
||||
Generate a unique hash for the prompt, dealer, and language combination.
|
||||
|
||||
:param prompt: The user's prompt text
|
||||
:type prompt: str
|
||||
:param dealer_id: The dealer's ID
|
||||
:type dealer_id: int
|
||||
:param language: The language code
|
||||
:type language: str
|
||||
:return: MD5 hash string
|
||||
:rtype: str
|
||||
Generate a unique MD5 hash based on the prompt, dealer ID, and language.
|
||||
"""
|
||||
cache_key = f"{prompt}:{dealer_id or 'all'}:{language}"
|
||||
return hashlib.md5(cache_key.encode()).hexdigest()
|
||||
|
||||
key = f"{prompt}:{dealer_id or 'all'}:{language}"
|
||||
return hashlib.md5(key.encode()).hexdigest()
|
||||
|
||||
def get_cached_result(self, prompt_hash, user, dealer_id):
|
||||
"""
|
||||
Retrieve a cached result if available and not expired.
|
||||
|
||||
:param prompt_hash: The hash key for the cache entry
|
||||
:type prompt_hash: str
|
||||
:param user: The user making the request
|
||||
:type user: User
|
||||
:param dealer_id: The dealer's ID
|
||||
:type dealer_id: int
|
||||
:return: Cached result or None if not found
|
||||
:rtype: dict or None
|
||||
Retrieve a cached analysis result based on hash, dealer, and optionally user.
|
||||
"""
|
||||
try:
|
||||
cache_entry = AnalysisCache.objects.filter(
|
||||
prompt_hash=prompt_hash,
|
||||
dealer_id=dealer_id,
|
||||
expires_at__gt=timezone.now()
|
||||
).first()
|
||||
|
||||
# If user is authenticated, also check user-specific cache
|
||||
# Check for user-specific cache if authenticated
|
||||
if user and user.is_authenticated:
|
||||
user_cache = AnalysisCache.objects.filter(
|
||||
prompt_hash=prompt_hash,
|
||||
user=user,
|
||||
expires_at__gt=timezone.now()
|
||||
).first()
|
||||
|
||||
# User-specific cache takes precedence
|
||||
if user_cache:
|
||||
return user_cache.result
|
||||
|
||||
return cache_entry.result if cache_entry else None
|
||||
|
||||
# Otherwise check for dealer-wide cache
|
||||
dealer_cache = AnalysisCache.objects.filter(
|
||||
prompt_hash=prompt_hash,
|
||||
dealer_id=dealer_id,
|
||||
expires_at__gt=timezone.now()
|
||||
).first()
|
||||
|
||||
return dealer_cache.result if dealer_cache else None
|
||||
except Exception as e:
|
||||
logger.warning(f"Error retrieving cache: {str(e)}")
|
||||
logger.warning(f"Cache retrieval failed: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def cache_result(self, prompt_hash, result, user, dealer_id, duration=3600):
|
||||
"""
|
||||
Store a result in the cache.
|
||||
|
||||
:param prompt_hash: The hash key for the cache entry
|
||||
:type prompt_hash: str
|
||||
:param result: The result to cache
|
||||
:type result: dict
|
||||
:param user: The user making the request
|
||||
:type user: User
|
||||
:param dealer_id: The dealer's ID
|
||||
:type dealer_id: int
|
||||
:param duration: Cache duration in seconds
|
||||
:type duration: int
|
||||
:return: None
|
||||
Save or update a cached result with an expiration timestamp.
|
||||
"""
|
||||
try:
|
||||
# Calculate expiration time
|
||||
expires_at = timezone.now() + timezone.timedelta(seconds=duration)
|
||||
|
||||
# Create or update cache entry
|
||||
AnalysisCache.objects.update_or_create(
|
||||
prompt_hash=prompt_hash,
|
||||
user=user if user and user.is_authenticated else None,
|
||||
@ -98,4 +58,4 @@ class CacheService:
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error caching result: {str(e)}")
|
||||
logger.warning(f"Cache saving failed: {str(e)}")
|
||||
@ -1,25 +1,18 @@
|
||||
import json
|
||||
import logging
|
||||
from django.apps import apps
|
||||
from django.http import JsonResponse
|
||||
from django.db.models import Count, Avg, Max, Min
|
||||
from langchain_ollama import OllamaLLM
|
||||
from langchain.chains import LLMChain
|
||||
from langchain.prompts import PromptTemplate
|
||||
from django.conf import settings
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_llm_instance():
|
||||
"""
|
||||
Initialize and return an Ollama LLM instance configured for Arabic support.
|
||||
|
||||
This function creates a new LLM instance with optimized parameters for
|
||||
both Arabic and English language processing. It reads configuration from
|
||||
Django settings or uses sensible defaults.
|
||||
|
||||
:return: Configured OllamaLLM instance or None if initialization fails
|
||||
:rtype: OllamaLLM or None
|
||||
"""
|
||||
try:
|
||||
# Get settings from Django settings or use defaults
|
||||
base_url = getattr(settings, 'OLLAMA_BASE_URL', 'http://localhost:11434')
|
||||
model = getattr(settings, 'OLLAMA_MODEL', 'qwen3:8b')
|
||||
temperature = getattr(settings, 'OLLAMA_TEMPERATURE', 0.2)
|
||||
@ -27,7 +20,7 @@ def get_llm_instance():
|
||||
top_k = getattr(settings, 'OLLAMA_TOP_K', 40)
|
||||
num_ctx = getattr(settings, 'OLLAMA_NUM_CTX', 4096)
|
||||
num_predict = getattr(settings, 'OLLAMA_NUM_PREDICT', 2048)
|
||||
|
||||
|
||||
return OllamaLLM(
|
||||
base_url=base_url,
|
||||
model=model,
|
||||
@ -45,23 +38,10 @@ def get_llm_instance():
|
||||
|
||||
|
||||
def get_llm_chain(language='en'):
|
||||
"""
|
||||
Create a LangChain for analyzing prompts in Arabic or English.
|
||||
|
||||
This function creates a chain that processes user prompts and extracts
|
||||
structured information about the analysis request. It supports both
|
||||
Arabic and English languages.
|
||||
|
||||
:param language: Language code ('en' or 'ar')
|
||||
:type language: str
|
||||
:return: LangChain for prompt analysis or None if initialization fails
|
||||
:rtype: LLMChain or None
|
||||
"""
|
||||
llm = get_llm_instance()
|
||||
if not llm:
|
||||
return None
|
||||
|
||||
# Define the prompt template based on language
|
||||
if language == 'ar':
|
||||
template = """
|
||||
قم بتحليل الاستعلام التالي وتحديد نوع التحليل المطلوب ونماذج البيانات المستهدفة وأي معلمات استعلام.
|
||||
@ -89,11 +69,82 @@ def get_llm_chain(language='en'):
|
||||
}
|
||||
"""
|
||||
|
||||
# Create the prompt template
|
||||
prompt_template = PromptTemplate(
|
||||
input_variables=["prompt"],
|
||||
template=template
|
||||
)
|
||||
|
||||
# Create and return the LLM chain
|
||||
return prompt_template | llm
|
||||
|
||||
|
||||
def analyze_models_with_orm(analysis_type, target_models, query_params):
|
||||
results = {}
|
||||
|
||||
for model_name in target_models:
|
||||
try:
|
||||
model = apps.get_model('your_app_name', model_name)
|
||||
except LookupError:
|
||||
results[model_name] = {"error": f"Model '{model_name}' not found"}
|
||||
continue
|
||||
|
||||
try:
|
||||
queryset = model.objects.filter(**query_params)
|
||||
|
||||
if analysis_type == 'count':
|
||||
results[model_name] = {'count': queryset.count()}
|
||||
|
||||
elif analysis_type == 'statistics':
|
||||
numeric_fields = [f.name for f in model._meta.fields if f.get_internal_type() in ['IntegerField', 'FloatField', 'DecimalField']]
|
||||
stats = {}
|
||||
for field in numeric_fields:
|
||||
stats[field] = {
|
||||
'avg': queryset.aggregate(avg=Avg(field))['avg'],
|
||||
'max': queryset.aggregate(max=Max(field))['max'],
|
||||
'min': queryset.aggregate(min=Min(field))['min']
|
||||
}
|
||||
results[model_name] = stats
|
||||
|
||||
elif analysis_type == 'relationship':
|
||||
related = {}
|
||||
for field in model._meta.get_fields():
|
||||
if field.is_relation and field.many_to_one:
|
||||
related[field.name] = queryset.values(field.name).annotate(count=Count(field.name)).count()
|
||||
results[model_name] = related
|
||||
|
||||
elif analysis_type == 'performance':
|
||||
results[model_name] = {'note': 'Performance analysis logic not implemented.'}
|
||||
|
||||
else:
|
||||
results[model_name] = list(queryset.values())
|
||||
|
||||
except Exception as e:
|
||||
results[model_name] = {'error': str(e)}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def analyze_prompt_and_return_json(request):
|
||||
try:
|
||||
prompt = request.POST.get('prompt')
|
||||
language = request.POST.get('language', 'en')
|
||||
|
||||
chain = get_llm_chain(language)
|
||||
if not chain:
|
||||
return JsonResponse({'success': False, 'error': 'LLM not initialized'})
|
||||
|
||||
result = chain.invoke({'prompt': prompt})
|
||||
parsed = json.loads(result)
|
||||
|
||||
analysis_type = parsed.get('analysis_type')
|
||||
target_models = parsed.get('target_models', [])
|
||||
query_params = parsed.get('query_params', {})
|
||||
|
||||
if not analysis_type or not target_models:
|
||||
return JsonResponse({'success': False, 'error': 'Incomplete analysis instruction returned by LLM'})
|
||||
|
||||
orm_results = analyze_models_with_orm(analysis_type, target_models, query_params)
|
||||
|
||||
return JsonResponse({'success': True, 'data': orm_results})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -3,455 +3,521 @@
|
||||
|
||||
{% block title %}
|
||||
{{ _("Haikalbot") }}
|
||||
{% endblock %}
|
||||
{% endblock title %}
|
||||
|
||||
{% block description %}
|
||||
AI assistant
|
||||
{% endblock %}
|
||||
{% endblock description %}
|
||||
|
||||
{% block customCSS %}
|
||||
<!-- No custom CSS as requested -->
|
||||
{% endblock %}
|
||||
<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 %}
|
||||
<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>
|
||||
<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>
|
||||
<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 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>
|
||||
<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 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 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');
|
||||
// Global configuration
|
||||
const MAX_MESSAGE_LENGTH = 400;
|
||||
const WARNING_THRESHOLD = 350;
|
||||
const isArabic = '{{ LANGUAGE_CODE }}' === 'ar';
|
||||
|
||||
// Enable/disable send button based on input
|
||||
messageInput.on('input', function() {
|
||||
sendMessageBtn.prop('disabled', !messageInput.val().trim());
|
||||
// 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);
|
||||
|
||||
// Auto-resize textarea
|
||||
this.style.height = 'auto';
|
||||
this.style.height = (this.scrollHeight) + 'px';
|
||||
});
|
||||
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 }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
$(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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Send message on button click
|
||||
sendMessageBtn.on('click', sendMessage);
|
||||
function resetInputUI() {
|
||||
messageInput.val('').css('height', 'auto');
|
||||
charCount.text('0').parent().removeClass('warning');
|
||||
sendMessageBtn.prop('disabled', true);
|
||||
}
|
||||
|
||||
// Use suggestion chips
|
||||
suggestionChips.on('click', function() {
|
||||
messageInput.val($(this).text().trim());
|
||||
sendMessageBtn.prop('disabled', false);
|
||||
sendMessage();
|
||||
});
|
||||
function processBotResponse(response) {
|
||||
hideTypingIndicator();
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
// Debug response structure
|
||||
console.log("API Response:", response);
|
||||
|
||||
// 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();
|
||||
let botResponse = '';
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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
|
||||
addMessage(botResponse, false);
|
||||
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';
|
||||
}
|
||||
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] || '');
|
||||
}
|
||||
|
||||
// Copy message functionality
|
||||
$(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);
|
||||
}
|
||||
|
||||
// Initialize
|
||||
scrollToBottom();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
<!-- JS will be loaded from static file or added separately -->
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
Loading…
x
Reference in New Issue
Block a user