Auto commit: 2025-11-14T09:41:35.163Z
This commit is contained in:
parent
cd996ba56a
commit
f24f716001
0
.perm_test_apache
Normal file
0
.perm_test_apache
Normal file
0
.perm_test_exec
Normal file
0
.perm_test_exec
Normal file
3
ai/__init__.py
Normal file
3
ai/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""Helpers for interacting with the Flatlogic AI proxy from Django code."""
|
||||
|
||||
from .local_ai_api import LocalAIApi, create_response, request, decode_json_from_response # noqa: F401
|
||||
282
ai/local_ai_api.py
Normal file
282
ai/local_ai_api.py
Normal file
@ -0,0 +1,282 @@
|
||||
"""
|
||||
LocalAIApi — lightweight Python client for the Flatlogic AI proxy.
|
||||
|
||||
Usage (inside the Django workspace):
|
||||
|
||||
from ai.local_ai_api import LocalAIApi
|
||||
|
||||
response = LocalAIApi.create_response({
|
||||
"input": [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Summarise this text in two sentences."},
|
||||
],
|
||||
"text": {"format": {"type": "json_object"}},
|
||||
})
|
||||
|
||||
if response.get("success"):
|
||||
data = LocalAIApi.decode_json_from_response(response)
|
||||
# ...
|
||||
|
||||
The helper automatically injects the project UUID header and falls back to
|
||||
reading executor/.env if environment variables are missing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
from urllib import error as urlerror
|
||||
from urllib import request as urlrequest
|
||||
|
||||
__all__ = [
|
||||
"LocalAIApi",
|
||||
"create_response",
|
||||
"request",
|
||||
"decode_json_from_response",
|
||||
]
|
||||
|
||||
|
||||
_CONFIG_CACHE: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class LocalAIApi:
|
||||
"""Static helpers mirroring the PHP implementation."""
|
||||
|
||||
@staticmethod
|
||||
def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
return create_response(params, options or {})
|
||||
|
||||
@staticmethod
|
||||
def request(path: Optional[str] = None, payload: Optional[Dict[str, Any]] = None,
|
||||
options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
return request(path, payload or {}, options or {})
|
||||
|
||||
@staticmethod
|
||||
def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
return decode_json_from_response(response)
|
||||
|
||||
|
||||
def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""Signature compatible with the OpenAI Responses API."""
|
||||
options = options or {}
|
||||
payload = dict(params)
|
||||
|
||||
if not isinstance(payload.get("input"), list) or not payload["input"]:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "input_missing",
|
||||
"message": 'Parameter "input" is required and must be a non-empty list.',
|
||||
}
|
||||
|
||||
cfg = _config()
|
||||
if not payload.get("model"):
|
||||
payload["model"] = cfg["default_model"]
|
||||
|
||||
return request(options.get("path"), payload, options)
|
||||
|
||||
|
||||
def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""Perform a raw request to the AI proxy."""
|
||||
cfg = _config()
|
||||
options = options or {}
|
||||
|
||||
resolved_path = path or options.get("path") or cfg["responses_path"]
|
||||
if not resolved_path:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "project_id_missing",
|
||||
"message": "PROJECT_ID is not defined; cannot resolve AI proxy endpoint.",
|
||||
}
|
||||
|
||||
project_uuid = cfg["project_uuid"]
|
||||
if not project_uuid:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "project_uuid_missing",
|
||||
"message": "PROJECT_UUID is not defined; aborting AI request.",
|
||||
}
|
||||
|
||||
if "project_uuid" not in payload and project_uuid:
|
||||
payload["project_uuid"] = project_uuid
|
||||
|
||||
url = _build_url(resolved_path, cfg["base_url"])
|
||||
timeout = int(options.get("timeout", cfg["timeout"]))
|
||||
verify_tls = options.get("verify_tls", cfg["verify_tls"])
|
||||
|
||||
headers: Dict[str, str] = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
cfg["project_header"]: project_uuid,
|
||||
}
|
||||
extra_headers = options.get("headers")
|
||||
if isinstance(extra_headers, Iterable):
|
||||
for header in extra_headers:
|
||||
if isinstance(header, str) and ":" in header:
|
||||
name, value = header.split(":", 1)
|
||||
headers[name.strip()] = value.strip()
|
||||
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
req = urlrequest.Request(url, data=body, method="POST")
|
||||
for name, value in headers.items():
|
||||
req.add_header(name, value)
|
||||
|
||||
context = None
|
||||
if not verify_tls:
|
||||
context = ssl.create_default_context()
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
try:
|
||||
with urlrequest.urlopen(req, timeout=timeout, context=context) as resp:
|
||||
status = resp.getcode()
|
||||
response_body = resp.read().decode("utf-8", errors="replace")
|
||||
except urlerror.HTTPError as exc:
|
||||
status = exc.getcode()
|
||||
response_body = exc.read().decode("utf-8", errors="replace")
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
return {
|
||||
"success": False,
|
||||
"error": "request_failed",
|
||||
"message": str(exc),
|
||||
}
|
||||
|
||||
decoded = None
|
||||
if response_body:
|
||||
try:
|
||||
decoded = json.loads(response_body)
|
||||
except json.JSONDecodeError:
|
||||
decoded = None
|
||||
|
||||
if 200 <= status < 300:
|
||||
return {
|
||||
"success": True,
|
||||
"status": status,
|
||||
"data": decoded if decoded is not None else response_body,
|
||||
}
|
||||
|
||||
error_message = "AI proxy request failed"
|
||||
if isinstance(decoded, dict):
|
||||
error_message = decoded.get("error") or decoded.get("message") or error_message
|
||||
elif response_body:
|
||||
error_message = response_body
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"status": status,
|
||||
"error": error_message,
|
||||
"response": decoded if decoded is not None else response_body,
|
||||
}
|
||||
|
||||
|
||||
def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Attempt to decode JSON emitted by the model (handles markdown fences)."""
|
||||
text = _extract_text(response)
|
||||
if text == "":
|
||||
return None
|
||||
|
||||
try:
|
||||
decoded = json.loads(text)
|
||||
if isinstance(decoded, dict):
|
||||
return decoded
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
stripped = text.strip()
|
||||
if stripped.startswith("```json"):
|
||||
stripped = stripped[7:]
|
||||
if stripped.endswith("```"):
|
||||
stripped = stripped[:-3]
|
||||
stripped = stripped.strip()
|
||||
if stripped and stripped != text:
|
||||
try:
|
||||
decoded = json.loads(stripped)
|
||||
if isinstance(decoded, dict):
|
||||
return decoded
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _extract_text(response: Dict[str, Any]) -> str:
|
||||
payload = response.get("data") if response.get("success") else response.get("response")
|
||||
if isinstance(payload, dict):
|
||||
output = payload.get("output")
|
||||
if isinstance(output, list):
|
||||
combined = ""
|
||||
for item in output:
|
||||
content = item.get("content") if isinstance(item, dict) else None
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "output_text" and block.get("text"):
|
||||
combined += str(block["text"])
|
||||
if combined:
|
||||
return combined
|
||||
choices = payload.get("choices")
|
||||
if isinstance(choices, list) and choices:
|
||||
message = choices[0].get("message")
|
||||
if isinstance(message, dict) and message.get("content"):
|
||||
return str(message["content"])
|
||||
if isinstance(payload, str):
|
||||
return payload
|
||||
return ""
|
||||
|
||||
|
||||
def _config() -> Dict[str, Any]:
|
||||
global _CONFIG_CACHE # noqa: PLW0603
|
||||
if _CONFIG_CACHE is not None:
|
||||
return _CONFIG_CACHE
|
||||
|
||||
_ensure_env_loaded()
|
||||
|
||||
base_url = os.getenv("AI_PROXY_BASE_URL", "https://flatlogic.com")
|
||||
project_id = os.getenv("PROJECT_ID") or None
|
||||
responses_path = os.getenv("AI_RESPONSES_PATH")
|
||||
if not responses_path and project_id:
|
||||
responses_path = f"/projects/{project_id}/ai-request"
|
||||
|
||||
_CONFIG_CACHE = {
|
||||
"base_url": base_url,
|
||||
"responses_path": responses_path,
|
||||
"project_id": project_id,
|
||||
"project_uuid": os.getenv("PROJECT_UUID"),
|
||||
"project_header": os.getenv("AI_PROJECT_HEADER", "project-uuid"),
|
||||
"default_model": os.getenv("AI_DEFAULT_MODEL", "gpt-5"),
|
||||
"timeout": int(os.getenv("AI_TIMEOUT", "30")),
|
||||
"verify_tls": os.getenv("AI_VERIFY_TLS", "true").lower() not in {"0", "false", "no"},
|
||||
}
|
||||
return _CONFIG_CACHE
|
||||
|
||||
|
||||
def _build_url(path: str, base_url: str) -> str:
|
||||
trimmed = path.strip()
|
||||
if trimmed.startswith("http://") or trimmed.startswith("https://"):
|
||||
return trimmed
|
||||
if trimmed.startswith("/"):
|
||||
return f"{base_url}{trimmed}"
|
||||
return f"{base_url}/{trimmed}"
|
||||
|
||||
|
||||
def _ensure_env_loaded() -> None:
|
||||
"""Populate os.environ from executor/.env if variables are missing."""
|
||||
if os.getenv("PROJECT_UUID") and os.getenv("PROJECT_ID"):
|
||||
return
|
||||
|
||||
env_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".env"))
|
||||
if not os.path.exists(env_path):
|
||||
return
|
||||
|
||||
try:
|
||||
with open(env_path, "r", encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
||||
continue
|
||||
key, value = stripped.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip().strip('\'"')
|
||||
if key and not os.getenv(key):
|
||||
os.environ[key] = value
|
||||
except OSError:
|
||||
pass
|
||||
BIN
assets/pasted-20251114-093632-0ea355c8.png
Normal file
BIN
assets/pasted-20251114-093632-0ea355c8.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 409 KiB |
BIN
assets/vm-shot-2025-11-14T09-36-02-794Z.jpg
Normal file
BIN
assets/vm-shot-2025-11-14T09-36-02-794Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
BIN
assets/vm-shot-2025-11-14T09-36-21-828Z.jpg
Normal file
BIN
assets/vm-shot-2025-11-14T09-36-21-828Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,8 +1,8 @@
|
||||
from django.contrib import admin
|
||||
from .models import Ticket
|
||||
from .models import Consumption
|
||||
|
||||
@admin.register(Ticket)
|
||||
class TicketAdmin(admin.ModelAdmin):
|
||||
list_display = ('subject', 'status', 'priority', 'requester_email', 'created_at')
|
||||
list_filter = ('status', 'priority')
|
||||
search_fields = ('subject', 'requester_email', 'description')
|
||||
@admin.register(Consumption)
|
||||
class ConsumptionAdmin(admin.ModelAdmin):
|
||||
list_display = ('drink_type', 'size', 'timestamp', 'session_key')
|
||||
list_filter = ('drink_type', 'size', 'timestamp')
|
||||
search_fields = ('session_key',)
|
||||
@ -1,7 +1,22 @@
|
||||
from django import forms
|
||||
from .models import Ticket
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.contrib.auth.models import User
|
||||
from .models import Consumption
|
||||
|
||||
class TicketForm(forms.ModelForm):
|
||||
class ConsumptionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = ['subject', 'requester_email', 'priority', 'description']
|
||||
model = Consumption
|
||||
fields = ['drink_type', 'size']
|
||||
labels = {
|
||||
'drink_type': 'Drink Type',
|
||||
'size': 'Size'
|
||||
}
|
||||
widgets = {
|
||||
'drink_type': forms.Select(attrs={'class': 'form-select form-select-lg mb-3'}),
|
||||
'size': forms.Select(attrs={'class': 'form-select form-select-lg mb-3'}),
|
||||
}
|
||||
|
||||
class SignUpForm(UserCreationForm):
|
||||
class Meta(UserCreationForm.Meta):
|
||||
model = User
|
||||
fields = ('username', 'email')
|
||||
|
||||
26
core/migrations/0002_consumption_delete_ticket.py
Normal file
26
core/migrations/0002_consumption_delete_ticket.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-14 09:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Consumption',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('drink_type', models.CharField(choices=[('coffee', 'Coffee'), ('tea', 'Tea'), ('decaf', 'Decaf'), ('energy', 'Energy Drink')], default='coffee', max_length=10)),
|
||||
('size', models.CharField(choices=[('small', 'Small (240ml)'), ('medium', 'Medium (350ml)'), ('large', 'Large (470ml)')], default='medium', max_length=10)),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('session_key', models.CharField(blank=True, db_index=True, max_length=40, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Ticket',
|
||||
),
|
||||
]
|
||||
21
core/migrations/0003_consumption_user.py
Normal file
21
core/migrations/0003_consumption_user.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-14 09:34
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_consumption_delete_ticket'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='consumption',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,25 +1,25 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
class Ticket(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('open', 'Open'),
|
||||
('in_progress', 'In Progress'),
|
||||
('closed', 'Closed'),
|
||||
|
||||
class Consumption(models.Model):
|
||||
DRINK_CHOICES = [
|
||||
('coffee', 'Coffee'),
|
||||
('tea', 'Tea'),
|
||||
('decaf', 'Decaf'),
|
||||
('energy', 'Energy Drink'),
|
||||
]
|
||||
SIZE_CHOICES = [
|
||||
('small', 'Small (240ml)'),
|
||||
('medium', 'Medium (350ml)'),
|
||||
('large', 'Large (470ml)'),
|
||||
]
|
||||
|
||||
PRIORITY_CHOICES = [
|
||||
('low', 'Low'),
|
||||
('medium', 'Medium'),
|
||||
('high', 'High'),
|
||||
]
|
||||
|
||||
subject = models.CharField(max_length=255)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open')
|
||||
priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default='medium')
|
||||
requester_email = models.EmailField()
|
||||
description = models.TextField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
|
||||
drink_type = models.CharField(max_length=10, choices=DRINK_CHOICES, default='coffee')
|
||||
size = models.CharField(max_length=10, choices=SIZE_CHOICES, default='medium')
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
session_key = models.CharField(max_length=40, db_index=True, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.subject
|
||||
return f"{self.get_drink_type_display()} ({self.get_size_display()}) at {self.timestamp}"
|
||||
|
||||
55
core/templates/base.html
Normal file
55
core/templates/base.html
Normal file
@ -0,0 +1,55 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Caffeine Tracker{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<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=Lato:wght@400;700&family=Playfair+Display:wght@700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v=1">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light shadow-sm">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">Caffeine Tracker</a>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
{% if user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<span class="nav-link">Hi, {{ user.username }}!</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'core:profile' %}">Profile</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'core:logout' %}">Logout</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'core:login' %}">Login</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'core:signup' %}">Sign Up</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="text-center py-4 mt-5">
|
||||
<div class="container">
|
||||
<p class="text-muted">© {% now "Y" %} Caffeine Tracker. All Rights Reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
14
core/templates/core/article_detail.html
Normal file
14
core/templates/core/article_detail.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ article.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<h1>{{ article.title }}</h1>
|
||||
<p class="text-muted">Published on {{ article.created_at|date:"F d, Y" }}</p>
|
||||
<hr>
|
||||
<div>
|
||||
{{ article.content|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,157 +1,69 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ project_name }}</title>
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
<meta property="twitter:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
{% if project_image_url %}
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
<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;600;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.08);
|
||||
--card-border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
{% block title %}Caffeine Tracker - Your Daily Coffee Companion{% endblock %}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
{% block content %}
|
||||
<div class="container-fluid hero-section">
|
||||
<div class="container text-center py-5">
|
||||
<h1 class="display-4 fw-bold">Stay Energized, Not Overloaded.</h1>
|
||||
<p class="lead col-lg-8 mx-auto">Mindfully track your caffeine intake for better sleep, health, and productivity. Start with one tap.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(130deg, 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='140' height='140' viewBox='0 0 140 140'><path d='M-20 20L160 20M20 -20L20 160' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||
animation: bg-pan 24s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(-140px, -140px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: clamp(2rem, 4vw, 3rem);
|
||||
width: min(640px, 92vw);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 20px;
|
||||
padding: clamp(2rem, 4vw, 3rem);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 1.2rem;
|
||||
font-weight: 700;
|
||||
font-size: clamp(2.2rem, 3vw + 1.3rem, 3rem);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.6rem 0;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.7;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 2.4rem;
|
||||
font-size: 0.86rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<div class="container my-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6 mb-4 mb-lg-0">
|
||||
<div class="card shadow-lg glass-card h-100">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<h2 class="card-title text-center mb-4">Log a Drink</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.drink_type.id_for_label }}" class="form-label">{{ form.drink_type.label }}</label>
|
||||
{{ form.drink_type }}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="{{ form.size.id_for_label }}" class="form-label">{{ form.size.label }}</label>
|
||||
{{ form.size }}
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg">Add Cup</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p>Appwizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p>This page will refresh automatically as the plan is implemented.</p>
|
||||
<p>
|
||||
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>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-lg summary-card h-100">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<h2 class="card-title text-center mb-4">Today's Summary</h2>
|
||||
<div class="text-center">
|
||||
<p class="display-1 fw-bold text-primary mb-0">{{ total_caffeine }}</p>
|
||||
<p class="lead text-muted">mg of Caffeine</p>
|
||||
<hr class="my-4">
|
||||
<p class="display-5 fw-bold">{{ total_drinks }}</p>
|
||||
<p class="lead text-muted">Drinks Logged</p>
|
||||
</div>
|
||||
<h4 class="mt-5 mb-3 text-center">Today's Log:</h4>
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for item in consumptions_today %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<strong class="text-capitalize">{{ item.get_drink_type_display }}</strong>
|
||||
<small class="text-muted ms-2">({{ item.get_size_display }})</small>
|
||||
</span>
|
||||
<span class="badge bg-light text-dark rounded-pill border">
|
||||
{{ item.timestamp|time:"H:i" }}
|
||||
</span>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="list-group-item text-center text-muted">No drinks logged yet today.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
54
core/templates/core/profile.html
Normal file
54
core/templates/core/profile.html
Normal file
@ -0,0 +1,54 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<h1 class="text-center mb-4">Your Profile</h1>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Total Drinks</h5>
|
||||
<p class="card-text display-4">{{ total_drinks }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Total Caffeine (mg)</h5>
|
||||
<p class="card-text display-4">{{ total_caffeine }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-5 mb-3">Your Consumption History</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Drink</th>
|
||||
<th>Size</th>
|
||||
<th>Caffeine (mg)</th>
|
||||
<th>Date & Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for consumption in consumptions %}
|
||||
<tr>
|
||||
<td>{{ consumption.get_drink_type_display }}</td>
|
||||
<td>{{ consumption.get_size_display }}</td>
|
||||
<td>{{ consumption.caffeine_amount }}</td>
|
||||
<td>{{ consumption.timestamp|date:"Y-m-d H:i" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">You haven't logged any drinks yet.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
20
core/templates/registration/login.html
Normal file
20
core/templates/registration/login.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-center mb-4">Login</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary w-100">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
20
core/templates/registration/signup.html
Normal file
20
core/templates/registration/signup.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-center mb-4">Sign Up</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary w-100">Sign Up</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
11
core/urls.py
11
core/urls.py
@ -1,7 +1,12 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
from .views import home
|
||||
app_name = 'core'
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
]
|
||||
path('', views.index, name='index'),
|
||||
path('signup/', views.signup, name='signup'),
|
||||
path('login/', views.login_view, name='login'),
|
||||
path('logout/', views.logout_view, name='logout'),
|
||||
path('profile/', views.profile, name='profile'),
|
||||
]
|
||||
128
core/views.py
128
core/views.py
@ -1,37 +1,109 @@
|
||||
import os
|
||||
import platform
|
||||
|
||||
from django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse_lazy
|
||||
from django.shortcuts import render, redirect
|
||||
from django.utils import timezone
|
||||
from django.views.generic.edit import CreateView
|
||||
from .models import Consumption
|
||||
from .forms import ConsumptionForm, SignUpForm
|
||||
from datetime import timedelta
|
||||
from django.contrib.auth import login, logout, authenticate
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
|
||||
from .forms import TicketForm
|
||||
from .models import Ticket
|
||||
# A simple mapping for caffeine content. This can be made more complex later.
|
||||
CAFFEINE_MAP = {
|
||||
'coffee': {'small': 95, 'medium': 140, 'large': 210},
|
||||
'tea': {'small': 26, 'medium': 40, 'large': 55},
|
||||
'energy': {'small': 80, 'medium': 120, 'large': 160},
|
||||
'decaf': {'small': 2, 'medium': 4, 'large': 7},
|
||||
}
|
||||
|
||||
def index(request):
|
||||
if not request.session.session_key:
|
||||
request.session.create()
|
||||
session_key = request.session.session_key
|
||||
|
||||
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()
|
||||
if request.method == 'POST':
|
||||
form = ConsumptionForm(request.POST)
|
||||
if form.is_valid():
|
||||
consumption = form.save(commit=False)
|
||||
if request.user.is_authenticated:
|
||||
consumption.user = request.user
|
||||
else:
|
||||
consumption.session_key = session_key
|
||||
consumption.save()
|
||||
return redirect('index')
|
||||
else:
|
||||
form = ConsumptionForm()
|
||||
|
||||
# Get today's consumption
|
||||
today = timezone.now().date()
|
||||
start_of_day = timezone.make_aware(timezone.datetime.combine(today, timezone.datetime.min.time()))
|
||||
end_of_day = start_of_day + timedelta(days=1)
|
||||
|
||||
if request.user.is_authenticated:
|
||||
consumptions_today = Consumption.objects.filter(
|
||||
user=request.user,
|
||||
timestamp__gte=start_of_day,
|
||||
timestamp__lt=end_of_day
|
||||
).order_by('-timestamp')
|
||||
else:
|
||||
consumptions_today = Consumption.objects.filter(
|
||||
session_key=session_key,
|
||||
timestamp__gte=start_of_day,
|
||||
timestamp__lt=end_of_day
|
||||
).order_by('-timestamp')
|
||||
|
||||
total_caffeine = sum(
|
||||
CAFFEINE_MAP.get(c.drink_type, {}).get(c.size, 0) for c in consumptions_today
|
||||
)
|
||||
total_drinks = consumptions_today.count()
|
||||
|
||||
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", ""),
|
||||
'form': form,
|
||||
'consumptions_today': consumptions_today,
|
||||
'total_caffeine': total_caffeine,
|
||||
'total_drinks': total_drinks,
|
||||
}
|
||||
return render(request, "core/index.html", context)
|
||||
return render(request, 'core/index.html', context)
|
||||
|
||||
def signup(request):
|
||||
if request.method == 'POST':
|
||||
form = SignUpForm(request.POST)
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
login(request, user)
|
||||
return redirect('index')
|
||||
else:
|
||||
form = SignUpForm()
|
||||
return render(request, 'registration/signup.html', {'form': form})
|
||||
|
||||
class TicketCreateView(CreateView):
|
||||
model = Ticket
|
||||
form_class = TicketForm
|
||||
template_name = "core/ticket_create.html"
|
||||
success_url = reverse_lazy("home")
|
||||
def login_view(request):
|
||||
if request.method == 'POST':
|
||||
form = AuthenticationForm(request, data=request.POST)
|
||||
if form.is_valid():
|
||||
username = form.cleaned_data.get('username')
|
||||
password = form.cleaned_data.get('password')
|
||||
user = authenticate(username=username, password=password)
|
||||
if user is not None:
|
||||
login(request, user)
|
||||
return redirect('index')
|
||||
else:
|
||||
form = AuthenticationForm()
|
||||
return render(request, 'registration/login.html', {'form': form})
|
||||
|
||||
def logout_view(request):
|
||||
logout(request)
|
||||
return redirect('index')
|
||||
|
||||
@login_required
|
||||
def profile(request):
|
||||
consumptions = Consumption.objects.filter(user=request.user).order_by('-timestamp')
|
||||
|
||||
# Calculate statistics
|
||||
total_drinks = consumptions.count()
|
||||
total_caffeine = sum(CAFFEINE_MAP.get(c.drink_type, {}).get(c.size, 0) for c in consumptions)
|
||||
|
||||
context = {
|
||||
'consumptions': consumptions,
|
||||
'total_drinks': total_drinks,
|
||||
'total_caffeine': total_caffeine,
|
||||
}
|
||||
return render(request, 'core/profile.html', context)
|
||||
|
||||
92
static/css/custom.css
Normal file
92
static/css/custom.css
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
Palette:
|
||||
Primary (Dark Brown): #4A2C2A
|
||||
Secondary (Cream): #F5F5DC
|
||||
Accent (Vibrant Orange): #FF7043
|
||||
Background (Light Beige): #FDFBF7
|
||||
Text: #333333
|
||||
*/
|
||||
|
||||
body {
|
||||
font-family: 'Lato', sans-serif;
|
||||
background-color: #FDFBF7;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, .navbar-brand, .display-4 {
|
||||
font-family: 'Playfair Display', serif;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: #fff !important;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
color: #4A2C2A !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
background: linear-gradient(135deg, #fdfbf7 0%, #f5f5dc 100%);
|
||||
padding: 4rem 0;
|
||||
color: #4A2C2A;
|
||||
}
|
||||
|
||||
.hero-section .display-4 {
|
||||
color: #4A2C2A;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #FF7043;
|
||||
border-color: #FF7043;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #e65a31;
|
||||
border-color: #e65a31;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #FF7043 !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: bold;
|
||||
color: #4A2C2A;
|
||||
}
|
||||
|
||||
.form-select-lg {
|
||||
padding: 0.75rem 1.25rem;
|
||||
font-size: 1.1rem;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
background-color: transparent;
|
||||
border-color: #eee !important;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: #F5F5DC;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user