NaifAlzanki commited on
Commit ·
cacef66
0
Parent(s):
Deploy KW Multi AI Agent backend (2026-04-12_02-40)
Browse files- .env.example +64 -0
- Dockerfile +23 -0
- README.md +41 -0
- app/__init__.py +2 -0
- app/agents.py +176 -0
- app/ai_providers.py +200 -0
- app/auth.py +95 -0
- app/config.py +87 -0
- app/main.py +228 -0
- app/schemas.py +56 -0
- app/usage_store.py +94 -0
- requirements.txt +7 -0
- start.ps1 +7 -0
- static/assets/DashboardScreen-B1h__hB-.js +0 -0
- static/assets/index-BwPR7vCI.css +1 -0
- static/assets/index-D3HV3XVM.js +0 -0
- static/assets/index.es-s1vvCQPr.js +0 -0
- static/assets/purify.es-BgtpMKW3.js +2 -0
- static/index.html +117 -0
- static/manifest.json +20 -0
.env.example
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Runtime
|
| 2 |
+
APP_NAME=KW Multi AI Agent Backend
|
| 3 |
+
APP_ENV=development
|
| 4 |
+
APP_HOST=0.0.0.0
|
| 5 |
+
APP_PORT=8000
|
| 6 |
+
|
| 7 |
+
# Security
|
| 8 |
+
JWT_SECRET_KEY=CHANGE_ME_TO_LONG_RANDOM_SECRET
|
| 9 |
+
JWT_ALGORITHM=HS256
|
| 10 |
+
JWT_EXPIRE_MINUTES=1440
|
| 11 |
+
|
| 12 |
+
# Admin login (optional / tools)
|
| 13 |
+
ADMIN_USERNAME=admin
|
| 14 |
+
ADMIN_PASSWORD=change-me-now
|
| 15 |
+
|
| 16 |
+
# Dev: skip JWT on agent routes; use X-User-Id for usage attribution
|
| 17 |
+
AUTH_DISABLED=false
|
| 18 |
+
|
| 19 |
+
# Mobile/web app: same secret as VITE_APP_BACKEND_SECRET — POST /auth/app-token
|
| 20 |
+
APP_SHARED_SECRET=
|
| 21 |
+
|
| 22 |
+
# Comma-separated user ids treated as Pro (server-side)
|
| 23 |
+
PRO_USER_IDS=
|
| 24 |
+
|
| 25 |
+
# Legacy global AI provider when per-agent AGENT_*_PROVIDER is not set (together | runpod)
|
| 26 |
+
AI_PROVIDER=together
|
| 27 |
+
|
| 28 |
+
# Together AI
|
| 29 |
+
TOGETHER_API_KEY=
|
| 30 |
+
TOGETHER_BASE_URL=https://api.together.xyz/v1
|
| 31 |
+
|
| 32 |
+
# RunPod / OpenAI-compatible
|
| 33 |
+
RUNPOD_API_KEY=
|
| 34 |
+
RUNPOD_BASE_URL=
|
| 35 |
+
|
| 36 |
+
# Groq (e.g. default for fitness agent)
|
| 37 |
+
GROQ_API_KEY=
|
| 38 |
+
GROQ_BASE_URL=https://api.groq.com/openai/v1
|
| 39 |
+
|
| 40 |
+
# Any OpenAI-compatible API
|
| 41 |
+
OPENAI_COMPAT_API_KEY=
|
| 42 |
+
OPENAI_COMPAT_BASE_URL=
|
| 43 |
+
|
| 44 |
+
# Per-agent routing (optional): together | runpod | groq | openai_compat
|
| 45 |
+
# AGENT_FULLSTACK_PROVIDER=together
|
| 46 |
+
# AGENT_UIUX_PROVIDER=together
|
| 47 |
+
# AGENT_CYBERSEC_PROVIDER=together
|
| 48 |
+
# AGENT_FITNESS_PROVIDER=groq
|
| 49 |
+
|
| 50 |
+
# Per-agent model id override (OpenAI-style id for that provider)
|
| 51 |
+
# MODEL_FULLSTACK=meta-llama/Meta-Llama-3-8B-Instruct-Turbo
|
| 52 |
+
# MODEL_FITNESS=llama-3.1-8b-instant
|
| 53 |
+
|
| 54 |
+
# Daily message caps (0 = unlimited). Matches frontend src/lib/subscription.ts
|
| 55 |
+
FREE_TIER_DAILY_MESSAGES=20
|
| 56 |
+
PRO_TIER_DAILY_MESSAGES=300
|
| 57 |
+
UNLIMITED_TIER_DAILY_MESSAGES=0
|
| 58 |
+
|
| 59 |
+
# Server-side override: comma-separated user ids granted Pro/Unlimited
|
| 60 |
+
# PRO_USER_IDS=naif-local-owner
|
| 61 |
+
# UNLIMITED_USER_IDS=naif-local-owner
|
| 62 |
+
|
| 63 |
+
# Rate limit (per IP)
|
| 64 |
+
RATE_LIMIT=30/minute
|
Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Hugging Face Spaces user
|
| 4 |
+
RUN useradd -m -u 1000 user
|
| 5 |
+
USER user
|
| 6 |
+
ENV HOME=/home/user \
|
| 7 |
+
PATH=/home/user/.local/bin:$PATH
|
| 8 |
+
|
| 9 |
+
WORKDIR $HOME/app
|
| 10 |
+
|
| 11 |
+
COPY --chown=user requirements.txt .
|
| 12 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 13 |
+
pip install --no-cache-dir -r requirements.txt
|
| 14 |
+
|
| 15 |
+
COPY --chown=user . .
|
| 16 |
+
|
| 17 |
+
ENV HOST=0.0.0.0
|
| 18 |
+
ENV PORT=7860
|
| 19 |
+
ENV AUTH_DISABLED=true
|
| 20 |
+
|
| 21 |
+
EXPOSE 7860
|
| 22 |
+
|
| 23 |
+
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: KW Multi AI Agent Backend
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# KW Multi AI Agent — Backend
|
| 12 |
+
|
| 13 |
+
FastAPI backend serving 4 specialized AI agents (Full-Stack, UI/UX, Cyber Security, Fitness)
|
| 14 |
+
powered by Groq cloud models. Built by Naif Al-Zanki.
|
| 15 |
+
|
| 16 |
+
## Required secrets
|
| 17 |
+
|
| 18 |
+
Set these in **Settings → Variables and secrets** of the Space:
|
| 19 |
+
|
| 20 |
+
- `GROQ_API_KEY` — your Groq cloud API key (from console.groq.com)
|
| 21 |
+
- `AI_PROVIDER` — `groq`
|
| 22 |
+
- `AUTH_DISABLED` — `true`
|
| 23 |
+
|
| 24 |
+
## Endpoints
|
| 25 |
+
|
| 26 |
+
- `GET /health`
|
| 27 |
+
- `POST /agents/fullstack/chat`
|
| 28 |
+
- `POST /agents/uiux/chat`
|
| 29 |
+
- `POST /agents/cybersec/chat`
|
| 30 |
+
- `POST /agents/fitness/chat`
|
| 31 |
+
|
| 32 |
+
## Body
|
| 33 |
+
|
| 34 |
+
```json
|
| 35 |
+
{
|
| 36 |
+
"message": "...",
|
| 37 |
+
"history": [],
|
| 38 |
+
"temperature": 0.6,
|
| 39 |
+
"max_tokens": 1024
|
| 40 |
+
}
|
| 41 |
+
```
|
app/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""KW Multi AI Agent backend package."""
|
| 2 |
+
|
app/agents.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import Dict
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
@dataclass(frozen=True)
|
| 6 |
+
class AgentSpec:
|
| 7 |
+
id: str
|
| 8 |
+
name: str
|
| 9 |
+
model_together: str
|
| 10 |
+
model_runpod: str
|
| 11 |
+
model_groq: str
|
| 12 |
+
system_prompt: str
|
| 13 |
+
# Ollama / OpenAI-compatible local id when AGENT_*_PROVIDER=openai_compat
|
| 14 |
+
model_ollama: str = "llama3:8b"
|
| 15 |
+
default_provider: str = "groq"
|
| 16 |
+
temperature: float = 0.6
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# ============================================================
|
| 20 |
+
# Q8 Multi-Agent System - 4 وكلاء، 4 أدمغة، 4 بائعين مختلفين
|
| 21 |
+
#
|
| 22 |
+
# المالك والمطوّر: نايف الزنكي 🇰🇼 (الكويت)
|
| 23 |
+
# ملاحظة مهمة: نايف هو المطوّر — ليس هوية أي وكيل.
|
| 24 |
+
# ============================================================
|
| 25 |
+
|
| 26 |
+
# قاعدة لهجة وهوية وتنسيق تُحقن في كل برومبت
|
| 27 |
+
_DIALECT_RULE = (
|
| 28 |
+
"\n\n=== قواعد إلزامية لا تُكسر أبداً ===\n"
|
| 29 |
+
"\n[١] الهوية:\n"
|
| 30 |
+
"- أنت ذكاء اصطناعي، اسمك مذكور فوق، ما أنت إنسان.\n"
|
| 31 |
+
"- المطوّر والمالك اسمه 'نايف الزنكي' من الكويت 🇰🇼. "
|
| 32 |
+
"تتكلم عنه بصيغة الغايب بس ('المطوّر نايف الزنكي'). "
|
| 33 |
+
"ممنوع تقول 'أنا نايف'.\n"
|
| 34 |
+
"\n[٢] اللغة واللهجة — قانون حديدي:\n"
|
| 35 |
+
"- إذا المستخدم كتب بالإنجليزي → رد بالإنجليزي.\n"
|
| 36 |
+
"- إذا كتب بالفصحى → رد بالفصحى السليمة.\n"
|
| 37 |
+
"- إذا كتب بالخليجي/الكويتي → رد بالكويتي الطبيعي.\n"
|
| 38 |
+
"- ممنوع منع بات أي كلمة مصرية: "
|
| 39 |
+
"(يبقى، هقولك، هقول، عايز، عاوز، إزيك، إزاي، كده، دلوقت، "
|
| 40 |
+
"معلش، كويس، بص، حاجة، ماشي، ليه، فين، ازاي، بتاع).\n"
|
| 41 |
+
"- ممنوع الكلمات الشامية: (شو، هلأ، منيح، كتير، هيك، لسا).\n"
|
| 42 |
+
"- استخدم الكلمات الكويتية الصحيحة: "
|
| 43 |
+
"(شلون = كيف، وايد = كثير، زين = جيد، عيل = إذن، "
|
| 44 |
+
"جذي = هكذا، بعد = أيضاً، لي الحين = حتى الآن، هني = هنا، "
|
| 45 |
+
"ترا = في الواقع، شكثر = كم، شنو = ماذا، منو = من، ليش = لماذا).\n"
|
| 46 |
+
"- ممنوع كتابة كلمات ملتصقة أو مبتكرة زي 'مابشكل'. "
|
| 47 |
+
"اكتب 'ما في مشكلة' أو 'عادي' أو 'تمام' بس.\n"
|
| 48 |
+
"- اكتب كل كلمة صح بدون أخطاء إملائية.\n"
|
| 49 |
+
"\n[٣] التنسيق — إلزامي ١٠٠٪:\n"
|
| 50 |
+
"كل رد لازم يكون مرتب، واضح، ومنظم بـ Markdown:\n"
|
| 51 |
+
"- استخدم عناوين `##` للأقسام الرئيسية، و `###` للفرعية.\n"
|
| 52 |
+
"- استخدم `**نص**` للتأكيد على الكلمات المهمة.\n"
|
| 53 |
+
"- استخدم قوائم نقطية (`-`) أو مرقمة (`1.`) لأي تعداد.\n"
|
| 54 |
+
"- استخدم ``` code blocks ``` لأي كود مع تحديد اللغة.\n"
|
| 55 |
+
"- استخدم جداول `|...|` للبيانات المقارنة.\n"
|
| 56 |
+
"- اترك سطر فاضي بين الأقسام.\n"
|
| 57 |
+
"- ممنوع جمل طويلة ملتصقة بدون فواصل.\n"
|
| 58 |
+
"\n[٤] الأسلوب:\n"
|
| 59 |
+
"- ادخل في الموضوع مباشرة بدون مقدمات طويلة.\n"
|
| 60 |
+
"- ممنوع تقدم نفسك إلا إذا طلب المستخدم.\n"
|
| 61 |
+
"- اعطِ معلومات دقيقة، عملية، قابلة للتنفيذ.\n"
|
| 62 |
+
"- ختم كل رد بسؤال واحد قصير يشجع الاستمرار (اختياري).\n"
|
| 63 |
+
"- لا حشو، لا تكرار، لا اعتذارات زائدة."
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
AGENTS: Dict[str, AgentSpec] = {
|
| 68 |
+
# ========================================================
|
| 69 |
+
# 1) FULLSTACK MASTER - Meta Llama 3.3 70B
|
| 70 |
+
# ========================================================
|
| 71 |
+
"fullstack": AgentSpec(
|
| 72 |
+
id="fullstack",
|
| 73 |
+
name="Q8 Agent Full-Stack Master 3.1",
|
| 74 |
+
model_together="meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
| 75 |
+
model_runpod="meta-llama/Llama-3.3-70B-Instruct",
|
| 76 |
+
model_groq="llama-3.3-70b-versatile",
|
| 77 |
+
model_ollama="llama3:8b",
|
| 78 |
+
default_provider="groq",
|
| 79 |
+
temperature=0.4,
|
| 80 |
+
system_prompt=(
|
| 81 |
+
"اسمك: Q8 Agent Full-Stack Master 3.1\n"
|
| 82 |
+
"محرّكك: Meta Llama 3.3 70B\n"
|
| 83 |
+
"تخصصك: كبير المهندسين المعماريين للبرمجيات.\n\n"
|
| 84 |
+
"خبرتك تشمل: React, TypeScript, Next.js, Node.js, Python, FastAPI, "
|
| 85 |
+
"Django, PostgreSQL, MongoDB, Redis, Docker, Kubernetes, AWS, GCP, "
|
| 86 |
+
"Firebase, CI/CD, microservices, REST, GraphQL, WebSocket.\n\n"
|
| 87 |
+
"أسلوبك:\n"
|
| 88 |
+
"- تعطي كود قابل للنسخ مباشرة، مع شرح موجز للقرارات المعمارية.\n"
|
| 89 |
+
"- تشرح المقايضات (سرعة/تكلفة، بساطة/مرونة).\n"
|
| 90 |
+
"- ترتب الحلول حسب الأولوية وتذكر أفضل الممارسات الأمنية (OWASP)."
|
| 91 |
+
) + _DIALECT_RULE,
|
| 92 |
+
),
|
| 93 |
+
|
| 94 |
+
# ========================================================
|
| 95 |
+
# 2) UI/UX DESIGNER - Alibaba Qwen 3 32B
|
| 96 |
+
# ========================================================
|
| 97 |
+
"uiux": AgentSpec(
|
| 98 |
+
id="uiux",
|
| 99 |
+
name="Q8 Agent UI/UX Designer 2.0",
|
| 100 |
+
model_together="Qwen/Qwen2.5-72B-Instruct-Turbo",
|
| 101 |
+
model_runpod="Qwen/Qwen2.5-32B-Instruct",
|
| 102 |
+
model_groq="qwen/qwen3-32b",
|
| 103 |
+
model_ollama="qwen2.5:7b",
|
| 104 |
+
default_provider="groq",
|
| 105 |
+
temperature=0.75,
|
| 106 |
+
system_prompt=(
|
| 107 |
+
"اسمك: Q8 Agent UI/UX Designer 2.0\n"
|
| 108 |
+
"محرّكك: Alibaba Qwen 3 32B\n"
|
| 109 |
+
"تخصصك: مدير الإبداع البصري وتجربة المستخدم.\n\n"
|
| 110 |
+
"خبرتك تشمل: Figma, Adobe XD, Framer, Tailwind, Material Design 3, "
|
| 111 |
+
"Apple HIG, WCAG 2.1 AA, design systems, motion design, "
|
| 112 |
+
"user flows, information architecture, journey maps, "
|
| 113 |
+
"typography, color theory, micro-interactions.\n\n"
|
| 114 |
+
"أسلوبك:\n"
|
| 115 |
+
"- توصيات بصرية محددة بأرقام (ألوان hex، مقاسات بـ px/rem، أوزان خطوط).\n"
|
| 116 |
+
"- ترتب المشاكل حسب الأولوية (P0 → P3).\n"
|
| 117 |
+
"- تقترح مكتبات جاهزة (shadcn/ui, Radix, Lucide, Framer Motion).\n"
|
| 118 |
+
"- تركز على إمكانية الوصول (contrast ratios, keyboard navigation, ARIA)."
|
| 119 |
+
) + _DIALECT_RULE,
|
| 120 |
+
),
|
| 121 |
+
|
| 122 |
+
# ========================================================
|
| 123 |
+
# 3) CYBER SECURITY PRO - OpenAI GPT-OSS 120B
|
| 124 |
+
# ========================================================
|
| 125 |
+
"cybersec": AgentSpec(
|
| 126 |
+
id="cybersec",
|
| 127 |
+
name="Q8 Agent Cyber Security Pro 3.5",
|
| 128 |
+
model_together="deepseek-ai/DeepSeek-R1-Distill-Llama-70B",
|
| 129 |
+
model_runpod="deepseek-ai/DeepSeek-R1-Distill-Llama-70B",
|
| 130 |
+
model_groq="openai/gpt-oss-120b",
|
| 131 |
+
model_ollama="deepseek-r1:8b",
|
| 132 |
+
default_provider="groq",
|
| 133 |
+
temperature=0.3,
|
| 134 |
+
system_prompt=(
|
| 135 |
+
"اسمك: Q8 Agent Cyber Security Pro 3.5\n"
|
| 136 |
+
"محرّكك: OpenAI GPT-OSS 120B (نموذج تفكير منطقي كبير)\n"
|
| 137 |
+
"تخصصك: كبير محللي الأمن السيبراني الدفاعي.\n\n"
|
| 138 |
+
"معرفتك تغطي: MITRE ATT&CK, NIST CSF, ISO 27001, SOC 2, GDPR, PCI-DSS, "
|
| 139 |
+
"OWASP Top 10, STRIDE threat modeling, SAST/DAST, AES/RSA encryption, "
|
| 140 |
+
"TLS/PKI, zero trust, SIEM, DFIR, compliance.\n\n"
|
| 141 |
+
"أسلوبك:\n"
|
| 142 |
+
"- دفاعي بحت — لا تعليمات هجومية أو استغلال.\n"
|
| 143 |
+
"- تفكير منطقي خطوة بخطوة قبل التوصية.\n"
|
| 144 |
+
"- تصنف المخاطر (Critical/High/Medium/Low) مع CVSS عند الإمكان.\n"
|
| 145 |
+
"- تقترح ضوابط ملموسة، وليس نصائح عامة."
|
| 146 |
+
) + _DIALECT_RULE,
|
| 147 |
+
),
|
| 148 |
+
|
| 149 |
+
# ========================================================
|
| 150 |
+
# 4) FITNESS & BODYBUILDER - OpenAI GPT-OSS 20B
|
| 151 |
+
# ========================================================
|
| 152 |
+
"fitness": AgentSpec(
|
| 153 |
+
id="fitness",
|
| 154 |
+
name="Q8 Agent Fitness & Bodybuilder Pro 3.3",
|
| 155 |
+
model_together="meta-llama/Llama-3.1-8B-Instruct-Turbo",
|
| 156 |
+
model_runpod="meta-llama/Llama-3.1-8B-Instruct",
|
| 157 |
+
model_groq="openai/gpt-oss-20b",
|
| 158 |
+
model_ollama="llama3:8b",
|
| 159 |
+
default_provider="groq",
|
| 160 |
+
temperature=0.7,
|
| 161 |
+
system_prompt=(
|
| 162 |
+
"اسمك: Q8 Agent Fitness & Bodybuilder Pro 3.3\n"
|
| 163 |
+
"محرّكك: OpenAI GPT-OSS 20B\n"
|
| 164 |
+
"تخصصك: مدرب لياقة بدنية وكمال أجسام افتراضي.\n\n"
|
| 165 |
+
"خبرتك تغطي: تصميم برامج (hypertrophy, strength, cutting, recomp, "
|
| 166 |
+
"PPL, Upper/Lower, Full Body)، التغذية الرياضية (macros, calories, "
|
| 167 |
+
"meal timing)، تقنية التمارين المركبة (squat, deadlift, bench, OHP, "
|
| 168 |
+
"rows)، الاستشفاء، المكملات الآمنة، الإصابات الشائعة والوقاية منها.\n\n"
|
| 169 |
+
"أسلوبك:\n"
|
| 170 |
+
"- ودي ومتحمس بدون مبالغة.\n"
|
| 171 |
+
"- خطط بأرقام محددة (sets × reps، grams بروتين، سعرات).\n"
|
| 172 |
+
"- لا تشخيصات طبية — اقترح طبيب عند الإصابات أو الأمراض المزمنة.\n"
|
| 173 |
+
"- لا ستيرويدات ولا مكملات خطرة."
|
| 174 |
+
) + _DIALECT_RULE,
|
| 175 |
+
),
|
| 176 |
+
}
|
app/ai_providers.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import re
|
| 5 |
+
from typing import Any, Dict, List, Tuple
|
| 6 |
+
|
| 7 |
+
import httpx
|
| 8 |
+
from fastapi import HTTPException
|
| 9 |
+
|
| 10 |
+
from .agents import AgentSpec
|
| 11 |
+
from .config import Settings
|
| 12 |
+
from .schemas import ChatMessage
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# يحذف كل صناديق التفكير الداخلية التي تطبعها نماذج reasoning
|
| 16 |
+
# مثل <think>...</think>, <reasoning>...</reasoning>, [THINK]...[/THINK]
|
| 17 |
+
_THINK_PATTERNS = [
|
| 18 |
+
re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE),
|
| 19 |
+
re.compile(r"<thinking>.*?</thinking>", re.DOTALL | re.IGNORECASE),
|
| 20 |
+
re.compile(r"<reasoning>.*?</reasoning>", re.DOTALL | re.IGNORECASE),
|
| 21 |
+
re.compile(r"\[THINK\].*?\[/THINK\]", re.DOTALL | re.IGNORECASE),
|
| 22 |
+
re.compile(r"<\|channel\|>analysis.*?<\|channel\|>final", re.DOTALL),
|
| 23 |
+
re.compile(r"<\|start\|>assistant<\|channel\|>analysis.*?<\|message\|>", re.DOTALL),
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _strip_thinking(text: str) -> str:
|
| 28 |
+
"""يحذف blocks التفكير الداخلي ثم ينظف الفراغات."""
|
| 29 |
+
if not text:
|
| 30 |
+
return text
|
| 31 |
+
cleaned = text
|
| 32 |
+
for pattern in _THINK_PATTERNS:
|
| 33 |
+
cleaned = pattern.sub("", cleaned)
|
| 34 |
+
# احذف tag مفتوح بدون إغلاق (بعض الموديلات تقطع منتصف التفكير)
|
| 35 |
+
if "</think>" not in cleaned and "<think>" in cleaned:
|
| 36 |
+
cleaned = cleaned.split("<think>", 1)[0]
|
| 37 |
+
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
|
| 38 |
+
return cleaned.strip()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class AIClient:
|
| 42 |
+
def __init__(self, settings: Settings):
|
| 43 |
+
self.settings = settings
|
| 44 |
+
|
| 45 |
+
def effective_provider(self, agent: AgentSpec) -> str:
|
| 46 |
+
env_key = f"AGENT_{agent.id.upper()}_PROVIDER"
|
| 47 |
+
raw = os.environ.get(env_key, "").strip().lower()
|
| 48 |
+
if raw in ("together", "runpod", "groq", "openai_compat"):
|
| 49 |
+
return raw
|
| 50 |
+
# Per-agent default (e.g. fitness -> groq) before legacy global AI_PROVIDER
|
| 51 |
+
dp = (agent.default_provider or "").strip().lower()
|
| 52 |
+
if dp in ("together", "runpod", "groq", "openai_compat"):
|
| 53 |
+
return dp
|
| 54 |
+
if self.settings.ai_provider == "runpod":
|
| 55 |
+
return "runpod"
|
| 56 |
+
return "together"
|
| 57 |
+
|
| 58 |
+
def resolve_model(self, agent: AgentSpec, provider: str) -> str:
|
| 59 |
+
env_key = f"MODEL_{agent.id.upper()}"
|
| 60 |
+
override = os.environ.get(env_key, "").strip()
|
| 61 |
+
if override:
|
| 62 |
+
return override
|
| 63 |
+
if provider == "runpod":
|
| 64 |
+
return agent.model_runpod
|
| 65 |
+
if provider == "groq":
|
| 66 |
+
return agent.model_groq or agent.model_together
|
| 67 |
+
if provider == "openai_compat":
|
| 68 |
+
return os.environ.get(env_key, "").strip() or agent.model_ollama
|
| 69 |
+
return agent.model_together
|
| 70 |
+
|
| 71 |
+
def _headers_for(self, provider: str) -> Dict[str, str]:
|
| 72 |
+
if provider == "runpod":
|
| 73 |
+
if not self.settings.runpod_api_key:
|
| 74 |
+
raise HTTPException(status_code=500, detail="RUNPOD_API_KEY is missing")
|
| 75 |
+
return {
|
| 76 |
+
"Authorization": f"Bearer {self.settings.runpod_api_key}",
|
| 77 |
+
"Content-Type": "application/json",
|
| 78 |
+
}
|
| 79 |
+
if provider == "groq":
|
| 80 |
+
if not self.settings.groq_api_key:
|
| 81 |
+
raise HTTPException(status_code=500, detail="GROQ_API_KEY is missing")
|
| 82 |
+
return {
|
| 83 |
+
"Authorization": f"Bearer {self.settings.groq_api_key}",
|
| 84 |
+
"Content-Type": "application/json",
|
| 85 |
+
}
|
| 86 |
+
if provider == "openai_compat":
|
| 87 |
+
if not self.settings.openai_compat_base_url:
|
| 88 |
+
raise HTTPException(
|
| 89 |
+
status_code=500,
|
| 90 |
+
detail="OPENAI_COMPAT_BASE_URL is missing (e.g. http://127.0.0.1:11434/v1 for Ollama)",
|
| 91 |
+
)
|
| 92 |
+
h: Dict[str, str] = {"Content-Type": "application/json"}
|
| 93 |
+
key = (self.settings.openai_compat_api_key or "").strip()
|
| 94 |
+
if key:
|
| 95 |
+
h["Authorization"] = f"Bearer {key}"
|
| 96 |
+
return h
|
| 97 |
+
if not self.settings.together_api_key:
|
| 98 |
+
raise HTTPException(status_code=500, detail="TOGETHER_API_KEY is missing")
|
| 99 |
+
return {
|
| 100 |
+
"Authorization": f"Bearer {self.settings.together_api_key}",
|
| 101 |
+
"Content-Type": "application/json",
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
def _base_url(self, provider: str) -> str:
|
| 105 |
+
if provider == "runpod":
|
| 106 |
+
if not self.settings.runpod_base_url:
|
| 107 |
+
raise HTTPException(status_code=500, detail="RUNPOD_BASE_URL is missing")
|
| 108 |
+
return self.settings.runpod_base_url
|
| 109 |
+
if provider == "groq":
|
| 110 |
+
return self.settings.groq_base_url
|
| 111 |
+
if provider == "openai_compat":
|
| 112 |
+
return self.settings.openai_compat_base_url
|
| 113 |
+
return self.settings.together_base_url
|
| 114 |
+
|
| 115 |
+
async def chat_completion(
|
| 116 |
+
self,
|
| 117 |
+
agent: AgentSpec,
|
| 118 |
+
user_message: str,
|
| 119 |
+
history: List[ChatMessage],
|
| 120 |
+
temperature: float,
|
| 121 |
+
max_tokens: int,
|
| 122 |
+
) -> Tuple[str, str, str, Dict[str, Any]]:
|
| 123 |
+
provider = self.effective_provider(agent)
|
| 124 |
+
model = self.resolve_model(agent, provider)
|
| 125 |
+
api_messages = [{"role": "system", "content": agent.system_prompt}]
|
| 126 |
+
api_messages.extend([m.model_dump() for m in history[-12:]])
|
| 127 |
+
api_messages.append({"role": "user", "content": user_message})
|
| 128 |
+
|
| 129 |
+
# Each agent has its own ideal temperature baked into AgentSpec.
|
| 130 |
+
# Caller-provided temperature is treated as a soft override only when
|
| 131 |
+
# it differs from the default 0.6 sentinel used by the API schema.
|
| 132 |
+
agent_temp = getattr(agent, "temperature", 0.6)
|
| 133 |
+
effective_temp = agent_temp if abs(temperature - 0.6) < 1e-6 else temperature
|
| 134 |
+
|
| 135 |
+
payload = {
|
| 136 |
+
"model": model,
|
| 137 |
+
"messages": api_messages,
|
| 138 |
+
"temperature": effective_temp,
|
| 139 |
+
"max_tokens": max_tokens,
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
url = f"{self._base_url(provider)}/chat/completions"
|
| 143 |
+
timeout = httpx.Timeout(120.0, connect=15.0)
|
| 144 |
+
|
| 145 |
+
# Groq fallback rotation: try each key in order on 429/401/403/5xx.
|
| 146 |
+
if provider == "groq":
|
| 147 |
+
groq_keys = self.settings.groq_api_keys
|
| 148 |
+
if not groq_keys:
|
| 149 |
+
raise HTTPException(status_code=500, detail="GROQ_API_KEY is missing")
|
| 150 |
+
last_err: str = ""
|
| 151 |
+
response = None
|
| 152 |
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
| 153 |
+
for idx, key in enumerate(groq_keys):
|
| 154 |
+
headers = {
|
| 155 |
+
"Authorization": f"Bearer {key}",
|
| 156 |
+
"Content-Type": "application/json",
|
| 157 |
+
}
|
| 158 |
+
try:
|
| 159 |
+
response = await client.post(url, headers=headers, json=payload)
|
| 160 |
+
if response.status_code in (401, 403, 429) or response.status_code >= 500:
|
| 161 |
+
last_err = f"key#{idx+1} HTTP {response.status_code}: {response.text[:200]}"
|
| 162 |
+
response = None
|
| 163 |
+
continue
|
| 164 |
+
response.raise_for_status()
|
| 165 |
+
break
|
| 166 |
+
except httpx.HTTPStatusError as exc:
|
| 167 |
+
last_err = f"key#{idx+1} HTTP {exc.response.status_code}: {exc.response.text[:200]}"
|
| 168 |
+
response = None
|
| 169 |
+
continue
|
| 170 |
+
except httpx.HTTPError as exc:
|
| 171 |
+
last_err = f"key#{idx+1} network: {exc}"
|
| 172 |
+
response = None
|
| 173 |
+
continue
|
| 174 |
+
if response is None:
|
| 175 |
+
raise HTTPException(
|
| 176 |
+
status_code=502,
|
| 177 |
+
detail=f"All Groq keys failed. Last error: {last_err}",
|
| 178 |
+
)
|
| 179 |
+
else:
|
| 180 |
+
headers = self._headers_for(provider)
|
| 181 |
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
| 182 |
+
try:
|
| 183 |
+
response = await client.post(url, headers=headers, json=payload)
|
| 184 |
+
response.raise_for_status()
|
| 185 |
+
except httpx.HTTPStatusError as exc:
|
| 186 |
+
detail = exc.response.text
|
| 187 |
+
raise HTTPException(status_code=502, detail=f"AI provider error: {detail}") from exc
|
| 188 |
+
except httpx.HTTPError as exc:
|
| 189 |
+
raise HTTPException(status_code=502, detail=f"AI provider network error: {exc}") from exc
|
| 190 |
+
|
| 191 |
+
data = response.json()
|
| 192 |
+
output = (
|
| 193 |
+
data.get("choices", [{}])[0]
|
| 194 |
+
.get("message", {})
|
| 195 |
+
.get("content", "")
|
| 196 |
+
.strip()
|
| 197 |
+
)
|
| 198 |
+
output = _strip_thinking(output)
|
| 199 |
+
usage = data.get("usage", {})
|
| 200 |
+
return output or "No response from model.", model, provider, usage
|
app/auth.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from datetime import datetime, timedelta, timezone
|
| 3 |
+
from typing import Annotated, Optional
|
| 4 |
+
|
| 5 |
+
from fastapi import Depends, HTTPException, Request, status
|
| 6 |
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
| 7 |
+
from jose import JWTError, jwt
|
| 8 |
+
|
| 9 |
+
from .config import Settings, get_settings
|
| 10 |
+
|
| 11 |
+
bearer_scheme = HTTPBearer(auto_error=False)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass(frozen=True)
|
| 15 |
+
class AuthContext:
|
| 16 |
+
user_id: str
|
| 17 |
+
tier: str # "free" | "pro" | "unlimited"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def create_access_token(
|
| 21 |
+
username: str,
|
| 22 |
+
settings: Settings,
|
| 23 |
+
*,
|
| 24 |
+
extra_claims: Optional[dict] = None,
|
| 25 |
+
) -> str:
|
| 26 |
+
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expire_minutes)
|
| 27 |
+
payload: dict = {"sub": username, "exp": expire}
|
| 28 |
+
if extra_claims:
|
| 29 |
+
payload.update(extra_claims)
|
| 30 |
+
return jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _decode_payload(token: str, settings: Settings) -> dict:
|
| 34 |
+
try:
|
| 35 |
+
return jwt.decode(token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm])
|
| 36 |
+
except (JWTError, ValueError) as exc:
|
| 37 |
+
raise HTTPException(
|
| 38 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 39 |
+
detail="Invalid or expired token",
|
| 40 |
+
) from exc
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def resolve_tier(user_id: str, payload: dict, settings: Settings) -> str:
|
| 44 |
+
raw = payload.get("tier")
|
| 45 |
+
if raw in ("free", "pro", "unlimited"):
|
| 46 |
+
return str(raw)
|
| 47 |
+
if user_id in settings.unlimited_user_ids:
|
| 48 |
+
return "unlimited"
|
| 49 |
+
if user_id in settings.pro_user_ids:
|
| 50 |
+
return "pro"
|
| 51 |
+
return "free"
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def decode_access_token(token: str, settings: Settings) -> AuthContext:
|
| 55 |
+
payload = _decode_payload(token, settings)
|
| 56 |
+
sub = payload.get("sub")
|
| 57 |
+
if not sub:
|
| 58 |
+
raise HTTPException(
|
| 59 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 60 |
+
detail="Invalid token subject",
|
| 61 |
+
)
|
| 62 |
+
user_id = str(sub)
|
| 63 |
+
tier = resolve_tier(user_id, payload, settings)
|
| 64 |
+
return AuthContext(user_id=user_id, tier=tier)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
async def get_auth_context(
|
| 68 |
+
request: Request,
|
| 69 |
+
creds: Annotated[Optional[HTTPAuthorizationCredentials], Depends(bearer_scheme)],
|
| 70 |
+
settings: Annotated[Settings, Depends(get_settings)],
|
| 71 |
+
) -> AuthContext:
|
| 72 |
+
if settings.auth_disabled:
|
| 73 |
+
header_uid = (request.headers.get("X-User-Id") or "").strip()
|
| 74 |
+
uid = header_uid or "local-dev"
|
| 75 |
+
if uid in settings.unlimited_user_ids:
|
| 76 |
+
tier = "unlimited"
|
| 77 |
+
elif uid in settings.pro_user_ids:
|
| 78 |
+
tier = "pro"
|
| 79 |
+
else:
|
| 80 |
+
tier = "free"
|
| 81 |
+
return AuthContext(user_id=uid, tier=tier)
|
| 82 |
+
if not creds or not creds.credentials:
|
| 83 |
+
raise HTTPException(
|
| 84 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 85 |
+
detail="Missing bearer token",
|
| 86 |
+
)
|
| 87 |
+
return decode_access_token(creds.credentials, settings)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def require_login(username: str, password: str, settings: Settings) -> None:
|
| 91 |
+
if username != settings.admin_username or password != settings.admin_password:
|
| 92 |
+
raise HTTPException(
|
| 93 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 94 |
+
detail="Invalid username or password",
|
| 95 |
+
)
|
app/config.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from functools import lru_cache
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def _split_csv(s: str) -> frozenset[str]:
|
| 10 |
+
return frozenset(x.strip() for x in (s or "").split(",") if x.strip())
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class Settings:
|
| 14 |
+
app_name: str = os.getenv("APP_NAME", "KW Multi AI Agent Backend")
|
| 15 |
+
app_env: str = os.getenv("APP_ENV", "development")
|
| 16 |
+
app_host: str = os.getenv("APP_HOST", "0.0.0.0")
|
| 17 |
+
app_port: int = int(os.getenv("APP_PORT", "8000"))
|
| 18 |
+
|
| 19 |
+
jwt_secret_key: str = os.getenv("JWT_SECRET_KEY", "CHANGE_ME")
|
| 20 |
+
jwt_algorithm: str = os.getenv("JWT_ALGORITHM", "HS256")
|
| 21 |
+
jwt_expire_minutes: int = int(os.getenv("JWT_EXPIRE_MINUTES", "1440"))
|
| 22 |
+
|
| 23 |
+
admin_username: str = os.getenv("ADMIN_USERNAME", "admin")
|
| 24 |
+
admin_password: str = os.getenv("ADMIN_PASSWORD", "change-me-now")
|
| 25 |
+
|
| 26 |
+
# If true, agent routes accept requests without Bearer; user id from X-User-Id or "local-dev"
|
| 27 |
+
auth_disabled: bool = os.getenv("AUTH_DISABLED", "false").strip().lower() in ("1", "true", "yes")
|
| 28 |
+
|
| 29 |
+
# Shared secret for mobile/web app to exchange JWT (set in app env + VITE_APP_BACKEND_SECRET)
|
| 30 |
+
app_shared_secret: str = os.getenv("APP_SHARED_SECRET", "").strip()
|
| 31 |
+
|
| 32 |
+
# Comma-separated user ids that get Pro tier (server-side; overrides free default)
|
| 33 |
+
pro_user_ids: frozenset[str] = _split_csv(os.getenv("PRO_USER_IDS", ""))
|
| 34 |
+
# Comma-separated user ids that get Unlimited tier (no daily cap)
|
| 35 |
+
unlimited_user_ids: frozenset[str] = _split_csv(os.getenv("UNLIMITED_USER_IDS", ""))
|
| 36 |
+
|
| 37 |
+
ai_provider: str = os.getenv("AI_PROVIDER", "together").strip().lower()
|
| 38 |
+
|
| 39 |
+
together_api_key: str = os.getenv("TOGETHER_API_KEY", "")
|
| 40 |
+
together_base_url: str = os.getenv("TOGETHER_BASE_URL", "https://api.together.xyz/v1").rstrip("/")
|
| 41 |
+
|
| 42 |
+
runpod_api_key: str = os.getenv("RUNPOD_API_KEY", "")
|
| 43 |
+
runpod_base_url: str = os.getenv("RUNPOD_BASE_URL", "").rstrip("/")
|
| 44 |
+
|
| 45 |
+
groq_api_key: str = os.getenv("GROQ_API_KEY", "")
|
| 46 |
+
groq_base_url: str = os.getenv("GROQ_BASE_URL", "https://api.groq.com/openai/v1").rstrip("/")
|
| 47 |
+
|
| 48 |
+
@property
|
| 49 |
+
def groq_api_keys(self) -> list[str]:
|
| 50 |
+
"""Return all Groq keys for fallback rotation.
|
| 51 |
+
|
| 52 |
+
Reads GROQ_API_KEY plus GROQ_API_KEY_2, GROQ_API_KEY_3, ...
|
| 53 |
+
Also accepts a comma-separated GROQ_API_KEYS env var.
|
| 54 |
+
Returns deduped, non-empty list preserving order.
|
| 55 |
+
"""
|
| 56 |
+
keys: list[str] = []
|
| 57 |
+
primary = (os.getenv("GROQ_API_KEY", "") or "").strip()
|
| 58 |
+
if primary:
|
| 59 |
+
keys.append(primary)
|
| 60 |
+
csv = (os.getenv("GROQ_API_KEYS", "") or "").strip()
|
| 61 |
+
if csv:
|
| 62 |
+
for k in csv.split(","):
|
| 63 |
+
k = k.strip()
|
| 64 |
+
if k and k not in keys:
|
| 65 |
+
keys.append(k)
|
| 66 |
+
for i in range(2, 11):
|
| 67 |
+
v = (os.getenv(f"GROQ_API_KEY_{i}", "") or "").strip()
|
| 68 |
+
if v and v not in keys:
|
| 69 |
+
keys.append(v)
|
| 70 |
+
return keys
|
| 71 |
+
|
| 72 |
+
# Generic OpenAI-compatible endpoint (e.g. other clouds)
|
| 73 |
+
openai_compat_api_key: str = os.getenv("OPENAI_COMPAT_API_KEY", "")
|
| 74 |
+
openai_compat_base_url: str = os.getenv("OPENAI_COMPAT_BASE_URL", "").rstrip("/")
|
| 75 |
+
|
| 76 |
+
rate_limit: str = os.getenv("RATE_LIMIT", "30/minute")
|
| 77 |
+
|
| 78 |
+
# Daily message caps (counting user messages, not tokens). 0 = unlimited.
|
| 79 |
+
# Matches frontend PLANS in src/lib/subscription.ts
|
| 80 |
+
free_tier_daily_messages: int = int(os.getenv("FREE_TIER_DAILY_MESSAGES", "20"))
|
| 81 |
+
pro_tier_daily_messages: int = int(os.getenv("PRO_TIER_DAILY_MESSAGES", "300"))
|
| 82 |
+
unlimited_tier_daily_messages: int = int(os.getenv("UNLIMITED_TIER_DAILY_MESSAGES", "0"))
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@lru_cache
|
| 86 |
+
def get_settings() -> Settings:
|
| 87 |
+
return Settings()
|
app/main.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from contextlib import asynccontextmanager
|
| 2 |
+
from typing import Annotated
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
from fastapi import Depends, FastAPI, HTTPException, Request
|
| 7 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
+
from fastapi.responses import JSONResponse
|
| 9 |
+
from fastapi.staticfiles import StaticFiles
|
| 10 |
+
from slowapi import Limiter, _rate_limit_exceeded_handler
|
| 11 |
+
from slowapi.errors import RateLimitExceeded
|
| 12 |
+
from slowapi.util import get_remote_address
|
| 13 |
+
|
| 14 |
+
from .agents import AGENTS
|
| 15 |
+
from .ai_providers import AIClient
|
| 16 |
+
from .auth import AuthContext, create_access_token, get_auth_context, require_login
|
| 17 |
+
from .config import Settings, get_settings
|
| 18 |
+
from .schemas import (
|
| 19 |
+
AgentChatRequest,
|
| 20 |
+
AgentChatResponse,
|
| 21 |
+
AppTokenRequest,
|
| 22 |
+
MeUsageResponse,
|
| 23 |
+
TokenRequest,
|
| 24 |
+
TokenResponse,
|
| 25 |
+
)
|
| 26 |
+
from .usage_store import (
|
| 27 |
+
add_message_used,
|
| 28 |
+
day_key_now,
|
| 29 |
+
get_messages_used,
|
| 30 |
+
init_db,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@asynccontextmanager
|
| 35 |
+
async def lifespan(_: FastAPI):
|
| 36 |
+
init_db()
|
| 37 |
+
yield
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
settings = get_settings()
|
| 41 |
+
limiter = Limiter(key_func=get_remote_address, default_limits=[settings.rate_limit])
|
| 42 |
+
|
| 43 |
+
app = FastAPI(title=settings.app_name, lifespan=lifespan)
|
| 44 |
+
app.state.limiter = limiter
|
| 45 |
+
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
| 46 |
+
|
| 47 |
+
app.add_middleware(
|
| 48 |
+
CORSMiddleware,
|
| 49 |
+
allow_origins=["*"],
|
| 50 |
+
allow_credentials=True,
|
| 51 |
+
allow_methods=["*"],
|
| 52 |
+
allow_headers=["*"],
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@app.exception_handler(Exception)
|
| 57 |
+
async def unhandled_exception_handler(_: Request, exc: Exception):
|
| 58 |
+
return JSONResponse(status_code=500, content={"detail": f"Server error: {exc}"})
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@app.get("/health")
|
| 62 |
+
async def health():
|
| 63 |
+
return {
|
| 64 |
+
"status": "ok",
|
| 65 |
+
"auth_disabled": settings.auth_disabled,
|
| 66 |
+
"agents": list(AGENTS.keys()),
|
| 67 |
+
"rate_limit": settings.rate_limit,
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _tier_daily_limit(tier: str, s: Settings) -> tuple[int, bool]:
|
| 72 |
+
"""Return (limit, unlimited). limit=0 means unlimited."""
|
| 73 |
+
if tier == "unlimited":
|
| 74 |
+
lim = s.unlimited_tier_daily_messages
|
| 75 |
+
return (lim, lim == 0)
|
| 76 |
+
if tier == "pro":
|
| 77 |
+
lim = s.pro_tier_daily_messages
|
| 78 |
+
return (lim, lim == 0)
|
| 79 |
+
lim = s.free_tier_daily_messages
|
| 80 |
+
return (lim, lim == 0)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _ensure_daily_quota(auth: AuthContext, s: Settings) -> None:
|
| 84 |
+
limit, unlimited = _tier_daily_limit(auth.tier, s)
|
| 85 |
+
if unlimited:
|
| 86 |
+
return
|
| 87 |
+
used = get_messages_used(auth.user_id, day_key_now())
|
| 88 |
+
if used >= limit:
|
| 89 |
+
raise HTTPException(
|
| 90 |
+
status_code=429,
|
| 91 |
+
detail=(
|
| 92 |
+
"انتهى حدك اليومي. ترقّ للاشتراك أو انتظر بكرة. "
|
| 93 |
+
"Daily message limit reached — upgrade or wait until tomorrow."
|
| 94 |
+
),
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _record_message(auth: AuthContext) -> int:
|
| 99 |
+
"""Count one chat message against the user's daily quota."""
|
| 100 |
+
return add_message_used(auth.user_id, day_key_now(), 1)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@app.get("/me/usage", response_model=MeUsageResponse)
|
| 104 |
+
@limiter.limit(settings.rate_limit)
|
| 105 |
+
async def me_usage(
|
| 106 |
+
request: Request,
|
| 107 |
+
auth: Annotated[AuthContext, Depends(get_auth_context)],
|
| 108 |
+
s: Annotated[Settings, Depends(get_settings)],
|
| 109 |
+
):
|
| 110 |
+
d = day_key_now()
|
| 111 |
+
used = get_messages_used(auth.user_id, d)
|
| 112 |
+
limit, unlimited = _tier_daily_limit(auth.tier, s)
|
| 113 |
+
return MeUsageResponse(
|
| 114 |
+
user_id=auth.user_id,
|
| 115 |
+
tier=auth.tier,
|
| 116 |
+
day=d,
|
| 117 |
+
messages_used=used,
|
| 118 |
+
messages_limit=limit,
|
| 119 |
+
unlimited=unlimited,
|
| 120 |
+
# legacy fields (kept for backwards compat)
|
| 121 |
+
month=d[:7],
|
| 122 |
+
tokens_used=used,
|
| 123 |
+
tokens_limit=limit,
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _tier_for_user(uid: str, s: Settings) -> str:
|
| 128 |
+
if uid in s.unlimited_user_ids:
|
| 129 |
+
return "unlimited"
|
| 130 |
+
if uid in s.pro_user_ids:
|
| 131 |
+
return "pro"
|
| 132 |
+
return "free"
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
@app.post("/auth/token", response_model=TokenResponse)
|
| 136 |
+
@limiter.limit(settings.rate_limit)
|
| 137 |
+
async def login(request: Request, body: TokenRequest, s: Annotated[Settings, Depends(get_settings)]):
|
| 138 |
+
require_login(body.username, body.password, s)
|
| 139 |
+
tier = _tier_for_user(body.username, s)
|
| 140 |
+
token = create_access_token(body.username, s, extra_claims={"tier": tier})
|
| 141 |
+
return TokenResponse(access_token=token)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
@app.post("/auth/app-token", response_model=TokenResponse)
|
| 145 |
+
@limiter.limit(settings.rate_limit)
|
| 146 |
+
async def app_token(request: Request, body: AppTokenRequest, s: Annotated[Settings, Depends(get_settings)]):
|
| 147 |
+
if not s.app_shared_secret or body.app_secret != s.app_shared_secret:
|
| 148 |
+
raise HTTPException(status_code=401, detail="Invalid app secret")
|
| 149 |
+
tier = _tier_for_user(body.user_id, s)
|
| 150 |
+
token = create_access_token(body.user_id, s, extra_claims={"tier": tier})
|
| 151 |
+
return TokenResponse(access_token=token)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
async def _run_agent_chat(agent_id: str, body: AgentChatRequest, s: Settings, auth: AuthContext) -> AgentChatResponse:
|
| 155 |
+
agent = AGENTS.get(agent_id)
|
| 156 |
+
if not agent:
|
| 157 |
+
raise HTTPException(status_code=404, detail=f"Unknown agent: {agent_id}")
|
| 158 |
+
|
| 159 |
+
_ensure_daily_quota(auth, s)
|
| 160 |
+
|
| 161 |
+
ai = AIClient(s)
|
| 162 |
+
output, model_used, provider_used, usage = await ai.chat_completion(
|
| 163 |
+
agent=agent,
|
| 164 |
+
user_message=body.message,
|
| 165 |
+
history=body.history,
|
| 166 |
+
temperature=body.temperature,
|
| 167 |
+
max_tokens=body.max_tokens,
|
| 168 |
+
)
|
| 169 |
+
_record_message(auth)
|
| 170 |
+
return AgentChatResponse(
|
| 171 |
+
agent_id=agent.id,
|
| 172 |
+
agent_name=agent.name,
|
| 173 |
+
model=model_used,
|
| 174 |
+
provider=provider_used,
|
| 175 |
+
output=output,
|
| 176 |
+
usage=usage,
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
@app.post("/agents/fullstack/chat", response_model=AgentChatResponse)
|
| 181 |
+
@limiter.limit(settings.rate_limit)
|
| 182 |
+
async def chat_fullstack(
|
| 183 |
+
request: Request,
|
| 184 |
+
body: AgentChatRequest,
|
| 185 |
+
auth: Annotated[AuthContext, Depends(get_auth_context)],
|
| 186 |
+
s: Annotated[Settings, Depends(get_settings)],
|
| 187 |
+
):
|
| 188 |
+
return await _run_agent_chat("fullstack", body, s, auth)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
@app.post("/agents/uiux/chat", response_model=AgentChatResponse)
|
| 192 |
+
@limiter.limit(settings.rate_limit)
|
| 193 |
+
async def chat_uiux(
|
| 194 |
+
request: Request,
|
| 195 |
+
body: AgentChatRequest,
|
| 196 |
+
auth: Annotated[AuthContext, Depends(get_auth_context)],
|
| 197 |
+
s: Annotated[Settings, Depends(get_settings)],
|
| 198 |
+
):
|
| 199 |
+
return await _run_agent_chat("uiux", body, s, auth)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
@app.post("/agents/cybersec/chat", response_model=AgentChatResponse)
|
| 203 |
+
@limiter.limit(settings.rate_limit)
|
| 204 |
+
async def chat_cybersec(
|
| 205 |
+
request: Request,
|
| 206 |
+
body: AgentChatRequest,
|
| 207 |
+
auth: Annotated[AuthContext, Depends(get_auth_context)],
|
| 208 |
+
s: Annotated[Settings, Depends(get_settings)],
|
| 209 |
+
):
|
| 210 |
+
return await _run_agent_chat("cybersec", body, s, auth)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
@app.post("/agents/fitness/chat", response_model=AgentChatResponse)
|
| 214 |
+
@limiter.limit(settings.rate_limit)
|
| 215 |
+
async def chat_fitness(
|
| 216 |
+
request: Request,
|
| 217 |
+
body: AgentChatRequest,
|
| 218 |
+
auth: Annotated[AuthContext, Depends(get_auth_context)],
|
| 219 |
+
s: Annotated[Settings, Depends(get_settings)],
|
| 220 |
+
):
|
| 221 |
+
return await _run_agent_chat("fitness", body, s, auth)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
# Serve built frontend (PWA) from /static and root.
|
| 225 |
+
# This must be the LAST route registered so API routes take priority.
|
| 226 |
+
_static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
|
| 227 |
+
if os.path.isdir(_static_dir):
|
| 228 |
+
app.mount("/", StaticFiles(directory=_static_dir, html=True), name="frontend")
|
app/schemas.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Literal, Optional
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel, Field
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
Role = Literal["system", "user", "assistant"]
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ChatMessage(BaseModel):
|
| 10 |
+
role: Role
|
| 11 |
+
content: str = Field(min_length=1)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class AgentChatRequest(BaseModel):
|
| 15 |
+
message: str = Field(min_length=1, description="Latest user message")
|
| 16 |
+
history: List[ChatMessage] = Field(default_factory=list, description="Optional short chat history")
|
| 17 |
+
temperature: float = Field(default=0.6, ge=0.0, le=2.0)
|
| 18 |
+
max_tokens: int = Field(default=768, ge=32, le=4096)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class AgentChatResponse(BaseModel):
|
| 22 |
+
agent_id: str
|
| 23 |
+
agent_name: str
|
| 24 |
+
model: str
|
| 25 |
+
provider: str
|
| 26 |
+
output: str
|
| 27 |
+
usage: Optional[dict] = None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class TokenRequest(BaseModel):
|
| 31 |
+
username: str
|
| 32 |
+
password: str
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class TokenResponse(BaseModel):
|
| 36 |
+
access_token: str
|
| 37 |
+
token_type: str = "bearer"
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class AppTokenRequest(BaseModel):
|
| 41 |
+
user_id: str = Field(min_length=1)
|
| 42 |
+
app_secret: str = Field(min_length=1)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class MeUsageResponse(BaseModel):
|
| 46 |
+
user_id: str
|
| 47 |
+
tier: str # "free" | "pro" | "unlimited"
|
| 48 |
+
day: str # YYYY-MM-DD (UTC)
|
| 49 |
+
messages_used: int
|
| 50 |
+
messages_limit: int # 0 when unlimited
|
| 51 |
+
unlimited: bool
|
| 52 |
+
# Legacy aliases kept so older clients still parse the payload.
|
| 53 |
+
month: str = ""
|
| 54 |
+
tokens_used: int = 0
|
| 55 |
+
tokens_limit: int = 0
|
| 56 |
+
|
app/usage_store.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SQLite-backed daily message usage per user.
|
| 2 |
+
|
| 3 |
+
Tracks number of chat messages per (user_id, day) — in UTC.
|
| 4 |
+
Used by main.py to enforce the subscription daily limit
|
| 5 |
+
(Free = 20/day, Pro = 300/day, Unlimited = no limit).
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import sqlite3
|
| 11 |
+
import threading
|
| 12 |
+
from datetime import datetime, timezone
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
DB_PATH = Path(__file__).resolve().parent.parent / "data" / "usage.sqlite"
|
| 16 |
+
_lock = threading.Lock()
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def day_key_now() -> str:
|
| 20 |
+
"""UTC day key in YYYY-MM-DD format."""
|
| 21 |
+
return datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Kept as alias for backwards-compat in case anything else imports it.
|
| 25 |
+
def month_key_now() -> str:
|
| 26 |
+
return datetime.now(timezone.utc).strftime("%Y-%m")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def init_db() -> None:
|
| 30 |
+
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
| 31 |
+
with sqlite3.connect(DB_PATH) as conn:
|
| 32 |
+
# New daily-messages table
|
| 33 |
+
conn.execute(
|
| 34 |
+
"""
|
| 35 |
+
CREATE TABLE IF NOT EXISTS usage_daily (
|
| 36 |
+
user_id TEXT NOT NULL,
|
| 37 |
+
day_key TEXT NOT NULL,
|
| 38 |
+
messages INTEGER NOT NULL DEFAULT 0,
|
| 39 |
+
PRIMARY KEY (user_id, day_key)
|
| 40 |
+
)
|
| 41 |
+
"""
|
| 42 |
+
)
|
| 43 |
+
# Legacy table kept for historical data — not written to anymore.
|
| 44 |
+
conn.execute(
|
| 45 |
+
"""
|
| 46 |
+
CREATE TABLE IF NOT EXISTS usage_monthly (
|
| 47 |
+
user_id TEXT NOT NULL,
|
| 48 |
+
month_key TEXT NOT NULL,
|
| 49 |
+
tokens INTEGER NOT NULL DEFAULT 0,
|
| 50 |
+
PRIMARY KEY (user_id, month_key)
|
| 51 |
+
)
|
| 52 |
+
"""
|
| 53 |
+
)
|
| 54 |
+
conn.commit()
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def get_messages_used(user_id: str, day_key: str) -> int:
|
| 58 |
+
with sqlite3.connect(DB_PATH) as conn:
|
| 59 |
+
row = conn.execute(
|
| 60 |
+
"SELECT messages FROM usage_daily WHERE user_id = ? AND day_key = ?",
|
| 61 |
+
(user_id, day_key),
|
| 62 |
+
).fetchone()
|
| 63 |
+
return int(row[0]) if row else 0
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def add_message_used(user_id: str, day_key: str, delta: int = 1) -> int:
|
| 67 |
+
if delta <= 0:
|
| 68 |
+
return get_messages_used(user_id, day_key)
|
| 69 |
+
with _lock:
|
| 70 |
+
with sqlite3.connect(DB_PATH) as conn:
|
| 71 |
+
conn.execute(
|
| 72 |
+
"""
|
| 73 |
+
INSERT INTO usage_daily (user_id, day_key, messages)
|
| 74 |
+
VALUES (?, ?, ?)
|
| 75 |
+
ON CONFLICT(user_id, day_key) DO UPDATE SET
|
| 76 |
+
messages = usage_daily.messages + excluded.messages
|
| 77 |
+
""",
|
| 78 |
+
(user_id, day_key, delta),
|
| 79 |
+
)
|
| 80 |
+
conn.commit()
|
| 81 |
+
row = conn.execute(
|
| 82 |
+
"SELECT messages FROM usage_daily WHERE user_id = ? AND day_key = ?",
|
| 83 |
+
(user_id, day_key),
|
| 84 |
+
).fetchone()
|
| 85 |
+
return int(row[0]) if row else delta
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# Legacy aliases (kept so nothing else imports break during rollout)
|
| 89 |
+
def get_tokens_used(user_id: str, month_key: str) -> int:
|
| 90 |
+
return get_messages_used(user_id, day_key_now())
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def add_tokens_used(user_id: str, month_key: str, delta: int) -> int:
|
| 94 |
+
return add_message_used(user_id, day_key_now(), 1)
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.116.1
|
| 2 |
+
uvicorn[standard]==0.35.0
|
| 3 |
+
httpx==0.28.1
|
| 4 |
+
python-dotenv==1.1.1
|
| 5 |
+
python-jose==3.5.0
|
| 6 |
+
passlib[bcrypt]==1.7.4
|
| 7 |
+
slowapi==0.1.9
|
start.ps1
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
$ErrorActionPreference = "Stop"
|
| 2 |
+
Set-Location $PSScriptRoot
|
| 3 |
+
if (-not (Test-Path ".\.venv\Scripts\python.exe")) {
|
| 4 |
+
python -m venv .venv
|
| 5 |
+
& .\.venv\Scripts\pip.exe install -r requirements.txt
|
| 6 |
+
}
|
| 7 |
+
& .\.venv\Scripts\uvicorn.exe app.main:app --host 0.0.0.0 --port 8000
|
static/assets/DashboardScreen-B1h__hB-.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/assets/index-BwPR7vCI.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-serif:ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-sky-400:oklch(74.6% .16 232.661);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-purple-500:oklch(62.7% .265 303.9);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-light:300;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--ease-in:cubic-bezier(.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--blur-md:12px;--blur-xl:24px;--blur-2xl:40px;--blur-3xl:64px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.inset-x-0{inset-inline:calc(var(--spacing) * 0)}.inset-x-3{inset-inline:calc(var(--spacing) * 3)}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.start\!{inset-inline-start:var(--spacing)!important}.end{inset-inline-end:var(--spacing)}.-top-2{top:calc(var(--spacing) * -2)}.-top-2\.5{top:calc(var(--spacing) * -2.5)}.-top-20{top:calc(var(--spacing) * -20)}.-top-px{top:-1px}.top-0{top:calc(var(--spacing) * 0)}.top-1\/2{top:50%}.top-1\/4{top:25%}.top-2{top:calc(var(--spacing) * 2)}.top-3{top:calc(var(--spacing) * 3)}.top-4{top:calc(var(--spacing) * 4)}.top-\[-10\%\]{top:-10%}.top-\[-20\%\]{top:-20%}.top-\[20\%\]{top:20%}.-right-1\/4{right:-25%}.-right-2{right:calc(var(--spacing) * -2)}.-right-20{right:calc(var(--spacing) * -20)}.right-0{right:calc(var(--spacing) * 0)}.right-1{right:calc(var(--spacing) * 1)}.right-3{right:calc(var(--spacing) * 3)}.right-\[-10\%\]{right:-10%}.-bottom-\[2px\]{bottom:-2px}.bottom-0{bottom:calc(var(--spacing) * 0)}.bottom-1\/4{bottom:25%}.bottom-\[-10\%\]{bottom:-10%}.bottom-\[-20\%\]{bottom:-20%}.bottom-\[calc\(env\(safe-area-inset-bottom\)\+1rem\)\]{bottom:calc(env(safe-area-inset-bottom) + 1rem)}.bottom-\[calc\(env\(safe-area-inset-bottom\)\+5\.5rem\)\]{bottom:calc(env(safe-area-inset-bottom) + 5.5rem)}.-left-1\/4{left:-25%}.-left-16{left:calc(var(--spacing) * -16)}.left-0{left:calc(var(--spacing) * 0)}.left-1{left:calc(var(--spacing) * 1)}.left-1\/2{left:50%}.left-1\/3{left:33.3333%}.left-2{left:calc(var(--spacing) * 2)}.left-3{left:calc(var(--spacing) * 3)}.left-4{left:calc(var(--spacing) * 4)}.left-\[-10\%\]{left:-10%}.isolate{isolation:isolate}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[1\]{z-index:1}.z-\[2\]{z-index:2}.z-\[60\]{z-index:60}.z-\[90\]{z-index:90}.z-\[100\]{z-index:100}.z-\[150\]{z-index:150}.z-\[200\]{z-index:200}.z-\[300\]{z-index:300}.z-\[400\]{z-index:400}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-1{margin-inline:calc(var(--spacing) * 1)}.mx-1\.5{margin-inline:calc(var(--spacing) * 1.5)}.mx-auto{margin-inline:auto}.my-2{margin-block:calc(var(--spacing) * 2)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-auto{margin-top:auto}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-10{margin-bottom:calc(var(--spacing) * 10)}.ml-auto{margin-left:auto}.\!inline{display:inline!important}.block{display:block}.contents{display:contents}.flex{display:flex}.flow-root{display:flow-root}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.inline-grid{display:inline-grid}.inline-table{display:inline-table}.list-item{display:list-item}.table{display:table}.table-caption{display:table-caption}.table-cell{display:table-cell}.table-column{display:table-column}.table-column-group{display:table-column-group}.table-footer-group{display:table-footer-group}.table-header-group{display:table-header-group}.table-row{display:table-row}.table-row-group{display:table-row-group}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-11{height:calc(var(--spacing) * 11)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.h-32{height:calc(var(--spacing) * 32)}.h-40{height:calc(var(--spacing) * 40)}.h-48{height:calc(var(--spacing) * 48)}.h-64{height:calc(var(--spacing) * 64)}.h-72{height:calc(var(--spacing) * 72)}.h-80{height:calc(var(--spacing) * 80)}.h-\[1px\]{height:1px}.h-\[2px\]{height:2px}.h-\[3px\]{height:3px}.h-\[4px\]{height:4px}.h-\[40\%\]{height:40%}.h-\[50\%\]{height:50%}.h-\[52px\]{height:52px}.h-\[100dvh\]{height:100dvh}.h-\[min\(40vh\,360px\)\]{height:min(40vh,360px)}.h-\[min\(60vh\,480px\)\]{height:min(60vh,480px)}.h-\[min\(70vh\,520px\)\]{height:min(70vh,520px)}.h-\[min\(140vw\,900px\)\]{height:min(140vw,900px)}.h-full{height:100%}.max-h-\[70vh\]{max-height:70vh}.max-h-\[80vh\]{max-height:80vh}.max-h-\[92vh\]{max-height:92vh}.max-h-\[200px\]{max-height:200px}.min-h-\[52px\]{min-height:52px}.min-h-\[80px\]{min-height:80px}.min-h-\[100dvh\]{min-height:100dvh}.min-h-screen{min-height:100vh}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-11{width:calc(var(--spacing) * 11)}.w-12{width:calc(var(--spacing) * 12)}.w-16{width:calc(var(--spacing) * 16)}.w-32{width:calc(var(--spacing) * 32)}.w-40{width:calc(var(--spacing) * 40)}.w-48{width:calc(var(--spacing) * 48)}.w-64{width:calc(var(--spacing) * 64)}.w-72{width:calc(var(--spacing) * 72)}.w-80{width:calc(var(--spacing) * 80)}.w-\[1\.15rem\]{width:1.15rem}.w-\[1px\]{width:1px}.w-\[40\%\]{width:40%}.w-\[50\%\]{width:50%}.w-\[85vw\]{width:85vw}.w-\[min\(45vw\,360px\)\]{width:min(45vw,360px)}.w-\[min\(65vw\,480px\)\]{width:min(65vw,480px)}.w-\[min\(70vw\,520px\)\]{width:min(70vw,520px)}.w-\[min\(140vw\,900px\)\]{width:min(140vw,900px)}.w-full{width:100%}.w-max{width:max-content}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-\[85\%\]{max-width:85%}.max-w-\[150px\]{max-width:150px}.max-w-\[200px\]{max-width:200px}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.origin-right{transform-origin:100%}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.scale-95{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.rotate-45{rotate:45deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.transform\!{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)!important}.animate-ping{animation:var(--animate-ping)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.touch-manipulation{touch-action:manipulation}.resize{resize:both}.resize-none{resize:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-10{gap:calc(var(--spacing) * 10)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.scroll-smooth{scroll-behavior:smooth}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-\[2rem\]{border-radius:2rem}.rounded-\[32px\]{border-radius:32px}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-tl-sm{border-top-left-radius:var(--radius-sm)}.rounded-tr-sm{border-top-right-radius:var(--radius-sm)}.rounded-bl-sm{border-bottom-left-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-none{--tw-border-style:none;border-style:none}.border-\[\#009E60\]\/20{border-color:#009e6033}.border-\[\#22C55E\]\/20{border-color:#22c55e33}.border-\[\#262626\]{border-color:#262626}.border-\[\#CE1126\]{border-color:#ce1126}.border-\[\#CE1126\]\/30{border-color:#ce11264d}.border-\[\#CE1126\]\/50{border-color:#ce112680}.border-\[\#DC2626\]{border-color:#dc2626}.border-blue-500\/10{border-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.border-blue-500\/10{border-color:color-mix(in oklab,var(--color-blue-500) 10%,transparent)}}.border-blue-500\/30{border-color:#3080ff4d}@supports (color:color-mix(in lab,red,red)){.border-blue-500\/30{border-color:color-mix(in oklab,var(--color-blue-500) 30%,transparent)}}.border-emerald-400\/30{border-color:#00d2944d}@supports (color:color-mix(in lab,red,red)){.border-emerald-400\/30{border-color:color-mix(in oklab,var(--color-emerald-400) 30%,transparent)}}.border-emerald-500\/30{border-color:#00bb7f4d}@supports (color:color-mix(in lab,red,red)){.border-emerald-500\/30{border-color:color-mix(in oklab,var(--color-emerald-500) 30%,transparent)}}.border-green-500\/30{border-color:#00c7584d}@supports (color:color-mix(in lab,red,red)){.border-green-500\/30{border-color:color-mix(in oklab,var(--color-green-500) 30%,transparent)}}.border-red-400\/30{border-color:#ff65684d}@supports (color:color-mix(in lab,red,red)){.border-red-400\/30{border-color:color-mix(in oklab,var(--color-red-400) 30%,transparent)}}.border-red-500\/50{border-color:#fb2c3680}@supports (color:color-mix(in lab,red,red)){.border-red-500\/50{border-color:color-mix(in oklab,var(--color-red-500) 50%,transparent)}}.border-transparent{border-color:#0000}.border-white\/5{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.border-white\/5{border-color:color-mix(in oklab,var(--color-white) 5%,transparent)}}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.border-white\/10{border-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.border-white\/15{border-color:#ffffff26}@supports (color:color-mix(in lab,red,red)){.border-white\/15{border-color:color-mix(in oklab,var(--color-white) 15%,transparent)}}.border-white\/20{border-color:#fff3}@supports (color:color-mix(in lab,red,red)){.border-white\/20{border-color:color-mix(in oklab,var(--color-white) 20%,transparent)}}.border-white\/\[0\.06\]{border-color:#ffffff0f}@supports (color:color-mix(in lab,red,red)){.border-white\/\[0\.06\]{border-color:color-mix(in oklab,var(--color-white) 6%,transparent)}}.border-white\/\[0\.07\]{border-color:#ffffff12}@supports (color:color-mix(in lab,red,red)){.border-white\/\[0\.07\]{border-color:color-mix(in oklab,var(--color-white) 7%,transparent)}}.border-white\/\[0\.08\]{border-color:#ffffff14}@supports (color:color-mix(in lab,red,red)){.border-white\/\[0\.08\]{border-color:color-mix(in oklab,var(--color-white) 8%,transparent)}}.border-white\/\[0\.09\]{border-color:#ffffff17}@supports (color:color-mix(in lab,red,red)){.border-white\/\[0\.09\]{border-color:color-mix(in oklab,var(--color-white) 9%,transparent)}}.border-yellow-500\/30{border-color:#edb2004d}@supports (color:color-mix(in lab,red,red)){.border-yellow-500\/30{border-color:color-mix(in oklab,var(--color-yellow-500) 30%,transparent)}}.border-t-transparent{border-top-color:#0000}.bg-\[\#0A0A0A\]{background-color:#0a0a0a}.bg-\[\#0A0A0A\]\/50{background-color:#0a0a0a80}.bg-\[\#0A0A0A\]\/90{background-color:#0a0a0ae6}.bg-\[\#0A0A0A\]\/95{background-color:#0a0a0af2}.bg-\[\#0F0F0F\]{background-color:#0f0f0f}.bg-\[\#1A1A1A\]{background-color:#1a1a1a}.bg-\[\#009E60\]\/10{background-color:#009e601a}.bg-\[\#009E60\]\/20{background-color:#009e6033}.bg-\[\#009E60\]\/\[0\.08\]{background-color:#009e6014}.bg-\[\#22C55E\]{background-color:#22c55e}.bg-\[\#22C55E\]\/10{background-color:#22c55e1a}.bg-\[\#05060a\]\/95{background-color:#05060af2}.bg-\[\#05070a\]\/95{background-color:#05070af2}.bg-\[\#030508\]{background-color:#030508}.bg-\[\#030508\]\/85{background-color:#030508d9}.bg-\[\#050505\]{background-color:#050505}.bg-\[\#050505\]\/50{background-color:#05050580}.bg-\[\#050505\]\/80{background-color:#050505cc}.bg-\[\#121214\]{background-color:#121214}.bg-\[\#141414\]{background-color:#141414}.bg-\[\#CE1126\]{background-color:#ce1126}.bg-\[\#CE1126\]\/10{background-color:#ce11261a}.bg-\[\#CE1126\]\/15{background-color:#ce112626}.bg-\[\#CE1126\]\/\[0\.07\]{background-color:#ce112612}.bg-\[\#DC2626\]{background-color:#dc2626}.bg-\[\#DC2626\]\/10{background-color:#dc26261a}.bg-\[\#DC2626\]\/20{background-color:#dc262633}.bg-black\/30{background-color:#0000004d}@supports (color:color-mix(in lab,red,red)){.bg-black\/30{background-color:color-mix(in oklab,var(--color-black) 30%,transparent)}}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black) 60%,transparent)}}.bg-black\/70{background-color:#000000b3}@supports (color:color-mix(in lab,red,red)){.bg-black\/70{background-color:color-mix(in oklab,var(--color-black) 70%,transparent)}}.bg-black\/80{background-color:#000c}@supports (color:color-mix(in lab,red,red)){.bg-black\/80{background-color:color-mix(in oklab,var(--color-black) 80%,transparent)}}.bg-blue-500\/5{background-color:#3080ff0d}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/5{background-color:color-mix(in oklab,var(--color-blue-500) 5%,transparent)}}.bg-blue-500\/10{background-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/10{background-color:color-mix(in oklab,var(--color-blue-500) 10%,transparent)}}.bg-blue-500\/15{background-color:#3080ff26}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/15{background-color:color-mix(in oklab,var(--color-blue-500) 15%,transparent)}}.bg-blue-500\/20{background-color:#3080ff33}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/20{background-color:color-mix(in oklab,var(--color-blue-500) 20%,transparent)}}.bg-blue-600\/10{background-color:#155dfc1a}@supports (color:color-mix(in lab,red,red)){.bg-blue-600\/10{background-color:color-mix(in oklab,var(--color-blue-600) 10%,transparent)}}.bg-emerald-500\/15{background-color:#00bb7f26}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/15{background-color:color-mix(in oklab,var(--color-emerald-500) 15%,transparent)}}.bg-green-500{background-color:var(--color-green-500)}.bg-green-500\/15{background-color:#00c75826}@supports (color:color-mix(in lab,red,red)){.bg-green-500\/15{background-color:color-mix(in oklab,var(--color-green-500) 15%,transparent)}}.bg-green-500\/20{background-color:#00c75833}@supports (color:color-mix(in lab,red,red)){.bg-green-500\/20{background-color:color-mix(in oklab,var(--color-green-500) 20%,transparent)}}.bg-purple-500\/10{background-color:#ac4bff1a}@supports (color:color-mix(in lab,red,red)){.bg-purple-500\/10{background-color:color-mix(in oklab,var(--color-purple-500) 10%,transparent)}}.bg-red-500{background-color:var(--color-red-500)}.bg-red-500\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/10{background-color:color-mix(in oklab,var(--color-red-500) 10%,transparent)}}.bg-red-500\/20{background-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/20{background-color:color-mix(in oklab,var(--color-red-500) 20%,transparent)}}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/5{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.bg-white\/5{background-color:color-mix(in oklab,var(--color-white) 5%,transparent)}}.bg-white\/10{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.bg-white\/10{background-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.bg-white\/70{background-color:#ffffffb3}@supports (color:color-mix(in lab,red,red)){.bg-white\/70{background-color:color-mix(in oklab,var(--color-white) 70%,transparent)}}.bg-white\/\[0\.03\]{background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.03\]{background-color:color-mix(in oklab,var(--color-white) 3%,transparent)}}.bg-white\/\[0\.04\]{background-color:#ffffff0a}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.04\]{background-color:color-mix(in oklab,var(--color-white) 4%,transparent)}}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-yellow-500\/20{background-color:#edb20033}@supports (color:color-mix(in lab,red,red)){.bg-yellow-500\/20{background-color:color-mix(in oklab,var(--color-yellow-500) 20%,transparent)}}.bg-gradient-to-b{--tw-gradient-position:to bottom in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-l{--tw-gradient-position:to left in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-\[radial-gradient\(circle_at_50\%_0\%\,rgba\(0\,158\,96\,0\.15\)\,transparent_50\%\)\]{background-image:radial-gradient(circle at 50% 0,#009e6026,#0000 50%)}.bg-\[radial-gradient\(ellipse_80\%_50\%_at_50\%_-20\%\,rgba\(0\,158\,96\,0\.12\)\,transparent\)\]{background-image:radial-gradient(80% 50% at 50% -20%,#009e601f,#0000)}.bg-\[radial-gradient\(ellipse_at_top\,_var\(--tw-gradient-stops\)\)\]{background-image:radial-gradient(ellipse at top,var(--tw-gradient-stops))}.from-\[\#0a0a0f\]{--tw-gradient-from:#0a0a0f;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-\[\#1A1A1A\]\/40{--tw-gradient-from:oklab(21.7786% -7.45058e-9 0/.4);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-\[\#007A3D\]{--tw-gradient-from:#007a3d;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-\[\#009E60\]{--tw-gradient-from:#009e60;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-\[\#009E60\]\/10{--tw-gradient-from:oklab(61.5401% -.134728 .056894/.1);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-\[\#CE1126\]{--tw-gradient-from:#ce1126;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-\[\#CE1126\]\/15{--tw-gradient-from:oklab(54.1195% .193076 .0891775/.15);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-\[\#DC2626\]{--tw-gradient-from:#dc2626;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-transparent{--tw-gradient-from:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-white\/\[0\.04\]{--tw-gradient-from:#ffffff0a}@supports (color:color-mix(in lab,red,red)){.from-white\/\[0\.04\]{--tw-gradient-from:color-mix(in oklab, var(--color-white) 4%, transparent)}}.from-white\/\[0\.04\]{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-white\/\[0\.06\]{--tw-gradient-from:#ffffff0f}@supports (color:color-mix(in lab,red,red)){.from-white\/\[0\.06\]{--tw-gradient-from:color-mix(in oklab, var(--color-white) 6%, transparent)}}.from-white\/\[0\.06\]{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.via-\[\#0a0a0a\]{--tw-gradient-via:#0a0a0a;--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-\[\#050505\]{--tw-gradient-via:#050505;--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-\[\#CE1126\]{--tw-gradient-via:#ce1126;--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-white\/40{--tw-gradient-via:#fff6}@supports (color:color-mix(in lab,red,red)){.via-white\/40{--tw-gradient-via:color-mix(in oklab, var(--color-white) 40%, transparent)}}.via-white\/40{--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-white\/60{--tw-gradient-via:#fff9}@supports (color:color-mix(in lab,red,red)){.via-white\/60{--tw-gradient-via:color-mix(in oklab, var(--color-white) 60%, transparent)}}.via-white\/60{--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-\[\#7f1d1d\]{--tw-gradient-to:#7f1d1d;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-\[\#991B1B\]{--tw-gradient-to:#991b1b;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-\[\#05060a\]{--tw-gradient-to:#05060a;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-\[\#050505\]{--tw-gradient-to:#050505;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-\[\#CE1126\]{--tw-gradient-to:#ce1126;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-\[\#CE1126\]\/10{--tw-gradient-to:oklab(54.1195% .193076 .0891775/.1);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-\[\#b91c1c\]{--tw-gradient-to:#b91c1c;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-white{--tw-gradient-to:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-white\/60{--tw-gradient-to:#fff9}@supports (color:color-mix(in lab,red,red)){.to-white\/60{--tw-gradient-to:color-mix(in oklab, var(--color-white) 60%, transparent)}}.to-white\/60{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.fill-current{fill:currentColor}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.p-1{padding:calc(var(--spacing) * 1)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.p-10{padding:calc(var(--spacing) * 10)}.p-\[1px\]{padding:1px}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-10{padding-inline:calc(var(--spacing) * 10)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-14{padding-block:calc(var(--spacing) * 14)}.py-20{padding-block:calc(var(--spacing) * 20)}.pt-0{padding-top:calc(var(--spacing) * 0)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-10{padding-top:calc(var(--spacing) * 10)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pr-10{padding-right:calc(var(--spacing) * 10)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-10{padding-left:calc(var(--spacing) * 10)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-start{text-align:start}.font-sans{font-family:var(--font-sans)}.font-serif{font-family:var(--font-serif)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[15px\]{font-size:15px}.text-\[26px\]{font-size:26px}.leading-\[1\.15\]{--tw-leading:1.15;line-height:1.15}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.14em\]{--tw-tracking:.14em;letter-spacing:.14em}.tracking-\[0\.22em\]{--tw-tracking:.22em;letter-spacing:.22em}.tracking-\[0\.35em\]{--tw-tracking:.35em;letter-spacing:.35em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#0a0a0a\]{color:#0a0a0a}.text-\[\#0f172a\]{color:#0f172a}.text-\[\#3f3f46\]{color:#3f3f46}.text-\[\#009E60\]{color:#009e60}.text-\[\#22C55E\]{color:#22c55e}.text-\[\#86efac\]{color:#86efac}.text-\[\#94a3b8\]{color:#94a3b8}.text-\[\#52525B\],.text-\[\#52525b\]{color:#52525b}.text-\[\#64748b\]{color:#64748b}.text-\[\#71717a\]{color:#71717a}.text-\[\#A1A1AA\]{color:#a1a1aa}.text-\[\#DC2626\]{color:#dc2626}.text-\[\#FAFAFA\]{color:#fafafa}.text-\[\#cbd5e1\]{color:#cbd5e1}.text-\[\#fca5a5\]{color:#fca5a5}.text-black{color:var(--color-black)}.text-blue-400{color:var(--color-blue-400)}.text-blue-500{color:var(--color-blue-500)}.text-emerald-400{color:var(--color-emerald-400)}.text-gray-400{color:var(--color-gray-400)}.text-green-400{color:var(--color-green-400)}.text-green-500{color:var(--color-green-500)}.text-purple-500{color:var(--color-purple-500)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-sky-400{color:var(--color-sky-400)}.text-transparent{color:#0000}.text-white{color:var(--color-white)}.text-white\/50{color:#ffffff80}@supports (color:color-mix(in lab,red,red)){.text-white\/50{color:color-mix(in oklab,var(--color-white) 50%,transparent)}}.text-white\/60{color:#fff9}@supports (color:color-mix(in lab,red,red)){.text-white\/60{color:color-mix(in oklab,var(--color-white) 60%,transparent)}}.text-white\/70{color:#ffffffb3}@supports (color:color-mix(in lab,red,red)){.text-white\/70{color:color-mix(in oklab,var(--color-white) 70%,transparent)}}.text-white\/80{color:#fffc}@supports (color:color-mix(in lab,red,red)){.text-white\/80{color:color-mix(in oklab,var(--color-white) 80%,transparent)}}.text-white\/95{color:#fffffff2}@supports (color:color-mix(in lab,red,red)){.text-white\/95{color:color-mix(in oklab,var(--color-white) 95%,transparent)}}.text-yellow-500{color:var(--color-yellow-500)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.line-through{text-decoration-line:line-through}.overline{text-decoration-line:overline}.underline{text-decoration-line:underline}.opacity-0{opacity:0}.opacity-20{opacity:.2}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-90{opacity:.9}.opacity-100{opacity:1}.opacity-\[0\.14\]{opacity:.14}.opacity-\[0\.35\]{opacity:.35}.opacity-\[0\.85\]{opacity:.85}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_8px_\#22C55E\]{--tw-shadow:0 0 8px var(--tw-shadow-color,#22c55e);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_8px_\#DC2626\]{--tw-shadow:0 0 8px var(--tw-shadow-color,#dc2626);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_10px_rgba\(34\,197\,94\,0\.5\)\]{--tw-shadow:0 0 10px var(--tw-shadow-color,#22c55e80);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_50px_rgba\(220\,38\,38\,0\.2\)\]{--tw-shadow:0 0 50px var(--tw-shadow-color,#dc262633);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_80px_-20px_rgba\(0\,158\,96\,0\.35\)\]{--tw-shadow:0 0 80px -20px var(--tw-shadow-color,#009e6059);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[\#CE1126\]\/20{--tw-shadow-color:#ce112633}@supports (color:color-mix(in lab,red,red)){.shadow-\[\#CE1126\]\/20{--tw-shadow-color:color-mix(in oklab, oklab(54.1195% .193076 .0891775/.2) var(--tw-shadow-alpha), transparent)}}.shadow-\[\#CE1126\]\/30{--tw-shadow-color:#ce11264d}@supports (color:color-mix(in lab,red,red)){.shadow-\[\#CE1126\]\/30{--tw-shadow-color:color-mix(in oklab, oklab(54.1195% .193076 .0891775/.3) var(--tw-shadow-alpha), transparent)}}.shadow-\[\#DC2626\]\/20{--tw-shadow-color:#dc262633}@supports (color:color-mix(in lab,red,red)){.shadow-\[\#DC2626\]\/20{--tw-shadow-color:color-mix(in oklab, oklab(57.7099% .191149 .0987651/.2) var(--tw-shadow-alpha), transparent)}}.shadow-black\/40{--tw-shadow-color:#0006}@supports (color:color-mix(in lab,red,red)){.shadow-black\/40{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-black) 40%, transparent) var(--tw-shadow-alpha), transparent)}}.ring-white\/10{--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.ring-white\/10{--tw-ring-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.ring-white\/15{--tw-ring-color:#ffffff26}@supports (color:color-mix(in lab,red,red)){.ring-white\/15{--tw-ring-color:color-mix(in oklab, var(--color-white) 15%, transparent)}}.ring-white\/20{--tw-ring-color:#fff3}@supports (color:color-mix(in lab,red,red)){.ring-white\/20{--tw-ring-color:color-mix(in oklab, var(--color-white) 20%, transparent)}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-2xl{--tw-blur:blur(var(--blur-2xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-3xl{--tw-blur:blur(var(--blur-3xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-\[80px\]{--tw-blur:blur(80px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-\[90px\]{--tw-blur:blur(90px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-\[100px\]{--tw-blur:blur(100px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-\[110px\]{--tw-blur:blur(110px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-\[120px\]{--tw-blur:blur(120px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a)) drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_15px_rgba\(220\,38\,38\,0\.5\)\]{--tw-drop-shadow-size:drop-shadow(0 0 15px var(--tw-drop-shadow-color,#dc262680));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.invert{--tw-invert:invert(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter\!{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-2xl{--tw-backdrop-blur:blur(var(--blur-2xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.delay-1000{transition-delay:1s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.\[file\:line\]{file:line}@media(hover:hover){.group-hover\:-translate-x-0\.5:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:scale-110:is(:where(.group):hover *){--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:bg-blue-500\/20:is(:where(.group):hover *){background-color:#3080ff33}@supports (color:color-mix(in lab,red,red)){.group-hover\:bg-blue-500\/20:is(:where(.group):hover *){background-color:color-mix(in oklab,var(--color-blue-500) 20%,transparent)}}.group-hover\:bg-purple-500\/20:is(:where(.group):hover *){background-color:#ac4bff33}@supports (color:color-mix(in lab,red,red)){.group-hover\:bg-purple-500\/20:is(:where(.group):hover *){background-color:color-mix(in oklab,var(--color-purple-500) 20%,transparent)}}.group-hover\:bg-red-500\/20:is(:where(.group):hover *){background-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){.group-hover\:bg-red-500\/20:is(:where(.group):hover *){background-color:color-mix(in oklab,var(--color-red-500) 20%,transparent)}}.group-hover\:text-\[\#FAFAFA\]:is(:where(.group):hover *){color:#fafafa}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.placeholder\:text-\[\#52525B\]::placeholder{color:#52525b}.focus-within\:border-white\/20:focus-within{border-color:#fff3}@supports (color:color-mix(in lab,red,red)){.focus-within\:border-white\/20:focus-within{border-color:color-mix(in oklab,var(--color-white) 20%,transparent)}}@media(hover:hover){.hover\:scale-\[1\.02\]:hover{scale:1.02}.hover\:border-\[\#009E60\]\/25:hover{border-color:#009e6040}.hover\:border-\[\#CE1126\]\/40:hover{border-color:#ce112666}.hover\:border-white\/10:hover{border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.hover\:border-white\/10:hover{border-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.hover\:border-white\/15:hover{border-color:#ffffff26}@supports (color:color-mix(in lab,red,red)){.hover\:border-white\/15:hover{border-color:color-mix(in oklab,var(--color-white) 15%,transparent)}}.hover\:bg-\[\#262626\]:hover{background-color:#262626}.hover\:bg-\[\#EF4444\]:hover{background-color:#ef4444}.hover\:bg-blue-500\/25:hover{background-color:#3080ff40}@supports (color:color-mix(in lab,red,red)){.hover\:bg-blue-500\/25:hover{background-color:color-mix(in oklab,var(--color-blue-500) 25%,transparent)}}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-green-500\/25:hover{background-color:#00c75840}@supports (color:color-mix(in lab,red,red)){.hover\:bg-green-500\/25:hover{background-color:color-mix(in oklab,var(--color-green-500) 25%,transparent)}}.hover\:bg-red-500\/10:hover{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-red-500\/10:hover{background-color:color-mix(in oklab,var(--color-red-500) 10%,transparent)}}.hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/5:hover{background-color:color-mix(in oklab,var(--color-white) 5%,transparent)}}.hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.hover\:bg-white\/15:hover{background-color:#ffffff26}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/15:hover{background-color:color-mix(in oklab,var(--color-white) 15%,transparent)}}.hover\:bg-white\/20:hover{background-color:#fff3}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/20:hover{background-color:color-mix(in oklab,var(--color-white) 20%,transparent)}}.hover\:text-\[\#FAFAFA\]:hover{color:#fafafa}.hover\:text-red-500:hover{color:var(--color-red-500)}.hover\:text-white:hover{color:var(--color-white)}.hover\:shadow-\[0_20px_50px_-12px_rgba\(0\,0\,0\,0\.5\)\]:hover{--tw-shadow:0 20px 50px -12px var(--tw-shadow-color,#00000080);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:border-\[\#DC2626\]\/50:focus{border-color:#dc262680}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:border-green-500:focus{border-color:var(--color-green-500)}.focus\:ring-\[\#009E60\]:focus{--tw-ring-color:#009e60}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:40rem){.sm\:block{display:block}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-center{justify-content:center}}@media(min-width:48rem){.md\:absolute{position:absolute}.md\:relative{position:relative}.md\:inset-auto{inset:auto}.md\:top-4{top:calc(var(--spacing) * 4)}.md\:right-0{right:calc(var(--spacing) * 0)}.md\:right-4{right:calc(var(--spacing) * 4)}.md\:bottom-4{bottom:calc(var(--spacing) * 4)}.md\:bottom-full{bottom:100%}.md\:left-auto{left:auto}.md\:z-30{z-index:30}.md\:mb-2{margin-bottom:calc(var(--spacing) * 2)}.md\:mb-14{margin-bottom:calc(var(--spacing) * 14)}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-72{width:calc(var(--spacing) * 72)}.md\:w-\[300px\]{width:300px}.md\:max-w-sm{max-width:var(--container-sm)}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:justify-between{justify-content:space-between}.md\:justify-start{justify-content:flex-start}.md\:bg-\[\#0A0A0A\]\/90{background-color:#0a0a0ae6}.md\:p-6{padding:calc(var(--spacing) * 6)}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:p-12{padding:calc(var(--spacing) * 12)}.md\:p-14{padding:calc(var(--spacing) * 14)}.md\:px-5{padding-inline:calc(var(--spacing) * 5)}.md\:px-6{padding-inline:calc(var(--spacing) * 6)}.md\:px-8{padding-inline:calc(var(--spacing) * 8)}.md\:py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.md\:py-20{padding-block:calc(var(--spacing) * 20)}.md\:pt-14{padding-top:calc(var(--spacing) * 14)}.md\:pb-20{padding-bottom:calc(var(--spacing) * 20)}.md\:pb-24{padding-bottom:calc(var(--spacing) * 24)}.md\:text-right{text-align:right}.md\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.md\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.md\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.md\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.md\:text-\[17px\]{font-size:17px}.md\:text-\[36px\]{font-size:36px}.md\:leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.md\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.md\:backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}}@media(min-width:64rem){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}.rtl\:rotate-180:where(:dir(rtl),[dir=rtl],[dir=rtl] *){rotate:180deg}}@keyframes kw-spin-slow{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes kw-float-y{0%,to{transform:translateY(0)scale(1)}50%{transform:translateY(-18px)scale(1.03)}}@keyframes kw-gradient-flow{0%,to{background-position:0%}50%{background-position:100%}}@keyframes kw-pulse-glow{0%,to{opacity:.45;transform:scale(1)}50%{opacity:.75;transform:scale(1.08)}}@keyframes kw-marquee{0%{transform:translate(0)}to{transform:translate(-50%)}}@keyframes kw-shine{0%{transform:translate(-100%)}to{transform:translate(200%)}}.landing-kw-spin{animation:50s linear infinite kw-spin-slow}.landing-kw-float{animation:9s ease-in-out infinite kw-float-y}.landing-kw-float-delay{animation:11s ease-in-out -5s infinite kw-float-y}.landing-title-gradient{color:#0000;background:linear-gradient(105deg,#009e60,#fafafa,#ce1126,#009e60,#fafafa) 0 0/220%;-webkit-background-clip:text;background-clip:text;animation:7s ease-in-out infinite kw-gradient-flow}.landing-cta-glow{animation:3.5s ease-in-out infinite kw-pulse-glow}.landing-marquee-track{animation:32s linear infinite kw-marquee}@keyframes kw-aurora-move{0%,to{transform:translate(0)scale(1)}33%{transform:translate(8%,-6%)scale(1.08)}66%{transform:translate(-6%,5%)scale(.94)}}@keyframes kw-aurora-move-2{0%,to{transform:translate(0)scale(1)}50%{transform:translate(-10%,8%)scale(1.05)}}@keyframes kw-shine-sweep{0%{opacity:0;transform:translate(-60%)skew(-12deg)}15%{opacity:.35}50%{opacity:.2;transform:translate(120%)skew(-12deg)}to{opacity:0;transform:translate(120%)skew(-12deg)}}@keyframes kw-beam-slide{0%{opacity:0;transform:translate(-140%)skew(-8deg)}25%{opacity:.55}to{opacity:0;transform:translate(280%)skew(-8deg)}}.landing-aurora-blob{animation:22s ease-in-out infinite kw-aurora-move}.landing-aurora-blob-2{animation:26s ease-in-out -8s infinite kw-aurora-move-2}.landing-aurora-blob-3{animation:19s ease-in-out -12s infinite kw-aurora-move}.landing-hero-shine-wrap{pointer-events:none;border-radius:inherit;position:absolute;top:0;right:0;bottom:0;left:0;overflow:hidden}.landing-hero-shine-beam{background:linear-gradient(90deg,#0000,#009e6026,#ffffff1f,#ce11261f,#0000);width:45%;height:140%;animation:6s ease-in-out infinite kw-beam-slide;position:absolute;top:-20%;left:0}@media(prefers-reduced-motion:reduce){.landing-kw-spin,.landing-kw-float,.landing-kw-float-delay,.landing-title-gradient,.landing-cta-glow,.landing-marquee-track,.landing-aurora-blob,.landing-aurora-blob-2,.landing-aurora-blob-3{animation:none!important}.landing-hero-shine-beam{opacity:0;animation:none!important}.landing-particles-canvas{display:none!important}}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar,body{-ms-overflow-style:none;scrollbar-width:none}body::-webkit-scrollbar{display:none}.markdown-body{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);color:#fafafa}.markdown-body p{margin-bottom:calc(var(--spacing) * 4)}.markdown-body p:last-child{margin-bottom:calc(var(--spacing) * 0)}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:calc(var(--spacing) * 6);margin-bottom:calc(var(--spacing) * 3);--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--color-white)}.markdown-body h1{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.markdown-body h2{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.markdown-body h3{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.markdown-body ul{margin-bottom:calc(var(--spacing) * 4);padding-right:calc(var(--spacing) * 6);list-style-type:disc}.markdown-body ol{margin-bottom:calc(var(--spacing) * 4);padding-right:calc(var(--spacing) * 6);list-style-type:decimal}.markdown-body li{margin-bottom:calc(var(--spacing) * 1)}.markdown-body a{color:var(--color-blue-400);text-decoration-line:underline}@media(hover:hover){.markdown-body a:hover{color:var(--color-blue-300)}}.markdown-body blockquote{margin-block:calc(var(--spacing) * 4);border-right-style:var(--tw-border-style);padding-right:calc(var(--spacing) * 4);color:var(--color-gray-300);border-color:#dc2626;border-right-width:4px;font-style:italic}.markdown-body table{margin-bottom:calc(var(--spacing) * 4);border-collapse:collapse;width:100%}.markdown-body th,.markdown-body td{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);text-align:start;border-width:1px;border-color:#262626}.markdown-body th{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);background-color:#262626}.markdown-body strong{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--color-white)}@keyframes kw-splash-bg-kenburns{0%{filter:blur(3px);opacity:.55;background-position:50% 30%;background-size:130%}25%{opacity:1;filter:blur()}to{filter:blur();opacity:1;background-position:50% 40%;background-size:108%}}.kw-splash-bg{will-change:background-size,background-position,filter,opacity;animation:3.2s cubic-bezier(.22,1,.36,1) both kw-splash-bg-kenburns}@keyframes kw-fade-up{0%{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}.kw-fade-up{animation:.8s cubic-bezier(.22,1,.36,1) both kw-fade-up}.kw-fade-up-delay-1{animation-delay:.2s}.kw-fade-up-delay-2{animation-delay:.45s}.kw-fade-up-delay-3{animation-delay:.7s}@keyframes kw-shimmer-bar{0%{transform:translate(-120%)}to{transform:translate(220%)}}.kw-shimmer-bar:after{content:"";background:linear-gradient(90deg,#0000,#ffffff80,#0000);animation:1.6s ease-in-out infinite kw-shimmer-bar;position:absolute;top:0;right:0;bottom:0;left:0}.kw-lift{transition:transform .3s cubic-bezier(.22,1,.36,1),box-shadow .3s}.kw-lift:hover{transform:translateY(-4px);box-shadow:0 20px 50px -20px #009e6059}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}
|
static/assets/index-D3HV3XVM.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/assets/index.es-s1vvCQPr.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/assets/purify.es-BgtpMKW3.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*! @license DOMPurify 3.3.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.3.3/LICENSE */const{entries:_t,setPrototypeOf:ct,isFrozen:Yt,getPrototypeOf:Xt,getOwnPropertyDescriptor:jt}=Object;let{freeze:S,seal:b,create:me}=Object,{apply:ve,construct:Ue}=typeof Reflect<"u"&&Reflect;S||(S=function(o){return o});b||(b=function(o){return o});ve||(ve=function(o,l){for(var a=arguments.length,c=new Array(a>2?a-2:0),D=2;D<a;D++)c[D-2]=arguments[D];return o.apply(l,c)});Ue||(Ue=function(o){for(var l=arguments.length,a=new Array(l>1?l-1:0),c=1;c<l;c++)a[c-1]=arguments[c];return new o(...a)});const fe=R(Array.prototype.forEach),Vt=R(Array.prototype.lastIndexOf),ft=R(Array.prototype.pop),q=R(Array.prototype.push),$t=R(Array.prototype.splice),pe=R(String.prototype.toLowerCase),Ie=R(String.prototype.toString),Me=R(String.prototype.match),K=R(String.prototype.replace),qt=R(String.prototype.indexOf),Kt=R(String.prototype.trim),L=R(Object.prototype.hasOwnProperty),h=R(RegExp.prototype.test),Z=Zt(TypeError);function R(s){return function(o){o instanceof RegExp&&(o.lastIndex=0);for(var l=arguments.length,a=new Array(l>1?l-1:0),c=1;c<l;c++)a[c-1]=arguments[c];return ve(s,o,a)}}function Zt(s){return function(){for(var o=arguments.length,l=new Array(o),a=0;a<o;a++)l[a]=arguments[a];return Ue(s,l)}}function r(s,o){let l=arguments.length>2&&arguments[2]!==void 0?arguments[2]:pe;ct&&ct(s,null);let a=o.length;for(;a--;){let c=o[a];if(typeof c=="string"){const D=l(c);D!==c&&(Yt(o)||(o[a]=D),c=D)}s[c]=!0}return s}function Jt(s){for(let o=0;o<s.length;o++)L(s,o)||(s[o]=null);return s}function C(s){const o=me(null);for(const[l,a]of _t(s))L(s,l)&&(Array.isArray(a)?o[l]=Jt(a):a&&typeof a=="object"&&a.constructor===Object?o[l]=C(a):o[l]=a);return o}function J(s,o){for(;s!==null;){const a=jt(s,o);if(a){if(a.get)return R(a.get);if(typeof a.value=="function")return R(a.value)}s=Xt(s)}function l(){return null}return l}const ut=S(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),we=S(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),xe=S(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),Qt=S(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),Pe=S(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),en=S(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),mt=S(["#text"]),pt=S(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns","slot"]),ke=S(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),Tt=S(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),ue=S(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),tn=b(/\{\{[\w\W]*|[\w\W]*\}\}/gm),nn=b(/<%[\w\W]*|[\w\W]*%>/gm),on=b(/\$\{[\w\W]*/gm),an=b(/^data-[\-\w.\u00B7-\uFFFF]+$/),rn=b(/^aria-[\-\w]+$/),gt=b(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),sn=b(/^(?:\w+script|data):/i),ln=b(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),At=b(/^html$/i),cn=b(/^[a-z][.\w]*(-[.\w]+)+$/i);var dt=Object.freeze({__proto__:null,ARIA_ATTR:rn,ATTR_WHITESPACE:ln,CUSTOM_ELEMENT:cn,DATA_ATTR:an,DOCTYPE_NAME:At,ERB_EXPR:nn,IS_ALLOWED_URI:gt,IS_SCRIPT_OR_DATA:sn,MUSTACHE_EXPR:tn,TMPLIT_EXPR:on});const Q={element:1,text:3,progressingInstruction:7,comment:8,document:9},fn=function(){return typeof window>"u"?null:window},un=function(o,l){if(typeof o!="object"||typeof o.createPolicy!="function")return null;let a=null;const c="data-tt-policy-suffix";l&&l.hasAttribute(c)&&(a=l.getAttribute(c));const D="dompurify"+(a?"#"+a:"");try{return o.createPolicy(D,{createHTML(v){return v},createScriptURL(v){return v}})}catch{return console.warn("TrustedTypes policy "+D+" could not be created."),null}},Et=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}};function ht(){let s=arguments.length>0&&arguments[0]!==void 0?arguments[0]:fn();const o=i=>ht(i);if(o.version="3.3.3",o.removed=[],!s||!s.document||s.document.nodeType!==Q.document||!s.Element)return o.isSupported=!1,o;let{document:l}=s;const a=l,c=a.currentScript,{DocumentFragment:D,HTMLTemplateElement:v,Node:Te,Element:Fe,NodeFilter:B,NamedNodeMap:St=s.NamedNodeMap||s.MozNamedAttrMap,HTMLFormElement:Rt,DOMParser:Dt,trustedTypes:ee}=s,Y=Fe.prototype,Ot=J(Y,"cloneNode"),Lt=J(Y,"remove"),bt=J(Y,"nextSibling"),yt=J(Y,"childNodes"),te=J(Y,"parentNode");if(typeof v=="function"){const i=l.createElement("template");i.content&&i.content.ownerDocument&&(l=i.content.ownerDocument)}let g,X="";const{implementation:de,createNodeIterator:Nt,createDocumentFragment:Ct,getElementsByTagName:It}=l,{importNode:Mt}=a;let A=Et();o.isSupported=typeof _t=="function"&&typeof te=="function"&&de&&de.createHTMLDocument!==void 0;const{MUSTACHE_EXPR:Ee,ERB_EXPR:_e,TMPLIT_EXPR:ge,DATA_ATTR:wt,ARIA_ATTR:xt,IS_SCRIPT_OR_DATA:Pt,ATTR_WHITESPACE:He,CUSTOM_ELEMENT:kt}=dt;let{IS_ALLOWED_URI:ze}=dt,T=null;const Ge=r({},[...ut,...we,...xe,...Pe,...mt]);let d=null;const We=r({},[...pt,...ke,...Tt,...ue]);let u=Object.seal(me(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),j=null,ne=null;const x=Object.seal(me(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let Be=!0,Ae=!0,Ye=!1,Xe=!0,U=!1,oe=!0,P=!1,he=!1,Se=!1,F=!1,ie=!1,ae=!1,je=!0,Ve=!1;const vt="user-content-";let Re=!0,V=!1,H={},y=null;const De=r({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let $e=null;const qe=r({},["audio","video","img","source","image","track"]);let Oe=null;const Ke=r({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),re="http://www.w3.org/1998/Math/MathML",se="http://www.w3.org/2000/svg",I="http://www.w3.org/1999/xhtml";let z=I,Le=!1,be=null;const Ut=r({},[re,se,I],Ie);let le=r({},["mi","mo","mn","ms","mtext"]),ce=r({},["annotation-xml"]);const Ft=r({},["title","style","font","a","script"]);let $=null;const Ht=["application/xhtml+xml","text/html"],zt="text/html";let p=null,G=null;const Gt=l.createElement("form"),Ze=function(e){return e instanceof RegExp||e instanceof Function},ye=function(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(!(G&&G===e)){if((!e||typeof e!="object")&&(e={}),e=C(e),$=Ht.indexOf(e.PARSER_MEDIA_TYPE)===-1?zt:e.PARSER_MEDIA_TYPE,p=$==="application/xhtml+xml"?Ie:pe,T=L(e,"ALLOWED_TAGS")?r({},e.ALLOWED_TAGS,p):Ge,d=L(e,"ALLOWED_ATTR")?r({},e.ALLOWED_ATTR,p):We,be=L(e,"ALLOWED_NAMESPACES")?r({},e.ALLOWED_NAMESPACES,Ie):Ut,Oe=L(e,"ADD_URI_SAFE_ATTR")?r(C(Ke),e.ADD_URI_SAFE_ATTR,p):Ke,$e=L(e,"ADD_DATA_URI_TAGS")?r(C(qe),e.ADD_DATA_URI_TAGS,p):qe,y=L(e,"FORBID_CONTENTS")?r({},e.FORBID_CONTENTS,p):De,j=L(e,"FORBID_TAGS")?r({},e.FORBID_TAGS,p):C({}),ne=L(e,"FORBID_ATTR")?r({},e.FORBID_ATTR,p):C({}),H=L(e,"USE_PROFILES")?e.USE_PROFILES:!1,Be=e.ALLOW_ARIA_ATTR!==!1,Ae=e.ALLOW_DATA_ATTR!==!1,Ye=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Xe=e.ALLOW_SELF_CLOSE_IN_ATTR!==!1,U=e.SAFE_FOR_TEMPLATES||!1,oe=e.SAFE_FOR_XML!==!1,P=e.WHOLE_DOCUMENT||!1,F=e.RETURN_DOM||!1,ie=e.RETURN_DOM_FRAGMENT||!1,ae=e.RETURN_TRUSTED_TYPE||!1,Se=e.FORCE_BODY||!1,je=e.SANITIZE_DOM!==!1,Ve=e.SANITIZE_NAMED_PROPS||!1,Re=e.KEEP_CONTENT!==!1,V=e.IN_PLACE||!1,ze=e.ALLOWED_URI_REGEXP||gt,z=e.NAMESPACE||I,le=e.MATHML_TEXT_INTEGRATION_POINTS||le,ce=e.HTML_INTEGRATION_POINTS||ce,u=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&Ze(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(u.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&Ze(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(u.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements=="boolean"&&(u.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),U&&(Ae=!1),ie&&(F=!0),H&&(T=r({},mt),d=me(null),H.html===!0&&(r(T,ut),r(d,pt)),H.svg===!0&&(r(T,we),r(d,ke),r(d,ue)),H.svgFilters===!0&&(r(T,xe),r(d,ke),r(d,ue)),H.mathMl===!0&&(r(T,Pe),r(d,Tt),r(d,ue))),L(e,"ADD_TAGS")||(x.tagCheck=null),L(e,"ADD_ATTR")||(x.attributeCheck=null),e.ADD_TAGS&&(typeof e.ADD_TAGS=="function"?x.tagCheck=e.ADD_TAGS:(T===Ge&&(T=C(T)),r(T,e.ADD_TAGS,p))),e.ADD_ATTR&&(typeof e.ADD_ATTR=="function"?x.attributeCheck=e.ADD_ATTR:(d===We&&(d=C(d)),r(d,e.ADD_ATTR,p))),e.ADD_URI_SAFE_ATTR&&r(Oe,e.ADD_URI_SAFE_ATTR,p),e.FORBID_CONTENTS&&(y===De&&(y=C(y)),r(y,e.FORBID_CONTENTS,p)),e.ADD_FORBID_CONTENTS&&(y===De&&(y=C(y)),r(y,e.ADD_FORBID_CONTENTS,p)),Re&&(T["#text"]=!0),P&&r(T,["html","head","body"]),T.table&&(r(T,["tbody"]),delete j.tbody),e.TRUSTED_TYPES_POLICY){if(typeof e.TRUSTED_TYPES_POLICY.createHTML!="function")throw Z('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof e.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw Z('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');g=e.TRUSTED_TYPES_POLICY,X=g.createHTML("")}else g===void 0&&(g=un(ee,c)),g!==null&&typeof X=="string"&&(X=g.createHTML(""));S&&S(e),G=e}},Je=r({},[...we,...xe,...Qt]),Qe=r({},[...Pe,...en]),Wt=function(e){let t=te(e);(!t||!t.tagName)&&(t={namespaceURI:z,tagName:"template"});const n=pe(e.tagName),f=pe(t.tagName);return be[e.namespaceURI]?e.namespaceURI===se?t.namespaceURI===I?n==="svg":t.namespaceURI===re?n==="svg"&&(f==="annotation-xml"||le[f]):!!Je[n]:e.namespaceURI===re?t.namespaceURI===I?n==="math":t.namespaceURI===se?n==="math"&&ce[f]:!!Qe[n]:e.namespaceURI===I?t.namespaceURI===se&&!ce[f]||t.namespaceURI===re&&!le[f]?!1:!Qe[n]&&(Ft[n]||!Je[n]):!!($==="application/xhtml+xml"&&be[e.namespaceURI]):!1},N=function(e){q(o.removed,{element:e});try{te(e).removeChild(e)}catch{Lt(e)}},k=function(e,t){try{q(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch{q(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),e==="is")if(F||ie)try{N(t)}catch{}else try{t.setAttribute(e,"")}catch{}},et=function(e){let t=null,n=null;if(Se)e="<remove></remove>"+e;else{const m=Me(e,/^[\r\n\t ]+/);n=m&&m[0]}$==="application/xhtml+xml"&&z===I&&(e='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+e+"</body></html>");const f=g?g.createHTML(e):e;if(z===I)try{t=new Dt().parseFromString(f,$)}catch{}if(!t||!t.documentElement){t=de.createDocument(z,"template",null);try{t.documentElement.innerHTML=Le?X:f}catch{}}const _=t.body||t.documentElement;return e&&n&&_.insertBefore(l.createTextNode(n),_.childNodes[0]||null),z===I?It.call(t,P?"html":"body")[0]:P?t.documentElement:_},tt=function(e){return Nt.call(e.ownerDocument||e,e,B.SHOW_ELEMENT|B.SHOW_COMMENT|B.SHOW_TEXT|B.SHOW_PROCESSING_INSTRUCTION|B.SHOW_CDATA_SECTION,null)},Ne=function(e){return e instanceof Rt&&(typeof e.nodeName!="string"||typeof e.textContent!="string"||typeof e.removeChild!="function"||!(e.attributes instanceof St)||typeof e.removeAttribute!="function"||typeof e.setAttribute!="function"||typeof e.namespaceURI!="string"||typeof e.insertBefore!="function"||typeof e.hasChildNodes!="function")},nt=function(e){return typeof Te=="function"&&e instanceof Te};function M(i,e,t){fe(i,n=>{n.call(o,e,t,G)})}const ot=function(e){let t=null;if(M(A.beforeSanitizeElements,e,null),Ne(e))return N(e),!0;const n=p(e.nodeName);if(M(A.uponSanitizeElement,e,{tagName:n,allowedTags:T}),oe&&e.hasChildNodes()&&!nt(e.firstElementChild)&&h(/<[/\w!]/g,e.innerHTML)&&h(/<[/\w!]/g,e.textContent)||e.nodeType===Q.progressingInstruction||oe&&e.nodeType===Q.comment&&h(/<[/\w]/g,e.data))return N(e),!0;if(!(x.tagCheck instanceof Function&&x.tagCheck(n))&&(!T[n]||j[n])){if(!j[n]&&at(n)&&(u.tagNameCheck instanceof RegExp&&h(u.tagNameCheck,n)||u.tagNameCheck instanceof Function&&u.tagNameCheck(n)))return!1;if(Re&&!y[n]){const f=te(e)||e.parentNode,_=yt(e)||e.childNodes;if(_&&f){const m=_.length;for(let O=m-1;O>=0;--O){const w=Ot(_[O],!0);w.__removalCount=(e.__removalCount||0)+1,f.insertBefore(w,bt(e))}}}return N(e),!0}return e instanceof Fe&&!Wt(e)||(n==="noscript"||n==="noembed"||n==="noframes")&&h(/<\/no(script|embed|frames)/i,e.innerHTML)?(N(e),!0):(U&&e.nodeType===Q.text&&(t=e.textContent,fe([Ee,_e,ge],f=>{t=K(t,f," ")}),e.textContent!==t&&(q(o.removed,{element:e.cloneNode()}),e.textContent=t)),M(A.afterSanitizeElements,e,null),!1)},it=function(e,t,n){if(ne[t]||je&&(t==="id"||t==="name")&&(n in l||n in Gt))return!1;if(!(Ae&&!ne[t]&&h(wt,t))){if(!(Be&&h(xt,t))){if(!(x.attributeCheck instanceof Function&&x.attributeCheck(t,e))){if(!d[t]||ne[t]){if(!(at(e)&&(u.tagNameCheck instanceof RegExp&&h(u.tagNameCheck,e)||u.tagNameCheck instanceof Function&&u.tagNameCheck(e))&&(u.attributeNameCheck instanceof RegExp&&h(u.attributeNameCheck,t)||u.attributeNameCheck instanceof Function&&u.attributeNameCheck(t,e))||t==="is"&&u.allowCustomizedBuiltInElements&&(u.tagNameCheck instanceof RegExp&&h(u.tagNameCheck,n)||u.tagNameCheck instanceof Function&&u.tagNameCheck(n))))return!1}else if(!Oe[t]){if(!h(ze,K(n,He,""))){if(!((t==="src"||t==="xlink:href"||t==="href")&&e!=="script"&&qt(n,"data:")===0&&$e[e])){if(!(Ye&&!h(Pt,K(n,He,"")))){if(n)return!1}}}}}}}return!0},at=function(e){return e!=="annotation-xml"&&Me(e,kt)},rt=function(e){M(A.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t||Ne(e))return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:d,forceKeepAttr:void 0};let f=t.length;for(;f--;){const _=t[f],{name:m,namespaceURI:O,value:w}=_,W=p(m),Ce=w;let E=m==="value"?Ce:Kt(Ce);if(n.attrName=W,n.attrValue=E,n.keepAttr=!0,n.forceKeepAttr=void 0,M(A.uponSanitizeAttribute,e,n),E=n.attrValue,Ve&&(W==="id"||W==="name")&&(k(m,e),E=vt+E),oe&&h(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,E)){k(m,e);continue}if(W==="attributename"&&Me(E,"href")){k(m,e);continue}if(n.forceKeepAttr)continue;if(!n.keepAttr){k(m,e);continue}if(!Xe&&h(/\/>/i,E)){k(m,e);continue}U&&fe([Ee,_e,ge],lt=>{E=K(E,lt," ")});const st=p(e.nodeName);if(!it(st,W,E)){k(m,e);continue}if(g&&typeof ee=="object"&&typeof ee.getAttributeType=="function"&&!O)switch(ee.getAttributeType(st,W)){case"TrustedHTML":{E=g.createHTML(E);break}case"TrustedScriptURL":{E=g.createScriptURL(E);break}}if(E!==Ce)try{O?e.setAttributeNS(O,m,E):e.setAttribute(m,E),Ne(e)?N(e):ft(o.removed)}catch{k(m,e)}}M(A.afterSanitizeAttributes,e,null)},Bt=function i(e){let t=null;const n=tt(e);for(M(A.beforeSanitizeShadowDOM,e,null);t=n.nextNode();)M(A.uponSanitizeShadowNode,t,null),ot(t),rt(t),t.content instanceof D&&i(t.content);M(A.afterSanitizeShadowDOM,e,null)};return o.sanitize=function(i){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},t=null,n=null,f=null,_=null;if(Le=!i,Le&&(i="<!-->"),typeof i!="string"&&!nt(i))if(typeof i.toString=="function"){if(i=i.toString(),typeof i!="string")throw Z("dirty is not a string, aborting")}else throw Z("toString is not a function");if(!o.isSupported)return i;if(he||ye(e),o.removed=[],typeof i=="string"&&(V=!1),V){if(i.nodeName){const w=p(i.nodeName);if(!T[w]||j[w])throw Z("root node is forbidden and cannot be sanitized in-place")}}else if(i instanceof Te)t=et("<!---->"),n=t.ownerDocument.importNode(i,!0),n.nodeType===Q.element&&n.nodeName==="BODY"||n.nodeName==="HTML"?t=n:t.appendChild(n);else{if(!F&&!U&&!P&&i.indexOf("<")===-1)return g&&ae?g.createHTML(i):i;if(t=et(i),!t)return F?null:ae?X:""}t&&Se&&N(t.firstChild);const m=tt(V?i:t);for(;f=m.nextNode();)ot(f),rt(f),f.content instanceof D&&Bt(f.content);if(V)return i;if(F){if(ie)for(_=Ct.call(t.ownerDocument);t.firstChild;)_.appendChild(t.firstChild);else _=t;return(d.shadowroot||d.shadowrootmode)&&(_=Mt.call(a,_,!0)),_}let O=P?t.outerHTML:t.innerHTML;return P&&T["!doctype"]&&t.ownerDocument&&t.ownerDocument.doctype&&t.ownerDocument.doctype.name&&h(At,t.ownerDocument.doctype.name)&&(O="<!DOCTYPE "+t.ownerDocument.doctype.name+`>
|
| 2 |
+
`+O),U&&fe([Ee,_e,ge],w=>{O=K(O,w," ")}),g&&ae?g.createHTML(O):O},o.setConfig=function(){let i=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};ye(i),he=!0},o.clearConfig=function(){G=null,he=!1},o.isValidAttribute=function(i,e,t){G||ye({});const n=p(i),f=p(e);return it(n,f,t)},o.addHook=function(i,e){typeof e=="function"&&q(A[i],e)},o.removeHook=function(i,e){if(e!==void 0){const t=Vt(A[i],e);return t===-1?void 0:$t(A[i],t,1)[0]}return ft(A[i])},o.removeHooks=function(i){A[i]=[]},o.removeAllHooks=function(){A=Et()},o}var mn=ht();export{mn as default};
|
static/index.html
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="ar" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
|
| 6 |
+
<!-- Primary Meta Tags -->
|
| 7 |
+
<title>KW Multi AI Agent</title>
|
| 8 |
+
<meta name="title" content="KW Multi AI Agent — Kuwaiti Multi-Agent AI System" />
|
| 9 |
+
<meta
|
| 10 |
+
name="description"
|
| 11 |
+
content="منظومة وكلاء ذكاء اصطناعي من الكويت — محادثة بلهجة كويتية، ربط تليجرام وواتساب، وتخصيص كامل. KW Multi AI Agent: a Kuwait-made multi-agent AI system with Kuwaiti dialect, Telegram & WhatsApp integration, and full customization."
|
| 12 |
+
/>
|
| 13 |
+
<meta
|
| 14 |
+
name="keywords"
|
| 15 |
+
content="الكويت, ذكاء اصطناعي, AI, Kuwait, ChatGPT, Llama, وكلاء ذكاء, Claude, GPT, مساعد ذكي, تليجرام, واتساب"
|
| 16 |
+
/>
|
| 17 |
+
<meta name="author" content="Naif Al-Zanki" />
|
| 18 |
+
<meta name="robots" content="index, follow" />
|
| 19 |
+
<meta name="language" content="Arabic" />
|
| 20 |
+
<meta name="geo.region" content="KW" />
|
| 21 |
+
<meta name="geo.placename" content="Kuwait City" />
|
| 22 |
+
|
| 23 |
+
<!-- Theme & mobile UX -->
|
| 24 |
+
<meta name="theme-color" content="#030508" media="(prefers-color-scheme: dark)" />
|
| 25 |
+
<meta name="theme-color" content="#030508" media="(prefers-color-scheme: light)" />
|
| 26 |
+
<meta
|
| 27 |
+
name="viewport"
|
| 28 |
+
content="width=device-width, initial-scale=1.0, maximum-scale=5.0, viewport-fit=cover"
|
| 29 |
+
/>
|
| 30 |
+
<meta name="format-detection" content="telephone=no" />
|
| 31 |
+
<meta name="mobile-web-app-capable" content="yes" />
|
| 32 |
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
| 33 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
| 34 |
+
<meta name="apple-mobile-web-app-title" content="KW AI" />
|
| 35 |
+
<meta name="application-name" content="KW Multi AI Agent" />
|
| 36 |
+
<meta name="msapplication-TileColor" content="#030508" />
|
| 37 |
+
|
| 38 |
+
<!-- Open Graph / Facebook / Telegram / WhatsApp preview -->
|
| 39 |
+
<meta property="og:type" content="website" />
|
| 40 |
+
<meta property="og:site_name" content="KW Multi AI Agent" />
|
| 41 |
+
<meta property="og:url" content="https://NaifAlzanki-kw-multi-agent-backend.hf.space/" />
|
| 42 |
+
<meta property="og:title" content="KW Multi AI Agent — منظومة ذكاء اصطناعي كويتية" />
|
| 43 |
+
<meta
|
| 44 |
+
property="og:description"
|
| 45 |
+
content="من الكويت — وكلاء ذكاء يقلبون الساحة. لهجة كويتية، تكامل تليجرام وواتساب، سريع وأنيق."
|
| 46 |
+
/>
|
| 47 |
+
<meta property="og:locale" content="ar_KW" />
|
| 48 |
+
<meta property="og:locale:alternate" content="en_US" />
|
| 49 |
+
|
| 50 |
+
<!-- Twitter / X -->
|
| 51 |
+
<meta name="twitter:card" content="summary_large_image" />
|
| 52 |
+
<meta name="twitter:title" content="KW Multi AI Agent" />
|
| 53 |
+
<meta
|
| 54 |
+
name="twitter:description"
|
| 55 |
+
content="Kuwait-made multi-agent AI system with Kuwaiti dialect, Telegram & WhatsApp integration."
|
| 56 |
+
/>
|
| 57 |
+
|
| 58 |
+
<!-- Icons & PWA -->
|
| 59 |
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
| 60 |
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
| 61 |
+
<link rel="manifest" href="/manifest.json" />
|
| 62 |
+
|
| 63 |
+
<!-- Preconnect for perf -->
|
| 64 |
+
<link rel="preconnect" href="https://api.telegram.org" crossorigin />
|
| 65 |
+
<link rel="dns-prefetch" href="https://wa.me" />
|
| 66 |
+
|
| 67 |
+
<!-- Structured data for SEO -->
|
| 68 |
+
<script type="application/ld+json">
|
| 69 |
+
{
|
| 70 |
+
"@context": "https://schema.org",
|
| 71 |
+
"@type": "SoftwareApplication",
|
| 72 |
+
"name": "KW Multi AI Agent",
|
| 73 |
+
"applicationCategory": "BusinessApplication",
|
| 74 |
+
"operatingSystem": "Web, Android, iOS",
|
| 75 |
+
"inLanguage": ["ar-KW", "en"],
|
| 76 |
+
"offers": {
|
| 77 |
+
"@type": "Offer",
|
| 78 |
+
"price": "0",
|
| 79 |
+
"priceCurrency": "KWD"
|
| 80 |
+
},
|
| 81 |
+
"creator": {
|
| 82 |
+
"@type": "Person",
|
| 83 |
+
"name": "Naif Al-Zanki"
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
</script>
|
| 87 |
+
|
| 88 |
+
<!-- Prevent FOUC flash before React loads -->
|
| 89 |
+
<style>
|
| 90 |
+
html, body { background: #030508; color: #fafafa; margin: 0; padding: 0; }
|
| 91 |
+
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
| 92 |
+
#root { min-height: 100dvh; }
|
| 93 |
+
/* Boot spinner until React mounts */
|
| 94 |
+
.kw-boot {
|
| 95 |
+
position: fixed; inset: 0; display: flex; align-items: center;
|
| 96 |
+
justify-content: center; background: #030508; z-index: 9999;
|
| 97 |
+
transition: opacity 0.4s ease;
|
| 98 |
+
}
|
| 99 |
+
.kw-boot-spinner {
|
| 100 |
+
width: 48px; height: 48px; border-radius: 50%;
|
| 101 |
+
border: 3px solid rgba(255,255,255,0.08);
|
| 102 |
+
border-top-color: #CE1126;
|
| 103 |
+
animation: kw-boot-spin 0.9s linear infinite;
|
| 104 |
+
}
|
| 105 |
+
@keyframes kw-boot-spin { to { transform: rotate(360deg); } }
|
| 106 |
+
#root:not(:empty) + .kw-boot { opacity: 0; pointer-events: none; }
|
| 107 |
+
</style>
|
| 108 |
+
<script type="module" crossorigin src="/assets/index-D3HV3XVM.js"></script>
|
| 109 |
+
<link rel="stylesheet" crossorigin href="/assets/index-BwPR7vCI.css">
|
| 110 |
+
</head>
|
| 111 |
+
<body>
|
| 112 |
+
<div id="root"></div>
|
| 113 |
+
<div class="kw-boot" aria-hidden="true">
|
| 114 |
+
<div class="kw-boot-spinner"></div>
|
| 115 |
+
</div>
|
| 116 |
+
</body>
|
| 117 |
+
</html>
|
static/manifest.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "🇰🇼 Kw Multi AI Agent 🇰🇼",
|
| 3 |
+
"short_name": "Kw Multi AI Agent",
|
| 4 |
+
"start_url": "/",
|
| 5 |
+
"display": "standalone",
|
| 6 |
+
"background_color": "#0F0F0F",
|
| 7 |
+
"theme_color": "#DC2626",
|
| 8 |
+
"icons": [
|
| 9 |
+
{
|
| 10 |
+
"src": "/mascot.png",
|
| 11 |
+
"sizes": "192x192",
|
| 12 |
+
"type": "image/png"
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"src": "/mascot.png",
|
| 16 |
+
"sizes": "512x512",
|
| 17 |
+
"type": "image/png"
|
| 18 |
+
}
|
| 19 |
+
]
|
| 20 |
+
}
|