diff --git a/ai/__pycache__/__init__.cpython-311.pyc b/ai/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..9beeae7 Binary files /dev/null and b/ai/__pycache__/__init__.cpython-311.pyc differ diff --git a/ai/__pycache__/local_ai_api.cpython-311.pyc b/ai/__pycache__/local_ai_api.cpython-311.pyc new file mode 100644 index 0000000..ae12bda Binary files /dev/null and b/ai/__pycache__/local_ai_api.cpython-311.pyc differ diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index b31aa6c..59e21ea 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index c173f30..987996d 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index a24cde7..6704599 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index b0fc129..50fb54d 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 63389ef..0ea02f2 100644 --- a/core/admin.py +++ b/core/admin.py @@ -8,7 +8,6 @@ from django.shortcuts import render from django.utils.html import format_html from django.contrib import messages from .whatsapp_utils import send_whatsapp_message_detailed -from django.core.mail import send_html_email from django.conf import settings from .mail import send_html_email import logging diff --git a/core/migrations/0018_alter_otpverification_purpose.py b/core/migrations/0018_alter_otpverification_purpose.py new file mode 100644 index 0000000..a4ff2d6 --- /dev/null +++ b/core/migrations/0018_alter_otpverification_purpose.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-01-26 06:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0017_driverrating'), + ] + + operations = [ + migrations.AlterField( + model_name='otpverification', + name='purpose', + field=models.CharField(choices=[('profile_update', 'Profile Update'), ('password_reset', 'Password Reset'), ('registration', 'Registration'), ('login', 'Login')], default='profile_update', max_length=20), + ), + ] diff --git a/core/migrations/__pycache__/0018_alter_otpverification_purpose.cpython-311.pyc b/core/migrations/__pycache__/0018_alter_otpverification_purpose.cpython-311.pyc new file mode 100644 index 0000000..60072e6 Binary files /dev/null and b/core/migrations/__pycache__/0018_alter_otpverification_purpose.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 2f2c56f..8d068a8 100644 --- a/core/models.py +++ b/core/models.py @@ -213,6 +213,7 @@ class OTPVerification(models.Model): ('profile_update', _('Profile Update')), ('password_reset', _('Password Reset')), ('registration', _('Registration')), + ('login', _('Login')), ) user = models.ForeignKey(User, on_delete=models.CASCADE) code = models.CharField(max_length=6) @@ -269,4 +270,4 @@ class DriverRating(models.Model): class Meta: verbose_name = _('Driver Rating') - verbose_name_plural = _('Driver Ratings') + verbose_name_plural = _('Driver Ratings') \ No newline at end of file diff --git a/core/templates/admin/dashboard.html b/core/templates/admin/dashboard.html index fe1ab63..8630e68 100644 --- a/core/templates/admin/dashboard.html +++ b/core/templates/admin/dashboard.html @@ -180,7 +180,7 @@ {% trans "Total Revenue" %}
💰
-
{{ stats.total_revenue|floatform:2 }} OMR
+
{{ stats.total_revenue|floatformat:3 }} OMR
diff --git a/core/templates/base.html b/core/templates/base.html index aedc38a..1eff527 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -245,5 +245,38 @@ + + + +
+
+
+ +
MasarX AI
+
+ +
+
+
+
+ {% trans "Hello! How can I help you with your shipments today?" %} +
+
+
+
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/core/templates/core/driver_dashboard.html b/core/templates/core/driver_dashboard.html index f932197..40ea4ec 100644 --- a/core/templates/core/driver_dashboard.html +++ b/core/templates/core/driver_dashboard.html @@ -20,37 +20,132 @@
- {% if available_parcels %} -
- {% for parcel in available_parcels %} -
-
-
-
{{ parcel.description|truncatechars:30 }}
-

{% trans "Pickup" %}: {{ parcel.pickup_address }}

-

{% trans "Delivery" %}: {{ parcel.delivery_address }}

