inital commit

This commit is contained in:
2026-05-17 18:29:30 -05:00
parent b827236fe2
commit 7c1e18bd59
4683 changed files with 159402 additions and 1 deletions

709
builder/services.py Normal file
View File

@@ -0,0 +1,709 @@
import html
import json
import shutil
import zipfile
from pathlib import Path
import requests
from django.conf import settings
from django.utils.text import slugify
from .models import ClientSite
from .template_registry import (
COPY_EXCLUDE_DIR_NAMES,
get_template_config,
resolve_source_path,
)
PERSONALIZE_EXTENSIONS = {".html", ".htm", ".css", ".js", ".jsx", ".md", ".txt", ".php"}
class SiteGenerator:
def __init__(self) -> None:
self.ollama_url = settings.OLLAMA_URL.rstrip("/")
self.ollama_model = settings.OLLAMA_MODEL
def build_all(self, site: ClientSite) -> ClientSite:
site_root = Path(settings.MEDIA_ROOT) / "generated" / str(site.id)
if site_root.exists():
shutil.rmtree(site_root)
site_root.mkdir(parents=True, exist_ok=True)
ai_payload = self._generate_ai_payload(site)
basic_zip = self._build_basic_site(site, ai_payload, site_root)
django_zip = self._build_django_demo(site, ai_payload, site_root)
site.ai_payload = ai_payload
site.basic_zip_path = str(basic_zip.relative_to(settings.MEDIA_ROOT))
site.django_zip_path = str(django_zip.relative_to(settings.MEDIA_ROOT))
site.status = ClientSite.Status.READY
site.last_error = ""
site.save(
update_fields=[
"ai_payload",
"basic_zip_path",
"django_zip_path",
"status",
"last_error",
"updated_at",
]
)
return site
def _generate_ai_payload(self, site: ClientSite) -> dict:
fallback = {
"tagline": f"{site.business_name} helps local customers with trusted service.",
"hero_headline": f"Welcome to {site.business_name}",
"hero_subheadline": site.business_description,
"services": [
"Professional support tailored to your business goals",
"Friendly service with clear communication",
"Fast turnaround with quality-focused delivery",
],
"testimonials": [
{"name": "Jordan T.", "quote": f"{site.business_name} made process simple and stress-free."},
{"name": "Casey M.", "quote": "Great communication, clear pricing, excellent final result."},
{"name": "Alex R.", "quote": "Reliable team. We will keep using them for future needs."},
],
"contact_prompt": "Tell us what you need and we will respond quickly.",
}
prompt = (
"You are website copy assistant. Respond in valid JSON only. "
"Use keys: tagline, hero_headline, hero_subheadline, services, testimonials, contact_prompt. "
"services must be 3 short strings. testimonials must be array of 3 objects with name and quote."
f"\nBusiness name: {site.business_name}"
f"\nBusiness description: {site.business_description}"
f"\nAddress: {site.business_address}"
f"\nPhone: {site.business_phone}"
f"\nOld website: {site.old_website or 'n/a'}"
)
try:
response = requests.post(
f"{self.ollama_url}/api/generate",
json={"model": self.ollama_model, "prompt": prompt, "stream": False},
timeout=20,
)
response.raise_for_status()
model_text = response.json().get("response", "").strip()
parsed = json.loads(model_text)
if not isinstance(parsed, dict):
return fallback
return {
"tagline": parsed.get("tagline") or fallback["tagline"],
"hero_headline": parsed.get("hero_headline") or fallback["hero_headline"],
"hero_subheadline": parsed.get("hero_subheadline") or fallback["hero_subheadline"],
"services": parsed.get("services") or fallback["services"],
"testimonials": parsed.get("testimonials") or fallback["testimonials"],
"contact_prompt": parsed.get("contact_prompt") or fallback["contact_prompt"],
}
except Exception:
return fallback
def _build_basic_site(self, site: ClientSite, ai_payload: dict, root: Path) -> Path:
basic_dir = root / "basic_site"
if basic_dir.exists():
shutil.rmtree(basic_dir)
source_path = resolve_source_path(site.template_option.source_folder)
if source_path.exists() and source_path.is_dir():
self._copy_client_template(source_path, basic_dir)
config = get_template_config(site.template_option.slug) or {}
self._personalize_template_tree(basic_dir, site, ai_payload, config)
self._write_client_readme(basic_dir, site, ai_payload, config)
basic_zip = root / "basic_site.zip"
self._zip_dir(basic_dir, basic_zip)
return basic_zip
basic_dir.mkdir(parents=True, exist_ok=True)
safe_name = html.escape(site.business_name)
safe_phone = html.escape(site.business_phone)
safe_address = html.escape(site.business_address)
safe_description = html.escape(site.business_description)
safe_tagline = html.escape(ai_payload["tagline"])
hero_title = html.escape(ai_payload["hero_headline"])
hero_sub = html.escape(ai_payload["hero_subheadline"])
services = ai_payload.get("services", [])
testimonials = ai_payload.get("testimonials", [])
nav = """
<nav>
<a href="index.html">Home</a>
<a href="about.html">About</a>
<a href="testimonials.html">Testimonials</a>
<a href="contact.html">Contact</a>
</nav>
"""
index_html = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{safe_name} | Home</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
{nav}
<header class="hero">
<h1>{hero_title}</h1>
<p>{hero_sub}</p>
<span class="tagline">{safe_tagline}</span>
</header>
<section>
<h2>What We Offer</h2>
<ul>
<li>{html.escape(services[0] if len(services) > 0 else "Quality-focused support")}</li>
<li>{html.escape(services[1] if len(services) > 1 else "Local-first service model")}</li>
<li>{html.escape(services[2] if len(services) > 2 else "Clear process from start to finish")}</li>
</ul>
</section>
</body>
</html>
"""
about_html = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{safe_name} | About</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
{nav}
<main>
<h1>About {safe_name}</h1>
<p>{safe_description}</p>
<p>Address: {safe_address}</p>
<p>Phone: {safe_phone}</p>
</main>
</body>
</html>
"""
t1 = testimonials[0] if len(testimonials) > 0 and isinstance(testimonials[0], dict) else {"name": "Client", "quote": "Great result."}
t2 = testimonials[1] if len(testimonials) > 1 and isinstance(testimonials[1], dict) else {"name": "Client", "quote": "Excellent service."}
t3 = testimonials[2] if len(testimonials) > 2 and isinstance(testimonials[2], dict) else {"name": "Client", "quote": "Would recommend."}
testimonials_html = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{safe_name} | Testimonials</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
{nav}
<main>
<h1>Testimonials</h1>
<blockquote>"{html.escape(str(t1.get("quote", "Great result.")))}" <cite>- {html.escape(str(t1.get("name", "Client")))}</cite></blockquote>
<blockquote>"{html.escape(str(t2.get("quote", "Excellent service.")))}" <cite>- {html.escape(str(t2.get("name", "Client")))}</cite></blockquote>
<blockquote>"{html.escape(str(t3.get("quote", "Would recommend.")))}" <cite>- {html.escape(str(t3.get("name", "Client")))}</cite></blockquote>
</main>
</body>
</html>
"""
contact_html = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{safe_name} | Contact</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
{nav}
<main>
<h1>Contact Us</h1>
<p>{html.escape(ai_payload["contact_prompt"])}</p>
<p>Phone: {safe_phone}</p>
<p>Address: {safe_address}</p>
</main>
</body>
</html>
"""
styles = """
body{font-family:Arial,sans-serif;margin:0;background:#f6f7fb;color:#1f2937;}
nav{display:flex;gap:1rem;padding:1rem 2rem;background:#111827;}
nav a{color:#fff;text-decoration:none;font-weight:600;}
.hero{padding:4rem 2rem;background:#2563eb;color:white;}
main,section{max-width:900px;margin:2rem auto;padding:0 1rem;}
blockquote{background:#fff;padding:1rem;border-left:4px solid #2563eb;border-radius:6px;}
.tagline{display:inline-block;margin-top:1rem;background:#1d4ed8;padding:.25rem .75rem;border-radius:999px;}
"""
(basic_dir / "index.html").write_text(index_html, encoding="utf-8")
(basic_dir / "about.html").write_text(about_html, encoding="utf-8")
(basic_dir / "testimonials.html").write_text(testimonials_html, encoding="utf-8")
(basic_dir / "contact.html").write_text(contact_html, encoding="utf-8")
(basic_dir / "styles.css").write_text(styles, encoding="utf-8")
basic_zip = root / "basic_site.zip"
self._zip_dir(basic_dir, basic_zip)
return basic_zip
def _copy_client_template(self, source_path: Path, destination: Path) -> None:
def ignore(_directory: str, names: list[str]) -> list[str]:
ignored: list[str] = []
for name in names:
if name in COPY_EXCLUDE_DIR_NAMES:
ignored.append(name)
elif name.endswith(".zip"):
ignored.append(name)
return ignored
shutil.copytree(source_path, destination, ignore=ignore)
def _personalize_template_tree(
self,
target_dir: Path,
site: ClientSite,
ai_payload: dict,
config: dict,
) -> None:
replacements = self._build_replacements(site, ai_payload, config)
for file_path in target_dir.rglob("*"):
if not file_path.is_file():
continue
if file_path.suffix.lower() not in PERSONALIZE_EXTENSIONS:
continue
if "bootstrap-icons" in file_path.parts and file_path.suffix == ".json":
continue
try:
content = file_path.read_text(encoding="utf-8")
except UnicodeDecodeError:
continue
updated = content
for old, new in replacements.items():
if old and new and old in updated:
updated = updated.replace(old, new)
if updated != content:
file_path.write_text(updated, encoding="utf-8")
info_path = target_dir / "client-site-info.json"
info_path.write_text(
json.dumps(
{
"business_name": site.business_name,
"business_address": site.business_address,
"business_phone": site.business_phone,
"business_description": site.business_description,
"old_website": site.old_website,
"ai_payload": ai_payload,
},
indent=2,
),
encoding="utf-8",
)
def _build_replacements(self, site: ClientSite, ai_payload: dict, config: dict) -> dict[str, str]:
replacements: dict[str, str] = {}
business_name = site.business_name
hero_headline = ai_payload.get("hero_headline", business_name)
hero_subheadline = ai_payload.get("hero_subheadline", site.business_description)
for token in config.get("brand_tokens", []):
replacements[token] = business_name
hero_strings = config.get("hero_strings", [])
if hero_strings:
replacements[hero_strings[0]] = hero_headline
for hero_string in hero_strings[1:]:
replacements[hero_string] = hero_subheadline
replacements.update(
{
"555-0100": site.business_phone,
"555-0101": site.business_phone,
"(555) 010-0100": site.business_phone,
"123 Main Street": site.business_address,
"San Francisco, California": site.business_address,
}
)
if site.old_website:
replacements["https://example.com"] = site.old_website
replacements["http://example.com"] = site.old_website
return replacements
def _write_client_readme(
self,
target_dir: Path,
site: ClientSite,
ai_payload: dict,
config: dict,
) -> None:
kind = config.get("kind", "static_html")
run_hint = "Open index.html in a browser."
if kind == "nextjs":
run_hint = "Run: npm install && npm run dev"
elif kind == "static_html":
run_hint = "Open index.html (or another index-*.html page) in a browser."
readme = f"""# {site.business_name} — generated site package
Template: {site.template_option.name}
Type: {kind}
## Business details
- Name: {site.business_name}
- Phone: {site.business_phone}
- Address: {site.business_address}
- Description: {site.business_description}
"""
if site.old_website:
readme += f"- Previous website: {site.old_website}\n"
readme += f"""
## AI-generated copy
- Tagline: {ai_payload.get("tagline", "")}
- Headline: {ai_payload.get("hero_headline", "")}
- Subheadline: {ai_payload.get("hero_subheadline", "")}
## How to preview
{run_hint}
See client-site-info.json for full generation payload.
"""
(target_dir / "README-CLIENT.md").write_text(readme, encoding="utf-8")
def _build_django_demo(self, site: ClientSite, ai_payload: dict, root: Path) -> Path:
project_slug = slugify(site.business_name)[:30] or "client-site"
django_dir = root / "django_demo"
django_dir.mkdir(parents=True, exist_ok=True)
files = {
"manage.py": self._django_manage_py(),
"client_demo/__init__.py": "",
"client_demo/settings.py": self._django_settings_py(site),
"client_demo/urls.py": self._django_urls_py(),
"client_demo/wsgi.py": self._django_wsgi_py(),
"shop/__init__.py": "",
"shop/apps.py": self._shop_apps_py(),
"shop/models.py": self._shop_models_py(),
"shop/forms.py": self._shop_forms_py(),
"shop/urls.py": self._shop_urls_py(),
"shop/views.py": self._shop_views_py(ai_payload),
"shop/admin.py": self._shop_admin_py(),
"shop/migrations/__init__.py": "",
"templates/base.html": self._shop_base_template(site),
"templates/shop/home.html": self._shop_home_template(),
"templates/shop/products.html": self._shop_products_template(),
"templates/shop/cart.html": self._shop_cart_template(),
"templates/shop/contact.html": self._shop_contact_template(),
"README.md": self._django_readme(project_slug),
}
for relative_path, content in files.items():
destination = django_dir / relative_path
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(content, encoding="utf-8")
django_zip = root / "django_demo.zip"
self._zip_dir(django_dir, django_zip)
return django_zip
def _zip_dir(self, directory: Path, output_zip: Path) -> None:
with zipfile.ZipFile(output_zip, "w", zipfile.ZIP_DEFLATED) as zipf:
for file_path in directory.rglob("*"):
if file_path.is_file():
zipf.write(file_path, file_path.relative_to(directory))
def _django_manage_py(self) -> str:
return """#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "client_demo.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
"""
def _django_settings_py(self, site: ClientSite) -> str:
return f"""from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = "replace-me"
DEBUG = True
ALLOWED_HOSTS = ["*"]
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"shop",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "client_demo.urls"
TEMPLATES = [{{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {{"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]}},
}}]
WSGI_APPLICATION = "client_demo.wsgi.application"
DATABASES = {{"default": {{"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3"}}}}
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
DEFAULT_FROM_EMAIL = "no-reply@example.com"
BUSINESS_NAME = {site.business_name!r}
BUSINESS_PHONE = {site.business_phone!r}
BUSINESS_ADDRESS = {site.business_address!r}
"""
def _django_urls_py(self) -> str:
return """from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("shop.urls")),
]
"""
def _django_wsgi_py(self) -> str:
return """import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "client_demo.settings")
application = get_wsgi_application()
"""
def _shop_apps_py(self) -> str:
return """from django.apps import AppConfig
class ShopConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "shop"
"""
def _shop_models_py(self) -> str:
return """from django.db import models
class Product(models.Model):
name = models.CharField(max_length=120)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
is_active = models.BooleanField(default=True)
def __str__(self):
return self.name
"""
def _shop_forms_py(self) -> str:
return """from django import forms
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea(attrs={"rows": 5}))
"""
def _shop_urls_py(self) -> str:
return """from django.urls import path
from . import views
urlpatterns = [
path("", views.home, name="home"),
path("products/", views.products, name="products"),
path("cart/add/<int:product_id>/", views.add_to_cart, name="add_to_cart"),
path("cart/", views.cart, name="cart"),
path("contact/", views.contact, name="contact"),
]
"""
def _shop_views_py(self, ai_payload: dict) -> str:
headline = ai_payload.get("hero_headline", "Welcome")
subheadline = ai_payload.get("hero_subheadline", "")
return f"""from django.conf import settings
from django.contrib import messages
from django.core.mail import send_mail
from django.shortcuts import get_object_or_404, redirect, render
from .forms import ContactForm
from .models import Product
def home(request):
return render(request, "shop/home.html", {{
"headline": {headline!r},
"subheadline": {subheadline!r},
}})
def products(request):
return render(request, "shop/products.html", {{"products": Product.objects.filter(is_active=True)}})
def add_to_cart(request, product_id):
product = get_object_or_404(Product, id=product_id, is_active=True)
cart_data = request.session.get("cart", {{}})
key = str(product.id)
cart_data[key] = cart_data.get(key, 0) + 1
request.session["cart"] = cart_data
messages.success(request, f"Added {{product.name}} to cart.")
return redirect("products")
def cart(request):
cart_data = request.session.get("cart", {{}})
items = []
total = 0
for product_id, qty in cart_data.items():
product = Product.objects.filter(id=product_id).first()
if not product:
continue
line_total = product.price * qty
items.append({{"product": product, "qty": qty, "line_total": line_total}})
total += line_total
return render(request, "shop/cart.html", {{"items": items, "total": total}})
def contact(request):
if request.method == "POST":
form = ContactForm(request.POST)
if form.is_valid():
subject = f"Contact request for {{settings.BUSINESS_NAME}}"
body = (
f"Name: {{form.cleaned_data['name']}}\\n"
f"Email: {{form.cleaned_data['email']}}\\n\\n"
f"Message:\\n{{form.cleaned_data['message']}}"
)
send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [settings.DEFAULT_FROM_EMAIL])
messages.success(request, "Message sent.")
return redirect("contact")
else:
form = ContactForm()
return render(request, "shop/contact.html", {{"form": form}})
"""
def _shop_admin_py(self) -> str:
return """from django.contrib import admin
from .models import Product
admin.site.register(Product)
"""
def _shop_base_template(self, site: ClientSite) -> str:
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{html.escape(site.business_name)} Demo</title>
<style>
body{{font-family:Arial,sans-serif;margin:0;background:#f5f6fb;color:#1f2937}}
nav{{background:#111827;padding:1rem;display:flex;gap:1rem}}
nav a{{color:white;text-decoration:none}}
.wrap{{max-width:960px;margin:0 auto;padding:2rem 1rem}}
.card{{background:white;padding:1rem;border-radius:8px;margin-bottom:1rem}}
.btn{{display:inline-block;background:#2563eb;color:white;padding:.5rem .75rem;border-radius:6px;text-decoration:none;border:none}}
</style>
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/products/">Products</a>
<a href="/cart/">Cart</a>
<a href="/contact/">Contact</a>
</nav>
<div class="wrap">
{{% if messages %}}
{{% for message in messages %}}<p class="card">{{{{ message }}}}</p>{{% endfor %}}
{{% endif %}}
{{% block content %}}{{% endblock %}}
</div>
</body>
</html>
"""
def _shop_home_template(self) -> str:
return """{% extends "base.html" %}
{% block content %}
<section class="card">
<h1>{{ headline }}</h1>
<p>{{ subheadline }}</p>
<a class="btn" href="/products/">Shop now</a>
</section>
{% endblock %}
"""
def _shop_products_template(self) -> str:
return """{% extends "base.html" %}
{% block content %}
<h1>Products</h1>
{% for product in products %}
<article class="card">
<h3>{{ product.name }}</h3>
<p>{{ product.description }}</p>
<p><strong>${{ product.price }}</strong></p>
<a class="btn" href="/cart/add/{{ product.id }}/">Add to cart</a>
</article>
{% empty %}
<p class="card">No products yet. Add from admin panel.</p>
{% endfor %}
{% endblock %}
"""
def _shop_cart_template(self) -> str:
return """{% extends "base.html" %}
{% block content %}
<h1>Your Cart</h1>
{% for item in items %}
<div class="card">
<strong>{{ item.product.name }}</strong> x {{ item.qty }} = ${{ item.line_total }}
</div>
{% empty %}
<p class="card">Cart empty.</p>
{% endfor %}
<p><strong>Total: ${{ total }}</strong></p>
{% endblock %}
"""
def _shop_contact_template(self) -> str:
return """{% extends "base.html" %}
{% block content %}
<h1>Contact</h1>
<form method="post" class="card">
{% csrf_token %}
{{ form.as_p }}
<button class="btn" type="submit">Send message</button>
</form>
{% endblock %}
"""
def _django_readme(self, project_slug: str) -> str:
return f"""# {project_slug}
Generated Django demo storefront.
## Run
1. pip install django
2. python manage.py migrate
3. python manage.py createsuperuser
4. python manage.py runserver
## Basic static site
Download the basic HTML package from the builder dashboard for the matching template design.
"""