2.2
This commit is contained in:
parent
d2dbea0724
commit
2a479c82a7
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/booking.cpython-311.pyc
Normal file
BIN
core/__pycache__/booking.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
28
core/booking.py
Normal 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}"
|
||||
28
core/migrations/0001_initial.py
Normal file
28
core/migrations/0001_initial.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
56
core/migrations/0002_contact_call_service_booking.py
Normal file
56
core/migrations/0002_contact_call_service_booking.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
core/migrations/0003_call_conversation_history.py
Normal file
18
core/migrations/0003_call_conversation_history.py
Normal 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),
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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}"
|
||||
@ -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>
|
||||
58
core/templates/core/business_detail.html
Normal file
58
core/templates/core/business_detail.html
Normal 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">← 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 %}
|
||||
@ -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 %}
|
||||
@ -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"),
|
||||
]
|
||||
|
||||
202
core/views.py
202
core/views.py
@ -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')
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
twilio==9.2.2
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user