- -
- {% trans "Weight" %}: {{ parcel.weight }} kg -
- - -
- {% trans "Shipper's Offer (Bid)" %} -
{{ parcel.price }} OMR
-
+ + +
+
{% trans "Browse Shipments" %}
+
+ + +
+
-
- {% csrf_token %} - -
+ {% if available_parcels %} + + +
+ {% for parcel in available_parcels %} +
+
+
+
{{ parcel.description|truncatechars:30 }}
+

{% trans "Pickup" %}: {{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }}

+

{% trans "Delivery" %}: {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}

+ +
+ {% trans "Weight" %}: {{ parcel.weight }} kg +
+ + +
+ {% trans "Shipper's Offer (Bid)" %} +
{{ parcel.price }} OMR
+
+ +
+ {% csrf_token %} + +
+
+ {% endfor %}
- {% endfor %} -
+ + +
+
+ {% for parcel in available_parcels %} +
+
+
+
+
{{ parcel.description|truncatechars:80 }}
+
+ + + {% trans "From" %}: {{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }} + + + + {% trans "To" %}: {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }} + + + {{ parcel.weight }} kg + +
+
+
+
+
{{ parcel.price }} OMR
+
+ {% csrf_token %} + +
+
+
+
+
+
+ {% endfor %} +
+
+ + + {% if available_parcels.has_other_pages %} + + {% endif %} + {% else %} -

{% trans "No shipments available at the moment." %}

+

{% trans "No shipments available at the moment." %}

{% endif %}
@@ -67,7 +162,7 @@ {{ parcel.get_status_display }}
{{ parcel.description|truncatechars:30 }}
-

{% trans "To" %}: {{ parcel.delivery_address }}

+

{% trans "To" %}: {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}

{% trans "Receiver" %}: {{ parcel.receiver_name }}

@@ -113,8 +208,8 @@ {{ parcel.created_at|date:"Y-m-d" }} #{{ parcel.tracking_number }} - {{ parcel.pickup_city.name|default:parcel.pickup_address }} - {{ parcel.delivery_city.name|default:parcel.delivery_address }} + {{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }} + {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }} {{ parcel.price }} OMR @@ -136,4 +231,39 @@
+ + {% endblock %} \ No newline at end of file diff --git a/core/templates/core/index.html b/core/templates/core/index.html index cb52783..c204a19 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -86,6 +86,91 @@
+ +
+
+
+ +
+

+ {% trans "Top Rated Drivers" %} +

+
+ {% for driver in top_drivers %} +
+
+ {% if driver.profile_picture %} + + {% else %} +
+ {{ driver.user.first_name|first|upper }} +
+ {% endif %} + + {{ forloop.counter }} + +
+
+
{{ driver.user.first_name }} {{ driver.user.last_name|first }}.
+ {% trans "Driver" %} +
+
+
+ {{ driver.avg_rating|floatformat:1 }} +
+ ({{ driver.rating_count }} {% trans "reviews" %}) +
+
+ {% empty %} +
+ {% trans "No ratings yet. Be the first!" %} +
+ {% endfor %} +
+
+ + +
+

+ {% trans "Top Shippers" %} +

+
+ {% for shipper in top_shippers %} +
+
+ {% if shipper.profile_picture %} + + {% else %} +
+ {{ shipper.user.first_name|first|upper }} +
+ {% endif %} + + {{ forloop.counter }} + +
+
+
{{ shipper.user.first_name }} {{ shipper.user.last_name|first }}.
+ {% trans "Shipper" %} +
+
+
+ {{ shipper.shipment_count }} +
+ {% trans "Shipments" %} +
+
+ {% empty %} +
+ {% trans "No shipments yet. Start shipping now!" %} +
+ {% endfor %} +
+
+
+
+
+ {% if testimonials %}
diff --git a/core/templates/core/login.html b/core/templates/core/login.html index 765521e..09f4d96 100644 --- a/core/templates/core/login.html +++ b/core/templates/core/login.html @@ -22,34 +22,81 @@

{% trans "Please login to your account" %}

