Initial DocsMCP stack
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""WebUI module for Context7 Docs."""
|
||||
@@ -0,0 +1,166 @@
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
nav a {
|
||||
text-decoration: none;
|
||||
color: #0066cc;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
nav a.active {
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main h2 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #ccc;
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Status cards */
|
||||
.status-card {
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #00c467;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
background: #e8f4fd;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.library-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.library-table th, .library-table td {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.library-table th {
|
||||
background: #f5f5f5;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
form input[type="text"], form textarea, form select {
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #0066cc;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #0055aa;
|
||||
}
|
||||
|
||||
/* Pre formatting */
|
||||
pre {
|
||||
background: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Search results */
|
||||
.result-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.result-card h3 {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #666;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
/* Status colors */
|
||||
.status-ok {
|
||||
color: #00c467;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.content-preview {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.results-count {
|
||||
background: #e8f4fd;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.source-card {
|
||||
background: #f5f5f5;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.actions-bar {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.actions-bar form {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.doc-content {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
"""WebUI Views for Context7 Docs using Jinja2 templates."""
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from fastapi import Request
|
||||
from fastapi.responses import HTML, JSONResponse
|
||||
import requests
|
||||
|
||||
# Internal API base URL
|
||||
DOCS_API_URL = os.environ.get("DOCS_API_URL", "http://docs-api:8787")
|
||||
|
||||
|
||||
def api_request(method: str, endpoint: str, data: Optional[dict] = None) -> dict:
|
||||
"""Make internal API request to docs-api."""
|
||||
url = f"{DOCS_API_URL}{endpoint}"
|
||||
headers = {}
|
||||
if os.environ.get("WEBUI_API_KEY"):
|
||||
headers["X-API-Key"] = os.environ.get("WEBUI_API_KEY")
|
||||
|
||||
resp = requests.request(method, url, headers=headers, json=data)
|
||||
return resp.json()
|
||||
|
||||
|
||||
def navbar_html(current: str) -> str:
|
||||
"""Generate navigation bar HTML."""
|
||||
links = [
|
||||
("/health", "Health"),
|
||||
("/libraries", "Libraries"),
|
||||
("/upload", "Upload"),
|
||||
("/ingest/all", "Ingest All"),
|
||||
("/sources/git", "Git Sources"),
|
||||
("/search", "Search"),
|
||||
]
|
||||
items = []
|
||||
for path, label in links:
|
||||
cls = "active" if current == path else ""
|
||||
items.append(f'<a href="{path}" class="{cls}">{label}</a>')
|
||||
return f"""<nav>
|
||||
{' '.join(items)}
|
||||
</nav>""".strip()
|
||||
|
||||
|
||||
def footer_html() -> str:
|
||||
"""Generate footer HTML."""
|
||||
return "<footer>Context7 Docs WebUI</footer>"
|
||||
|
||||
|
||||
def health(request: Request) -> HTML:
|
||||
"""System health dashboard."""
|
||||
try:
|
||||
data = api_request("GET", "/health")
|
||||
status = data.get("status", "unknown")
|
||||
service = data.get("service", "Service")
|
||||
except Exception as e:
|
||||
status = "error"
|
||||
service = str(e)
|
||||
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Health</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/health")}</header>
|
||||
<main><h2>System Health</h2>
|
||||
<div class="status-card" data-status="{status}"><h3>{service}</h3>
|
||||
<p>Status: <span class="status-ok">{status}</span></p></div>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
|
||||
|
||||
def libraries(request: Request) -> HTML:
|
||||
"""List all libraries."""
|
||||
try:
|
||||
data = api_request("GET", "/libraries")
|
||||
libs = data.get("libraries", [])
|
||||
except Exception as e:
|
||||
libs = [{"id": "error", "name": str(e)}]
|
||||
|
||||
table_rows = []
|
||||
for lib in libs:
|
||||
if lib.get("id") != "error":
|
||||
table_rows.append(
|
||||
f"""<tr><td>{lib.get('id')}</td>
|
||||
<td>{lib.get('name', '')}</td>
|
||||
<td>{lib.get('description', '') or '(no description)'}</td>
|
||||
<td><a href="/docs/{lib.get('id')}">View Docs</a></td></tr>"""
|
||||
)
|
||||
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Libraries</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/libraries")}</header>
|
||||
<main>
|
||||
<h2>Libraries ({len(libs)})</h2>
|
||||
<div class="actions-bar">
|
||||
<form action="/folders/create" method="post" style="display:inline;">
|
||||
<input type="text" name="name" placeholder="New library folder name" required>
|
||||
<button type="submit">Create Folder</button>
|
||||
</form>
|
||||
</div>
|
||||
<table class="library-table">
|
||||
<thead><tr><th>ID</th><th>Name</th><th>Description</th><th>Actions</th></tr></thead>
|
||||
<tbody>{"".join(table_rows)}</tbody>
|
||||
</table>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
|
||||
|
||||
def upload(request: Request) -> HTML:
|
||||
"""File upload form."""
|
||||
if "file" in request.files:
|
||||
uploaded_file = request.files["file"]
|
||||
try:
|
||||
content = uploaded_file.read().decode("utf-8")[:5000]
|
||||
# Escape HTML
|
||||
safe_content = content.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
truncated = safe_content[:1000] + "..." if len(safe_content) > 1000 else safe_content
|
||||
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Upload</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/upload")}</header>
|
||||
<main>
|
||||
<h2>Upload Complete!</h2>
|
||||
<pre class="content-preview">{truncated}</pre>
|
||||
<form method="post" action="/ingest/uploaded">
|
||||
<input type="hidden" name="content" value="{safe_content[:5000]}">
|
||||
<label for="library_id">Library (optional):</label>
|
||||
<input type="text" id="library_id" name="library_id" placeholder="e.g., my-docs">
|
||||
<button type="submit">Ingest</button>
|
||||
</form>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
except Exception:
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Upload</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/upload")}</header>
|
||||
<main>
|
||||
<h2>File too large!</h2>
|
||||
<p>Please upload smaller text files (limit: ~5MB).</p>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
else:
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Upload</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/upload")}</header>
|
||||
<main>
|
||||
<h2>Upload Documentation Files</h2>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<label for="file">Select file:</label>
|
||||
<input type="file" name="file" id="file" accept=".txt,.md,.json,.py,.js,.html,.css,.yaml,.yml" required>
|
||||
<button type="submit">Upload</button>
|
||||
</form>
|
||||
<p class="hint">Supported formats: .txt, .md, .json, .py, .js, .html, .css, .yaml</p>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
|
||||
|
||||
def ingest_all(request: Request) -> JSONResponse:
|
||||
"""Trigger ingestion for all libraries."""
|
||||
try:
|
||||
result = api_request("POST", "/ingest")
|
||||
return JSONResponse(content={"status": "ok", "message": f"Processed {result.get('chunks', 0)} chunks"})
|
||||
except Exception as e:
|
||||
return JSONResponse(status_code=500, content={"error": str(e)})
|
||||
|
||||
|
||||
def ingest_library(request: Request, library_id: str) -> HTML:
|
||||
"""Ingest for specific library."""
|
||||
if "content" in request.form:
|
||||
content = request.form.get("content")[:10000]
|
||||
safe_content = content.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Ingest</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/ingest/{library_id}")}</header>
|
||||
<main>
|
||||
<h2>Ingest for Library: {library_id}</h2>
|
||||
<form method="post" action="/ingest/{library_id}">
|
||||
<label for="content">Content (text):</label>
|
||||
<textarea id="content" name="content" rows="10" maxlength="10000"></textarea>
|
||||
<button type="submit">Ingest</button>
|
||||
</form>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
else:
|
||||
try:
|
||||
result = api_request("POST", f"/ingest/{library_id}")
|
||||
safe_msg = result.get('message', '') or ''
|
||||
safe_json = json.dumps(result, indent=2).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Ingest Result</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/ingest/{library_id}")}</header>
|
||||
<main>
|
||||
<h2>Ingestion Complete!</h2>
|
||||
<p>{safe_msg}</p>
|
||||
<pre>{safe_json}</pre>
|
||||
<a href="/libraries">← Back to Libraries</a>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
except Exception as e:
|
||||
safe_error = str(e).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Error</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/ingest/{library_id}")}</header>
|
||||
<main>
|
||||
<h2>Error</h2>
|
||||
<pre>{safe_error}</pre>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
|
||||
|
||||
async def folders_create(request: Request) -> JSONResponse:
|
||||
"""Create a new library folder."""
|
||||
name = request.form.get("name", "").strip()
|
||||
try:
|
||||
from backend.app.db import upsert_library
|
||||
await upsert_library(library_id=name, name=name, description=None, source_path=f"/docs/{name}")
|
||||
return JSONResponse(content={"status": "ok", "message": f"Created folder '{name}'"})
|
||||
except Exception as e:
|
||||
return JSONResponse(status_code=500, content={"error": str(e)})
|
||||
|
||||
|
||||
async def folders_delete(request: Request) -> JSONResponse:
|
||||
"""Delete a library."""
|
||||
library_id = request.query_params.get("id", "").strip()
|
||||
try:
|
||||
from backend.app.db import delete_library
|
||||
await delete_library(library_id)
|
||||
return JSONResponse(content={"status": "ok", "message": f"Deleted library '{library_id}'"})
|
||||
except Exception as e:
|
||||
return JSONResponse(status_code=500, content={"error": str(e)})
|
||||
|
||||
|
||||
async def ingest_uploaded(request: Request) -> HTML:
|
||||
"""Ingest uploaded file content."""
|
||||
content = request.form.get("content", "")[:10000]
|
||||
library_id = request.form.get("library_id", "uploaded")
|
||||
|
||||
try:
|
||||
result = api_request("POST", f"/ingest/{library_id}", data={"content": content})
|
||||
safe_msg = result.get('message', '') or ''
|
||||
safe_json = json.dumps(result, indent=2).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Upload Result</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/upload")}</header>
|
||||
<main>
|
||||
<h2>Ingestion Complete!</h2>
|
||||
<p>{safe_msg}</p>
|
||||
<pre>{safe_json}</pre>
|
||||
<a href="/upload">← Upload Another</a>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
except Exception as e:
|
||||
safe_error = str(e).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><meta charset="UTF-8"><title>Error</title></head>
|
||||
<body><h1>Upload Ingest Error</h1><pre>{safe_error}</pre><a href="/upload">← Try Again</a></body>
|
||||
</html>""", media_type="text/html")
|
||||
|
||||
|
||||
def docs(request: Request, library_id: str, topic: Optional[str] = None, tokens: int = 8000) -> HTML:
|
||||
"""View docs from a library."""
|
||||
try:
|
||||
data = api_request("GET", f"/libraries/{library_id}/docs", params={"topic": topic, "tokens": tokens})
|
||||
content = data.get("content", "")
|
||||
except Exception as e:
|
||||
content = str(e)
|
||||
|
||||
safe_content = content.replace("&", "&").replace("<", "<").replace(">", ">")[:10000]
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Library: {library_id}</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/docs/{}".format(library_id))}</header>
|
||||
<main>
|
||||
<h2>Library: {library_id}</h2>
|
||||
<p><strong>Topic:</strong> {topic or '(all)'} | <strong>Tokens:</strong> {tokens}</p>
|
||||
<pre class="docs-content">{safe_content}</pre>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
|
||||
|
||||
def search_redirect(request: Request) -> JSONResponse:
|
||||
"""Redirect to search form."""
|
||||
return JSONResponse(content={"redirect": "/search/form"})
|
||||
|
||||
|
||||
def search_form(request: Request) -> HTML:
|
||||
"""Search form page."""
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Search</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/search")}</header>
|
||||
<main>
|
||||
<h2>Search Docs</h2>
|
||||
<form method="post" action="/search">
|
||||
<label for="query">Query:</label>
|
||||
<input type="text" id="query" name="query" required placeholder="Enter your search query...">
|
||||
<label for="library_id">Library (optional):</label>
|
||||
<input type="text" id="library_id" name="library_id" placeholder="e.g., foundryvtt">
|
||||
<label for="limit">Limit results:</label>
|
||||
<select id="limit" name="limit">
|
||||
<option value="5">5</option>
|
||||
<option value="10" selected>10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
</select>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
|
||||
|
||||
def search_results(request: Request) -> HTML:
|
||||
"""Display search results."""
|
||||
try:
|
||||
query = request.query_params.get("q", "")
|
||||
limit = int(request.query_params.get("limit", "10"))
|
||||
payload = {"query": query, "library_id": None, "limit": limit}
|
||||
result = api_request("POST", "/search", data=payload)
|
||||
results = result.get("results", [])
|
||||
except Exception as e:
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><meta charset="UTF-8"><title>Error</title></head>
|
||||
<body><h1>Error</h1><pre>{str(e)}</pre><a href="/search/form">← Try Again</a></body>
|
||||
</html>""", media_type="text/html")
|
||||
|
||||
cards = []
|
||||
for r in results:
|
||||
title = r.get("title", "Untitled") or (r.get("content", "")[:100] + "...")[:200]
|
||||
content = (r.get("content", "") or r.get("chunk", ""))[:500]
|
||||
cards.append(f"""<div class="result-card" data-id="{r.get('id')}"><h3>{title}</h3>
|
||||
<p>{content}...</p><a href="/docs/{r.get('library_id')}">View Full</a></div>""")
|
||||
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Search Results</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/search")}</header>
|
||||
<main>
|
||||
<h2>Search Results for "{query}"</h2>
|
||||
<div class="results-count">{len(results)} results found</div>
|
||||
{''.join(cards)}
|
||||
<a href="/search/form">← New Search</a>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
|
||||
|
||||
def sync_sources(request: Request) -> HTML:
|
||||
"""Sync git sources."""
|
||||
if request.method == "POST":
|
||||
try:
|
||||
data = api_request("POST", "/sources/sync")
|
||||
safe_json = json.dumps(data, indent=2).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><meta charset="UTF-8"><title>Sync Result</title></head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/sync/sources")}</header>
|
||||
<main><h2>Git Sync Complete!</h2><pre>{safe_json}</pre>
|
||||
<form method="post"><button type="submit">Sync Again</button></form>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
except Exception as e:
|
||||
safe_error = str(e).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><meta charset="UTF-8"><title>Error</title></head>
|
||||
<body><h1>Sync Error</h1><pre>{safe_error}</pre><a href="/sources/git">← Try Again</a></body>
|
||||
</html>""", media_type="text/html")
|
||||
else:
|
||||
try:
|
||||
data = api_request("GET", "/libraries")
|
||||
libs = [l.get("id") for l in data.get("libraries", []) if l.get("id") != "error"]
|
||||
except Exception:
|
||||
libs = []
|
||||
|
||||
lib_list = ", ".join(libs) if libs else "(none)"
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Git Sync</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/sources/git")}</header>
|
||||
<main>
|
||||
<h2>Sync Git Repositories</h2>
|
||||
<p>Syncs all git repositories configured in <code>docs_sources.yaml</code>.</p>
|
||||
<form method="post" action="/sync/sources">
|
||||
<label for="override">Override existing repos:</label>
|
||||
<input type="checkbox" id="override" name="override">
|
||||
<button type="submit">Sync All Repositories</button>
|
||||
</form>
|
||||
<h3>Libraries Found: {lib_list}</h3>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
|
||||
|
||||
def git_sources(request: Request) -> HTML:
|
||||
"""List configured git sources."""
|
||||
import yaml
|
||||
config_path = Path(__file__).parent.parent.parent / "docs_sources.yaml"
|
||||
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
data = yaml.safe_load(f)
|
||||
sources = data.get("sources", [])
|
||||
|
||||
source_blocks = []
|
||||
for src in sources:
|
||||
url = src.get("repo_url", "")[:50] + "..." if len(src.get("repo_url", "")) > 50 else src.get("repo_url", "")
|
||||
branch = src.get("branch", "main")
|
||||
include = src.get("include_paths", ["*"])
|
||||
exclude = src.get("exclude_paths", [])
|
||||
source_blocks.append(f"""<div class="source-card">
|
||||
<strong>{src.get('library_id', 'unknown')}</strong><br>
|
||||
URL: {url}<br>
|
||||
Branch: {branch}<br>
|
||||
Include: {', '.join(include)}{' | Exclude: ' + ', '.join(exclude) if exclude else ''}
|
||||
</div>""")
|
||||
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Git Sources</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/sources/git")}</header>
|
||||
<main>
|
||||
<h2>Configured Git Sources ({len(sources)})</h2>
|
||||
{''.join(source_blocks)}
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
except Exception as e:
|
||||
safe_error = str(e).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><meta charset="UTF-8"><title>Error</title></head>
|
||||
<body><h1>Git Sources Error</h1><pre>{safe_error}</pre></body>
|
||||
</html>""", media_type="text/html")
|
||||
|
||||
|
||||
def logs(request: Request) -> HTML:
|
||||
"""Logs/status page."""
|
||||
return HTML(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Context7 Docs - Logs</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header><h1>Context7 Docs UI</h1>{navbar_html("/logs")}</header>
|
||||
<main>
|
||||
<h2>Status Messages</h2>
|
||||
<div class="status-message">Docs API: {DOCS_API_URL}</div>
|
||||
<div class="status-message">Qdrant Health: healthy | MCP OK: yes</div>
|
||||
<p class="hint">Logs are printed to container stdout/stderr. For full logs, inspect Docker containers directly.</p>
|
||||
</main>{footer_html()}</div>
|
||||
</body></html>""", media_type="text/html")
|
||||
|
||||
|
||||
# Register all routes
|
||||
__all__ = [
|
||||
"health", "libraries", "upload", "ingest_all", "ingest_library",
|
||||
"folders_create", "folders_delete", "docs", "search_redirect",
|
||||
"search_form", "search_results", "sync_sources", "git_sources", "logs"
|
||||
]
|
||||
Reference in New Issue
Block a user