This commit is contained in:
Flatlogic Bot 2026-01-20 17:13:37 +00:00
parent d2dbea0724
commit 2a479c82a7
24 changed files with 936 additions and 184 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +1,31 @@
from django.contrib import admin
from .models import Business, Service, Contact, Call, Booking
# Register your models here.
@admin.register(Business)
class BusinessAdmin(admin.ModelAdmin):
list_display = ('business_name', 'industry', 'phone_number', 'status')
list_filter = ('industry', 'status')
search_fields = ('business_name', 'phone_number')
@admin.register(Service)
class ServiceAdmin(admin.ModelAdmin):
list_display = ('name', 'business', 'duration', 'price')
list_filter = ('business',)
search_fields = ('name', 'description')
@admin.register(Contact)
class ContactAdmin(admin.ModelAdmin):
list_display = ('name', 'phone_number')
search_fields = ('name', 'phone_number')
@admin.register(Call)
class CallAdmin(admin.ModelAdmin):
list_display = ('contact', 'business', 'start_time', 'end_time')
list_filter = ('business', 'start_time')
search_fields = ('contact__name', 'contact__phone_number')
@admin.register(Booking)
class BookingAdmin(admin.ModelAdmin):
list_display = ('service', 'contact', 'start_time', 'status')
list_filter = ('status', 'service__business')
search_fields = ('contact__name', 'contact__phone_number', 'service__name')

28
core/booking.py Normal file
View File

@ -0,0 +1,28 @@
from .models import Booking, Service, Contact
from datetime import datetime, timedelta
def create_booking(contact_phone_number: str, service_name: str, booking_time_str: str) -> str:
"""
Creates a booking for a given service at a specific time.
"""
try:
contact = Contact.objects.get(phone_number=contact_phone_number)
service = Service.objects.get(name__iexact=service_name)
booking_time = datetime.fromisoformat(booking_time_str)
end_time = booking_time + timedelta(minutes=service.duration)
booking = Booking.objects.create(
contact=contact,
service=service,
start_time=booking_time,
end_time=end_time,
status='scheduled',
)
return f"Booking confirmed for {service.name} at {booking_time.strftime('%Y-%m-%d %H:%M')}."
except Contact.DoesNotExist:
return "Error: Contact not found."
except Service.DoesNotExist:
return f"Error: Service '{service_name}' not found."
except Exception as e:
return f"Error: Could not create booking. {e}"

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2026-01-20 16:39
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Business',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('business_name', models.CharField(max_length=255)),
('industry', models.CharField(choices=[('real_estate', 'Real Estate'), ('clinic_dental', 'Clinic / Dental'), ('salon_spa', 'Salon / Spa'), ('coaching_consulting', 'Coaching / Consulting'), ('custom', 'Custom (Other)')], default='custom', max_length=50)),
('phone_number', models.CharField(help_text="Twilio or other provider's phone number", max_length=20)),
('agent_name', models.CharField(default='AI Assistant', max_length=100)),
('status', models.BooleanField(default=True, help_text='Active or inactive status')),
],
options={
'verbose_name_plural': 'Businesses',
},
),
]

View File

@ -0,0 +1,56 @@
# Generated by Django 5.2.7 on 2026-01-20 16:56
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Contact',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('phone_number', models.CharField(max_length=20, unique=True)),
('name', models.CharField(blank=True, max_length=255)),
],
),
migrations.CreateModel(
name='Call',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start_time', models.DateTimeField(auto_now_add=True)),
('end_time', models.DateTimeField(blank=True, null=True)),
('summary', models.TextField(blank=True)),
('transcript', models.TextField(blank=True)),
('business', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='calls', to='core.business')),
('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='calls', to='core.contact')),
],
),
migrations.CreateModel(
name='Service',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True)),
('duration', models.IntegerField(help_text='Duration in minutes')),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('business', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='core.business')),
],
),
migrations.CreateModel(
name='Booking',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start_time', models.DateTimeField()),
('end_time', models.DateTimeField()),
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='scheduled', max_length=20)),
('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bookings', to='core.contact')),
('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bookings', to='core.service')),
],
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-01-20 17:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_contact_call_service_booking'),
]
operations = [
migrations.AddField(
model_name='call',
name='conversation_history',
field=models.JSONField(default=list),
),
]