- - {% csrf_token %} - {% for field in form %} -
- - {{ field }} - {% if field.errors %} -
{{ field.errors }}
- {% endif %} -
- {% endfor %} - -
-
- -
- - {% trans "Forgot Password?" %} - + + + +
+ +
+ + {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
{{ field.errors }}
+ {% endif %} +
+ {% endfor %} + + + + +
- - -
- {% trans "Don't have an account?" %} - {% trans "Register" %} + +
+
+
+ + +
+
+ +
+ +
+
+
+ + +
+
+ +
+ +
+
- +
+ +
+ {% trans "Don't have an account?" %} + {% trans "Register" %} +
+
@@ -58,6 +105,15 @@
-{% endblock %} + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/shipper_dashboard.html b/core/templates/core/shipper_dashboard.html index 0d706f0..6d7df9a 100644 --- a/core/templates/core/shipper_dashboard.html +++ b/core/templates/core/shipper_dashboard.html @@ -20,10 +20,26 @@
- +
+ + +
+
{% trans "Current Shipments" %}
+
+ + +
+
+ {% if active_parcels %} -
+ + +
{% for parcel in active_parcels %}
@@ -35,8 +51,8 @@
{{ parcel.description|truncatechars:30 }}
-

{% trans "From" %}: {{ parcel.pickup_address }}

-

{% trans "To" %}: {{ parcel.delivery_address }}

+

{% trans "From" %}: {{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }}

+

{% trans "To" %}: {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }}

{{ parcel.price }} OMR @@ -61,6 +77,100 @@
{% endfor %}
+ + +
+
+ {% for parcel in active_parcels %} +
+
+
+
+
+
{{ parcel.description|truncatechars:60 }}
+ #{{ parcel.tracking_number }} +
+
+ + + {% trans "From" %}: {{ parcel.pickup_governate.name }} / {{ parcel.pickup_city.name }} + + + + {% trans "To" %}: {{ parcel.delivery_governate.name }} / {{ parcel.delivery_city.name }} + + + + {% if parcel.carrier %}{{ parcel.carrier.get_full_name|default:parcel.carrier.username }}{% else %}{% trans "Waiting" %}{% endif %} + +
+
+
+
+
+ {{ parcel.price }} OMR + + {{ parcel.get_status_display }} + +
+ + {% if parcel.payment_status == 'pending' and payments_enabled %} + + {% trans "Pay Now" %} + + {% else %} + + {{ parcel.get_payment_status_display }} + + {% endif %} +
+
+
+
+
+ {% endfor %} +
+
+ + + {% if active_parcels.has_other_pages %} + + {% endif %} + {% else %}

{% trans "You have no active shipments." %}

