From d4901ca6defd5b30ea34c749ef572678d5afa052 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 27 Nov 2025 05:28:21 +0000 Subject: [PATCH] Auto commit: 2025-11-27T05:28:21.824Z --- .perm_test_apache | 0 .perm_test_exec | 0 ai/__init__.py | 3 + ai/local_ai_api.py | 420 ++++++++++++++++++ config/__pycache__/__init__.cpython-311.pyc | Bin 159 -> 159 bytes config/__pycache__/settings.cpython-311.pyc | Bin 4210 -> 4210 bytes config/__pycache__/urls.cpython-311.pyc | Bin 1143 -> 1143 bytes config/__pycache__/wsgi.cpython-311.pyc | Bin 679 -> 679 bytes core/__pycache__/__init__.cpython-311.pyc | Bin 157 -> 157 bytes core/__pycache__/admin.cpython-311.pyc | Bin 777 -> 777 bytes core/__pycache__/apps.cpython-311.pyc | Bin 524 -> 524 bytes core/__pycache__/forms.cpython-311.pyc | Bin 847 -> 847 bytes core/__pycache__/models.cpython-311.pyc | Bin 1409 -> 1409 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 347 bytes core/__pycache__/views.cpython-311.pyc | Bin 2006 -> 1463 bytes core/context_processors.py | 13 + .../__pycache__/0001_initial.cpython-311.pyc | Bin 1660 -> 1660 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 168 -> 168 bytes core/templates/base.html | 30 ++ core/templates/core/article_detail.html | 14 + core/templates/core/index.html | 212 +++------ core/views.py | 41 +- static/css/custom.css | 93 ++++ 23 files changed, 642 insertions(+), 184 deletions(-) create mode 100644 .perm_test_apache create mode 100644 .perm_test_exec create mode 100644 ai/__init__.py create mode 100644 ai/local_ai_api.py create mode 100644 core/context_processors.py create mode 100644 core/templates/base.html create mode 100644 core/templates/core/article_detail.html create mode 100644 static/css/custom.css diff --git a/.perm_test_apache b/.perm_test_apache new file mode 100644 index 0000000..e69de29 diff --git a/.perm_test_exec b/.perm_test_exec new file mode 100644 index 0000000..e69de29 diff --git a/ai/__init__.py b/ai/__init__.py new file mode 100644 index 0000000..37a7b09 --- /dev/null +++ b/ai/__init__.py @@ -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 diff --git a/ai/local_ai_api.py b/ai/local_ai_api.py new file mode 100644 index 0000000..bcff732 --- /dev/null +++ b/ai/local_ai_api.py @@ -0,0 +1,420 @@ +""" +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) + # ... + +# Typical successful payload (truncated): +# { +# "id": "resp_xxx", +# "status": "completed", +# "output": [ +# {"type": "reasoning", "summary": []}, +# {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]} +# ], +# "usage": { "input_tokens": 123, "output_tokens": 456 } +# } + +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 time +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", + "fetch_status", + "await_response", + "extract_text", + "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 extract_text(response: Dict[str, Any]) -> str: + return extract_text(response) + + @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"] + + initial = request(options.get("path"), payload, options) + if not initial.get("success"): + return initial + + data = initial.get("data") + if isinstance(data, dict) and "ai_request_id" in data: + ai_request_id = data["ai_request_id"] + poll_timeout = int(options.get("poll_timeout", 300)) + poll_interval = int(options.get("poll_interval", 5)) + return await_response(ai_request_id, { + "interval": poll_interval, + "timeout": poll_timeout, + "headers": options.get("headers"), + "timeout_per_call": options.get("timeout"), + }) + + return initial + + +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"]) + opt_timeout = options.get("timeout") + timeout = int(cfg["timeout"] if opt_timeout is None else opt_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") + return _http_request(url, "POST", body, headers, timeout, verify_tls) + + +def fetch_status(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Fetch status for a queued AI request.""" + cfg = _config() + options = options or {} + + project_uuid = cfg["project_uuid"] + if not project_uuid: + return { + "success": False, + "error": "project_uuid_missing", + "message": "PROJECT_UUID is not defined; aborting status check.", + } + + status_path = _resolve_status_path(ai_request_id, cfg) + url = _build_url(status_path, cfg["base_url"]) + + opt_timeout = options.get("timeout") + timeout = int(cfg["timeout"] if opt_timeout is None else opt_timeout) + verify_tls = options.get("verify_tls", cfg["verify_tls"]) + + headers: Dict[str, str] = { + "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() + + return _http_request(url, "GET", None, headers, timeout, verify_tls) + + +def await_response(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Poll status endpoint until the request is complete or timed out.""" + options = options or {} + timeout = int(options.get("timeout", 300)) + interval = int(options.get("interval", 5)) + if interval <= 0: + interval = 5 + per_call_timeout = options.get("timeout_per_call") + + deadline = time.time() + max(timeout, interval) + + while True: + status_resp = fetch_status(ai_request_id, { + "headers": options.get("headers"), + "timeout": per_call_timeout, + "verify_tls": options.get("verify_tls"), + }) + if status_resp.get("success"): + data = status_resp.get("data") or {} + if isinstance(data, dict): + status_value = data.get("status") + if status_value == "success": + return { + "success": True, + "status": 200, + "data": data.get("response", data), + } + if status_value == "failed": + return { + "success": False, + "status": 500, + "error": str(data.get("error") or "AI request failed"), + "data": data, + } + else: + return status_resp + + if time.time() >= deadline: + return { + "success": False, + "error": "timeout", + "message": "Timed out waiting for AI response.", + } + time.sleep(interval) + + +def extract_text(response: Dict[str, Any]) -> str: + """Public helper to extract plain text from a Responses payload.""" + return _extract_text(response) + + +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-mini"), + "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 _resolve_status_path(ai_request_id: Any, cfg: Dict[str, Any]) -> str: + base_path = (cfg.get("responses_path") or "").rstrip("/") + if not base_path: + return f"/ai-request/{ai_request_id}/status" + if not base_path.endswith("/ai-request"): + base_path = f"{base_path}/ai-request" + return f"{base_path}/{ai_request_id}/status" + + +def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[str, str], + timeout: int, verify_tls: bool) -> Dict[str, Any]: + """ + Shared HTTP helper for GET/POST requests. + """ + req = urlrequest.Request(url, data=body, method=method.upper()) + 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 _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 diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 3d6501c67fa5c80fdda8a8ee57862699aabd319d..d97834881dd288754e3d176b22fd7829e675b32d 100644 GIT binary patch delta 20 acmbQwIG>ScIWI340}xDkpq@FAXEFdXLj_C# delta 20 acmbQwIG>ScIWI340}vQ{e#)50GZ_Fd1qB`e diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index dadfaa7db630a06a9bc0da9edbc375273aefa0ed..d5239ba9d8e4baa378f94c842a84f194f1c01a65 100644 GIT binary patch delta 21 bcmeyQ@JWGZIWI340}xDkpq{yrCrtnVNmm9l delta 21 bcmeyQ@JWGZIWI340}wn|@;zfCPnrM#PVNU1 diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 139db1071801d00bd7e1a167305bffd074d952f6..42327b6c1f57e072090a76775fdcf4e4cf22a9bf 100644 GIT binary patch delta 21 bcmey)@tuQbIWI340}xDkpq{yrCz}NTMvVpr delta 21 bcmey)@tuQbIWI340}vQ{e#+R$lg$DEL?H#( diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index 79ce690f602e05f6bfa8a8e253edcb377296b788..5df183afac73396ed63d6b02c1ccbd99274bb926 100644 GIT binary patch delta 21 bcmZ3^x}23~IWI340}xDkpq{yrXEqZ6Ir#;S delta 21 bcmZ3^x}23~IWI340}vQ{e#+R$Gn)wjH;n~g diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc index 3b7774ea363dc0bd4dc92284fe89acea2051acd3..d9cdfd61a9f690c84ac2a764377a5dd09a7be046 100644 GIT binary patch delta 20 acmbQsIG2%UIWI340}xDkpq@FAXCeSG^#w)% delta 20 acmbQsIG2%UIWI340}vQ{e#)50GZ6qUw*?pg diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5e41572949c7873f8ad5478be4973869ac513d51..a45e2d789c5a0fdd613591849c0cce53651f0c75 100644 GIT binary patch delta 21 bcmeBV>ty3u&dbZi00dJWsAq2E`O5?VHZujb delta 21 bcmeBV>ty3u&dbZi00g(yzh!LX`O5?VI57qi diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 6435d92a257f85ac41d6fd22a9e528c3da4a1ec5..8f9997f1ca4b99187ba92ad3fe46fd3ed0253bc5 100644 GIT binary patch delta 20 acmeBS>0#kn&dbZi00dJWsAn$ZVFCazd<3fi delta 20 acmeBS>0#kn&dbZi00hRKpE4HmFaZE9J_LOL diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index f6e5c4e3024d0f534a8b93046d15a44a0c8eeba6..70337f275507c45d5c2ef31ea4a8ee2b7436622d 100644 GIT binary patch delta 21 bcmX@lcAkxAIWI340}xDkpq{yr$C()bKd=Rf delta 21 bcmX@lcAkxAIWI340}!OEe9PF#eXW+~#*6K}Vs0#Ou?cHaEv?ach%d-LXf-)db# z(0={xM*LF)q2J5N->o(W=hpyygD9d(f>Px`Q4p(78D%0= z=G)3)1$rI&c>)5CP$+Dojc@C8iK?_&p=G*4FI8xjuF+P7F4J|oQK2^N(9IGJ)W-^a z?O2JG$2Iv6j$=fx9M`@BmLE!{s)1Qo3-hze9xNk<6Oz$58{j+?jAeKn%Lo%Z6r7Gi ziSvk$Lz1&zvHl4M7c#sf(l8>iC{)W1dO=X!jiemrw=^wI$$;g(QI-!Al1IIer`j;$ zl6MU!Fm=y&4aW@hu5MY3Ilj$I*L&~5wy$rEw@t&xg82Vyjh<% zGLo|C(on=93>6Dl6K8;uIG6Zik`9X{zcS`v5o2u22z_5t#$4Rbha_ZLb?n2Kjq~2{(X>&n7A@FZ9+3YC;sni9)VY58 z`3#{QQf{xmv{}t^W}F^s5S|ESF~YT7O-3g ZF{>+zGC|kR{{77wEybIm|BC9wzW{Y-rqcib literal 2006 zcmZ`(&2Q936d!wcz21)`&W4tJ#pJA?V%j1zOmzNm$ozG=l3ywe)Hb% z%?~3Zc?9FF3q=E*-j z479_0?hBiOCEO9YnTVF?30CU9NR#Y?c5QI}H@-|+=}nR529D9No${FX$C{LI!+SPU zn*tq6Z168R;jN4%Z9`N$QDj{EKl`$8?z7~_^8GZONIW6PmX+H~57!dvm~Wk5i3+CD z8de>{vutlw4NQWur#5X`Qyt%cda7VivA1p$-*d4?RSQ$Yc7kfFXw>UXyVcsLn!d~A zi}m`KTw=LPjzJxN)iztXW!zM6(hUc*i5n}|uPiRy(m!3ixv;WyYhaWLKH(DYNaRP^~=u`nh_pqn_KE-I{$i*}l4) zFFl<5w)(KzUg%5c(6R8~%x?K)w>;Y`&+e9Idgb@K67l%h2GSK{&;Ro>LVcS%fBB$#nRS^&g9(F)o$TzuW+`L zKD!4&B9oJ3Bu%z=8Z?g(M*ND?L<|=k&S;V)Xju_q*%G&*sdomAt>vr~Ksn8n_|o|M zZfPBi(nO=1MF&vBofDNaO4Twisve%7U0!$``60Z5tPnIz69)lD(vOH71syrU$$JUmQZNUkJ(yv!)Lsn7 zC)-!T1E<>8!qVZbx$oAyrQ^NQ@%B|dV@KRaWr9p`SMLI0QrruKXD>#g5^JhK%_kIq z6GQ-Hg?%+d6~uuS6Agt{j`4hGUll_OS`HK2tYXWiB+LJn;b#xxJV;Saf+#^zM&kpX z;(-hf{aXGw&g*&d0T_7If)GeQB?v-@a_wk_XsjL0m*{k7c!lU_XLyC^SZ8>J=tyUH tg~Lw3=Swuz8D1eOcZOGAQiKotXh4pqg%f@BZ?e_;m1}?d4^wpVzX0UP4AcMs diff --git a/core/context_processors.py b/core/context_processors.py new file mode 100644 index 0000000..0bf87c3 --- /dev/null +++ b/core/context_processors.py @@ -0,0 +1,13 @@ +import os +import time + +def project_context(request): + """ + Adds project-specific environment variables to the template context globally. + """ + return { + "project_description": os.getenv("PROJECT_DESCRIPTION", ""), + "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + # Used for cache-busting static assets + "deployment_timestamp": int(time.time()), + } diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc index 64d8a5533a01aeb9823459fa0e2a5787563687ab..f305ce4d143437ab015b0aa300cbfd207042ed77 100644 GIT binary patch delta 21 bcmeyv^M{9LIWI340}xDkpq{yrr+^IrNBIUH delta 21 bcmeyv^M{9LIWI340}$N&_cLQ7PXQYMPud4l diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 58b1c14eb06fea9cfb9a0d59788218572a75b51c..b84044f2e684d9e06e7aeee7092c47aa26c898f9 100644 GIT binary patch delta 20 acmZ3%xPp;qIWI340}xDkpq@FAXAS@}q6KFF delta 20 acmZ3%xPp;qIWI340}vQ{e#)50GY0@MWCb|@ diff --git a/core/templates/base.html b/core/templates/base.html new file mode 100644 index 0000000..681818c --- /dev/null +++ b/core/templates/base.html @@ -0,0 +1,30 @@ + + + + + + {% block title %}Knowledge Base{% endblock %} + {% if project_description %} + + + + {% endif %} + {% if project_image_url %} + + + {% endif %} + + + + + {% load static %} + + {% block head %}{% endblock %} + + + + {% block content %}{% endblock %} + + + + diff --git a/core/templates/core/article_detail.html b/core/templates/core/article_detail.html new file mode 100644 index 0000000..8820990 --- /dev/null +++ b/core/templates/core/article_detail.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block title %}{{ article.title }}{% endblock %} + +{% block content %} +
+