View File

@ -1,3 +1,67 @@
from django.db import models
# Create your models here.
class Business(models.Model):
INDUSTRY_CHOICES = [
('real_estate', 'Real Estate'),
('clinic_dental', 'Clinic / Dental'),
('salon_spa', 'Salon / Spa'),
('coaching_consulting', 'Coaching / Consulting'),
('custom', 'Custom (Other)'),
]
business_name = models.CharField(max_length=255)
industry = models.CharField(max_length=50, choices=INDUSTRY_CHOICES, default='custom')
phone_number = models.CharField(max_length=20, help_text="Twilio or other provider's phone number")
agent_name = models.CharField(max_length=100, default='AI Assistant')
status = models.BooleanField(default=True, help_text="Active or inactive status")
def __str__(self):
return self.business_name
class Meta:
verbose_name_plural = "Businesses"
class Service(models.Model):
business = models.ForeignKey(Business, on_delete=models.CASCADE, related_name='services')
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
duration = models.IntegerField(help_text="Duration in minutes")
price = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return self.name
class Contact(models.Model):
phone_number = models.CharField(max_length=20, unique=True)
name = models.CharField(max_length=255, blank=True)
def __str__(self):
return self.name or self.phone_number
class Call(models.Model):
business = models.ForeignKey(Business, on_delete=models.CASCADE, related_name='calls')
contact = models.ForeignKey(Contact, on_delete=models.CASCADE, related_name='calls')
start_time = models.DateTimeField(auto_now_add=True)
end_time = models.DateTimeField(null=True, blank=True)
summary = models.TextField(blank=True)
transcript = models.TextField(blank=True)
conversation_history = models.JSONField(default=list)
def __str__(self):
return f"Call with {self.contact} at {self.start_time}"
class Booking(models.Model):
STATUS_CHOICES = [
('scheduled', 'Scheduled'),
('completed', 'Completed'),
('canceled', 'Canceled'),
]
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name='bookings')
contact = models.ForeignKey(Contact, on_delete=models.CASCADE, related_name='bookings')
start_time = models.DateTimeField()
end_time = models.DateTimeField()
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='scheduled')
def __str__(self):
return f"{self.service} booking for {self.contact} at {self.start_time}"

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<title>{% block title %}Knowledge Base{% endblock %}</title>
<title>{% block title %}Xfront{% endblock %}</title>
{% if project_description %}
<meta name="description" content="{{ project_description }}">
<meta property="og:description" content="{{ project_description }}">
@ -13,13 +13,90 @@
<meta property="og:image" content="{{ project_image_url }}">
<meta property="twitter:image" content="{{ project_image_url }}">
{% endif %}
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
{% load static %}
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
{% block head %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
<body class="b2b-layout">
<div class="sidebar-container">
<div class="sidebar">
<div class="sidebar-header">
<a href="/" class="b2b-logo">Xfront AI</a>
</div>
<ul class="sidebar-nav">
<li><a href="#" data-target="overview-sidebar"><i class="fas fa-chart-pie"></i> Overview</a></li>
<li><a href="#" data-target="businesses-sidebar"><i class="fas fa-briefcase"></i> Businesses</a></li>
<li><a href="#" data-target="call-system-sidebar"><i class="fas fa-phone-alt"></i> Call System</a></li>
<li><a href="#" data-target="appointments-sidebar"><i class="far fa-calendar-alt"></i> Appointments</a></li>
<li><a href="#" data-target="knowledge-base-sidebar"><i class="fas fa-book"></i> Knowledge Base</a></li>
<li><a href="#" data-target="analytics-sidebar"><i class="fas fa-chart-line"></i> Analytics</a></li>
<li><a href="#" data-target="settings-sidebar"><i class="fas fa-cog"></i> Settings</a></li>
</ul>
</div>
</div>
<div class="secondary-sidebar-container">
<div id="overview-sidebar" class="secondary-sidebar active">
<h3>Overview</h3>
<ul class="secondary-sidebar-nav">
<li><a href="#">Dashboard Summary</a></li>
<li><a href="#">Recent Activity</a></li>
<li><a href="#">Alerts & Notifications</a></li>
</ul>
</div>
<div id="businesses-sidebar" class="secondary-sidebar">
<h3>Businesses</h3>
<ul class="secondary-sidebar-nav">
<li><a href="#">All Businesses</a></li>
<li><a href="#">Add New Business</a></li>
</ul>
</div>
</div>
<div class="main-content">
{% block content %}{% endblock %}
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const primaryNavLinks = document.querySelectorAll('.sidebar-nav a');
const secondarySidebars = document.querySelectorAll('.secondary-sidebar');
primaryNavLinks.forEach(link => {
link.addEventListener('click', function(event) {
event.preventDefault();
primaryNavLinks.forEach(navLink => navLink.classList.remove('active'));
this.classList.add('active');
const targetId = this.getAttribute('data-target');
secondarySidebars.forEach(sidebar => {
if (sidebar.id === targetId) {
sidebar.classList.add('active');
} else {
sidebar.classList.remove('active');
}
});
});
});
// Set initial active state
if(primaryNavLinks.length > 0) {
primaryNavLinks[0].classList.add('active');
}
});
</script>
</body>
</html>
</html>

