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 inspect
|
||||||
|
import hashlib
|
||||||
from django.db import models
|
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.translation import gettext_lazy as _
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
def _localized_keys(language):
|
def _localized_keys(language):
|
||||||
"""
|
return {
|
||||||
Get localized key names based on language.
|
'type': 'نوع' if language == 'ar' else 'type',
|
||||||
|
'model': 'النموذج' if language == 'ar' else 'model',
|
||||||
:param language: Language code ('en' or 'ar')
|
'count': 'العدد' if language == 'ar' else 'count',
|
||||||
:type language: str
|
'filters': 'الفلاتر_المطبقة' if language == 'ar' else 'filters_applied',
|
||||||
:return: Dictionary of localized keys
|
'error': 'خطأ' if language == 'ar' else 'error',
|
||||||
:rtype: dict
|
'chart_type': 'نوع_الرسم_البياني' if language == 'ar' else 'chart_type',
|
||||||
"""
|
'labels': 'التسميات' if language == 'ar' else 'labels',
|
||||||
if language == 'ar':
|
'data': 'البيانات' if language == 'ar' else 'data',
|
||||||
return {
|
'visualization_data': 'بيانات_الرسم_البياني' if language == 'ar' else 'visualization_data',
|
||||||
'type': 'نوع', 'model': 'النموذج', 'count': 'العدد', 'filters': 'الفلاتر_المطبقة',
|
'field': 'الحقل' if language == 'ar' else 'field',
|
||||||
'error': 'خطأ', 'chart_type': 'نوع_الرسم_البياني', 'labels': 'التسميات', 'data': 'البيانات',
|
'value': 'القيمة' if language == 'ar' else 'value',
|
||||||
'visualization_data': 'بيانات_الرسم_البياني', 'field': 'الحقل', 'value': 'القيمة',
|
'statistic_type': 'نوع_الإحصاء' if language == 'ar' else 'statistic_type',
|
||||||
'statistic_type': 'نوع_الإحصاء', 'results': 'النتائج', 'title': 'العنوان'
|
'results': 'النتائج' if language == 'ar' else 'results',
|
||||||
}
|
'title': 'العنوان' if language == 'ar' else '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'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def generate_count_insight(models, query_params, dealer_id=None, language='en'):
|
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)
|
keys = _localized_keys(language)
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
@ -59,7 +41,9 @@ def generate_count_insight(models, query_params, dealer_id=None, language='en'):
|
|||||||
|
|
||||||
filters = {}
|
filters = {}
|
||||||
for key, value in query_params.items():
|
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:
|
try:
|
||||||
field = model._meta.get_field(key)
|
field = model._meta.get_field(key)
|
||||||
if isinstance(field, models.IntegerField):
|
if isinstance(field, models.IntegerField):
|
||||||
@ -76,13 +60,12 @@ def generate_count_insight(models, query_params, dealer_id=None, language='en'):
|
|||||||
results.append({
|
results.append({
|
||||||
keys['model']: model.__name__,
|
keys['model']: model.__name__,
|
||||||
keys['count']: queryset.count(),
|
keys['count']: queryset.count(),
|
||||||
keys['filters']: filters
|
keys['filters']: filters,
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results.append({
|
results.append({
|
||||||
keys['model']: model.__name__,
|
keys['model']: model.__name__,
|
||||||
keys['error']: str(e)
|
keys['error']: str(e),
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -91,30 +74,17 @@ def generate_count_insight(models, query_params, dealer_id=None, language='en'):
|
|||||||
keys['visualization_data']: {
|
keys['visualization_data']: {
|
||||||
keys['chart_type']: 'bar',
|
keys['chart_type']: 'bar',
|
||||||
keys['labels']: [r[keys['model']] for r in results if keys['count'] in r],
|
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'):
|
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)
|
keys = _localized_keys(language)
|
||||||
results = []
|
results = []
|
||||||
field = query_params.get('field')
|
field = query_params.get('field')
|
||||||
operation = query_params.get('operation', 'average')
|
operation = query_params.get('operation', 'average')
|
||||||
|
stat_map = {'average': Avg, 'sum': Sum, 'max': Max, 'min': Min}
|
||||||
|
|
||||||
for model in models:
|
for model in models:
|
||||||
try:
|
try:
|
||||||
@ -128,37 +98,28 @@ def generate_statistics_insight(models, query_params, dealer_id=None, language='
|
|||||||
elif hasattr(model, 'dealer'):
|
elif hasattr(model, 'dealer'):
|
||||||
queryset = queryset.filter(dealer=dealer_id)
|
queryset = queryset.filter(dealer=dealer_id)
|
||||||
|
|
||||||
filters = {}
|
filters = {
|
||||||
for k, v in query_params.items():
|
k: v for k, v in query_params.items()
|
||||||
if k not in ['field', 'operation'] and hasattr(model, k):
|
if k not in ['field', 'operation'] and hasattr(model, k)
|
||||||
filters[k] = v
|
}
|
||||||
|
|
||||||
if filters:
|
if filters:
|
||||||
queryset = queryset.filter(**filters)
|
queryset = queryset.filter(**filters)
|
||||||
|
|
||||||
stat_map = {
|
value = queryset.aggregate(val=stat_map.get(operation, Count)(field))['val']
|
||||||
'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()
|
|
||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
keys['model']: model.__name__,
|
keys['model']: model.__name__,
|
||||||
keys['field']: field,
|
keys['field']: field,
|
||||||
keys['statistic_type']: operation,
|
keys['statistic_type']: operation,
|
||||||
keys['value']: value,
|
keys['value']: value,
|
||||||
keys['filters']: filters
|
keys['filters']: filters,
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
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 {
|
return {
|
||||||
keys['type']: keys['type'] + '_analysis',
|
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'):
|
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 = []
|
recs = []
|
||||||
for model in model_classes:
|
for model in model_classes:
|
||||||
for field in model._meta.fields:
|
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'):
|
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)
|
keys = _localized_keys(language)
|
||||||
fields_info = [
|
fields_info = [{
|
||||||
{
|
'name': f.name,
|
||||||
'name': f.name,
|
'type': f.__class__.__name__,
|
||||||
'type': f.__class__.__name__,
|
'null': f.null,
|
||||||
'null': f.null,
|
'blank': f.blank,
|
||||||
'blank': f.blank,
|
'unique': f.unique,
|
||||||
'unique': f.unique,
|
'pk': f.primary_key
|
||||||
'pk': f.primary_key
|
} for f in model._meta.fields]
|
||||||
} for f in model._meta.fields
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
qs = model.objects.all()
|
qs = model.objects.all()
|
||||||
if dealer_id:
|
if dealer_id:
|
||||||
if hasattr(model, 'dealer_id'):
|
if hasattr(model, 'dealer'):
|
||||||
qs = qs.filter(dealer_id=dealer_id)
|
qs = qs.filter(dealer_id=dealer_id)
|
||||||
elif hasattr(model, 'dealer'):
|
elif hasattr(model, 'dealer'):
|
||||||
qs = qs.filter(dealer=dealer_id)
|
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'):
|
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"
|
from_ = "من" if language == 'ar' else "from"
|
||||||
to_ = "إلى" if language == 'ar' else "to"
|
to_ = "إلى" if language == 'ar' else "to"
|
||||||
rel_type = "نوع" if language == 'ar' else "type"
|
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({
|
relationships.append({
|
||||||
from_: model.__name__,
|
from_: model.__name__,
|
||||||
to_: field.related_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:
|
for field in model._meta.many_to_many:
|
||||||
relationships.append({
|
relationships.append({
|
||||||
@ -283,35 +204,22 @@ 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'):
|
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 = []
|
issues = []
|
||||||
|
|
||||||
for model in models:
|
for model in models:
|
||||||
for field in model._meta.fields:
|
for field in model._meta.fields:
|
||||||
if isinstance(field, ForeignKey) and not field.db_index:
|
if isinstance(field, ForeignKey) and not field.db_index:
|
||||||
issues.append({
|
issues.append({
|
||||||
'model': model.__name__,
|
# 'model': model.__name__,
|
||||||
'field': field.name,
|
'field': field.name,
|
||||||
'issue': 'Missing index on ForeignKey'
|
'issue': 'Missing index on ForeignKey'
|
||||||
})
|
})
|
||||||
if isinstance(field, models.CharField) and not field.db_index and field.name in ['name', 'title']:
|
# if isinstance(field, models.CharField) and not field.db_index and field.name in ['name', 'title']:
|
||||||
issues.append({
|
# issues.append({
|
||||||
'model': model.__name__,
|
# 'model': model.__name__,
|
||||||
'field': field.name,
|
# 'field': field.name,
|
||||||
'issue': 'Unindexed CharField used in filtering'
|
# 'issue': 'Unindexed CharField used in filtering'
|
||||||
})
|
# })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'type': 'تحليل_الأداء' if language == 'ar' else 'performance_analysis',
|
'type': 'تحليل_الأداء' if language == 'ar' else 'performance_analysis',
|
||||||
|
|||||||
@ -1,93 +1,53 @@
|
|||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from ..models import AnalysisCache
|
from ..models import AnalysisCache
|
||||||
import hashlib
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CacheService:
|
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):
|
def generate_hash(self, prompt, dealer_id, language):
|
||||||
"""
|
"""
|
||||||
Generate a unique hash for the prompt, dealer, and language combination.
|
Generate a unique MD5 hash based on the prompt, dealer ID, and language.
|
||||||
|
|
||||||
: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
|
|
||||||
"""
|
"""
|
||||||
cache_key = f"{prompt}:{dealer_id or 'all'}:{language}"
|
key = f"{prompt}:{dealer_id or 'all'}:{language}"
|
||||||
return hashlib.md5(cache_key.encode()).hexdigest()
|
return hashlib.md5(key.encode()).hexdigest()
|
||||||
|
|
||||||
def get_cached_result(self, prompt_hash, user, dealer_id):
|
def get_cached_result(self, prompt_hash, user, dealer_id):
|
||||||
"""
|
"""
|
||||||
Retrieve a cached result if available and not expired.
|
Retrieve a cached analysis result based on hash, dealer, and optionally user.
|
||||||
|
|
||||||
: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
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
cache_entry = AnalysisCache.objects.filter(
|
# Check for user-specific cache if authenticated
|
||||||
prompt_hash=prompt_hash,
|
|
||||||
dealer_id=dealer_id,
|
|
||||||
expires_at__gt=timezone.now()
|
|
||||||
).first()
|
|
||||||
|
|
||||||
# If user is authenticated, also check user-specific cache
|
|
||||||
if user and user.is_authenticated:
|
if user and user.is_authenticated:
|
||||||
user_cache = AnalysisCache.objects.filter(
|
user_cache = AnalysisCache.objects.filter(
|
||||||
prompt_hash=prompt_hash,
|
prompt_hash=prompt_hash,
|
||||||
user=user,
|
user=user,
|
||||||
expires_at__gt=timezone.now()
|
expires_at__gt=timezone.now()
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
# User-specific cache takes precedence
|
|
||||||
if user_cache:
|
if user_cache:
|
||||||
return user_cache.result
|
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:
|
except Exception as e:
|
||||||
logger.warning(f"Error retrieving cache: {str(e)}")
|
logger.warning(f"Cache retrieval failed: {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def cache_result(self, prompt_hash, result, user, dealer_id, duration=3600):
|
def cache_result(self, prompt_hash, result, user, dealer_id, duration=3600):
|
||||||
"""
|
"""
|
||||||
Store a result in the cache.
|
Save or update a cached result with an expiration timestamp.
|
||||||
|
|
||||||
: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
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Calculate expiration time
|
|
||||||
expires_at = timezone.now() + timezone.timedelta(seconds=duration)
|
expires_at = timezone.now() + timezone.timedelta(seconds=duration)
|
||||||
|
|
||||||
# Create or update cache entry
|
|
||||||
AnalysisCache.objects.update_or_create(
|
AnalysisCache.objects.update_or_create(
|
||||||
prompt_hash=prompt_hash,
|
prompt_hash=prompt_hash,
|
||||||
user=user if user and user.is_authenticated else None,
|
user=user if user and user.is_authenticated else None,
|
||||||
@ -98,4 +58,4 @@ class CacheService:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
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_ollama import OllamaLLM
|
||||||
from langchain.chains import LLMChain
|
from langchain.chains import LLMChain
|
||||||
from langchain.prompts import PromptTemplate
|
from langchain.prompts import PromptTemplate
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_llm_instance():
|
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:
|
try:
|
||||||
# Get settings from Django settings or use defaults
|
|
||||||
base_url = getattr(settings, 'OLLAMA_BASE_URL', 'http://localhost:11434')
|
base_url = getattr(settings, 'OLLAMA_BASE_URL', 'http://localhost:11434')
|
||||||
model = getattr(settings, 'OLLAMA_MODEL', 'qwen3:8b')
|
model = getattr(settings, 'OLLAMA_MODEL', 'qwen3:8b')
|
||||||
temperature = getattr(settings, 'OLLAMA_TEMPERATURE', 0.2)
|
temperature = getattr(settings, 'OLLAMA_TEMPERATURE', 0.2)
|
||||||
@ -45,23 +38,10 @@ def get_llm_instance():
|
|||||||
|
|
||||||
|
|
||||||
def get_llm_chain(language='en'):
|
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()
|
llm = get_llm_instance()
|
||||||
if not llm:
|
if not llm:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Define the prompt template based on language
|
|
||||||
if language == 'ar':
|
if language == 'ar':
|
||||||
template = """
|
template = """
|
||||||
قم بتحليل الاستعلام التالي وتحديد نوع التحليل المطلوب ونماذج البيانات المستهدفة وأي معلمات استعلام.
|
قم بتحليل الاستعلام التالي وتحديد نوع التحليل المطلوب ونماذج البيانات المستهدفة وأي معلمات استعلام.
|
||||||
@ -89,11 +69,82 @@ def get_llm_chain(language='en'):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Create the prompt template
|
|
||||||
prompt_template = PromptTemplate(
|
prompt_template = PromptTemplate(
|
||||||
input_variables=["prompt"],
|
input_variables=["prompt"],
|
||||||
template=template
|
template=template
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create and return the LLM chain
|
|
||||||
return prompt_template | llm
|
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 %}
|
{% block title %}
|
||||||
{{ _("Haikalbot") }}
|
{{ _("Haikalbot") }}
|
||||||
{% endblock %}
|
{% endblock title %}
|
||||||
|
|
||||||
{% block description %}
|
{% block description %}
|
||||||
AI assistant
|
AI assistant
|
||||||
{% endblock %}
|
{% endblock description %}
|
||||||
|
|
||||||
{% block customCSS %}
|
{% block customCSS %}
|
||||||
<!-- No custom CSS as requested -->
|
<style>
|
||||||
{% endblock %}
|
.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 %}
|
{% block content %}
|
||||||
<style>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
.chat-container {
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
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 shadow-none mb-3">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<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">
|
<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-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" />
|
<img class="d-light-none" src="{% static 'images/favicons/haikalbot_v2.png' %}" alt="{% trans 'home' %}" width="32" />
|
||||||
</div>
|
</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 %}">
|
||||||
<div class="d-flex gap-3">
|
<i class="fas fa-trash-alt text-danger"></i>
|
||||||
<span id="clearChatBtn" class="translate-middle-y cursor-pointer" title="{% if LANGUAGE_CODE == 'ar' %}مسح المحادثة{% else %}Clear Chat{% endif %}">
|
</span>
|
||||||
<i class="fas fa-trash-alt text-danger"></i>
|
<span id="exportChatBtn" class="translate-middle-y cursor-pointer" title="{% if LANGUAGE_CODE == 'ar' %}تصدير المحادثة{% else %}Export Chat{% endif %}">
|
||||||
</span>
|
<i class="fas fa-download text-success"></i>
|
||||||
<span id="exportChatBtn" class="translate-middle-y cursor-pointer" title="{% if LANGUAGE_CODE == 'ar' %}تصدير المحادثة{% else %}Export Chat{% endif %}">
|
</span>
|
||||||
<i class="fas fa-download text-success"></i>
|
</div>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div id="chatMessages" class="overflow-auto p-3" style="height: 60vh;">
|
<div id="chatMessages" class="overflow-auto p-3" style="height: 60vh;"></div>
|
||||||
<!-- Chat messages will be appended here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-100 border-top p-3">
|
<div class="bg-100 border-top p-3">
|
||||||
<div class="d-flex gap-2 flex-wrap mb-3" id="suggestionChips">
|
<div class="d-flex gap-2 flex-wrap mb-3" id="suggestionChips">
|
||||||
<button class="btn btn-sm btn-outline-primary suggestion-chip">
|
<button class="btn btn-sm btn-outline-primary suggestion-chip">{{ _("How many cars are in inventory")}}?</button>
|
||||||
{{ _("How many cars are in inventory")}}?
|
<button class="btn btn-sm btn-outline-primary suggestion-chip">{{ _("Show me sales analysis")}}</button>
|
||||||
</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">
|
|
||||||
{{ _("Show me sales analysis")}}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-outline-primary suggestion-chip">
|
|
||||||
{{ _("What are the best-selling cars")}}?
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
|
<div class="textarea-container mb-3">
|
||||||
|
<label for="messageInput"></label>
|
||||||
<div class="textarea-container mb-3">
|
<textarea class="form-control chat-textarea" id="messageInput" rows="3" placeholder="{{ _("Type your message here")}}..." maxlength="400"></textarea>
|
||||||
<label for="messageInput"></label>
|
<div class="textarea-footer">
|
||||||
<textarea class="form-control chat-textarea" id="messageInput" rows="3"
|
<div class="character-count">
|
||||||
placeholder="{{ _("Type your message here")}}..."></textarea>
|
<span id="charCount">0</span>/400
|
||||||
<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>
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
// Global configuration
|
||||||
const messageInput = $('#messageInput');
|
const MAX_MESSAGE_LENGTH = 400;
|
||||||
const charCount = $('#charCount');
|
const WARNING_THRESHOLD = 350;
|
||||||
const sendMessageBtn = $('#sendMessageBtn');
|
const isArabic = '{{ LANGUAGE_CODE }}' === 'ar';
|
||||||
const chatMessages = $('#chatMessages');
|
|
||||||
const clearChatBtn = $('#clearChatBtn');
|
|
||||||
const exportChatBtn = $('#exportChatBtn');
|
|
||||||
const suggestionChips = $('.suggestion-chip');
|
|
||||||
|
|
||||||
// Enable/disable send button based on input
|
// Chart rendering function
|
||||||
messageInput.on('input', function() {
|
function renderInsightChart(labels, data, chartType = 'bar', title = 'Insight Chart') {
|
||||||
sendMessageBtn.prop('disabled', !messageInput.val().trim());
|
const canvasId = 'chart_' + Date.now();
|
||||||
|
const chartHtml = `<div class="chart-container"><canvas id="${canvasId}"></canvas></div>`;
|
||||||
|
$('#chatMessages').append(chartHtml);
|
||||||
|
|
||||||
// Auto-resize textarea
|
new Chart(document.getElementById(canvasId), {
|
||||||
this.style.height = 'auto';
|
type: chartType,
|
||||||
this.style.height = (this.scrollHeight) + 'px';
|
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)
|
$(document).ready(function() {
|
||||||
messageInput.on('keydown', function(e) {
|
// DOM elements
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
const messageInput = $('#messageInput');
|
||||||
e.preventDefault();
|
const charCount = $('#charCount');
|
||||||
if (!sendMessageBtn.prop('disabled')) {
|
const sendMessageBtn = $('#sendMessageBtn');
|
||||||
sendMessage();
|
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
|
function resetInputUI() {
|
||||||
sendMessageBtn.on('click', sendMessage);
|
messageInput.val('').css('height', 'auto');
|
||||||
|
charCount.text('0').parent().removeClass('warning');
|
||||||
|
sendMessageBtn.prop('disabled', true);
|
||||||
|
}
|
||||||
|
|
||||||
// Use suggestion chips
|
function processBotResponse(response) {
|
||||||
suggestionChips.on('click', function() {
|
hideTypingIndicator();
|
||||||
messageInput.val($(this).text().trim());
|
|
||||||
sendMessageBtn.prop('disabled', false);
|
|
||||||
sendMessage();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear chat
|
// Debug response structure
|
||||||
clearChatBtn.on('click', function() {
|
console.log("API Response:", response);
|
||||||
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
|
let botResponse = '';
|
||||||
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`;
|
// Check for direct response first
|
||||||
});
|
if (response.response) {
|
||||||
|
botResponse = response.response;
|
||||||
// Create and trigger download
|
}
|
||||||
const blob = new Blob([chatContent], { type: 'text/plain' });
|
// Then check for insights data
|
||||||
const url = URL.createObjectURL(blob);
|
else if (hasInsightsData(response)) {
|
||||||
const a = document.createElement('a');
|
botResponse = formatInsightsResponse(response);
|
||||||
a.href = url;
|
}
|
||||||
a.download = 'chat-export-' + new Date().toISOString().slice(0, 10) + '.txt';
|
// Fallback
|
||||||
document.body.appendChild(a);
|
else {
|
||||||
a.click();
|
botResponse = isArabic ?
|
||||||
document.body.removeChild(a);
|
'عذرًا، لم أتمكن من معالجة طلبك. يبدو أن هيكل الاستجابة غير متوقع.' :
|
||||||
URL.revokeObjectURL(url);
|
'Sorry, I couldn\'t process your request. The response structure appears unexpected.';
|
||||||
});
|
console.error("Unexpected response structure:", response);
|
||||||
|
|
||||||
// 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
|
addMessage(botResponse, false);
|
||||||
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();
|
scrollToBottom();
|
||||||
});
|
}
|
||||||
messageInput.addEventListener('input', function() {
|
|
||||||
const currentLength = this.value.length;
|
|
||||||
charCount.textContent = currentLength;
|
|
||||||
|
|
||||||
// Optional: Add warning when approaching limit
|
function hasInsightsData(response) {
|
||||||
if (currentLength > 350) {
|
return response.insights || response['التحليلات'] ||
|
||||||
charCount.style.color = 'red';
|
response.recommendations || response['التوصيات'];
|
||||||
} else {
|
}
|
||||||
charCount.style.color = 'inherit';
|
|
||||||
}
|
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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block customJS %}
|
|
||||||
<!-- JS will be loaded from static file or added separately -->
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user