Add LLM endpoints, web frontend, and rate limiting config
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 9s
Build and Deploy / build-api (push) Successful in 33s
Build and Deploy / build-web (push) Failing after 41s

- Added OpenAI-compatible LLM endpoints to API backend - Introduced web
frontend with Jinja2 templates and static assets - Implemented API proxy
routes in web service - Added sample db.json data for items, users,
orders, reviews, categories, llm_requests - Updated ADC and Helm configs
for separate AI and standard rate limiting - Upgraded FastAPI, Uvicorn,
and added httpx, Jinja2, python-multipart dependencies - Added API
configuration modal and client-side JS for web app
This commit is contained in:
d.viti
2025-10-07 17:29:12 +02:00
parent 78baa5ad21
commit ed660dce5a
16 changed files with 1551 additions and 138 deletions

12
web/.env.example Normal file
View File

@@ -0,0 +1,12 @@
# API Backend Configuration
# Set this to the base URL where the API service is running
# Local development
API_BASE_URL=http://localhost:8001
# Production
# API_BASE_URL=https://commandware.it/api
# Other examples
# API_BASE_URL=http://api:8001
# API_BASE_URL=http://192.168.1.100:8001

View File

@@ -1,137 +1,133 @@
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import uvicorn
import os
import subprocess
import httpx
app = FastAPI(title="Web Demo Application")
# Build MkDocs documentation on startup
def build_docs():
docs_dir = os.path.join(os.path.dirname(__file__), "docs")
site_dir = os.path.join(os.path.dirname(__file__), "site")
# Get the directory where this script is located
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
if os.path.exists(docs_dir):
# API Configuration - can be set via environment variable
API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8001")
# Mount static files
static_dir = os.path.join(BASE_DIR, "static")
if os.path.exists(static_dir):
app.mount("/static", StaticFiles(directory=static_dir), name="static")
# Setup templates
templates_dir = os.path.join(BASE_DIR, "templates")
templates = Jinja2Templates(directory=templates_dir)
# HTTP client for API calls
async def api_request(method: str, endpoint: str, **kwargs):
"""Make a request to the API backend"""
url = f"{API_BASE_URL}{endpoint}"
async with httpx.AsyncClient(timeout=30.0) as client:
try:
subprocess.run(
["mkdocs", "build", "-f", os.path.join(docs_dir, "mkdocs.yml"), "-d", site_dir],
check=True,
capture_output=True
)
print(f"✓ Documentation built successfully at {site_dir}")
return True
except subprocess.CalledProcessError as e:
print(f"✗ Failed to build documentation: {e.stderr.decode()}")
return False
except FileNotFoundError:
print("✗ MkDocs not installed. Install with: pip install mkdocs mkdocs-material")
return False
return False
# Build docs on startup
@app.on_event("startup")
async def startup_event():
build_docs()
# Mount static documentation site at /docs
site_dir = os.path.join(os.path.dirname(__file__), "site")
if os.path.exists(site_dir):
app.mount("/docs", StaticFiles(directory=site_dir, html=True), name="docs")
# Simple HTML template inline
HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<title>Web Demo</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #333;
border-bottom: 2px solid #4CAF50;
padding-bottom: 10px;
}
.info-box {
background-color: #e8f5e9;
padding: 15px;
margin: 20px 0;
border-left: 4px solid #4CAF50;
border-radius: 4px;
}
.metric {
display: inline-block;
margin: 10px 20px 10px 0;
padding: 10px 20px;
background-color: #2196F3;
color: white;
border-radius: 4px;
}
.doc-link {
display: inline-block;
margin: 20px 10px 0 0;
padding: 12px 24px;
background-color: #673AB7;
color: white;
text-decoration: none;
border-radius: 4px;
transition: background-color 0.3s;
}
.doc-link:hover {
background-color: #512DA8;
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to Web Demo Application</h1>
<div class="info-box">
<h2>Application Information</h2>
<p><strong>Service:</strong> Web Frontend</p>
<p><strong>Status:</strong> ✓ Running</p>
<p><strong>Version:</strong> 1.0.0</p>
</div>
<h2>Metrics Dashboard</h2>
<div>
<span class="metric">Requests: 1,234</span>
<span class="metric">Uptime: 99.9%</span>
<span class="metric">Users: 567</span>
</div>
<div class="info-box">
<h3>About</h3>
<p>This is a demo FastAPI web application serving HTML content.
It demonstrates a simple web interface with metrics and information display.</p>
</div>
<div>
<a href="/docs/" class="doc-link">📚 View Documentation</a>
<a href="/health" class="doc-link">🏥 Health Check</a>
</div>
</div>
</body>
</html>
"""
response = await client.request(method, url, **kwargs)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
raise HTTPException(status_code=e.response.status_code, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"API request failed: {str(e)}")
# ===== ROUTES - HTML Pages =====
@app.get("/", response_class=HTMLResponse)
async def root():
"""Serve the main webpage"""
return HTML_TEMPLATE
async def home(request: Request):
"""Serve the home page"""
return templates.TemplateResponse("index.html", {"request": request})
@app.get("/items", response_class=HTMLResponse)
async def items_page(request: Request):
"""Serve the items page"""
return templates.TemplateResponse("items.html", {"request": request})
@app.get("/users", response_class=HTMLResponse)
async def users_page(request: Request):
"""Serve the users page"""
return templates.TemplateResponse("users.html", {"request": request})
@app.get("/llm", response_class=HTMLResponse)
async def llm_page(request: Request):
"""Serve the LLM chat page"""
return templates.TemplateResponse("llm.html", {"request": request})
# ===== API PROXY ENDPOINTS =====
@app.get("/api/items")
async def proxy_get_items():
"""Proxy GET /items to API backend"""
return await api_request("GET", "/items")
@app.get("/api/items/{item_id}")
async def proxy_get_item(item_id: int):
"""Proxy GET /items/{id} to API backend"""
return await api_request("GET", f"/items/{item_id}")
@app.get("/api/users")
async def proxy_get_users():
"""Proxy GET /users to API backend"""
return await api_request("GET", "/users")
@app.get("/api/users/{user_id}")
async def proxy_get_user(user_id: int):
"""Proxy GET /users/{id} to API backend"""
return await api_request("GET", f"/users/{user_id}")
@app.post("/api/llm/chat")
async def proxy_llm_chat(request: Request):
"""Proxy POST /llm/chat to API backend"""
body = await request.json()
return await api_request("POST", "/llm/chat", json=body)
@app.get("/api/llm/models")
async def proxy_llm_models():
"""Proxy GET /llm/models to API backend"""
return await api_request("GET", "/llm/models")
@app.get("/api/llm/health")
async def proxy_llm_health():
"""Proxy GET /llm/health to API backend"""
return await api_request("GET", "/llm/health")
# ===== WEB HEALTH CHECK =====
@app.get("/health")
async def health():
"""Health check endpoint"""
return {"status": "healthy", "service": "web"}
# Try to connect to API backend
api_status = "unknown"
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(f"{API_BASE_URL}/health")
if response.status_code == 200:
api_status = "healthy"
else:
api_status = "unhealthy"
except:
api_status = "unreachable"
return {
"status": "healthy",
"service": "web",
"version": "1.0.0",
"api_backend": API_BASE_URL,
"api_status": api_status
}
# ===== CONFIG ENDPOINT =====
@app.get("/api/config")
async def get_config():
"""Get current API configuration"""
return {
"api_base_url": API_BASE_URL
}
if __name__ == "__main__":
print(f"Starting Web service")
print(f"API Backend: {API_BASE_URL}")
uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@@ -1,4 +1,5 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
mkdocs==1.5.3
mkdocs-material==9.5.3
fastapi==0.109.0
uvicorn==0.27.0
jinja2==3.1.3
python-multipart==0.0.6
httpx==0.26.0

503
web/static/css/style.css Normal file
View File

@@ -0,0 +1,503 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* Navbar */
.navbar {
background-color: #2c3e50;
color: white;
padding: 1rem 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.nav-brand h2 {
display: inline-block;
margin: 0;
}
.nav-menu {
list-style: none;
display: inline-block;
float: right;
}
.nav-menu li {
display: inline-block;
margin-left: 30px;
}
.nav-menu a {
color: white;
text-decoration: none;
transition: color 0.3s;
}
.nav-menu a:hover {
color: #3498db;
}
/* Main content */
main {
min-height: calc(100vh - 200px);
padding: 40px 20px;
}
/* Hero section */
.hero {
text-align: center;
padding: 60px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
margin-bottom: 40px;
}
.hero h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
}
/* Cards grid */
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.card {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
text-align: center;
transition:
transform 0.3s,
box-shadow 0.3s;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.card-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.card h3 {
margin-bottom: 1rem;
color: #2c3e50;
}
/* Buttons */
.btn {
display: inline-block;
padding: 10px 20px;
background-color: #3498db;
color: white;
text-decoration: none;
border-radius: 5px;
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #2980b9;
}
.btn-primary {
background-color: #667eea;
}
.btn-primary:hover {
background-color: #5568d3;
}
.btn-sm {
padding: 5px 15px;
font-size: 0.9rem;
}
/* Info section */
.info-section {
background: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 40px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.features-list {
list-style: none;
padding-left: 0;
}
.features-list li {
padding: 10px 0;
font-size: 1.1rem;
}
/* Stats */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 40px;
}
.stat-box {
background: white;
padding: 30px;
border-radius: 10px;
text-align: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.stat-box h3 {
font-size: 2.5rem;
color: #667eea;
margin-bottom: 10px;
}
/* Items grid */
.items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.item-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.item-card.out-of-stock {
opacity: 0.6;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.item-description {
color: #666;
margin-bottom: 15px;
}
.item-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.price {
font-size: 1.5rem;
font-weight: bold;
color: #27ae60;
}
/* Table */
.table-container {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow-x: auto;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.data-table th {
background-color: #f8f9fa;
font-weight: 600;
}
/* Badges */
.badge {
padding: 5px 10px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
}
.badge-success {
background-color: #d4edda;
color: #155724;
}
.badge-danger {
background-color: #f8d7da;
color: #721c24;
}
/* Chat */
.chat-container {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 800px;
margin: 0 auto;
}
.chat-messages {
height: 400px;
overflow-y: auto;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
margin-bottom: 20px;
}
.user-message,
.assistant-message,
.system-message {
padding: 10px 15px;
margin-bottom: 10px;
border-radius: 10px;
max-width: 80%;
}
.user-message {
background-color: #667eea;
color: white;
margin-left: auto;
text-align: right;
}
.assistant-message {
background-color: white;
border: 1px solid #ddd;
}
.system-message {
background-color: #e3f2fd;
color: #1976d2;
text-align: center;
max-width: 100%;
font-size: 0.9rem;
}
.chat-input-container {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.chat-input-container textarea {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-family: inherit;
resize: vertical;
}
.chat-info {
text-align: center;
color: #666;
}
/* Page header */
.page-header {
margin-bottom: 30px;
}
.page-header h1 {
font-size: 2rem;
margin-bottom: 10px;
}
/* Footer */
.footer {
background-color: #2c3e50;
color: white;
text-align: center;
padding: 20px 0;
margin-top: 40px;
}
/* Modal */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
background-color: white;
margin: 5% auto;
padding: 0;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
max-width: 600px;
animation: modalFadeIn 0.3s;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
padding: 20px 30px;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
margin: 0;
color: #2c3e50;
}
.close {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.3s;
}
.close:hover,
.close:focus {
color: #000;
}
.modal-body {
padding: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.form-control {
width: 100%;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1rem;
font-family: inherit;
}
.form-control:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-hint {
display: block;
margin-top: 8px;
color: #666;
font-size: 0.9rem;
}
.form-hint code {
background-color: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: "Courier New", monospace;
font-size: 0.85rem;
}
.config-info {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
border-left: 4px solid #667eea;
}
.config-info strong {
color: #2c3e50;
}
.modal-actions {
margin-top: 30px;
display: flex;
gap: 10px;
justify-content: flex-end;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
/* Utility */
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.error {
color: #f44336;
text-align: center;
padding: 20px;
}

123
web/static/js/app.js Normal file
View File

@@ -0,0 +1,123 @@
// Global API configuration
const DEFAULT_API_BASE = "/api";
const API_BASE_KEY = "api_base_url";
// Get API base URL from localStorage or use default
function getApiBaseUrl() {
return localStorage.getItem(API_BASE_KEY) || DEFAULT_API_BASE;
}
// Set API base URL
function setApiBaseUrl(url) {
localStorage.setItem(API_BASE_KEY, url);
}
// Export for global access
window.API_BASE = getApiBaseUrl();
// API Configuration Modal Functions
function openApiConfig(event) {
if (event) event.preventDefault();
const modal = document.getElementById("api-config-modal");
const input = document.getElementById("api-base-url");
const currentUrl = document.getElementById("current-api-url");
input.value = getApiBaseUrl();
currentUrl.textContent = getApiBaseUrl();
modal.style.display = "block";
}
function closeApiConfig() {
const modal = document.getElementById("api-config-modal");
modal.style.display = "none";
}
function saveApiConfig() {
const input = document.getElementById("api-base-url");
const url = input.value.trim();
if (!url) {
alert("Please enter a valid API base URL");
return;
}
// Remove trailing slash if present
const cleanUrl = url.endsWith("/") ? url.slice(0, -1) : url;
setApiBaseUrl(cleanUrl);
window.API_BASE = cleanUrl;
showNotification("API configuration saved. Reloading page...", "success");
setTimeout(() => {
window.location.reload();
}, 1000);
}
function resetApiConfig() {
if (confirm("Reset API configuration to default (/api)?")) {
setApiBaseUrl(DEFAULT_API_BASE);
window.API_BASE = DEFAULT_API_BASE;
showNotification("API configuration reset. Reloading page...", "success");
setTimeout(() => {
window.location.reload();
}, 1000);
}
}
// Close modal when clicking outside
window.onclick = function (event) {
const modal = document.getElementById("api-config-modal");
if (event.target === modal) {
closeApiConfig();
}
};
// Utility functions
function showNotification(message, type = "info") {
console.log(`[${type.toUpperCase()}] ${message}`);
// Could be extended with toast notifications
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString() + " " + date.toLocaleTimeString();
}
function formatPrice(price) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(price);
}
// API call wrapper
async function apiCall(endpoint, options = {}) {
try {
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
showNotification(error.message, "error");
throw error;
}
}
// Export for use in templates
window.apiCall = apiCall;
window.showNotification = showNotification;
window.formatDate = formatDate;
window.formatPrice = formatPrice;

95
web/templates/base.html Normal file
View File

@@ -0,0 +1,95 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}API Demo{% endblock %}</title>
<link rel="stylesheet" href="/static/css/style.css" />
</head>
<body>
<nav class="navbar">
<div class="container">
<div class="nav-brand">
<h2>🚀 API7EE Demo</h2>
</div>
<ul class="nav-menu">
<li><a href="/">Home</a></li>
<li><a href="/items">Items</a></li>
<li><a href="/users">Users</a></li>
<li><a href="/llm">LLM Chat</a></li>
<li><a href="/api/docs" target="_blank">API Docs</a></li>
<li>
<a href="#" onclick="openApiConfig(event)"
>⚙️ API Config</a
>
</li>
</ul>
</div>
</nav>
<!-- API Configuration Modal -->
<div id="api-config-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>⚙️ API Configuration</h2>
<span class="close" onclick="closeApiConfig()"
>&times;</span
>
</div>
<div class="modal-body">
<p>Configure the base URL for API requests:</p>
<div class="form-group">
<label for="api-base-url">API Base URL:</label>
<input
type="text"
id="api-base-url"
placeholder="https://commandware.it/api"
class="form-control"
/>
<small class="form-hint">
Examples:
<code>/api</code> (relative),
<code>https://commandware.it/api</code> (absolute),
<code>http://localhost:8001</code> (local)
</small>
</div>
<div class="form-group">
<label>Current Configuration:</label>
<div class="config-info">
<strong>Base URL:</strong>
<span id="current-api-url">-</span>
</div>
</div>
<div class="modal-actions">
<button
class="btn btn-primary"
onclick="saveApiConfig()"
>
Save Configuration
</button>
<button
class="btn btn-secondary"
onclick="resetApiConfig()"
>
Reset to Default
</button>
<button class="btn" onclick="closeApiConfig()">
Cancel
</button>
</div>
</div>
</div>
</div>
<main class="container">{% block content %}{% endblock %}</main>
<footer class="footer">
<div class="container">
<p>&copy; 2025 API7EE Demo | Powered by FastAPI & API7</p>
</div>
</footer>
<script src="/static/js/app.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

86
web/templates/index.html Normal file
View File

@@ -0,0 +1,86 @@
{% extends "base.html" %}
{% block title %}Home - API Demo{% endblock %}
{% block content %}
<div class="hero">
<h1>Welcome to API7EE Demo Platform</h1>
<p class="subtitle">Explore our API services with real-time data and AI-powered features</p>
</div>
<div class="cards-grid">
<div class="card">
<div class="card-icon">📦</div>
<h3>Items Management</h3>
<p>Browse and manage products in our catalog</p>
<a href="/items" class="btn btn-primary">View Items</a>
</div>
<div class="card">
<div class="card-icon">👥</div>
<h3>Users</h3>
<p>Manage user accounts and profiles</p>
<a href="/users" class="btn btn-primary">View Users</a>
</div>
<div class="card">
<div class="card-icon">🤖</div>
<h3>AI Chat (LLM)</h3>
<p>Chat with our videogame expert AI assistant</p>
<a href="/llm" class="btn btn-primary">Start Chat</a>
</div>
<div class="card">
<div class="card-icon">📚</div>
<h3>API Documentation</h3>
<p>Explore our OpenAPI/Swagger documentation</p>
<a href="/api/docs" target="_blank" class="btn btn-primary">Open Docs</a>
</div>
</div>
<div class="info-section">
<h2>Features</h2>
<ul class="features-list">
<li>✅ RESTful API with FastAPI</li>
<li>✅ AI Rate Limiting (100 tokens/60s for LLM)</li>
<li>✅ Standard Rate Limiting (100 req/60s per IP)</li>
<li>✅ OpenAI-compatible LLM endpoint</li>
<li>✅ Real-time data management</li>
<li>✅ Swagger/OpenAPI documentation</li>
</ul>
</div>
<div class="stats">
<div class="stat-box">
<h3 id="items-count">-</h3>
<p>Total Items</p>
</div>
<div class="stat-box">
<h3 id="users-count">-</h3>
<p>Active Users</p>
</div>
<div class="stat-box">
<h3>AI Ready</h3>
<p>LLM Service</p>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Fetch stats from API
fetch('/api/items')
.then(res => res.json())
.then(data => {
document.getElementById('items-count').textContent = data.length;
})
.catch(err => console.error('Error fetching items:', err));
fetch('/api/users')
.then(res => res.json())
.then(data => {
document.getElementById('users-count').textContent = data.length;
})
.catch(err => console.error('Error fetching users:', err));
</script>
{% endblock %}

55
web/templates/items.html Normal file
View File

@@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block title %}Items - API Demo{% endblock %}
{% block content %}
<div class="page-header">
<h1>📦 Items Catalog</h1>
<p>Browse all available products</p>
</div>
<div id="items-container" class="items-grid">
<div class="loading">Loading items...</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const API_BASE = '/api';
async function loadItems() {
try {
const response = await fetch(`${API_BASE}/items`);
const items = await response.json();
const container = document.getElementById('items-container');
container.innerHTML = items.map(item => `
<div class="item-card ${!item.in_stock ? 'out-of-stock' : ''}">
<div class="item-header">
<h3>${item.name}</h3>
<span class="badge ${item.in_stock ? 'badge-success' : 'badge-danger'}">
${item.in_stock ? 'In Stock' : 'Out of Stock'}
</span>
</div>
<p class="item-description">${item.description || 'No description'}</p>
<div class="item-footer">
<span class="price">$${item.price.toFixed(2)}</span>
<button class="btn btn-sm" onclick="viewItem(${item.id})">View Details</button>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading items:', error);
document.getElementById('items-container').innerHTML =
'<div class="error">Failed to load items. Please try again.</div>';
}
}
function viewItem(id) {
alert(`View item details for ID: ${id}\n\nAPI Endpoint: /api/items/${id}`);
}
// Load items on page load
loadItems();
</script>
{% endblock %}

135
web/templates/llm.html Normal file
View File

@@ -0,0 +1,135 @@
{% extends "base.html" %} {% block title %}LLM Chat - API Demo{% endblock %} {%
block content %}
<div class="page-header">
<h1>🤖 AI Chat - Videogame Expert</h1>
<p>Chat with our AI assistant (Rate limited: 100 tokens/60s)</p>
</div>
<div class="chat-container">
<div class="chat-messages" id="chat-messages">
<div class="system-message">
Welcome! Ask me anything about videogames. I'm powered by the
videogame-expert model.
</div>
</div>
<div class="chat-input-container">
<textarea
id="chat-input"
placeholder="Type your message here..."
rows="3"
></textarea>
<button id="send-btn" class="btn btn-primary" onclick="sendMessage()">
Send Message
</button>
</div>
<div class="chat-info">
<small>
Model: <strong>videogame-expert</strong> | Status:
<span id="status">Ready</span> | Rate Limit:
<strong>100 tokens/60s</strong>
</small>
</div>
</div>
{% endblock %} {% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
<script>
const API_BASE = "/api";
let isProcessing = false;
function addMessage(content, isUser = false) {
const messagesDiv = document.getElementById("chat-messages");
const messageDiv = document.createElement("div");
messageDiv.className = isUser ? "user-message" : "assistant-message";
if (isUser) {
messageDiv.textContent = content;
} else {
// Parse markdown for assistant messages
messageDiv.innerHTML = marked.parse(content);
}
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function setStatus(text, isError = false) {
const statusSpan = document.getElementById("status");
statusSpan.textContent = text;
statusSpan.style.color = isError ? "#f44336" : "#4CAF50";
}
async function sendMessage() {
if (isProcessing) return;
const input = document.getElementById("chat-input");
const prompt = input.value.trim();
if (!prompt) {
alert("Please enter a message");
return;
}
// Add user message
addMessage(prompt, true);
input.value = "";
// Set processing state
isProcessing = true;
document.getElementById("send-btn").disabled = true;
setStatus("Processing...");
try {
const response = await fetch(`${API_BASE}/llm/chat`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: prompt,
max_tokens: 150,
temperature: 0.7,
model: "videogame-expert",
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "API request failed");
}
const data = await response.json();
// Add assistant response
addMessage(data.response, false);
// Show tokens used
const tokensInfo = `Tokens used: ${data.tokens_used}`;
const infoDiv = document.createElement("div");
infoDiv.className = "system-message";
infoDiv.textContent = tokensInfo;
document.getElementById("chat-messages").appendChild(infoDiv);
setStatus("Ready");
} catch (error) {
console.error("Error:", error);
addMessage(`Error: ${error.message}`, false);
setStatus("Error", true);
} finally {
isProcessing = false;
document.getElementById("send-btn").disabled = false;
}
}
// Allow Enter to send (Shift+Enter for newline)
document
.getElementById("chat-input")
.addEventListener("keydown", function (e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
</script>
{% endblock %}

69
web/templates/users.html Normal file
View File

@@ -0,0 +1,69 @@
{% extends "base.html" %}
{% block title %}Users - API Demo{% endblock %}
{% block content %}
<div class="page-header">
<h1>👥 Users</h1>
<p>Manage user accounts</p>
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="users-table-body">
<tr>
<td colspan="5" class="loading">Loading users...</td>
</tr>
</tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
<script>
const API_BASE = '/api';
async function loadUsers() {
try {
const response = await fetch(`${API_BASE}/users`);
const users = await response.json();
const tbody = document.getElementById('users-table-body');
tbody.innerHTML = users.map(user => `
<tr>
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.email}</td>
<td>
<span class="badge ${user.active ? 'badge-success' : 'badge-danger'}">
${user.active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<button class="btn btn-sm" onclick="viewUser(${user.id})">View</button>
</td>
</tr>
`).join('');
} catch (error) {
console.error('Error loading users:', error);
document.getElementById('users-table-body').innerHTML =
'<tr><td colspan="5" class="error">Failed to load users</td></tr>';
}
}
function viewUser(id) {
alert(`View user details for ID: ${id}\n\nAPI Endpoint: /api/users/${id}`);
}
loadUsers();
</script>
{% endblock %}