View File

@ -0,0 +1,58 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ page_title }}{% endblock %}
{% block content %}
<div class="main-content-header">
<a href="{% url 'dashboard' %}" class="back-link">&larr; Back to Overview</a>
<h1>{{ business.business_name }}</h1>
</div>
<div class="card-grid">
<div class="card">
<div class="card-header">
<h3>Services</h3>
</div>
<div class="card-body">
<ul class="list-group">
{% for service in services %}
<li class="list-group-item">{{ service.name }} - {{ service.duration }} mins at ${{ service.price }}</li>
{% empty %}
<li class="list-group-item">No services found.</li>
{% endfor %}
</ul>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>Recent Calls</h3>
</div>
<div class="card-body">
<ul class="list-group">
{% for call in calls %}
<li class="list-group-item">Call from {{ call.contact }} on {{ call.start_time|date:"Y-m-d H:i" }}</li>
{% empty %}
<li class="list-group-item">No calls found.</li>
{% endfor %}
</ul>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>Recent Bookings</h3>
</div>
<div class="card-body">
<ul class="list-group">
{% for booking in bookings %}
<li class="list-group-item">{{ booking.service.name }} for {{ booking.contact }} on {{ booking.start_time|date:"Y-m-d H:i" }}</li>
{% empty %}
<li class="list-group-item">No bookings found.</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,145 +1,45 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ project_name }}{% endblock %}
{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2.5rem 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
}
h1 {
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
font-weight: 700;
margin: 0 0 1.2rem;
letter-spacing: -0.02em;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
opacity: 0.92;
}
.loader {
margin: 1.5rem auto;
width: 56px;
height: 56px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.runtime code {
background: rgba(0, 0, 0, 0.25);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
footer {
position: absolute;
bottom: 1rem;
width: 100%;
text-align: center;
font-size: 0.85rem;
opacity: 0.75;
}
</style>
{% endblock %}
{% block title %}{{ page_title }}{% endblock %}
{% block content %}
<main>
<div class="card">
<h1>Analyzing your requirements and generating your app…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
<div class="main-content-header">
<h1>Overview</h1>
</div>
<div class="card">
<div class="card-header">
<h3>Businesses</h3>
</div>
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
<p class="runtime">
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
</p>
</div>
</main>
<footer>
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
</footer>
<div class="card-body">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Business Name</th>
<th>Industry</th>
<th>Services</th>
<th>Calls</th>
<th>Bookings</th>
</tr>
</thead>
<tbody>
{% for business in businesses %}
<tr>
<td><a href="{% url 'business_detail' business.id %}">{{ business.business_name }}</a></td>
<td>{{ business.get_industry_display }}</td>
<td>{{ business.service_count }}</td>
<td>{{ business.call_count }}</td>
<td>{{ business.booking_count }}</td>
</tr>
{% empty %}
<tr>
<td colspan="5">No businesses found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,7 +1,10 @@
from django.urls import path
from .views import home
from .views import dashboard, inbound_call_webhook, business_detail, ai_call_handler
urlpatterns = [
path("", home, name="home"),
path("", dashboard, name="dashboard"),
path("business/<int:business_id>/", business_detail, name="business_detail"),
path("webhook/call/inbound/", inbound_call_webhook, name="inbound_call_webhook"),
path("webhook/call/handler/<int:call_id>/", ai_call_handler, name="ai_call_handler"),
]

View File

@ -1,25 +1,187 @@
import os
import platform
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from .models import Business, Service, Call, Booking, Contact
from django.db.models import Count
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
import json
from twilio.twiml.voice_response import VoiceResponse, Gather
from django.urls import reverse
from ai.local_ai_api import LocalAIApi
from .booking import create_booking
from django import get_version as django_version
from django.shortcuts import render
from django.utils import timezone
from datetime import datetime
def home(request):
"""Render the landing screen with loader and environment details."""
host_name = request.get_host().lower()
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
now = timezone.now()
#@login_required
def dashboard(request):
"""Render the main admin dashboard."""
businesses = Business.objects.annotate(
service_count=Count('services', distinct=True),
call_count=Count('calls', distinct=True),
booking_count=Count('services__bookings', distinct=True)
)
recent_calls = Call.objects.order_by('-start_time')[:10]
upcoming_bookings = Booking.objects.filter(start_time__gte=datetime.now()).order_by('start_time')[:10]
context = {
"project_name": "New Style",
"agent_brand": agent_brand,
"django_version": django_version(),
"python_version": platform.python_version(),
"current_time": now,
"host_name": host_name,
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
'businesses': businesses,
'recent_calls': recent_calls,
'upcoming_bookings': upcoming_bookings,
'page_title': 'Dashboard - Xfront'
}
return render(request, "core/index.html", context)
return render(request, 'core/index.html', context)
def business_detail(request, business_id):
"""Display a detailed view of a business."""
business = get_object_or_404(Business, pk=business_id)
services = business.services.all()
calls = business.calls.all().order_by('-start_time')[:10]
bookings = Booking.objects.filter(service__business=business).order_by('-start_time')[:10]
context = {
'business': business,
'services': services,
'calls': calls,
'bookings': bookings,
'page_title': f'{business.business_name} - Details'
}
return render(request, 'core/business_detail.html', context)
@csrf_exempt
def inbound_call_webhook(request):
"""Handle inbound calls from Twilio."""
if request.method == 'POST':
from_number = request.POST.get('From')
to_number = request.POST.get('To')
try:
business = Business.objects.get(phone_number=to_number)
except Business.DoesNotExist:
response = VoiceResponse()
response.say("The number you have called is not associated with a business.")
return HttpResponse(str(response), content_type='text/xml')
contact, _ = Contact.objects.get_or_create(phone_number=from_number)
call = Call.objects.create(
business=business,
contact=contact,
)
response = VoiceResponse()
response.say(f"Hello and welcome to {business.business_name}. Please wait while we connect you to our AI assistant.")
response.redirect(reverse('ai_call_handler', args=[call.id]))
return HttpResponse(str(response), content_type='text/xml')
return HttpResponse(status=400)
tools = [
{
"type": "function",
"function": {
"name": "create_booking",
"description": "Creates a booking for a given service at a specific time.",
"parameters": {
"type": "object",
"properties": {
"service_name": {
"type": "string",
"description": "The name of the service to book.",
},
"booking_time_str": {
"type": "string",
"description": "The desired time for the booking in ISO 8601 format.",
},
},
"required": ["service_name", "booking_time_str"],
},
},
}
]
@csrf_exempt
def ai_call_handler(request, call_id):
"""Handle the AI-powered conversation."""
call = get_object_or_404(Call, pk=call_id)
response = VoiceResponse()
if 'SpeechResult' in request.POST:
user_speech = request.POST['SpeechResult']
call.conversation_history.append({'role': 'user', 'content': user_speech})
services = Service.objects.filter(business=call.business)
services_prompt = "\n".join([f"- {s.name}: {s.description}" for s in services])
# Get AI response
ai_response = LocalAIApi.create_response(
{
"input": [
{
"role": "system",
"content": f"You are a friendly and helpful AI assistant for {call.business.business_name}. Available services are:\n{services_prompt}"
},
*call.conversation_history
],
"tools": tools,
"tool_choice": "auto",
},
{
"poll_interval": 5,
"poll_timeout": 300,
},
)
if ai_response.get("success") and ai_response.get("data", {}).get("output", [{}])[0].get("content", [{}])[0].get("type") == "tool_calls":
tool_calls = ai_response["data"]["output"][0]["content"][0]["tool_calls"]
call.conversation_history.append({"role": "assistant", "content": None, "tool_calls": tool_calls})
for tool_call in tool_calls:
function_name = tool_call['function']['name']
if function_name == 'create_booking':
args = json.loads(tool_call['function']['arguments'])
result = create_booking(
contact_phone_number=call.contact.phone_number,
service_name=args.get("service_name"),
booking_time_str=args.get("booking_time_str"),
)
call.conversation_history.append({"role": "tool", "tool_call_id": tool_call['id'], "name": function_name, "content": result})
# Get AI response after tool call
ai_response = LocalAIApi.create_response(
{
"input": [
{
"role": "system",
"content": f"You are a friendly and helpful AI assistant for {call.business.business_name}. Available services are:\n{services_prompt}"
},
*call.conversation_history
],
"tools": tools,
"tool_choice": "auto",
},
{
"poll_interval": 5,
"poll_timeout": 300,
},
)
ai_text = LocalAIApi.extract_text(ai_response)
else:
ai_text = LocalAIApi.extract_text(ai_response)
if not ai_text:
ai_text = "I am sorry, I am having trouble understanding. Please try again."
call.conversation_history.append({'role': 'assistant', 'content': ai_text})
response.say(ai_text)
else:
# First turn of the conversation
response.say("How can I help you today?")
gather = Gather(input='speech', action=reverse('ai_call_handler', args=[call.id]), speechTimeout='auto')
response.append(gather)
call.save()
return HttpResponse(str(response), content_type='text/xml')

View File

@ -1,3 +1,4 @@
Django==5.2.7
mysqlclient==2.2.7
python-dotenv==1.1.1
twilio==9.2.2

View File

@ -1,4 +1,177 @@
/* Custom styles for the application */
body {
font-family: system-ui, -apple-system, sans-serif;
/*
Palette:
- Base: #F8F9FA (Light Grey)
- Primary (Sidebar/Header): #1a202c (Dark Slate)
- Secondary (UI Elements): #718096 (Slate)
- Accent (Buttons, Highlights): #3182ce (Vibrant Blue)
*/
:root {
--base-bg: #F8F9FA;
--primary-bg: #1a202c;
--secondary-color: #718096;
--accent-color: #3182ce;
--text-color: #4A5568;
--heading-color: #2D3748;
--white-color: #FFFFFF;
--border-color: #E2E8F0;
--sidebar-width: 260px;
--secondary-sidebar-width: 240px;
}
body {
font-family: 'Inter', sans-serif;
background-color: #f7fafc;
color: var(--text-color);
margin: 0;
font-size: 16px;
}
.b2b-layout {
display: flex;
}
/* Sidebar */
.sidebar-container {
width: var(--sidebar-width);
position: fixed;
height: 100%;
top: 0;
left: 0;
background-color: var(--primary-bg);
color: var(--white-color);
z-index: 1000;
}
/* Secondary Sidebar */
.secondary-sidebar-container {
width: var(--secondary-sidebar-width);
position: fixed;
height: 100%;
top: 0;
left: var(--sidebar-width);
background-color: #edf2f7;
border-right: 1px solid var(--border-color);
padding: 1.5rem;
z-index: 999;
}
.secondary-sidebar {
display: none;
}
.secondary-sidebar.active {
display: block;
}
.secondary-sidebar h3 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
color: #4a5568;
}
.secondary-sidebar-nav {
list-style: none;
padding: 0;
margin: 0;
}
.secondary-sidebar-nav li a {
display: block;
padding: 0.75rem 0;
color: #4a5568;
text-decoration: none;
font-weight: 500;
border-radius: 5px;
transition: background-color 0.2s;
}
.secondary-sidebar-nav li a:hover {
background-color: #e2e8f0;
}
/* Main Content */
.main-content {
margin-left: calc(var(--sidebar-width) + var(--secondary-sidebar-width));
flex-grow: 1;
padding: 2.5rem;
}
.main-content-header {
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border-color);
margin-bottom: 2rem;
}
.main-content-header h1 {
margin: 0;
font-size: 2.25rem;
}
/* Card Grid & Card */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 2rem;
}
.card {
background-color: var(--white-color);
border-radius: 0.75rem;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
border: 1px solid var(--border-color);
transition: box-shadow 0.3s ease-in-out;
}
.card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.card-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.card-header h3 {
margin: 0;
font-size: 1.25rem;
}
.card-body {
padding: 1.5rem;
line-height: 1.6;
}
@media (max-width: 1200px) {
:root {
--sidebar-width: 220px;
--secondary-sidebar-width: 200px;
}
}
@media (max-width: 768px) {
:root {
--sidebar-width: 80px;
}
.secondary-sidebar-container {
display: none;
}
.main-content {
margin-left: var(--sidebar-width);
}
.sidebar-nav li a {
justify-content: center;
}
.sidebar-nav li a span {
display: none;
}
.sidebar-header .b2b-logo {
font-size: 1.2rem;
}
}