@@ -135,4 +245,39 @@
-{% endblock %} + + +{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 2cda543..4f4230f 100644 --- a/core/urls.py +++ b/core/urls.py @@ -43,6 +43,7 @@ urlpatterns = [ path('article/1/', views.article_detail, name='article_detail'), path('ajax/get-governates/', views.get_governates, name='get_governates'), path('ajax/get-cities/', views.get_cities, name='get_cities'), + path('ajax/chatbot/', views.chatbot, name='chatbot'), path('privacy-policy/', views.privacy_policy, name='privacy_policy'), path('terms-conditions/', views.terms_conditions, name='terms_conditions'), path('contact/', views.contact, name='contact'), @@ -50,4 +51,8 @@ urlpatterns = [ path('profile/', views.profile_view, name='profile'), path('profile/edit/', views.edit_profile, name='edit_profile'), path('profile/verify-otp/', views.verify_otp_view, name='verify_otp'), + + # OTP Login + path('login/request-otp/', views.request_login_otp, name='request_login_otp'), + path('login/verify-otp/', views.verify_login_otp, name='verify_login_otp'), ] \ No newline at end of file diff --git a/core/views.py b/core/views.py index 0160bae..f67acf2 100644 --- a/core/views.py +++ b/core/views.py @@ -13,6 +13,9 @@ from django.urls import reverse from .payment_utils import ThawaniPay from django.conf import settings from django.core.mail import send_mail +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.views.decorators.http import require_POST +from django.db.models import Avg, Count import random import string from .whatsapp_utils import ( @@ -23,6 +26,8 @@ from .whatsapp_utils import ( send_whatsapp_message ) from .mail import send_contact_message, send_html_email +import json +from ai.local_ai_api import LocalAIApi def index(request): tracking_id = request.GET.get('tracking_id') @@ -36,11 +41,24 @@ def index(request): testimonials = Testimonial.objects.filter(is_active=True) + # Top 5 Drivers (by Average Rating) + top_drivers = Profile.objects.filter(role='car_owner').annotate( + avg_rating=Avg('user__received_ratings__rating'), + rating_count=Count('user__received_ratings') + ).filter(rating_count__gt=0).order_by('-avg_rating')[:5] + + # Top 5 Shippers (by Shipment Count) + top_shippers = Profile.objects.filter(role='shipper').annotate( + shipment_count=Count('user__sent_parcels') + ).order_by('-shipment_count')[:5] + return render(request, 'core/index.html', { 'parcel': parcel, 'error': error, 'tracking_id': tracking_id, - 'testimonials': testimonials + 'testimonials': testimonials, + 'top_drivers': top_drivers, + 'top_shippers': top_shippers }) def register(request): @@ -58,14 +76,16 @@ def register(request): # Send OTP method = form.cleaned_data.get('verification_method', 'email') + otp_msg = _("Your Masar Verification Code is %(code)s") % {'code': code} + if method == 'whatsapp': phone = user.profile.phone_number - send_whatsapp_message(phone, f"Your verification code is: {code}") + send_whatsapp_message(phone, otp_msg) messages.info(request, _("Verification code sent to WhatsApp.")) else: send_html_email( subject=_('Verification Code'), - message=f'Your verification code is: {code}', + message=otp_msg, recipient_list=[user.email], title=_('Welcome to Masar!'), request=request @@ -124,9 +144,20 @@ def dashboard(request): if profile.role == 'shipper': all_parcels = Parcel.objects.filter(shipper=request.user).order_by('-created_at') - active_parcels = all_parcels.exclude(status__in=['delivered', 'cancelled']) + active_parcels_list = all_parcels.exclude(status__in=['delivered', 'cancelled']) history_parcels = all_parcels.filter(status__in=['delivered', 'cancelled']) + # Pagination for Active Shipments + page = request.GET.get('page', 1) + paginator = Paginator(active_parcels_list, 9) # Show 9 parcels per page + + try: + active_parcels = paginator.page(page) + except PageNotAnInteger: + active_parcels = paginator.page(1) + except EmptyPage: + active_parcels = paginator.page(paginator.num_pages) + platform_profile = PlatformProfile.objects.first() payments_enabled = platform_profile.enable_payment if platform_profile else True @@ -142,9 +173,20 @@ def dashboard(request): payments_enabled = platform_profile.enable_payment if platform_profile else True if payments_enabled: - available_parcels = Parcel.objects.filter(status='pending', payment_status='paid').order_by('-created_at') + available_parcels_list = Parcel.objects.filter(status='pending', payment_status='paid').order_by('-created_at') else: - available_parcels = Parcel.objects.filter(status='pending').order_by('-created_at') + available_parcels_list = Parcel.objects.filter(status='pending').order_by('-created_at') + + # Pagination for Available Shipments + page = request.GET.get('page', 1) + paginator = Paginator(available_parcels_list, 9) # Show 9 parcels per page + + try: + available_parcels = paginator.page(page) + except PageNotAnInteger: + available_parcels = paginator.page(1) + except EmptyPage: + available_parcels = paginator.page(paginator.num_pages) # Active: Picked up or In Transit my_parcels = Parcel.objects.filter(carrier=request.user).exclude(status__in=['delivered', 'cancelled']).order_by('-created_at') @@ -360,10 +402,12 @@ def edit_profile(request): # 4. Send OTP method = data.get('otp_method', 'email') + otp_msg = _("Your Masar Update Code is %(code)s") % {'code': code} + if method == 'whatsapp': # Use current phone if available, else new phone phone = request.user.profile.phone_number or data['phone_number'] - send_whatsapp_message(phone, f"Your verification code is: {code}") + send_whatsapp_message(phone, otp_msg) messages.info(request, _("Verification code sent to WhatsApp.")) else: # Default to email @@ -371,7 +415,7 @@ def edit_profile(request): target_email = data['email'] send_html_email( subject=_('Verification Code'), - message=f'Your verification code is: {code}', + message=otp_msg, recipient_list=[target_email], title=_('Profile Update Verification'), request=request @@ -473,3 +517,136 @@ def rate_driver(request, parcel_id): 'form': form, 'parcel': parcel }) + +@require_POST +def request_login_otp(request): + identifier = request.POST.get('identifier') + + if not identifier: + return JsonResponse({'success': False, 'message': _('Please enter an email or phone number.')}) + + # Clean identifier + identifier = identifier.strip() + + user = None + method = 'email' + + # Try to find user by email + user = User.objects.filter(email__iexact=identifier).first() + + # If not found, try by phone number + if not user: + profile = Profile.objects.filter(phone_number=identifier).first() + if profile: + user = profile.user + method = 'whatsapp' + else: + # Fallback: maybe they entered a phone without country code or with? + # For now, simplistic search + pass + + if not user: + # Don't reveal if user exists or not for security, but for UX on this project we can be a bit more helpful + return JsonResponse({'success': False, 'message': _('User not found with this email or phone number.')}) + + if not user.is_active: + return JsonResponse({'success': False, 'message': _('Account is inactive. Please verify registration first.')}) + + # Generate OTP + code = ''.join(random.choices(string.digits, k=6)) + OTPVerification.objects.create(user=user, code=code, purpose='login') + + # Send OTP + otp_msg = _("Your Masar Login Code is %(code)s. Do not share this code.") % {'code': code} + + try: + if method == 'whatsapp': + phone = user.profile.phone_number + send_whatsapp_message(phone, otp_msg) + message_sent = _("OTP sent to your WhatsApp.") + else: + send_html_email( + subject=_('Login OTP'), + message=otp_msg, + recipient_list=[user.email], + title=_('Login Verification'), + request=request + ) + message_sent = _("OTP sent to your email.") + + return JsonResponse({'success': True, 'message': message_sent, 'user_id': user.id}) + except Exception as e: + return JsonResponse({'success': False, 'message': _('Failed to send OTP. Please try again.')}) + +@require_POST +def verify_login_otp(request): + user_id = request.POST.get('user_id') + code = request.POST.get('code') + + if not user_id or not code: + return JsonResponse({'success': False, 'message': _('Invalid request.')}) + + try: + user = User.objects.get(id=user_id) + otp = OTPVerification.objects.filter( + user=user, + code=code, + purpose='login', + is_verified=False + ).latest('created_at') + + if otp.is_valid(): + # Cleanup + otp.is_verified = True + otp.save() + + # Login + login(request, user) + + return JsonResponse({'success': True, 'redirect_url': reverse('dashboard')}) + else: + return JsonResponse({'success': False, 'message': _('Invalid or expired OTP.')}) + + except (User.DoesNotExist, OTPVerification.DoesNotExist): + return JsonResponse({'success': False, 'message': _('Invalid OTP.')}) + +@require_POST +def chatbot(request): + try: + data = json.loads(request.body) + user_message = data.get("message", "") + language = data.get("language", "en") + + if not user_message: + return JsonResponse({"success": False, "error": "Empty message"}) + + system_prompt = ( + "You are MasarX AI, a helpful and professional assistant for the Masar logistics platform. " + "The platform connects shippers with drivers for small parcel deliveries. " + "Answer the user's questions about shipping, tracking, becoming a driver, or general support. " + "If the user speaks Arabic, reply in Arabic. If English, reply in English. " + "Keep responses concise and helpful." + ) + + if language == "ar": + system_prompt += " The user is currently browsing in Arabic." + else: + system_prompt += " The user is currently browsing in English." + + response = LocalAIApi.create_response({ + "input": [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_message}, + ] + }) + + if response.get("success"): + text = LocalAIApi.extract_text(response) + return JsonResponse({"success": True, "response": text}) + else: + return JsonResponse({"success": False, "error": response.get("error", "AI Error")}) + + except json.JSONDecodeError: + return JsonResponse({"success": False, "error": "Invalid JSON"}) + except Exception as e: + return JsonResponse({"success": False, "error": str(e)}) diff --git a/locale/ar/LC_MESSAGES/django.mo b/locale/ar/LC_MESSAGES/django.mo index 4e3a693..84a3de1 100644 Binary files a/locale/ar/LC_MESSAGES/django.mo and b/locale/ar/LC_MESSAGES/django.mo differ diff --git a/locale/ar/LC_MESSAGES/django.po b/locale/ar/LC_MESSAGES/django.po index d69c812..ec3afaf 100644 --- a/locale/ar/LC_MESSAGES/django.po +++ b/locale/ar/LC_MESSAGES/django.po @@ -1315,4 +1315,18 @@ msgstr "شكراً لملاحظاتك!" #~ msgstr "لم ترسل أي شحنات بعد." #~ msgid "Find Loads" -#~ msgstr "البحث عن شحنات" \ No newline at end of file +#~ msgstr "البحث عن شحنات" +msgid "Top Rated Drivers" +msgstr "أفضل السائقين تقييماً" + +msgid "Top Shippers" +msgstr "أفضل الشاحنين" + +msgid "No ratings yet. Be the first!" +msgstr "لا توجد تقييمات بعد. كن الأول!" + +msgid "No shipments yet. Start shipping now!" +msgstr "لا توجد شحنات بعد. ابدأ الشحن الآن!" + +msgid "Shipments" +msgstr "شحنات" diff --git a/static/css/custom.css b/static/css/custom.css index f76f993..5a80a12 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -103,4 +103,62 @@ h1, h2, h3, h4, h5, h6 { .status-pending { background: #FFE8CC; color: #D9480F; } .status-picked_up { background: #E3FAFC; color: #0B7285; } .status-in_transit { background: #E7F5FF; color: #1864AB; } -.status-delivered { background: #EBFBEE; color: #2B8A3E; } \ No newline at end of file +.status-delivered { background: #EBFBEE; color: #2B8A3E; } + +/* Chat Widget */ +#masar-chat-widget { + position: fixed; + bottom: 90px; + right: 20px; + width: 350px; + height: 500px; + z-index: 9999; + display: flex; + flex-direction: column; +} + +#masar-chat-toggle { + position: fixed; + bottom: 20px; + right: 20px; + width: 60px; + height: 60px; + border-radius: 50%; + z-index: 9999; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + background-color: var(--accent-orange); + color: white; + border: none; + transition: transform 0.2s; +} + +#masar-chat-toggle:hover { + transform: scale(1.05); +} + +/* RTL Support */ +[dir="rtl"] #masar-chat-widget { + right: auto; + left: 20px; +} +[dir="rtl"] #masar-chat-toggle { + right: auto; + left: 20px; +} + +.typing-dots span { + display: inline-block; + width: 8px; + height: 8px; + background-color: #adb5bd; + border-radius: 50%; + margin: 0 2px; + animation: typing 1s infinite; +} +.typing-dots span:nth-child(2) { animation-delay: 0.2s; } +.typing-dots span:nth-child(3) { animation-delay: 0.4s; } + +@keyframes typing { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-5px); } +} diff --git a/static/js/chat.js b/static/js/chat.js new file mode 100644 index 0000000..8646d82 --- /dev/null +++ b/static/js/chat.js @@ -0,0 +1,118 @@ +document.addEventListener('DOMContentLoaded', function() { + const chatWidget = document.getElementById('masar-chat-widget'); + const chatToggle = document.getElementById('masar-chat-toggle'); + const chatClose = document.getElementById('masar-chat-close'); + const chatForm = document.getElementById('masar-chat-form'); + const chatInput = document.getElementById('masar-chat-input'); + const chatMessages = document.getElementById('masar-chat-messages'); + + if (!chatWidget) return; + + // Toggle Chat + function toggleChat() { + if (chatWidget.classList.contains('d-none')) { + chatWidget.classList.remove('d-none'); + setTimeout(() => chatInput.focus(), 100); + } else { + chatWidget.classList.add('d-none'); + } + } + + chatToggle.addEventListener('click', toggleChat); + chatClose.addEventListener('click', toggleChat); + + // Send Message + chatForm.addEventListener('submit', function(e) { + e.preventDefault(); + const message = chatInput.value.trim(); + if (!message) return; + + // Add User Message + addMessage(message, 'user'); + chatInput.value = ''; + + // Show Typing Indicator + const typingId = addTypingIndicator(); + + // Send to Backend + fetch('/ajax/chatbot/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + }, + body: JSON.stringify({ + message: message, + language: document.documentElement.lang || 'en' + }) + }) + .then(response => response.json()) + .then(data => { + removeMessage(typingId); + if (data.success) { + addMessage(data.response, 'bot'); + } else { + addMessage('Sorry, I encountered an error.', 'bot'); + } + }) + .catch(error => { + removeMessage(typingId); + addMessage('Sorry, connection error.', 'bot'); + console.error('Error:', error); + }); + }); + + function addMessage(text, sender) { + const div = document.createElement('div'); + div.className = `d-flex mb-3 ${sender === 'user' ? 'justify-content-end' : 'justify-content-start'}`; + + const bubble = document.createElement('div'); + bubble.className = `p-3 rounded-3 shadow-sm ${sender === 'user' ? 'bg-primary text-white' : 'bg-light text-dark'}`; + bubble.style.maxWidth = '80%'; + bubble.style.wordWrap = 'break-word'; + // Convert newlines to
for basic formatting + bubble.innerHTML = text.replace(/\n/g, '
'); + + div.appendChild(bubble); + chatMessages.appendChild(div); + chatMessages.scrollTop = chatMessages.scrollHeight; + return div.id; + } + + function addTypingIndicator() { + const id = 'typing-' + Date.now(); + const div = document.createElement('div'); + div.id = id; + div.className = 'd-flex mb-3 justify-content-start'; + div.innerHTML = "" + + "
" + + "
" + + " " + + "
" + + "
" + ; + chatMessages.appendChild(div); + chatMessages.scrollTop = chatMessages.scrollHeight; + return id; + } + + function removeMessage(id) { + const el = document.getElementById(id); + if (el) el.remove(); + } + + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } +}); diff --git a/update_base.py b/update_base.py new file mode 100644 index 0000000..15994fc --- /dev/null +++ b/update_base.py @@ -0,0 +1,50 @@ +import os + +with open("core/templates/base.html", "r") as f: + content = f.read() + +widget_html = """ + + + +
+
+
+ +
MasarX AI
+
+ +
+ +
+
+
+ {% trans "Hello! How can I help you with your shipments today?" %} +
+
+
+ +
+
+ + +
+
+
+ + +" + +if "masar-chat-toggle" not in content: + new_content = content.replace("", widget_html + "\n") + with open("core/templates/base.html", "w") as f: + f.write(new_content) + print("Updated base.html") +else: + print("Widget already exists") diff --git a/update_base_v2.py b/update_base_v2.py new file mode 100644 index 0000000..d418f08 --- /dev/null +++ b/update_base_v2.py @@ -0,0 +1,36 @@ + + + +
+
+
+ +
MasarX AI
+
+ +
+ +
+
+
+ {% trans "Hello! How can I help you with your shipments today?" %} +
+
+
+ +
+
+ + +
+
+
+ + + \ No newline at end of file