{{ article.title }}

+

Published on {{ article.created_at|date:"F d, Y" }}

+
+
+ {{ article.content|safe }} +
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index f4e4991..3b164e5 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,157 +1,63 @@ - - +{% extends 'base.html' %} +{% load static %} - - - - {{ project_name }} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

Appwizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} — - UTC {{ current_time|date:"Y-m-d H:i:s" }} -

+{% block content %} +
- + + - \ No newline at end of file +
+
+

Find Your Next Favorite Thing

+

A curated collection of high-quality products, just for you.

+ Shop Now +
+
+ +
+

Featured Products

+
+ {% for product in products %} +
+
+ {{ product.name }} +
+
{{ product.name }}
+

${{ product.price }}

+ Add to Cart +
+
+
+ {% endfor %} +
+
+ + +{% endblock %} diff --git a/core/views.py b/core/views.py index c1a6d45..c0723c9 100644 --- a/core/views.py +++ b/core/views.py @@ -1,37 +1,16 @@ -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.utils import timezone -from django.views.generic.edit import CreateView - -from .forms import TicketForm -from .models import Ticket - 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() - + """Render the landing screen with a product showcase.""" + products = [ + {'name': 'Modern Armchair', 'price': '349.99', 'image': 'https://images.unsplash.com/photo-1592078615290-033ee584e267?q=80&w=2160&auto=format&fit=crop'}, + {'name': 'Minimalist Lamp', 'price': '89.99', 'image': 'https://images.unsplash.com/photo-1543198126-a8ad3e443351?q=80&w=2592&auto=format&fit=crop'}, + {'name': 'Wooden Coffee Table', 'price': '229.99', 'image': 'https://images.unsplash.com/photo-1611110323561-34282125a48e?q=80&w=2070&auto=format&fit=crop'}, + {'name': 'Cozy Throw Blanket', 'price': '59.99', 'image': 'https://images.unsplash.com/photo-1576014131795-d4477304130a?q=80&w=2070&auto=format&fit=crop'}, + {'name': 'Abstract Wall Art', 'price': '149.99', 'image': 'https://images.unsplash.com/photo-1547891654-e66ed7ebb968?q=80&w=2070&auto=format&fit=crop'}, + {'name': 'Scented Candle Set', 'price': '39.99', 'image': 'https://images.unsplash.com/photo-1600855966839-01c9a2537503?q=80&w=2070&auto=format&fit=crop'}, + ] 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", ""), + 'products': products, } return render(request, "core/index.html", context) - - -class TicketCreateView(CreateView): - model = Ticket - form_class = TicketForm - template_name = "core/ticket_create.html" - success_url = reverse_lazy("home") diff --git a/static/css/custom.css b/static/css/custom.css new file mode 100644 index 0000000..79c86f0 --- /dev/null +++ b/static/css/custom.css @@ -0,0 +1,93 @@ +:root { + --primary-color: #FFFFFF; + --secondary-color: #1A1A1A; + --accent-color-1: #4A90E2; + --accent-color-2: #F5A623; + --background-color: #F8F9FA; + --heading-font: 'Poppins', sans-serif; + --body-font: 'Roboto', sans-serif; +} + +body { + font-family: var(--body-font); + background-color: var(--background-color); + color: var(--secondary-color); +} + +h1, h2, h3, h4, h5, h6 { + font-family: var(--heading-font); + font-weight: 600; +} + +.btn-primary { + background-color: var(--accent-color-1); + border-color: var(--accent-color-1); + font-weight: 600; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + transition: background-color 0.3s ease, border-color 0.3s ease; +} + +.btn-primary:hover { + background-color: #357ABD; + border-color: #357ABD; +} + +.hero-section { + background: linear-gradient(135deg, #6DD5FA, #4A90E2); + color: var(--primary-color); + padding: 6rem 0; + text-align: center; +} + +.hero-section h1 { + font-size: 3.5rem; + font-weight: 700; +} + +.hero-section p { + font-size: 1.25rem; + max-width: 600px; + margin: 1rem auto; +} + +.product-grid { + padding: 4rem 0; +} + +.product-card { + background-color: var(--primary-color); + border: none; + border-radius: 0.75rem; + overflow: hidden; + box-shadow: 0 4px 15px rgba(0,0,0,0.05); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.product-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0,0,0,0.1); +} + +.product-card .card-img-top { + width: 100%; + height: 200px; + object-fit: cover; +} + +.product-card .card-body { + padding: 1.5rem; +} + +.product-card .card-title { + font-size: 1.2rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.product-card .card-text { + font-size: 1.1rem; + font-weight: 600; + color: var(--accent-color-1); + margin-bottom: 1rem; +} \ No newline at end of file