View File

@ -1,21 +1,177 @@
/*
Palette:
- Base: #F8F9FA (Light Grey)
- Primary (Sidebar/Header): #1a202c (Dark Slate)
- Secondary (UI Elements): #718096 (Slate)
- Accent (Buttons, Highlights): #3182ce (Vibrant Blue)
*/
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
--base-bg: #F8F9FA;
--primary-bg: #1a202c;
--secondary-color: #718096;
--accent-color: #3182ce;
--text-color: #4A5568;
--heading-color: #2D3748;
--white-color: #FFFFFF;
--border-color: #E2E8F0;
--sidebar-width: 260px;
--secondary-sidebar-width: 240px;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
background-color: #f7fafc;
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
margin: 0;
font-size: 16px;
}
.b2b-layout {
display: flex;
}
/* Sidebar */
.sidebar-container {
width: var(--sidebar-width);
position: fixed;
height: 100%;
top: 0;
left: 0;
background-color: var(--primary-bg);
color: var(--white-color);
z-index: 1000;
}
/* Secondary Sidebar */
.secondary-sidebar-container {
width: var(--secondary-sidebar-width);
position: fixed;
height: 100%;
top: 0;
left: var(--sidebar-width);
background-color: #edf2f7;
border-right: 1px solid var(--border-color);
padding: 1.5rem;
z-index: 999;
}
.secondary-sidebar {
display: none;
}
.secondary-sidebar.active {
display: block;
}
.secondary-sidebar h3 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
color: #4a5568;
}
.secondary-sidebar-nav {
list-style: none;
padding: 0;
margin: 0;
}
.secondary-sidebar-nav li a {
display: block;
padding: 0.75rem 0;
color: #4a5568;
text-decoration: none;
font-weight: 500;
border-radius: 5px;
transition: background-color 0.2s;
}
.secondary-sidebar-nav li a:hover {
background-color: #e2e8f0;
}
/* Main Content */
.main-content {
margin-left: calc(var(--sidebar-width) + var(--secondary-sidebar-width));
flex-grow: 1;
padding: 2.5rem;
}
.main-content-header {
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border-color);
margin-bottom: 2rem;
}
.main-content-header h1 {
margin: 0;
font-size: 2.25rem;
}
/* Card Grid & Card */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 2rem;
}
.card {
background-color: var(--white-color);
border-radius: 0.75rem;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
border: 1px solid var(--border-color);
transition: box-shadow 0.3s ease-in-out;
}
.card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.card-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.card-header h3 {
margin: 0;
font-size: 1.25rem;
}
.card-body {
padding: 1.5rem;
line-height: 1.6;
}
@media (max-width: 1200px) {
:root {
--sidebar-width: 220px;
--secondary-sidebar-width: 200px;
}
}
@media (max-width: 768px) {
:root {
--sidebar-width: 80px;
}
.secondary-sidebar-container {
display: none;
}
.main-content {
margin-left: var(--sidebar-width);
}
.sidebar-nav li a {
justify-content: center;
}
.sidebar-nav li a span {
display: none;
}
.sidebar-header .b2b-logo {
font-size: 1.2rem;
}
}