Spaces:
Running
Running
feat: obscure admin routes (/qaz), split public/private docs, and refine dashboard UI
Browse files- admin_router.py +1 -1
- main.py +52 -13
- static/docs.html +114 -16
- static/{admin.html → qaz.html} +5 -5
- static/qazmlpdocs.html +0 -885
admin_router.py
CHANGED
|
@@ -6,7 +6,7 @@ import uuid
|
|
| 6 |
|
| 7 |
from db import get_supabase
|
| 8 |
|
| 9 |
-
router = APIRouter(prefix="/
|
| 10 |
|
| 11 |
# --- Models ---
|
| 12 |
|
|
|
|
| 6 |
|
| 7 |
from db import get_supabase
|
| 8 |
|
| 9 |
+
router = APIRouter(prefix="/qaz", tags=["Admin"])
|
| 10 |
|
| 11 |
# --- Models ---
|
| 12 |
|
main.py
CHANGED
|
@@ -116,30 +116,30 @@ app.include_router(admin_router)
|
|
| 116 |
@app.get("/qazmlp", include_in_schema=False)
|
| 117 |
async def admin_page():
|
| 118 |
"""Serve the Secret Admin Dashboard."""
|
| 119 |
-
return FileResponse("static/
|
| 120 |
|
| 121 |
|
| 122 |
-
@app.get("/
|
| 123 |
async def admin_stats():
|
| 124 |
"""Return raw model stats for dashboard."""
|
| 125 |
return JSONResponse(engine.get_stats())
|
| 126 |
|
| 127 |
|
| 128 |
-
@app.post("/
|
| 129 |
async def admin_test_all():
|
| 130 |
"""Trigger parallel testing of all models."""
|
| 131 |
results = await engine.test_all_models()
|
| 132 |
return JSONResponse(results)
|
| 133 |
|
| 134 |
|
| 135 |
-
@app.post("/
|
| 136 |
async def admin_clear_stats():
|
| 137 |
"""Clear all stats."""
|
| 138 |
engine.clear_stats()
|
| 139 |
return JSONResponse({"status": "cleared"})
|
| 140 |
|
| 141 |
|
| 142 |
-
@app.get("/
|
| 143 |
async def admin_debug_g4f():
|
| 144 |
"""
|
| 145 |
Verbose Debug for G4F Provider on Server.
|
|
@@ -203,18 +203,60 @@ async def admin_debug_g4f():
|
|
| 203 |
return HTMLResponse(f"<pre>{output}</pre>")
|
| 204 |
|
| 205 |
|
|
|
|
| 206 |
# ---------- Custom Swagger UI ----------
|
| 207 |
@app.get("/docs", include_in_schema=False)
|
| 208 |
-
async def
|
| 209 |
-
"""Serve
|
| 210 |
return get_swagger_ui_html(
|
| 211 |
-
openapi_url=
|
| 212 |
-
title=f"{API_TITLE} -
|
| 213 |
swagger_css_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css",
|
| 214 |
swagger_js_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
|
| 215 |
swagger_favicon_url="https://fastapi.tiangolo.com/img/favicon.png",
|
| 216 |
)
|
| 217 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
# ---------- Search Routes ----------
|
| 220 |
@app.post("/search")
|
|
@@ -274,10 +316,7 @@ async def deep_research_endpoint(request: Request):
|
|
| 274 |
# ---------- Routes ----------
|
| 275 |
|
| 276 |
|
| 277 |
-
|
| 278 |
-
async def qazmlp_docs():
|
| 279 |
-
"""Serve the Secured Dashboard."""
|
| 280 |
-
return FileResponse("static/qazmlpdocs.html")
|
| 281 |
|
| 282 |
@app.get("/docs/public", include_in_schema=False)
|
| 283 |
async def public_docs_page():
|
|
|
|
| 116 |
@app.get("/qazmlp", include_in_schema=False)
|
| 117 |
async def admin_page():
|
| 118 |
"""Serve the Secret Admin Dashboard."""
|
| 119 |
+
return FileResponse("static/qaz.html")
|
| 120 |
|
| 121 |
|
| 122 |
+
@app.get("/qaz/stats", include_in_schema=False)
|
| 123 |
async def admin_stats():
|
| 124 |
"""Return raw model stats for dashboard."""
|
| 125 |
return JSONResponse(engine.get_stats())
|
| 126 |
|
| 127 |
|
| 128 |
+
@app.post("/qaz/test_all", include_in_schema=False)
|
| 129 |
async def admin_test_all():
|
| 130 |
"""Trigger parallel testing of all models."""
|
| 131 |
results = await engine.test_all_models()
|
| 132 |
return JSONResponse(results)
|
| 133 |
|
| 134 |
|
| 135 |
+
@app.post("/qaz/clear_stats", include_in_schema=False)
|
| 136 |
async def admin_clear_stats():
|
| 137 |
"""Clear all stats."""
|
| 138 |
engine.clear_stats()
|
| 139 |
return JSONResponse({"status": "cleared"})
|
| 140 |
|
| 141 |
|
| 142 |
+
@app.get("/qaz/debug_g4f", include_in_schema=False)
|
| 143 |
async def admin_debug_g4f():
|
| 144 |
"""
|
| 145 |
Verbose Debug for G4F Provider on Server.
|
|
|
|
| 203 |
return HTMLResponse(f"<pre>{output}</pre>")
|
| 204 |
|
| 205 |
|
| 206 |
+
# ---------- Custom Swagger UI ----------
|
| 207 |
# ---------- Custom Swagger UI ----------
|
| 208 |
@app.get("/docs", include_in_schema=False)
|
| 209 |
+
async def public_swagger_ui():
|
| 210 |
+
"""Serve Public Swagger UI (No Admin)."""
|
| 211 |
return get_swagger_ui_html(
|
| 212 |
+
openapi_url="/openapi_public.json",
|
| 213 |
+
title=f"{API_TITLE} - Public Docs",
|
| 214 |
swagger_css_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css",
|
| 215 |
swagger_js_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
|
| 216 |
swagger_favicon_url="https://fastapi.tiangolo.com/img/favicon.png",
|
| 217 |
)
|
| 218 |
|
| 219 |
+
@app.get("/qazmlpdocs", include_in_schema=False)
|
| 220 |
+
async def admin_swagger_ui():
|
| 221 |
+
"""Serve Admin Swagger UI (Full)."""
|
| 222 |
+
return get_swagger_ui_html(
|
| 223 |
+
openapi_url=app.openapi_url, # Default includes Admin
|
| 224 |
+
title=f"{API_TITLE} - Admin Docs",
|
| 225 |
+
swagger_css_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css",
|
| 226 |
+
swagger_js_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
|
| 227 |
+
swagger_favicon_url="https://fastapi.tiangolo.com/img/favicon.png",
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
@app.get("/openapi_public.json", include_in_schema=False)
|
| 231 |
+
async def get_public_openapi():
|
| 232 |
+
"""Generate OpenAPI schema without Admin routes."""
|
| 233 |
+
if app.openapi_schema:
|
| 234 |
+
schema = app.openapi_schema.copy()
|
| 235 |
+
else:
|
| 236 |
+
schema = app.openapi()
|
| 237 |
+
|
| 238 |
+
# Deep copy to avoid modifying the cached schema
|
| 239 |
+
import copy
|
| 240 |
+
public_schema = copy.deepcopy(schema)
|
| 241 |
+
|
| 242 |
+
# Filter paths
|
| 243 |
+
paths_to_remove = []
|
| 244 |
+
for path, methods in public_schema.get("paths", {}).items():
|
| 245 |
+
# Check if any Method in this path has "Admin" tag
|
| 246 |
+
is_admin = False
|
| 247 |
+
for method, details in methods.items():
|
| 248 |
+
if "tags" in details and "Admin" in details["tags"]:
|
| 249 |
+
is_admin = True
|
| 250 |
+
break
|
| 251 |
+
|
| 252 |
+
if is_admin or path.startswith("/qaz") or path.startswith("/admin"):
|
| 253 |
+
paths_to_remove.append(path)
|
| 254 |
+
|
| 255 |
+
for p in paths_to_remove:
|
| 256 |
+
del public_schema["paths"][p]
|
| 257 |
+
|
| 258 |
+
return JSONResponse(public_schema)
|
| 259 |
+
|
| 260 |
|
| 261 |
# ---------- Search Routes ----------
|
| 262 |
@app.post("/search")
|
|
|
|
| 316 |
# ---------- Routes ----------
|
| 317 |
|
| 318 |
|
| 319 |
+
|
|
|
|
|
|
|
|
|
|
| 320 |
|
| 321 |
@app.get("/docs/public", include_in_schema=False)
|
| 322 |
async def public_docs_page():
|
static/docs.html
CHANGED
|
@@ -269,6 +269,19 @@
|
|
| 269 |
margin-bottom: 12px;
|
| 270 |
border-radius: var(--radius-sm);
|
| 271 |
border: 1px solid var(--border);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
}
|
| 273 |
|
| 274 |
.demo-btn {
|
|
@@ -432,7 +445,6 @@
|
|
| 432 |
<div class="demo-response visible" style="color: #a78bfa;">
|
| 433 |
curl https://kiwa001-kai-api-gateway.hf.space/v1/chat/completions \
|
| 434 |
-H "Content-Type: application/json" \
|
| 435 |
-
-H "Authorization: Bearer sk-kai-demo-public" \
|
| 436 |
-d '{"model": "gemini-3-flash", "messages": [{"role": "user", "content": "Hello!"}]}'
|
| 437 |
</div>
|
| 438 |
</div>
|
|
@@ -442,23 +454,13 @@
|
|
| 442 |
placeholder='{"message": "What is AI?"}'>What is the capital of France?</textarea>
|
| 443 |
|
| 444 |
<select id="chat-basic-model" class="demo-select" style="margin-top:10px;">
|
| 445 |
-
<option value="gemini-3-flash">
|
| 446 |
-
<option value="gpt-4o">gpt-4o</option>
|
| 447 |
-
<option value="gpt-4o-mini">gpt-4o-mini</option>
|
| 448 |
-
<option value="glm-4">glm-4</option>
|
| 449 |
-
<option value="mistral-large">mistral-large</option>
|
| 450 |
</select>
|
| 451 |
|
| 452 |
<button class="demo-btn" onclick="runDemo('chat-basic')">Run Request ▶</button>
|
| 453 |
<div id="chat-basic-status" class="demo-status"></div>
|
| 454 |
|
| 455 |
-
|
| 456 |
-
<option value="gemini-3-flash">gemini-3-flash (Default)</option>
|
| 457 |
-
<option value="gpt-4o">gpt-4o</option>
|
| 458 |
-
<option value="gpt-4o-mini">gpt-4o-mini</option>
|
| 459 |
-
<option value="glm-4">glm-4</option>
|
| 460 |
-
<option value="mistral-large">mistral-large</option>
|
| 461 |
-
</select>
|
| 462 |
|
| 463 |
<div id="chat-basic-res" class="demo-response"></div>
|
| 464 |
</div>
|
|
@@ -483,6 +485,11 @@
|
|
| 483 |
<div class="endpoint-docs">
|
| 484 |
<p style="color:var(--text-secondary); font-size:14px;">Returns a JSON list of all models supported
|
| 485 |
by the API, ranked by quality.</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
</div>
|
| 487 |
<div class="endpoint-demo">
|
| 488 |
<span class="demo-label">Try It Live</span>
|
|
@@ -517,6 +524,29 @@
|
|
| 517 |
</td>
|
| 518 |
</tr>
|
| 519 |
</table>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
</div>
|
| 521 |
<div class="endpoint-demo">
|
| 522 |
<span class="demo-label">Try It Live</span>
|
|
@@ -594,6 +624,75 @@
|
|
| 594 |
// Global flag for Pie Chart
|
| 595 |
window.pieChartRendered = false;
|
| 596 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
async function runDemo(type) {
|
| 598 |
const resBox = document.getElementById(type + '-res');
|
| 599 |
const statusBox = document.getElementById(type + '-status');
|
|
@@ -613,8 +712,7 @@
|
|
| 613 |
try {
|
| 614 |
let url, body, method = 'POST';
|
| 615 |
let headers = {
|
| 616 |
-
'Content-Type': 'application/json'
|
| 617 |
-
'Authorization': 'Bearer sk-kai-demo-public' // Use Demo Key for Dashboard
|
| 618 |
};
|
| 619 |
|
| 620 |
if (type === 'chat-basic') {
|
|
@@ -720,7 +818,7 @@
|
|
| 720 |
try {
|
| 721 |
const t = new Date().getTime();
|
| 722 |
const [statsRes, modelsRes] = await Promise.all([
|
| 723 |
-
fetch(`/
|
| 724 |
fetch(`/models?t=${t}`)
|
| 725 |
]);
|
| 726 |
|
|
|
|
| 269 |
margin-bottom: 12px;
|
| 270 |
border-radius: var(--radius-sm);
|
| 271 |
border: 1px solid var(--border);
|
| 272 |
+
appearance: none;
|
| 273 |
+
/* Remove default arrow to ensure styling applies */
|
| 274 |
+
-webkit-appearance: none;
|
| 275 |
+
background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%239898aa%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
|
| 276 |
+
background-repeat: no-repeat;
|
| 277 |
+
background-position: right 12px top 50%;
|
| 278 |
+
background-size: 12px auto;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.demo-select option {
|
| 282 |
+
background: var(--bg-card);
|
| 283 |
+
color: var(--text-primary);
|
| 284 |
+
padding: 10px;
|
| 285 |
}
|
| 286 |
|
| 287 |
.demo-btn {
|
|
|
|
| 445 |
<div class="demo-response visible" style="color: #a78bfa;">
|
| 446 |
curl https://kiwa001-kai-api-gateway.hf.space/v1/chat/completions \
|
| 447 |
-H "Content-Type: application/json" \
|
|
|
|
| 448 |
-d '{"model": "gemini-3-flash", "messages": [{"role": "user", "content": "Hello!"}]}'
|
| 449 |
</div>
|
| 450 |
</div>
|
|
|
|
| 454 |
placeholder='{"message": "What is AI?"}'>What is the capital of France?</textarea>
|
| 455 |
|
| 456 |
<select id="chat-basic-model" class="demo-select" style="margin-top:10px;">
|
| 457 |
+
<option value="gemini-3-flash" selected>Loading models...</option>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
</select>
|
| 459 |
|
| 460 |
<button class="demo-btn" onclick="runDemo('chat-basic')">Run Request ▶</button>
|
| 461 |
<div id="chat-basic-status" class="demo-status"></div>
|
| 462 |
|
| 463 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
|
| 465 |
<div id="chat-basic-res" class="demo-response"></div>
|
| 466 |
</div>
|
|
|
|
| 485 |
<div class="endpoint-docs">
|
| 486 |
<p style="color:var(--text-secondary); font-size:14px;">Returns a JSON list of all models supported
|
| 487 |
by the API, ranked by quality.</p>
|
| 488 |
+
<br>
|
| 489 |
+
<span class="demo-label">Example Request</span>
|
| 490 |
+
<div class="demo-response visible" style="color: #a78bfa;">
|
| 491 |
+
curl https://kiwa001-kai-api-gateway.hf.space/models
|
| 492 |
+
</div>
|
| 493 |
</div>
|
| 494 |
<div class="endpoint-demo">
|
| 495 |
<span class="demo-label">Try It Live</span>
|
|
|
|
| 524 |
</td>
|
| 525 |
</tr>
|
| 526 |
</table>
|
| 527 |
+
<br>
|
| 528 |
+
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;">
|
| 529 |
+
<span class="demo-label" style="margin-bottom:0;">Example Request</span>
|
| 530 |
+
<div style="display:flex; gap:8px;">
|
| 531 |
+
<button onclick="toggleSearchEx('simple')" id="btn-ex-simple"
|
| 532 |
+
style="padding:4px 8px; border-radius:4px; border:1px solid var(--accent); background:var(--accent); color:white; font-size:11px; cursor:pointer;">Simple</button>
|
| 533 |
+
<button onclick="toggleSearchEx('deep')" id="btn-ex-deep"
|
| 534 |
+
style="padding:4px 8px; border-radius:4px; border:1px solid var(--border); background:transparent; color:var(--text-muted); font-size:11px; cursor:pointer;">Deep
|
| 535 |
+
Research</button>
|
| 536 |
+
</div>
|
| 537 |
+
</div>
|
| 538 |
+
|
| 539 |
+
<div id="ex-code-simple" class="demo-response visible" style="color: #a78bfa;">
|
| 540 |
+
curl -X POST https://kiwa001-kai-api-gateway.hf.space/search \
|
| 541 |
+
-H "Content-Type: application/json" \
|
| 542 |
+
-d '{"query": "Machine Learning", "limit": 5}'
|
| 543 |
+
</div>
|
| 544 |
+
|
| 545 |
+
<div id="ex-code-deep" class="demo-response" style="color: #a78bfa; display:none;">
|
| 546 |
+
curl -X POST https://kiwa001-kai-api-gateway.hf.space/deep_research \
|
| 547 |
+
-H "Content-Type: application/json" \
|
| 548 |
+
-d '{"query": "Future of AI", "limit": 2}'
|
| 549 |
+
</div>
|
| 550 |
</div>
|
| 551 |
<div class="endpoint-demo">
|
| 552 |
<span class="demo-label">Try It Live</span>
|
|
|
|
| 624 |
// Global flag for Pie Chart
|
| 625 |
window.pieChartRendered = false;
|
| 626 |
|
| 627 |
+
// Auto-load models for dropdown
|
| 628 |
+
async function loadModelsDropdown() {
|
| 629 |
+
const select = document.getElementById('chat-basic-model');
|
| 630 |
+
if (!select) return;
|
| 631 |
+
|
| 632 |
+
try {
|
| 633 |
+
const res = await fetch('/models');
|
| 634 |
+
if (!res.ok) throw new Error("Failed to fetch models");
|
| 635 |
+
|
| 636 |
+
const data = await res.json();
|
| 637 |
+
const models = Array.isArray(data) ? data : (data.models || []);
|
| 638 |
+
|
| 639 |
+
if (models.length === 0) throw new Error("No models returned");
|
| 640 |
+
|
| 641 |
+
select.innerHTML = '';
|
| 642 |
+
models.forEach(m => {
|
| 643 |
+
const opt = document.createElement('option');
|
| 644 |
+
opt.value = m.id;
|
| 645 |
+
opt.textContent = m.id;
|
| 646 |
+
if (m.id === 'gemini-3-flash') opt.selected = true;
|
| 647 |
+
select.appendChild(opt);
|
| 648 |
+
});
|
| 649 |
+
} catch (e) {
|
| 650 |
+
console.error("Model load error", e);
|
| 651 |
+
// Fallback if empty or error
|
| 652 |
+
if (select.options.length <= 1) {
|
| 653 |
+
select.innerHTML = `
|
| 654 |
+
<option value="gemini-3-flash" selected>gemini-3-flash (Default)</option>
|
| 655 |
+
<option value="gpt-4o">gpt-4o</option>
|
| 656 |
+
<option value="gpt-4o-mini">gpt-4o-mini</option>
|
| 657 |
+
<option value="glm-4">glm-4</option>
|
| 658 |
+
<option value="mistral-large">mistral-large</option>
|
| 659 |
+
`;
|
| 660 |
+
}
|
| 661 |
+
}
|
| 662 |
+
}
|
| 663 |
+
document.addEventListener('DOMContentLoaded', loadModelsDropdown);
|
| 664 |
+
|
| 665 |
+
function toggleSearchEx(mode) {
|
| 666 |
+
const simpleBtn = document.getElementById('btn-ex-simple');
|
| 667 |
+
const deepBtn = document.getElementById('btn-ex-deep');
|
| 668 |
+
const simpleCode = document.getElementById('ex-code-simple');
|
| 669 |
+
const deepCode = document.getElementById('ex-code-deep');
|
| 670 |
+
|
| 671 |
+
if (mode === 'simple') {
|
| 672 |
+
simpleCode.style.display = 'block';
|
| 673 |
+
deepCode.style.display = 'none';
|
| 674 |
+
|
| 675 |
+
simpleBtn.style.background = 'var(--accent)';
|
| 676 |
+
simpleBtn.style.borderColor = 'var(--accent)';
|
| 677 |
+
simpleBtn.style.color = 'white';
|
| 678 |
+
|
| 679 |
+
deepBtn.style.background = 'transparent';
|
| 680 |
+
deepBtn.style.borderColor = 'var(--border)';
|
| 681 |
+
deepBtn.style.color = 'var(--text-muted)';
|
| 682 |
+
} else {
|
| 683 |
+
simpleCode.style.display = 'none';
|
| 684 |
+
deepCode.style.display = 'block';
|
| 685 |
+
|
| 686 |
+
deepBtn.style.background = 'var(--accent)';
|
| 687 |
+
deepBtn.style.borderColor = 'var(--accent)';
|
| 688 |
+
deepBtn.style.color = 'white';
|
| 689 |
+
|
| 690 |
+
simpleBtn.style.background = 'transparent';
|
| 691 |
+
simpleBtn.style.borderColor = 'var(--border)';
|
| 692 |
+
simpleBtn.style.color = 'var(--text-muted)';
|
| 693 |
+
}
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
async function runDemo(type) {
|
| 697 |
const resBox = document.getElementById(type + '-res');
|
| 698 |
const statusBox = document.getElementById(type + '-status');
|
|
|
|
| 712 |
try {
|
| 713 |
let url, body, method = 'POST';
|
| 714 |
let headers = {
|
| 715 |
+
'Content-Type': 'application/json'
|
|
|
|
| 716 |
};
|
| 717 |
|
| 718 |
if (type === 'chat-basic') {
|
|
|
|
| 818 |
try {
|
| 819 |
const t = new Date().getTime();
|
| 820 |
const [statsRes, modelsRes] = await Promise.all([
|
| 821 |
+
fetch(`/qaz/stats?t=${t}`),
|
| 822 |
fetch(`/models?t=${t}`)
|
| 823 |
]);
|
| 824 |
|
static/{admin.html → qaz.html}
RENAMED
|
@@ -268,7 +268,7 @@
|
|
| 268 |
|
| 269 |
try {
|
| 270 |
// Call backend
|
| 271 |
-
const res = await fetch('/
|
| 272 |
const results = await res.json();
|
| 273 |
|
| 274 |
console.log("Test Results:", results);
|
|
@@ -297,7 +297,7 @@
|
|
| 297 |
btn.disabled = true;
|
| 298 |
|
| 299 |
try {
|
| 300 |
-
await fetch('/
|
| 301 |
alert("Stats cleared successfully.");
|
| 302 |
} catch (e) {
|
| 303 |
alert("Clear failed: " + e.message);
|
|
@@ -308,7 +308,7 @@
|
|
| 308 |
}
|
| 309 |
async function loadKeys() {
|
| 310 |
try {
|
| 311 |
-
const res = await fetch('/
|
| 312 |
const keys = await res.json();
|
| 313 |
|
| 314 |
const tbody = document.getElementById('keys-list');
|
|
@@ -348,7 +348,7 @@
|
|
| 348 |
if (!name) return;
|
| 349 |
|
| 350 |
try {
|
| 351 |
-
const res = await fetch('/
|
| 352 |
method: 'POST',
|
| 353 |
headers: { 'Content-Type': 'application/json' },
|
| 354 |
body: JSON.stringify({ name: name, limit_tokens: 1000000 })
|
|
@@ -369,7 +369,7 @@
|
|
| 369 |
async function revokeKey(id) {
|
| 370 |
if (!confirm("Are you sure you want to delete this key? Access will be immediately revoked.")) return;
|
| 371 |
try {
|
| 372 |
-
await fetch(`/
|
| 373 |
loadKeys();
|
| 374 |
} catch (e) {
|
| 375 |
alert("Error: " + e.message);
|
|
|
|
| 268 |
|
| 269 |
try {
|
| 270 |
// Call backend
|
| 271 |
+
const res = await fetch('/qaz/test_all', { method: 'POST' });
|
| 272 |
const results = await res.json();
|
| 273 |
|
| 274 |
console.log("Test Results:", results);
|
|
|
|
| 297 |
btn.disabled = true;
|
| 298 |
|
| 299 |
try {
|
| 300 |
+
await fetch('/qaz/clear_stats', { method: 'POST' });
|
| 301 |
alert("Stats cleared successfully.");
|
| 302 |
} catch (e) {
|
| 303 |
alert("Clear failed: " + e.message);
|
|
|
|
| 308 |
}
|
| 309 |
async function loadKeys() {
|
| 310 |
try {
|
| 311 |
+
const res = await fetch('/qaz/keys');
|
| 312 |
const keys = await res.json();
|
| 313 |
|
| 314 |
const tbody = document.getElementById('keys-list');
|
|
|
|
| 348 |
if (!name) return;
|
| 349 |
|
| 350 |
try {
|
| 351 |
+
const res = await fetch('/qaz/keys', {
|
| 352 |
method: 'POST',
|
| 353 |
headers: { 'Content-Type': 'application/json' },
|
| 354 |
body: JSON.stringify({ name: name, limit_tokens: 1000000 })
|
|
|
|
| 369 |
async function revokeKey(id) {
|
| 370 |
if (!confirm("Are you sure you want to delete this key? Access will be immediately revoked.")) return;
|
| 371 |
try {
|
| 372 |
+
await fetch(`/qaz/keys/${id}`, { method: 'DELETE' });
|
| 373 |
loadKeys();
|
| 374 |
} catch (e) {
|
| 375 |
alert("Error: " + e.message);
|
static/qazmlpdocs.html
DELETED
|
@@ -1,885 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
|
| 4 |
-
<head>
|
| 5 |
-
<meta charset="UTF-8">
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
-
<title>K-AI API — Feel free to AI</title>
|
| 8 |
-
<meta name="description" content="K-AI API — Free AI proxy API. No signup, no API keys. Feel free to AI.">
|
| 9 |
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
-
<link
|
| 11 |
-
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
| 12 |
-
rel="stylesheet">
|
| 13 |
-
<!-- Chart.js -->
|
| 14 |
-
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 15 |
-
<style>
|
| 16 |
-
:root {
|
| 17 |
-
--bg-primary: #0a0a0f;
|
| 18 |
-
--bg-secondary: #12121a;
|
| 19 |
-
--bg-card: #16161f;
|
| 20 |
-
--bg-card-hover: #1c1c28;
|
| 21 |
-
--border: #2a2a3a;
|
| 22 |
-
--text-primary: #f0f0f5;
|
| 23 |
-
--text-secondary: #9898aa;
|
| 24 |
-
--text-muted: #6b6b80;
|
| 25 |
-
--accent: #6366f1;
|
| 26 |
-
--accent-hover: #818cf8;
|
| 27 |
-
--accent-glow: rgba(99, 102, 241, 0.15);
|
| 28 |
-
--gradient-hero: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%);
|
| 29 |
-
--radius-lg: 16px;
|
| 30 |
-
--radius-sm: 8px;
|
| 31 |
-
|
| 32 |
-
/* Ranking Colors */
|
| 33 |
-
--success: #22c55e;
|
| 34 |
-
--error: #ef4444;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
* {
|
| 38 |
-
margin: 0;
|
| 39 |
-
padding: 0;
|
| 40 |
-
box-sizing: border-box;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
body {
|
| 44 |
-
font-family: 'Inter', -apple-system, sans-serif;
|
| 45 |
-
background: var(--bg-primary);
|
| 46 |
-
color: var(--text-primary);
|
| 47 |
-
line-height: 1.6;
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
.container {
|
| 51 |
-
max-width: 1000px;
|
| 52 |
-
margin: 0 auto;
|
| 53 |
-
padding: 0 24px;
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
/* ─── Hero ─── */
|
| 57 |
-
.hero {
|
| 58 |
-
text-align: center;
|
| 59 |
-
padding: 100px 0 80px;
|
| 60 |
-
background: radial-gradient(circle at top center, rgba(99, 102, 241, 0.08) 0%, transparent 70%);
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
.hero h1 {
|
| 64 |
-
font-size: 72px;
|
| 65 |
-
font-weight: 900;
|
| 66 |
-
letter-spacing: -3px;
|
| 67 |
-
background: var(--gradient-hero);
|
| 68 |
-
-webkit-background-clip: text;
|
| 69 |
-
background-clip: text;
|
| 70 |
-
-webkit-text-fill-color: transparent;
|
| 71 |
-
margin-bottom: 8px;
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
.hero .motto {
|
| 75 |
-
font-size: 24px;
|
| 76 |
-
font-weight: 300;
|
| 77 |
-
letter-spacing: 2px;
|
| 78 |
-
color: var(--text-secondary);
|
| 79 |
-
text-transform: uppercase;
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
.hero .desc {
|
| 83 |
-
margin-top: 24px;
|
| 84 |
-
font-size: 18px;
|
| 85 |
-
color: var(--text-muted);
|
| 86 |
-
max-width: 600px;
|
| 87 |
-
margin-left: auto;
|
| 88 |
-
margin-right: auto;
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
/* ─── Section ─── */
|
| 92 |
-
section {
|
| 93 |
-
padding: 60px 0;
|
| 94 |
-
border-top: 1px solid var(--bg-secondary);
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
.section-title {
|
| 98 |
-
font-size: 28px;
|
| 99 |
-
font-weight: 700;
|
| 100 |
-
margin-bottom: 40px;
|
| 101 |
-
display: flex;
|
| 102 |
-
align-items: center;
|
| 103 |
-
gap: 12px;
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
.section-title .icon {
|
| 107 |
-
width: 32px;
|
| 108 |
-
height: 32px;
|
| 109 |
-
background: var(--bg-card);
|
| 110 |
-
border-radius: 8px;
|
| 111 |
-
display: flex;
|
| 112 |
-
align-items: center;
|
| 113 |
-
justify-content: center;
|
| 114 |
-
font-size: 16px;
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
/* ─── Endpoint Block ─── */
|
| 118 |
-
.endpoint-block {
|
| 119 |
-
background: var(--bg-card);
|
| 120 |
-
border: 1px solid var(--border);
|
| 121 |
-
border-radius: var(--radius-lg);
|
| 122 |
-
overflow: hidden;
|
| 123 |
-
margin-bottom: 40px;
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
.endpoint-header {
|
| 127 |
-
padding: 24px;
|
| 128 |
-
border-bottom: 1px solid var(--border);
|
| 129 |
-
display: flex;
|
| 130 |
-
justify-content: space-between;
|
| 131 |
-
align-items: flex-start;
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
.endpoint-title h3 {
|
| 135 |
-
font-size: 20px;
|
| 136 |
-
font-weight: 700;
|
| 137 |
-
margin-bottom: 4px;
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
.endpoint-title p {
|
| 141 |
-
color: var(--text-secondary);
|
| 142 |
-
font-size: 14px;
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
.method-badge {
|
| 146 |
-
font-family: 'JetBrains Mono', monospace;
|
| 147 |
-
font-size: 13px;
|
| 148 |
-
font-weight: 700;
|
| 149 |
-
padding: 4px 10px;
|
| 150 |
-
border-radius: 6px;
|
| 151 |
-
background: rgba(99, 102, 241, 0.2);
|
| 152 |
-
color: #818cf8;
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
.method-badge.GET {
|
| 156 |
-
background: rgba(34, 197, 94, 0.2);
|
| 157 |
-
color: #4ade80;
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
.method-badge.LIVE {
|
| 161 |
-
background: rgba(245, 158, 11, 0.2);
|
| 162 |
-
color: #fbbf24;
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
.endpoint-body {
|
| 166 |
-
display: grid;
|
| 167 |
-
grid-template-columns: 1.2fr 0.8fr;
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
.endpoint-body.full-width {
|
| 171 |
-
grid-template-columns: 1fr;
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
@media (max-width: 800px) {
|
| 175 |
-
|
| 176 |
-
.endpoint-body,
|
| 177 |
-
#analytics-body {
|
| 178 |
-
flex-direction: column !important;
|
| 179 |
-
display: flex !important;
|
| 180 |
-
}
|
| 181 |
-
}
|
| 182 |
-
|
| 183 |
-
.endpoint-docs {
|
| 184 |
-
padding: 24px;
|
| 185 |
-
border-right: 1px solid var(--border);
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
.endpoint-demo {
|
| 189 |
-
padding: 24px;
|
| 190 |
-
background: var(--bg-secondary);
|
| 191 |
-
}
|
| 192 |
-
|
| 193 |
-
.param-table {
|
| 194 |
-
width: 100%;
|
| 195 |
-
border-collapse: collapse;
|
| 196 |
-
font-size: 14px;
|
| 197 |
-
margin-top: 16px;
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
.param-table th {
|
| 201 |
-
text-align: left;
|
| 202 |
-
color: var(--text-muted);
|
| 203 |
-
font-weight: 600;
|
| 204 |
-
padding-bottom: 8px;
|
| 205 |
-
border-bottom: 1px solid var(--border);
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
.param-table td {
|
| 209 |
-
padding: 12px 0;
|
| 210 |
-
/* More padding for ranking */
|
| 211 |
-
border-bottom: 1px solid var(--border);
|
| 212 |
-
color: var(--text-secondary);
|
| 213 |
-
font-family: 'JetBrains Mono', monospace;
|
| 214 |
-
font-size: 13px;
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
.param-table tr:last-child td {
|
| 218 |
-
border-bottom: none;
|
| 219 |
-
}
|
| 220 |
-
|
| 221 |
-
.param-name {
|
| 222 |
-
font-family: 'JetBrains Mono', monospace;
|
| 223 |
-
color: var(--accent-hover);
|
| 224 |
-
}
|
| 225 |
-
|
| 226 |
-
.param-desc {
|
| 227 |
-
color: var(--text-secondary);
|
| 228 |
-
}
|
| 229 |
-
|
| 230 |
-
.param-req {
|
| 231 |
-
color: var(--text-muted);
|
| 232 |
-
font-size: 12px;
|
| 233 |
-
margin-left: 6px;
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
/* ─── Interactive Demo ─── */
|
| 237 |
-
.demo-label {
|
| 238 |
-
font-size: 11px;
|
| 239 |
-
font-weight: 700;
|
| 240 |
-
text-transform: uppercase;
|
| 241 |
-
letter-spacing: 1px;
|
| 242 |
-
color: var(--text-muted);
|
| 243 |
-
margin-bottom: 12px;
|
| 244 |
-
display: block;
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
.demo-input {
|
| 248 |
-
width: 100%;
|
| 249 |
-
background: var(--bg-input);
|
| 250 |
-
border: 1px solid var(--border);
|
| 251 |
-
border-radius: var(--radius-sm);
|
| 252 |
-
padding: 10px;
|
| 253 |
-
color: var(--text-primary);
|
| 254 |
-
font-family: 'JetBrains Mono', monospace;
|
| 255 |
-
font-size: 13px;
|
| 256 |
-
margin-bottom: 12px;
|
| 257 |
-
}
|
| 258 |
-
|
| 259 |
-
.demo-input:focus {
|
| 260 |
-
outline: none;
|
| 261 |
-
border-color: var(--accent);
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
-
.demo-select {
|
| 265 |
-
width: 100%;
|
| 266 |
-
background: var(--bg-card);
|
| 267 |
-
color: var(--text-primary);
|
| 268 |
-
padding: 10px;
|
| 269 |
-
margin-bottom: 12px;
|
| 270 |
-
border-radius: var(--radius-sm);
|
| 271 |
-
border: 1px solid var(--border);
|
| 272 |
-
}
|
| 273 |
-
|
| 274 |
-
.demo-btn {
|
| 275 |
-
width: 100%;
|
| 276 |
-
padding: 8px 12px;
|
| 277 |
-
/* Reduced padding */
|
| 278 |
-
background: var(--accent);
|
| 279 |
-
color: white;
|
| 280 |
-
border: none;
|
| 281 |
-
border-radius: var(--radius-sm);
|
| 282 |
-
font-weight: 600;
|
| 283 |
-
cursor: pointer;
|
| 284 |
-
transition: all 0.2s;
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
.demo-btn:hover {
|
| 288 |
-
background: var(--accent-hover);
|
| 289 |
-
}
|
| 290 |
-
|
| 291 |
-
.demo-btn:disabled {
|
| 292 |
-
opacity: 0.5;
|
| 293 |
-
cursor: wait;
|
| 294 |
-
}
|
| 295 |
-
|
| 296 |
-
.demo-response {
|
| 297 |
-
margin-top: 16px;
|
| 298 |
-
background: #0d0d14;
|
| 299 |
-
border: 1px solid var(--border);
|
| 300 |
-
border-radius: var(--radius-sm);
|
| 301 |
-
padding: 12px;
|
| 302 |
-
font-family: 'JetBrains Mono', monospace;
|
| 303 |
-
font-size: 11px;
|
| 304 |
-
/* Reduced font size */
|
| 305 |
-
color: var(--text-secondary);
|
| 306 |
-
white-space: pre-wrap;
|
| 307 |
-
display: none;
|
| 308 |
-
max-height: 500px;
|
| 309 |
-
/* Scrollable */
|
| 310 |
-
/* Scrollable */
|
| 311 |
-
overflow-y: auto;
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
-
.demo-response pre {
|
| 315 |
-
white-space: pre-wrap;
|
| 316 |
-
word-wrap: break-word;
|
| 317 |
-
margin: 0;
|
| 318 |
-
font-family: inherit;
|
| 319 |
-
}
|
| 320 |
-
|
| 321 |
-
.demo-status {
|
| 322 |
-
margin-top: 12px;
|
| 323 |
-
font-weight: 600;
|
| 324 |
-
font-size: 13px;
|
| 325 |
-
display: none;
|
| 326 |
-
}
|
| 327 |
-
|
| 328 |
-
.demo-response.visible {
|
| 329 |
-
display: block;
|
| 330 |
-
}
|
| 331 |
-
|
| 332 |
-
.demo-response.success {
|
| 333 |
-
border-color: rgba(34, 197, 94, 0.3);
|
| 334 |
-
color: #4ade80;
|
| 335 |
-
}
|
| 336 |
-
|
| 337 |
-
.demo-response.error {
|
| 338 |
-
border-color: rgba(239, 68, 68, 0.3);
|
| 339 |
-
color: #f87171;
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
/* ─── Ranking Badges & Styles ─── */
|
| 343 |
-
.rank-badge {
|
| 344 |
-
display: inline-block;
|
| 345 |
-
width: 24px;
|
| 346 |
-
height: 24px;
|
| 347 |
-
line-height: 24px;
|
| 348 |
-
text-align: center;
|
| 349 |
-
border-radius: 50%;
|
| 350 |
-
background: var(--border);
|
| 351 |
-
color: var(--text-muted);
|
| 352 |
-
font-weight: bold;
|
| 353 |
-
font-size: 11px;
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
tr:nth-child(1) .rank-badge {
|
| 357 |
-
background: #Eab308;
|
| 358 |
-
color: #000;
|
| 359 |
-
}
|
| 360 |
-
|
| 361 |
-
/* Gold */
|
| 362 |
-
tr:nth-child(2) .rank-badge {
|
| 363 |
-
background: #94a3b8;
|
| 364 |
-
color: #000;
|
| 365 |
-
}
|
| 366 |
-
|
| 367 |
-
/* Silver */
|
| 368 |
-
tr:nth-child(3) .rank-badge {
|
| 369 |
-
background: #b45309;
|
| 370 |
-
color: #fff;
|
| 371 |
-
}
|
| 372 |
-
|
| 373 |
-
/* Bronze */
|
| 374 |
-
|
| 375 |
-
.score-good {
|
| 376 |
-
color: var(--success);
|
| 377 |
-
}
|
| 378 |
-
|
| 379 |
-
.score-bad {
|
| 380 |
-
color: var(--error);
|
| 381 |
-
}
|
| 382 |
-
|
| 383 |
-
/* Charts */
|
| 384 |
-
canvas {
|
| 385 |
-
max-height: 250px;
|
| 386 |
-
width: 100%;
|
| 387 |
-
}
|
| 388 |
-
|
| 389 |
-
/* ─── Footer ─── */
|
| 390 |
-
footer {
|
| 391 |
-
text-align: center;
|
| 392 |
-
padding: 40px;
|
| 393 |
-
color: var(--text-muted);
|
| 394 |
-
font-size: 14px;
|
| 395 |
-
}
|
| 396 |
-
</style>
|
| 397 |
-
</head>
|
| 398 |
-
|
| 399 |
-
<body>
|
| 400 |
-
|
| 401 |
-
<div class="hero">
|
| 402 |
-
<div class="container">
|
| 403 |
-
<h1>K-AI API</h1>
|
| 404 |
-
<p class="motto">Feel free to AI</p>
|
| 405 |
-
<p class="desc">The completely free AI proxy. No signup. No keys. Just code.</p>
|
| 406 |
-
</div>
|
| 407 |
-
</div>
|
| 408 |
-
|
| 409 |
-
<div class="container">
|
| 410 |
-
|
| 411 |
-
<!-- POST /chat (Basic) -->
|
| 412 |
-
<div class="endpoint-block">
|
| 413 |
-
<div class="endpoint-header">
|
| 414 |
-
<div class="endpoint-title">
|
| 415 |
-
<h3>Chat Completion</h3>
|
| 416 |
-
<p>Send a message and get a response from the best available AI.</p>
|
| 417 |
-
</div>
|
| 418 |
-
<span class="method-badge">POST /v1/chat/completions</span>
|
| 419 |
-
</div>
|
| 420 |
-
<div class="endpoint-body">
|
| 421 |
-
<div class="endpoint-docs">
|
| 422 |
-
<span class="demo-label">Parameters</span>
|
| 423 |
-
<table class="param-table">
|
| 424 |
-
<tr>
|
| 425 |
-
<td><span class="param-name">message</span></td>
|
| 426 |
-
<td><span class="param-desc">Your prompt</span><span class="param-req">(required)</span>
|
| 427 |
-
</td>
|
| 428 |
-
</tr>
|
| 429 |
-
</table>
|
| 430 |
-
<br>
|
| 431 |
-
<span class="demo-label">Example Request</span>
|
| 432 |
-
<div class="demo-response visible" style="color: #a78bfa;">
|
| 433 |
-
curl https://kiwa001-kai-api-gateway.hf.space/v1/chat/completions \
|
| 434 |
-
-H "Content-Type: application/json" \
|
| 435 |
-
-H "Authorization: Bearer sk-kai-demo-public" \
|
| 436 |
-
-d '{"model": "gemini-3-flash", "messages": [{"role": "user", "content": "Hello!"}]}'
|
| 437 |
-
</div>
|
| 438 |
-
</div>
|
| 439 |
-
<div class="endpoint-demo">
|
| 440 |
-
<span class="demo-label">Try It Live</span>
|
| 441 |
-
<textarea id="chat-basic-input" class="demo-input" rows="3"
|
| 442 |
-
placeholder='{"message": "What is AI?"}'>What is the capital of France?</textarea>
|
| 443 |
-
|
| 444 |
-
<select id="chat-basic-model" class="demo-select" style="margin-top:10px;">
|
| 445 |
-
<option value="gemini-3-flash">gemini-3-flash (Default)</option>
|
| 446 |
-
<option value="gpt-4o">gpt-4o</option>
|
| 447 |
-
<option value="gpt-4o-mini">gpt-4o-mini</option>
|
| 448 |
-
<option value="glm-4">glm-4</option>
|
| 449 |
-
<option value="mistral-large">mistral-large</option>
|
| 450 |
-
</select>
|
| 451 |
-
|
| 452 |
-
<button class="demo-btn" onclick="runDemo('chat-basic')">Run Request ▶</button>
|
| 453 |
-
<div id="chat-basic-status" class="demo-status"></div>
|
| 454 |
-
|
| 455 |
-
<select id="chat-basic-model" class="demo-select" style="margin-top:10px;">
|
| 456 |
-
<option value="gemini-3-flash">gemini-3-flash (Default)</option>
|
| 457 |
-
<option value="gpt-4o">gpt-4o</option>
|
| 458 |
-
<option value="gpt-4o-mini">gpt-4o-mini</option>
|
| 459 |
-
<option value="glm-4">glm-4</option>
|
| 460 |
-
<option value="mistral-large">mistral-large</option>
|
| 461 |
-
</select>
|
| 462 |
-
|
| 463 |
-
<div id="chat-basic-res" class="demo-response"></div>
|
| 464 |
-
</div>
|
| 465 |
-
</div>
|
| 466 |
-
</div>
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
<!-- GET /models -->
|
| 474 |
-
<div class="endpoint-block">
|
| 475 |
-
<div class="endpoint-header">
|
| 476 |
-
<div class="endpoint-title">
|
| 477 |
-
<h3>List Models</h3>
|
| 478 |
-
<p>Get all currently available AI models.</p>
|
| 479 |
-
</div>
|
| 480 |
-
<span class="method-badge GET">GET /models</span>
|
| 481 |
-
</div>
|
| 482 |
-
<div class="endpoint-body">
|
| 483 |
-
<div class="endpoint-docs">
|
| 484 |
-
<p style="color:var(--text-secondary); font-size:14px;">Returns a JSON list of all models supported
|
| 485 |
-
by the API, ranked by quality.</p>
|
| 486 |
-
</div>
|
| 487 |
-
<div class="endpoint-demo">
|
| 488 |
-
<span class="demo-label">Try It Live</span>
|
| 489 |
-
<button class="demo-btn" onclick="runDemo('models')">Fetch Models ▶</button>
|
| 490 |
-
<div id="models-res" class="demo-response"></div>
|
| 491 |
-
</div>
|
| 492 |
-
</div>
|
| 493 |
-
</div>
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
<!-- POST /search & /deep_research -->
|
| 497 |
-
<div class="endpoint-block">
|
| 498 |
-
<div class="endpoint-header">
|
| 499 |
-
<div class="endpoint-title">
|
| 500 |
-
<h3>Web Search & Research</h3>
|
| 501 |
-
<p>Reverse-engineered web search and deep content gathering. No API keys required.</p>
|
| 502 |
-
</div>
|
| 503 |
-
<span class="method-badge">POST /search</span>
|
| 504 |
-
</div>
|
| 505 |
-
<div class="endpoint-body">
|
| 506 |
-
<div class="endpoint-docs">
|
| 507 |
-
<span class="demo-label">Endpoints</span>
|
| 508 |
-
<ul style="color:var(--text-secondary); font-size:13px; margin-left:18px; margin-bottom:10px;">
|
| 509 |
-
<li><code>/search</code>: Standard web search (Links)</li>
|
| 510 |
-
<li><code>/deep_research</code>: Deep content gathering (Scraper)</li>
|
| 511 |
-
</ul>
|
| 512 |
-
<span class="demo-label">Parameters</span>
|
| 513 |
-
<table class="param-table">
|
| 514 |
-
<tr>
|
| 515 |
-
<td><span class="param-name">query</span></td>
|
| 516 |
-
<td><span class="param-desc">Search topic</span><span class="param-req">(required)</span>
|
| 517 |
-
</td>
|
| 518 |
-
</tr>
|
| 519 |
-
</table>
|
| 520 |
-
</div>
|
| 521 |
-
<div class="endpoint-demo">
|
| 522 |
-
<span class="demo-label">Try It Live</span>
|
| 523 |
-
<input id="search-query" class="demo-input" placeholder="Query" value="When was Python released?">
|
| 524 |
-
<select id="search-mode" class="demo-select">
|
| 525 |
-
<option value="search">Simple Search (Links)</option>
|
| 526 |
-
<option value="deep">Deep Research (Content Gathering)</option>
|
| 527 |
-
</select>
|
| 528 |
-
<button class="demo-btn" onclick="runSearch()">Execute Search ▶</button>
|
| 529 |
-
<div id="search-res" class="demo-response"></div>
|
| 530 |
-
</div>
|
| 531 |
-
</div>
|
| 532 |
-
</div>
|
| 533 |
-
|
| 534 |
-
<!-- Live Ranking Container -->
|
| 535 |
-
<div class="endpoint-block">
|
| 536 |
-
<div class="endpoint-header">
|
| 537 |
-
<div class="endpoint-title">
|
| 538 |
-
<h3>🏆 Live Model Ranking (Time-Weighted)</h3>
|
| 539 |
-
<p>Real-time performance tracked by the engine (Speed & Reliability).</p>
|
| 540 |
-
</div>
|
| 541 |
-
<span class="method-badge LIVE">LIVE UPDATES</span>
|
| 542 |
-
</div>
|
| 543 |
-
<div class="endpoint-body full-width">
|
| 544 |
-
<div style="padding: 24px; width: 100%; overflow-x: auto;">
|
| 545 |
-
<table class="param-table" id="rankings-table">
|
| 546 |
-
<thead>
|
| 547 |
-
<tr>
|
| 548 |
-
<th width="50">#</th>
|
| 549 |
-
<th>Model ID</th>
|
| 550 |
-
<th>Score</th>
|
| 551 |
-
<th>Avg Time</th>
|
| 552 |
-
<th>Success</th>
|
| 553 |
-
<th>Fail</th>
|
| 554 |
-
</tr>
|
| 555 |
-
</thead>
|
| 556 |
-
<tbody>
|
| 557 |
-
<tr>
|
| 558 |
-
<td colspan="7" style="text-align:center; padding: 20px;">Loading live stats...</td>
|
| 559 |
-
</tr>
|
| 560 |
-
</tbody>
|
| 561 |
-
</table>
|
| 562 |
-
</div>
|
| 563 |
-
</div>
|
| 564 |
-
</div>
|
| 565 |
-
|
| 566 |
-
<!-- Network Analytics (Graphs) -->
|
| 567 |
-
<div class="endpoint-block">
|
| 568 |
-
<div class="endpoint-header">
|
| 569 |
-
<div class="endpoint-title">
|
| 570 |
-
<h3>Network Analytics</h3>
|
| 571 |
-
<p>Live Latency vs. Reliability & Provider Distribution.</p>
|
| 572 |
-
</div>
|
| 573 |
-
</div>
|
| 574 |
-
<div class="endpoint-body" id="analytics-body"
|
| 575 |
-
style="display: flex; flex-direction: column; gap: 20px; padding: 20px;">
|
| 576 |
-
<div style="background: var(--bg-secondary); padding: 15px; border-radius: 8px; flex: 1;">
|
| 577 |
-
<h4 style="margin-bottom:10px; color:var(--text-secondary); font-size:12px; font-weight:700;">SPEED
|
| 578 |
-
vs RELIABILITY</h4>
|
| 579 |
-
<canvas id="scatterChart"></canvas>
|
| 580 |
-
</div>
|
| 581 |
-
<div style="background: var(--bg-secondary); padding: 15px; border-radius: 8px; flex: 1;">
|
| 582 |
-
<h4 style="margin-bottom:10px; color:var(--text-secondary); font-size:12px; font-weight:700;">
|
| 583 |
-
PROVIDER DISTRIBUTION</h4>
|
| 584 |
-
<canvas id="pieChart"></canvas>
|
| 585 |
-
</div>
|
| 586 |
-
</div>
|
| 587 |
-
</div>
|
| 588 |
-
|
| 589 |
-
</div>
|
| 590 |
-
|
| 591 |
-
<footer>K-AI API — Feel free to AI</footer>
|
| 592 |
-
|
| 593 |
-
<script>
|
| 594 |
-
// Global flag for Pie Chart
|
| 595 |
-
window.pieChartRendered = false;
|
| 596 |
-
|
| 597 |
-
async function runDemo(type) {
|
| 598 |
-
const resBox = document.getElementById(type + '-res');
|
| 599 |
-
const statusBox = document.getElementById(type + '-status');
|
| 600 |
-
|
| 601 |
-
resBox.className = 'demo-response visible';
|
| 602 |
-
resBox.style.display = 'none'; // Hide content while loading
|
| 603 |
-
resBox.innerHTML = '';
|
| 604 |
-
|
| 605 |
-
if (statusBox) {
|
| 606 |
-
statusBox.style.display = 'block';
|
| 607 |
-
statusBox.innerHTML = 'Sending Request... <span class="loading-spin"></span>';
|
| 608 |
-
statusBox.style.color = 'var(--text-muted)';
|
| 609 |
-
}
|
| 610 |
-
|
| 611 |
-
const startTime = Date.now();
|
| 612 |
-
|
| 613 |
-
try {
|
| 614 |
-
let url, body, method = 'POST';
|
| 615 |
-
let headers = {
|
| 616 |
-
'Content-Type': 'application/json',
|
| 617 |
-
'Authorization': 'Bearer sk-kai-demo-public' // Use Demo Key for Dashboard
|
| 618 |
-
};
|
| 619 |
-
|
| 620 |
-
if (type === 'chat-basic') {
|
| 621 |
-
// Simple Chat
|
| 622 |
-
const inputVal = document.getElementById('chat-basic-input').value;
|
| 623 |
-
const modelVal = document.getElementById('chat-basic-model').value || "gemini-3-flash";
|
| 624 |
-
|
| 625 |
-
if (!inputVal) { alert("Please enter a message"); return; }
|
| 626 |
-
|
| 627 |
-
url = '/v1/chat/completions';
|
| 628 |
-
body = {
|
| 629 |
-
model: modelVal,
|
| 630 |
-
messages: [{ role: "user", content: inputVal }]
|
| 631 |
-
};
|
| 632 |
-
}
|
| 633 |
-
else if (type === 'chat-adv') {
|
| 634 |
-
// Advanced Chat
|
| 635 |
-
const model = document.getElementById('chat-adv-model').value || "gemini-3-flash";
|
| 636 |
-
const userMsg = document.getElementById('chat-adv-msg').value;
|
| 637 |
-
|
| 638 |
-
if (!userMsg) { alert("Please enter a message"); return; }
|
| 639 |
-
|
| 640 |
-
url = '/v1/chat/completions';
|
| 641 |
-
body = {
|
| 642 |
-
model: model,
|
| 643 |
-
messages: [
|
| 644 |
-
{ role: "user", content: userMsg }
|
| 645 |
-
]
|
| 646 |
-
};
|
| 647 |
-
}
|
| 648 |
-
else if (type === 'models') {
|
| 649 |
-
// List Models
|
| 650 |
-
url = '/models'; // This is GET, no body
|
| 651 |
-
method = 'GET';
|
| 652 |
-
body = undefined;
|
| 653 |
-
// Keep models as public? Or require auth?
|
| 654 |
-
// Usually /models is authenticated in OpenAI but let's keep it open for now or add auth.
|
| 655 |
-
}
|
| 656 |
-
|
| 657 |
-
const response = await fetch(url, {
|
| 658 |
-
method: method,
|
| 659 |
-
headers: headers,
|
| 660 |
-
body: body ? JSON.stringify(body) : undefined
|
| 661 |
-
});
|
| 662 |
-
|
| 663 |
-
const data = await response.json();
|
| 664 |
-
const duration = Date.now() - startTime;
|
| 665 |
-
|
| 666 |
-
if (!response.ok) throw new Error(data.detail || 'Request failed');
|
| 667 |
-
|
| 668 |
-
if (!response.ok) throw new Error(data.detail || 'Request failed');
|
| 669 |
-
|
| 670 |
-
if (statusBox) {
|
| 671 |
-
statusBox.innerHTML = `Success (${duration}ms)`;
|
| 672 |
-
statusBox.style.color = 'var(--success)';
|
| 673 |
-
}
|
| 674 |
-
|
| 675 |
-
resBox.style.display = 'block';
|
| 676 |
-
resBox.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
|
| 677 |
-
|
| 678 |
-
} catch (err) {
|
| 679 |
-
resBox.innerHTML = `
|
| 680 |
-
<div style="margin-bottom:5px; font-weight:bold; color:var(--error);">
|
| 681 |
-
Error
|
| 682 |
-
</div>
|
| 683 |
-
<pre>${err.message}</pre>
|
| 684 |
-
`;
|
| 685 |
-
}
|
| 686 |
-
}
|
| 687 |
-
|
| 688 |
-
async function runSearch() {
|
| 689 |
-
const query = document.getElementById('search-query').value;
|
| 690 |
-
const mode = document.getElementById('search-mode').value;
|
| 691 |
-
const resBox = document.getElementById('search-res');
|
| 692 |
-
|
| 693 |
-
if (!query) { alert("Please enter a query"); return; }
|
| 694 |
-
|
| 695 |
-
resBox.className = 'demo-response visible';
|
| 696 |
-
resBox.textContent = '⏳ Searching... (Deep Research may take 10s+)';
|
| 697 |
-
|
| 698 |
-
const endpoint = mode === 'deep' ? '/deep_research' : '/search';
|
| 699 |
-
|
| 700 |
-
try {
|
| 701 |
-
const res = await fetch(endpoint, {
|
| 702 |
-
method: 'POST',
|
| 703 |
-
headers: { 'Content-Type': 'application/json' },
|
| 704 |
-
body: JSON.stringify({ query: query })
|
| 705 |
-
});
|
| 706 |
-
const data = await res.json();
|
| 707 |
-
resBox.className = 'demo-response visible ' + (res.ok ? 'success' : 'error');
|
| 708 |
-
resBox.innerText = JSON.stringify(data, null, 2);
|
| 709 |
-
} catch (e) {
|
| 710 |
-
resBox.innerText = 'Error: ' + e.message;
|
| 711 |
-
}
|
| 712 |
-
}
|
| 713 |
-
|
| 714 |
-
// --- Live Ranking Logic ---
|
| 715 |
-
|
| 716 |
-
let availableModelsSet = new Set();
|
| 717 |
-
let scatterChart, pieChart;
|
| 718 |
-
|
| 719 |
-
async function fetchRankingStats() {
|
| 720 |
-
try {
|
| 721 |
-
const t = new Date().getTime();
|
| 722 |
-
const [statsRes, modelsRes] = await Promise.all([
|
| 723 |
-
fetch(`/admin/stats?t=${t}`),
|
| 724 |
-
fetch(`/models?t=${t}`)
|
| 725 |
-
]);
|
| 726 |
-
|
| 727 |
-
if (!statsRes.ok) {
|
| 728 |
-
throw new Error(`Stats HTTP ${statsRes.status}`);
|
| 729 |
-
}
|
| 730 |
-
const stats = await statsRes.json();
|
| 731 |
-
|
| 732 |
-
availableModelsSet.clear();
|
| 733 |
-
if (modelsRes.ok) {
|
| 734 |
-
const data = await modelsRes.json();
|
| 735 |
-
// Robust handling: Support both array and object wrapper
|
| 736 |
-
const modelsList = Array.isArray(data) ? data : (data.models || []);
|
| 737 |
-
|
| 738 |
-
if (Array.isArray(modelsList)) {
|
| 739 |
-
modelsList.forEach(m => availableModelsSet.add(`${m.provider}/${m.model}`));
|
| 740 |
-
} else {
|
| 741 |
-
console.error("Expected array but got:", data);
|
| 742 |
-
}
|
| 743 |
-
}
|
| 744 |
-
|
| 745 |
-
renderDashboard(stats);
|
| 746 |
-
} catch (e) {
|
| 747 |
-
console.error("Failed to fetch ranking", e);
|
| 748 |
-
const tbody = document.querySelector('#rankings-table tbody');
|
| 749 |
-
// Only replace if we haven't rendered data yet (or if it's the loading state)
|
| 750 |
-
if (tbody.innerHTML.includes('Loading')) {
|
| 751 |
-
tbody.innerHTML = `<tr><td colspan="7" style="color:#ef4444; text-align:center; padding: 20px;">
|
| 752 |
-
Failed to load stats: ${e.message}<br>
|
| 753 |
-
<small style="opacity:0.7">If on Hugging Face, check "Logs" tab for backend errors.</small>
|
| 754 |
-
</td></tr>`;
|
| 755 |
-
}
|
| 756 |
-
}
|
| 757 |
-
}
|
| 758 |
-
|
| 759 |
-
function calculateScore(s, f, timeMs, cf) {
|
| 760 |
-
let base = s - (f * 2);
|
| 761 |
-
let penalty = (timeMs || 0) / 1000.0;
|
| 762 |
-
let score = base - penalty;
|
| 763 |
-
if (cf >= 3) return score - 100000;
|
| 764 |
-
return score;
|
| 765 |
-
}
|
| 766 |
-
|
| 767 |
-
function renderDashboard(data) {
|
| 768 |
-
let rows = [];
|
| 769 |
-
let providerCounts = {};
|
| 770 |
-
let scatterData = [];
|
| 771 |
-
|
| 772 |
-
for (let [key, val] of Object.entries(data)) {
|
| 773 |
-
let score = calculateScore(val.success, val.failure, val.avg_time_ms, val.consecutive_failures);
|
| 774 |
-
rows.push({ id: key, ...val, score: score });
|
| 775 |
-
|
| 776 |
-
// Provider stats for Pie
|
| 777 |
-
let prov = key.split('/')[0];
|
| 778 |
-
providerCounts[prov] = (providerCounts[prov] || 0) + val.success;
|
| 779 |
-
|
| 780 |
-
// Scatter Data
|
| 781 |
-
scatterData.push({
|
| 782 |
-
x: val.avg_time_ms || 0,
|
| 783 |
-
y: score,
|
| 784 |
-
id: key
|
| 785 |
-
});
|
| 786 |
-
}
|
| 787 |
-
rows.sort((a, b) => b.score - a.score);
|
| 788 |
-
|
| 789 |
-
// Render Table
|
| 790 |
-
const tbody = document.querySelector('#rankings-table tbody');
|
| 791 |
-
tbody.innerHTML = '';
|
| 792 |
-
|
| 793 |
-
if (rows.length === 0) {
|
| 794 |
-
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; padding: 20px;">No stats available yet. Make a request!</td></tr>';
|
| 795 |
-
} else {
|
| 796 |
-
rows.forEach((row, index) => {
|
| 797 |
-
const tr = document.createElement('tr');
|
| 798 |
-
let scoreClass = row.score > 0 ? 'score-good' : 'score-bad';
|
| 799 |
-
let timeStr = row.avg_time_ms ? Math.round(row.avg_time_ms) + 'ms' : '-';
|
| 800 |
-
|
| 801 |
-
tr.innerHTML = `
|
| 802 |
-
<td><span class="rank-badge">${index + 1}</span></td>
|
| 803 |
-
<td><b>${row.id}</b></td>
|
| 804 |
-
<td class="${scoreClass}">${row.score.toFixed(2)}</td>
|
| 805 |
-
<td>${timeStr}</td>
|
| 806 |
-
<td>${row.success}</td>
|
| 807 |
-
<td>${row.failure}</td>
|
| 808 |
-
`;
|
| 809 |
-
tbody.appendChild(tr);
|
| 810 |
-
});
|
| 811 |
-
}
|
| 812 |
-
|
| 813 |
-
// Render Charts
|
| 814 |
-
updateScatterChart(scatterData);
|
| 815 |
-
updatePieChart(providerCounts);
|
| 816 |
-
}
|
| 817 |
-
|
| 818 |
-
function updateScatterChart(data) {
|
| 819 |
-
const ctx = document.getElementById('scatterChart').getContext('2d');
|
| 820 |
-
if (scatterChart) {
|
| 821 |
-
scatterChart.data.datasets[0].data = data;
|
| 822 |
-
scatterChart.update('none');
|
| 823 |
-
return;
|
| 824 |
-
}
|
| 825 |
-
const validData = data.filter(d => d.x > 0);
|
| 826 |
-
scatterChart = new Chart(ctx, {
|
| 827 |
-
type: 'scatter',
|
| 828 |
-
data: {
|
| 829 |
-
datasets: [{
|
| 830 |
-
label: 'Models',
|
| 831 |
-
data: validData,
|
| 832 |
-
backgroundColor: '#8b5cf6',
|
| 833 |
-
borderColor: '#8b5cf6',
|
| 834 |
-
}]
|
| 835 |
-
},
|
| 836 |
-
options: {
|
| 837 |
-
responsive: true,
|
| 838 |
-
maintainAspectRatio: false,
|
| 839 |
-
animation: { duration: 1000 },
|
| 840 |
-
scales: {
|
| 841 |
-
x: { type: 'linear', position: 'bottom', title: { display: true, text: 'Time (ms)', color: '#6b6b80' }, grid: { color: '#2a2a3a' } },
|
| 842 |
-
y: { title: { display: true, text: 'Score', color: '#6b6b80' }, grid: { color: '#2a2a3a' } }
|
| 843 |
-
},
|
| 844 |
-
plugins: {
|
| 845 |
-
legend: { display: false },
|
| 846 |
-
tooltip: { callbacks: { label: (ctx) => ctx.raw.id + ': ' + ctx.raw.y.toFixed(2) } }
|
| 847 |
-
}
|
| 848 |
-
}
|
| 849 |
-
});
|
| 850 |
-
}
|
| 851 |
-
|
| 852 |
-
function updatePieChart(counts) {
|
| 853 |
-
if (window.pieChartRendered) return;
|
| 854 |
-
const ctx = document.getElementById('pieChart').getContext('2d');
|
| 855 |
-
window.pieChartRendered = true;
|
| 856 |
-
pieChart = new Chart(ctx, {
|
| 857 |
-
type: 'doughnut',
|
| 858 |
-
data: {
|
| 859 |
-
labels: Object.keys(counts),
|
| 860 |
-
datasets: [{
|
| 861 |
-
data: Object.values(counts),
|
| 862 |
-
backgroundColor: ['#8b5cf6', '#22c55e', '#f59e0b', '#ef4444', '#ec4899', '#3b82f6'],
|
| 863 |
-
borderWidth: 0
|
| 864 |
-
}]
|
| 865 |
-
},
|
| 866 |
-
options: {
|
| 867 |
-
responsive: true,
|
| 868 |
-
maintainAspectRatio: false,
|
| 869 |
-
animation: { duration: 1000 },
|
| 870 |
-
plugins: {
|
| 871 |
-
legend: { position: 'right', labels: { color: '#9898aa', font: { size: 10 } } }
|
| 872 |
-
}
|
| 873 |
-
}
|
| 874 |
-
});
|
| 875 |
-
}
|
| 876 |
-
|
| 877 |
-
// Init Live Ranking
|
| 878 |
-
fetchRankingStats();
|
| 879 |
-
setInterval(fetchRankingStats, 5000);
|
| 880 |
-
|
| 881 |
-
</script>
|
| 882 |
-
|
| 883 |
-
</body>
|
| 884 |
-
|
| 885 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|