AuthorBot Cursor commited on
Commit
b6b69a1
·
1 Parent(s): 5ee07a1

refactor: HF routing like RAG 1.2 — /ui, /widget, /admin, port 7860

Browse files

Mount Gradio chat at /ui, serve widget and admin pages with CORS, redirect / to /ui, enable persistent storage in README, switch Space port to 7860.

Co-authored-by: Cursor <cursoragent@cursor.com>

Dockerfile CHANGED
@@ -1,5 +1,6 @@
1
  # ============================================================
2
  # AuthorBot RAG API — Root Dockerfile for HuggingFace Spaces
 
3
  # ============================================================
4
  FROM python:3.11-slim AS builder
5
 
@@ -13,7 +14,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
13
 
14
  WORKDIR /app
15
 
16
- # Copy and install Python dependencies from backend directory
17
  COPY backend/requirements.txt .
18
  RUN pip install --no-cache-dir --upgrade pip && \
19
  pip install --no-cache-dir -r requirements.txt
@@ -39,27 +39,23 @@ ENV UPLOAD_STORAGE_PATH=/data/uploads
39
  COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
40
  COPY --from=builder /usr/local/bin /usr/local/bin
41
 
42
- # Copy ONLY backend application code to /app
43
  COPY backend /app
44
 
45
  RUN mkdir -p /app/logs /data/chroma /data/uploads /data/redis && \
46
  printf 'dir /data/redis\nbind 127.0.0.1\nport 6379\ndaemonize yes\nlogfile /dev/null\n' > /app/redis-hf.conf
47
 
48
- # HuggingFace Spaces: run as non-root user (uid 1000)
49
  RUN adduser --disabled-password --gecos "" --uid 1000 appuser && \
50
  chown -R appuser:appuser /app /data
51
  USER appuser
52
 
53
- # Pre-download models at build time (avoids cold-start delays)
54
  RUN python -c "from sentence_transformers import CrossEncoder; CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')" || true
55
  RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')" || true
56
 
57
- # Verify the app imports cleanly before deployment
58
- RUN python -c "from app.main import app; print('App import OK:', app.title)"
59
 
60
- HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=5 \
61
- CMD curl -f http://localhost:8080/health || exit 1
62
 
63
- EXPOSE 8080
64
 
65
- CMD ["sh", "-c", "mkdir -p /data/chroma /data/uploads /data/redis && redis-server /app/redis-hf.conf && sleep 1 && alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8080 --workers 1 --loop uvloop --http httptools"]
 
1
  # ============================================================
2
  # AuthorBot RAG API — Root Dockerfile for HuggingFace Spaces
3
+ # Routing: /ui (Gradio) · /widget · /admin · /api/v1 · /health
4
  # ============================================================
5
  FROM python:3.11-slim AS builder
6
 
 
14
 
15
  WORKDIR /app
16
 
 
17
  COPY backend/requirements.txt .
18
  RUN pip install --no-cache-dir --upgrade pip && \
19
  pip install --no-cache-dir -r requirements.txt
 
39
  COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
40
  COPY --from=builder /usr/local/bin /usr/local/bin
41
 
 
42
  COPY backend /app
43
 
44
  RUN mkdir -p /app/logs /data/chroma /data/uploads /data/redis && \
45
  printf 'dir /data/redis\nbind 127.0.0.1\nport 6379\ndaemonize yes\nlogfile /dev/null\n' > /app/redis-hf.conf
46
 
 
47
  RUN adduser --disabled-password --gecos "" --uid 1000 appuser && \
48
  chown -R appuser:appuser /app /data
49
  USER appuser
50
 
 
51
  RUN python -c "from sentence_transformers import CrossEncoder; CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')" || true
52
  RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')" || true
53
 
54
+ RUN python -c "from app.main import app; print('App import OK')"
 
55
 
56
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=180s --retries=5 \
57
+ CMD curl -f http://localhost:7860/health || exit 1
58
 
59
+ EXPOSE 7860
60
 
61
+ CMD ["sh", "-c", "mkdir -p /data/chroma /data/uploads /data/redis && redis-server /app/redis-hf.conf && sleep 1 && alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 7860 --workers 1 --loop uvloop --http httptools"]
README.md CHANGED
@@ -5,7 +5,8 @@ colorFrom: indigo
5
  colorTo: purple
6
  sdk: docker
7
  pinned: false
8
- app_port: 8080
 
9
  ---
10
 
11
  # AuthorBot RAG Chatbot SaaS
 
5
  colorTo: purple
6
  sdk: docker
7
  pinned: false
8
+ app_port: 7860
9
+ storage: true
10
  ---
11
 
12
  # AuthorBot RAG Chatbot SaaS
backend/app.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face Space entry point."""
2
+
3
+ from app.main import app # noqa: F401
4
+
5
+ if __name__ == "__main__":
6
+ import uvicorn
7
+
8
+ uvicorn.run("app.main:app", host="0.0.0.0", port=7860, workers=1)
backend/app/config.py CHANGED
@@ -124,6 +124,10 @@ class Settings(BaseSettings):
124
  default=None,
125
  validation_alias=AliasChoices("SUBSCRIPTION_SECRET", "SUBSCRIPTION_TOKEN_SECRET"),
126
  )
 
 
 
 
127
  # Legacy HF secret name — TOTP secrets are stored per SuperAdmin in the DB.
128
  SUPERADMIN_TOTP_SECRET: str | None = Field(default=None, exclude=True)
129
 
 
124
  default=None,
125
  validation_alias=AliasChoices("SUBSCRIPTION_SECRET", "SUBSCRIPTION_TOKEN_SECRET"),
126
  )
127
+ DEMO_SUBSCRIPTION_TOKEN: str | None = Field(
128
+ default=None,
129
+ validation_alias=AliasChoices("DEMO_SUBSCRIPTION_TOKEN", "SUBSCRIPTION_TOKEN"),
130
+ )
131
  # Legacy HF secret name — TOTP secrets are stored per SuperAdmin in the DB.
132
  SUPERADMIN_TOTP_SECRET: str | None = Field(default=None, exclude=True)
133
 
backend/app/main.py CHANGED
@@ -1,22 +1,27 @@
1
  """Author RAG Chatbot SaaS — FastAPI Application Factory.
2
 
3
- This module creates and configures the FastAPI application instance.
4
- All middleware, exception handlers, and routers are registered here.
 
 
 
 
 
5
  """
6
 
7
  from contextlib import asynccontextmanager
8
- from typing import AsyncGenerator
9
-
10
  from pathlib import Path
 
11
 
 
12
  import structlog
13
- from fastapi import FastAPI, Request
14
  from fastapi.middleware.cors import CORSMiddleware
15
- from fastapi.responses import HTMLResponse, JSONResponse
16
  from fastapi.staticfiles import StaticFiles
17
 
18
- from app.config import get_settings
19
  from app.api.v1 import auth, books, documents, chatbot, analytics, settings, links, superadmin
 
20
  from app.exceptions.base import AppException
21
  from app.exceptions.auth import AuthError, InvalidTokenError, ExpiredTokenError, AccountLockedError
22
  from app.exceptions.access import (
@@ -25,6 +30,7 @@ from app.exceptions.access import (
25
  )
26
  from app.middleware.logging_middleware import LoggingMiddleware
27
  from app.middleware.rate_limit_middleware import RateLimitMiddleware
 
28
 
29
  logger = structlog.get_logger(__name__)
30
  STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
@@ -33,13 +39,18 @@ STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
33
  @asynccontextmanager
34
  async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
35
  """Handle application startup and shutdown lifecycle."""
36
- logger.info("Starting AuthorBot API", env=get_settings().APP_ENV)
 
 
 
 
 
37
  yield
38
  logger.info("AuthorBot API shutting down")
39
 
40
 
41
- def create_app() -> FastAPI:
42
- """Create and configure the FastAPI application."""
43
  cfg = get_settings()
44
 
45
  app = FastAPI(
@@ -54,6 +65,7 @@ def create_app() -> FastAPI:
54
  _register_middleware(app, cfg)
55
  _register_exception_handlers(app)
56
  _register_routers(app)
 
57
 
58
  return app
59
 
@@ -64,10 +76,10 @@ def _register_middleware(app: FastAPI, cfg) -> None:
64
  app.add_middleware(RateLimitMiddleware)
65
  app.add_middleware(
66
  CORSMiddleware,
67
- allow_origins=cfg.ALLOWED_ORIGINS,
68
  allow_credentials=True,
69
- allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
70
- allow_headers=["Authorization", "Content-Type", "X-Subscription-Token"],
71
  )
72
 
73
 
@@ -125,7 +137,7 @@ def _register_exception_handlers(app: FastAPI) -> None:
125
 
126
 
127
  def _register_routers(app: FastAPI) -> None:
128
- """Register all API routers."""
129
  prefix = "/api/v1"
130
  app.include_router(auth.router, prefix=f"{prefix}/auth", tags=["Authentication"])
131
  app.include_router(books.router, prefix=f"{prefix}/books", tags=["Books"])
@@ -136,24 +148,53 @@ def _register_routers(app: FastAPI) -> None:
136
  app.include_router(links.router, prefix=f"{prefix}/links", tags=["Links"])
137
  app.include_router(superadmin.router, prefix=f"{prefix}/superadmin", tags=["Super Admin"])
138
 
139
- @app.get("/", response_class=HTMLResponse, include_in_schema=False)
140
- async def home() -> HTMLResponse:
141
- """Landing page with links to the chat demo and API docs."""
142
- return HTMLResponse((STATIC_DIR / "index.html").read_text(encoding="utf-8"))
143
 
144
- @app.get("/demo", response_class=HTMLResponse, include_in_schema=False)
145
- async def chat_demo() -> HTMLResponse:
146
- """Embedded chat widget demo page."""
147
- return HTMLResponse((STATIC_DIR / "demo.html").read_text(encoding="utf-8"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
  @app.get("/health", tags=["Health"])
150
  async def health_check() -> dict:
151
- """System health check — verifies DB, Redis, ChromaDB, and model status."""
152
  from app.dependencies import check_health
153
  return await check_health()
154
 
155
- if STATIC_DIR.is_dir():
156
- app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
157
 
 
158
 
159
- app = create_app()
 
 
1
  """Author RAG Chatbot SaaS — FastAPI Application Factory.
2
 
3
+ Routing follows the Hugging Face Space pattern:
4
+ /ui — Gradio chat (main entry for visitors)
5
+ /widget — Embeddable chat widget (CORS-friendly)
6
+ /admin — Admin portal
7
+ /api/v1 — REST API
8
+ /health — Health check
9
+ / — Redirect to /ui
10
  """
11
 
12
  from contextlib import asynccontextmanager
 
 
13
  from pathlib import Path
14
+ from typing import AsyncGenerator
15
 
16
+ import gradio as gr
17
  import structlog
18
+ from fastapi import FastAPI, HTTPException, Request
19
  from fastapi.middleware.cors import CORSMiddleware
20
+ from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
21
  from fastapi.staticfiles import StaticFiles
22
 
 
23
  from app.api.v1 import auth, books, documents, chatbot, analytics, settings, links, superadmin
24
+ from app.config import get_settings
25
  from app.exceptions.base import AppException
26
  from app.exceptions.auth import AuthError, InvalidTokenError, ExpiredTokenError, AccountLockedError
27
  from app.exceptions.access import (
 
30
  )
31
  from app.middleware.logging_middleware import LoggingMiddleware
32
  from app.middleware.rate_limit_middleware import RateLimitMiddleware
33
+ from app.ui_gradio import demo
34
 
35
  logger = structlog.get_logger(__name__)
36
  STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
 
39
  @asynccontextmanager
40
  async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
41
  """Handle application startup and shutdown lifecycle."""
42
+ cfg = get_settings()
43
+ logger.info("Starting AuthorBot API", env=cfg.APP_ENV)
44
+ if Path("/data").exists():
45
+ logger.info("Persistent HF storage detected at /data")
46
+ else:
47
+ logger.warning("No /data mount — data may not persist across rebuilds")
48
  yield
49
  logger.info("AuthorBot API shutting down")
50
 
51
 
52
+ def create_fastapi_app() -> FastAPI:
53
+ """Create and configure the inner FastAPI application."""
54
  cfg = get_settings()
55
 
56
  app = FastAPI(
 
65
  _register_middleware(app, cfg)
66
  _register_exception_handlers(app)
67
  _register_routers(app)
68
+ _register_public_routes(app)
69
 
70
  return app
71
 
 
76
  app.add_middleware(RateLimitMiddleware)
77
  app.add_middleware(
78
  CORSMiddleware,
79
+ allow_origins=["*"],
80
  allow_credentials=True,
81
+ allow_methods=["*"],
82
+ allow_headers=["*"],
83
  )
84
 
85
 
 
137
 
138
 
139
  def _register_routers(app: FastAPI) -> None:
140
+ """Register all API routers under /api/v1."""
141
  prefix = "/api/v1"
142
  app.include_router(auth.router, prefix=f"{prefix}/auth", tags=["Authentication"])
143
  app.include_router(books.router, prefix=f"{prefix}/books", tags=["Books"])
 
148
  app.include_router(links.router, prefix=f"{prefix}/links", tags=["Links"])
149
  app.include_router(superadmin.router, prefix=f"{prefix}/superadmin", tags=["Super Admin"])
150
 
 
 
 
 
151
 
152
+ def _register_public_routes(app: FastAPI) -> None:
153
+ """Public-facing pages and static assets (registered before Gradio mount)."""
154
+
155
+ if STATIC_DIR.is_dir():
156
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
157
+
158
+ @app.get("/", include_in_schema=False)
159
+ async def root() -> RedirectResponse:
160
+ """Space home → Gradio chat UI."""
161
+ return RedirectResponse(url="/ui")
162
+
163
+ @app.get("/demo", include_in_schema=False)
164
+ async def demo_redirect() -> RedirectResponse:
165
+ """Legacy demo URL → widget."""
166
+ return RedirectResponse(url="/widget")
167
+
168
+ @app.get("/widget", include_in_schema=False)
169
+ async def get_widget() -> FileResponse:
170
+ """Embeddable chat widget with CORS headers for external sites."""
171
+ path = STATIC_DIR / "addon.html"
172
+ if not path.is_file():
173
+ raise HTTPException(status_code=404, detail="Widget not found")
174
+ return FileResponse(
175
+ path,
176
+ headers={
177
+ "Access-Control-Allow-Origin": "*",
178
+ "Cache-Control": "no-cache, no-store, must-revalidate",
179
+ },
180
+ )
181
+
182
+ @app.get("/admin", include_in_schema=False)
183
+ async def get_admin() -> FileResponse:
184
+ """Author admin portal."""
185
+ path = STATIC_DIR / "admin.html"
186
+ if not path.is_file():
187
+ raise HTTPException(status_code=404, detail="Admin panel not found")
188
+ return FileResponse(path)
189
 
190
  @app.get("/health", tags=["Health"])
191
  async def health_check() -> dict:
192
+ """System health check — verifies DB, Redis, and ChromaDB."""
193
  from app.dependencies import check_health
194
  return await check_health()
195
 
 
 
196
 
197
+ fastapi_app = create_fastapi_app()
198
 
199
+ # Mount Gradio at /ui — MUST be last (wraps the FastAPI app for HF Spaces)
200
+ app = gr.mount_gradio_app(fastapi_app, demo, path="/ui")
backend/app/ui_gradio.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio chat UI mounted at /ui (Hugging Face Spaces entry point)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+ import gradio as gr
7
+
8
+ from app.config import get_settings
9
+
10
+ settings = get_settings()
11
+ API_BASE = "http://127.0.0.1:7860/api/v1"
12
+
13
+
14
+ async def _ensure_session(client: httpx.AsyncClient, token: str, session_id: str | None) -> tuple[str, str]:
15
+ """Create a chat session if needed and return (session_id, welcome)."""
16
+ if session_id:
17
+ return session_id, ""
18
+ response = await client.post(
19
+ f"{API_BASE}/chatbot/session",
20
+ headers={"X-Subscription-Token": token},
21
+ )
22
+ response.raise_for_status()
23
+ data = response.json()
24
+ return data["session_id"], data.get("welcome_message", "")
25
+
26
+
27
+ async def chat_fn(message: str, history: list, token: str, session_id: str | None):
28
+ """Send a message to the AuthorBot RAG pipeline."""
29
+ if not message or not message.strip():
30
+ return "", history, session_id
31
+
32
+ token = (token or settings.DEMO_SUBSCRIPTION_TOKEN or "").strip()
33
+ if not token:
34
+ history = history + [
35
+ {"role": "user", "content": message},
36
+ {"role": "assistant", "content": "Add DEMO_SUBSCRIPTION_TOKEN in Space secrets, or paste your subscription token above."},
37
+ ]
38
+ return "", history, session_id
39
+
40
+ try:
41
+ async with httpx.AsyncClient(timeout=120.0) as client:
42
+ session_id, welcome = await _ensure_session(client, token, session_id)
43
+ if welcome and not history:
44
+ history = history + [{"role": "assistant", "content": welcome}]
45
+
46
+ response = await client.post(
47
+ f"{API_BASE}/chatbot/chat",
48
+ headers={"X-Subscription-Token": token},
49
+ json={"session_id": session_id, "message": message.strip()},
50
+ )
51
+ response.raise_for_status()
52
+ reply = response.json().get("text", "No response.")
53
+ except Exception as exc:
54
+ reply = f"Sorry, something went wrong: {exc}"
55
+
56
+ history = history + [
57
+ {"role": "user", "content": message},
58
+ {"role": "assistant", "content": reply},
59
+ ]
60
+ return "", history, session_id
61
+
62
+
63
+ with gr.Blocks(title="AuthorBot") as demo:
64
+ gr.Markdown("# AuthorBot — AI Book Advisor")
65
+ gr.Markdown("Chat with your book-trained assistant. Set your subscription token once below.")
66
+
67
+ token_box = gr.Textbox(
68
+ label="Subscription token",
69
+ type="password",
70
+ placeholder="Paste token from SuperAdmin grant…",
71
+ value=settings.DEMO_SUBSCRIPTION_TOKEN or "",
72
+ )
73
+ chatbot = gr.Chatbot(height=420, show_label=False)
74
+ msg = gr.Textbox(placeholder="Ask about a book…", show_label=False, scale=9)
75
+ send = gr.Button("Send", variant="primary", scale=1)
76
+ session_state = gr.State(value=None)
77
+
78
+ send.click(chat_fn, [msg, chatbot, token_box, session_state], [msg, chatbot, session_state])
79
+ msg.submit(chat_fn, [msg, chatbot, token_box, session_state], [msg, chatbot, session_state])
80
+
81
+ gr.Markdown(
82
+ "**Links:** [Widget](/widget) · [Admin](/admin) · [Health](/health) · [API docs](/docs)"
83
+ )
84
+
85
+ if __name__ == "__main__":
86
+ demo.launch(server_name="0.0.0.0", server_port=7860)
backend/requirements.txt CHANGED
@@ -34,6 +34,9 @@ tiktoken==0.7.0
34
  chromadb==0.5.0
35
  numpy==1.26.4 # chromadb 0.5.x incompatible with NumPy 2.x
36
 
 
 
 
37
  # ── Free Local ML Models ──────────────────────────────────
38
  sentence-transformers==3.0.1 # MiniLM intent + book confidence
39
  torch==2.3.0 # Required by sentence-transformers
 
34
  chromadb==0.5.0
35
  numpy==1.26.4 # chromadb 0.5.x incompatible with NumPy 2.x
36
 
37
+ # ── Hugging Face UI ───────────────────────────────────────
38
+ gradio==5.9.1
39
+
40
  # ── Free Local ML Models ──────────────────────────────────
41
  sentence-transformers==3.0.1 # MiniLM intent + book confidence
42
  torch==2.3.0 # Required by sentence-transformers
backend/static/addon.html ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AuthorBot Widget</title>
7
+ <style>
8
+ body {
9
+ margin: 0; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
10
+ background: linear-gradient(135deg, #0d0f1a, #13161f); color: #94a3b8;
11
+ display: flex; align-items: center; justify-content: center; padding: 24px;
12
+ }
13
+ .card { max-width: 520px; text-align: center; }
14
+ h1 { color: #e2e8f0; margin-bottom: 8px; }
15
+ p { line-height: 1.6; }
16
+ .warn { color: #fcd34d; margin-top: 16px; font-size: 14px; }
17
+ code { background: rgba(255,255,255,0.08); padding: 2px 6px; border-radius: 4px; }
18
+ </style>
19
+ </head>
20
+ <body>
21
+ <div class="card">
22
+ <h1>AuthorBot Widget</h1>
23
+ <p>Chat bubble is in the bottom-right corner. Use <code>?token=YOUR_TOKEN</code> in the URL if needed.</p>
24
+ <p id="warn" class="warn" style="display:none">Missing token — add <code>?token=...</code> to the URL.</p>
25
+ </div>
26
+ <script>
27
+ const params = new URLSearchParams(window.location.search);
28
+ const token = params.get('token') || '';
29
+ if (!token) document.getElementById('warn').style.display = 'block';
30
+ window.AuthorBotConfig = {
31
+ token: token,
32
+ theme: params.get('theme') || 'midnight',
33
+ position: params.get('position') || 'bottom-right',
34
+ autoOpenDelay: Number(params.get('autoOpen') || 1),
35
+ apiBase: window.location.origin + '/api/v1',
36
+ };
37
+ </script>
38
+ <script src="/static/widget.js"></script>
39
+ </body>
40
+ </html>
backend/static/admin.html ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AuthorBot Admin</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
11
+ background: #0d0f1a; color: #e2e8f0; min-height: 100vh; padding: 32px 20px;
12
+ }
13
+ .wrap { max-width: 720px; margin: 0 auto; }
14
+ h1 { font-size: 1.75rem; margin-bottom: 8px; }
15
+ .sub { color: #94a3b8; margin-bottom: 28px; }
16
+ .panel {
17
+ background: #13161f; border: 1px solid rgba(255,255,255,0.08);
18
+ border-radius: 16px; padding: 24px; margin-bottom: 20px;
19
+ }
20
+ label { display: block; font-size: 13px; color: #94a3b8; margin-bottom: 6px; }
21
+ input {
22
+ width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.12);
23
+ background: #0d0f1a; color: #e2e8f0; margin-bottom: 12px;
24
+ }
25
+ button, a.btn {
26
+ display: inline-block; padding: 10px 18px; border-radius: 8px; border: none;
27
+ background: #6366f1; color: white; font-weight: 600; cursor: pointer; text-decoration: none;
28
+ }
29
+ button:hover, a.btn:hover { background: #4f46e5; }
30
+ .links { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; }
31
+ .links a { color: #a5b4fc; text-decoration: none; font-size: 14px; }
32
+ .msg { margin-top: 12px; font-size: 14px; color: #4ade80; }
33
+ .err { color: #f87171; }
34
+ #dashboard { display: none; }
35
+ </style>
36
+ </head>
37
+ <body>
38
+ <div class="wrap">
39
+ <h1>AuthorBot Admin</h1>
40
+ <p class="sub">Manage books, uploads, and settings via the API.</p>
41
+
42
+ <div class="panel" id="login-panel">
43
+ <h2 style="margin-bottom:16px;font-size:1.1rem">Sign in</h2>
44
+ <label>Email</label>
45
+ <input id="email" type="email" placeholder="you@example.com">
46
+ <label>Password</label>
47
+ <input id="password" type="password" placeholder="Password">
48
+ <button type="button" id="login-btn">Login</button>
49
+ <p id="login-msg"></p>
50
+ <p style="margin-top:12px;font-size:13px;color:#64748b">
51
+ No account? Register via <a href="/docs#/Authentication/register_api_v1_auth_register_post" style="color:#a5b4fc">API docs</a>.
52
+ </p>
53
+ </div>
54
+
55
+ <div class="panel" id="dashboard">
56
+ <p class="msg" id="welcome"></p>
57
+ <div class="links">
58
+ <a href="/ui">Open chat UI</a>
59
+ <a href="/widget">Open widget</a>
60
+ <a href="/docs">API docs</a>
61
+ <a href="/health">Health</a>
62
+ </div>
63
+ <div class="links" style="margin-top:20px">
64
+ <a href="#" id="books-link">List books (JSON)</a>
65
+ <a href="#" id="logout-link">Logout</a>
66
+ </div>
67
+ <pre id="output" style="margin-top:16px;background:#0d0f1a;padding:12px;border-radius:8px;overflow:auto;font-size:12px;display:none"></pre>
68
+ </div>
69
+ </div>
70
+
71
+ <script>
72
+ const API = window.location.origin + '/api/v1';
73
+ const token = localStorage.getItem('access_token');
74
+
75
+ function showDashboard(email) {
76
+ document.getElementById('login-panel').style.display = 'none';
77
+ document.getElementById('dashboard').style.display = 'block';
78
+ document.getElementById('welcome').textContent = 'Signed in as ' + email;
79
+ }
80
+
81
+ if (token) showDashboard(localStorage.getItem('user_email') || 'author');
82
+
83
+ document.getElementById('login-btn').onclick = async () => {
84
+ const msg = document.getElementById('login-msg');
85
+ msg.textContent = '';
86
+ msg.className = '';
87
+ try {
88
+ const res = await fetch(API + '/auth/login', {
89
+ method: 'POST',
90
+ headers: { 'Content-Type': 'application/json' },
91
+ body: JSON.stringify({
92
+ email: document.getElementById('email').value,
93
+ password: document.getElementById('password').value,
94
+ }),
95
+ });
96
+ const data = await res.json();
97
+ if (!res.ok) throw new Error(data.detail || 'Login failed');
98
+ localStorage.setItem('access_token', data.access_token);
99
+ localStorage.setItem('refresh_token', data.refresh_token);
100
+ localStorage.setItem('user_email', document.getElementById('email').value);
101
+ showDashboard(document.getElementById('email').value);
102
+ } catch (e) {
103
+ msg.textContent = e.message;
104
+ msg.className = 'err';
105
+ }
106
+ };
107
+
108
+ document.getElementById('books-link').onclick = async (e) => {
109
+ e.preventDefault();
110
+ const out = document.getElementById('output');
111
+ out.style.display = 'block';
112
+ const res = await fetch(API + '/books/', {
113
+ headers: { Authorization: 'Bearer ' + localStorage.getItem('access_token') },
114
+ });
115
+ out.textContent = JSON.stringify(await res.json(), null, 2);
116
+ };
117
+
118
+ document.getElementById('logout-link').onclick = (e) => {
119
+ e.preventDefault();
120
+ localStorage.clear();
121
+ location.reload();
122
+ };
123
+ </script>
124
+ </body>
125
+ </html>
backend/static/demo.html DELETED
@@ -1,66 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>AuthorBot Chat Demo</title>
7
- <style>
8
- * { margin: 0; padding: 0; box-sizing: border-box; }
9
- body {
10
- font-family: -apple-system, 'Inter', sans-serif;
11
- background: linear-gradient(135deg, #0d0f1a 0%, #13161f 100%);
12
- min-height: 100vh; color: #e2e8f0;
13
- display: flex; flex-direction: column; align-items: center; justify-content: center;
14
- padding: 24px;
15
- }
16
- .hero { text-align: center; max-width: 560px; }
17
- h1 { font-size: 2.5rem; font-weight: 900; margin-bottom: 14px;
18
- background: linear-gradient(135deg, #6366f1, #a78bfa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
19
- p { color: #94a3b8; font-size: 1.125rem; line-height: 1.7; margin-bottom: 24px; }
20
- .warn {
21
- background: rgba(251, 191, 36, 0.12); border: 1px solid rgba(251, 191, 36, 0.35);
22
- color: #fcd34d; padding: 12px 16px; border-radius: 12px; font-size: 14px; margin-bottom: 24px;
23
- }
24
- .controls { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-bottom: 32px; }
25
- .btn {
26
- padding: 10px 22px; border-radius: 24px; border: 1px solid rgba(255,255,255,0.12);
27
- background: rgba(255,255,255,0.06); color: #e2e8f0; cursor: pointer; font-size: 14px;
28
- font-weight: 600; font-family: inherit; text-decoration: none;
29
- }
30
- .btn:hover { background: rgba(99,102,241,0.25); border-color: #6366f1; }
31
- code { background: rgba(255,255,255,0.08); padding: 2px 6px; border-radius: 4px; font-size: 13px; }
32
- </style>
33
- </head>
34
- <body>
35
- <div class="hero">
36
- <div style="font-size:3rem;margin-bottom:16px">✨</div>
37
- <h1>AuthorBot Chat</h1>
38
- <p id="intro">Click the chat bubble in the bottom-right corner to talk to the bot.</p>
39
- <div id="token-warn" class="warn" style="display:none">
40
- Add your subscription token to the URL:<br>
41
- <code>/demo?token=YOUR_SUBSCRIPTION_TOKEN</code>
42
- </div>
43
- <div class="controls">
44
- <button class="btn" onclick="AuthorBot.open()">Open Chat</button>
45
- <button class="btn" onclick="AuthorBot.close()">Close Chat</button>
46
- <a class="btn" href="/">← Back home</a>
47
- </div>
48
- </div>
49
-
50
- <script>
51
- const params = new URLSearchParams(window.location.search);
52
- const token = params.get('token') || '';
53
- if (!token) {
54
- document.getElementById('token-warn').style.display = 'block';
55
- }
56
- window.AuthorBotConfig = {
57
- token: token,
58
- theme: params.get('theme') || 'midnight',
59
- position: params.get('position') || 'bottom-right',
60
- autoOpenDelay: Number(params.get('autoOpen') || 2),
61
- apiBase: window.location.origin + '/api/v1',
62
- };
63
- </script>
64
- <script src="/static/widget.js"></script>
65
- </body>
66
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/static/index.html DELETED
@@ -1,70 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>AuthorBot RAG</title>
7
- <style>
8
- * { box-sizing: border-box; margin: 0; padding: 0; }
9
- body {
10
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
11
- background: #0d0f1a; color: #e2e8f0; min-height: 100vh;
12
- display: flex; align-items: center; justify-content: center; padding: 24px;
13
- }
14
- .card {
15
- max-width: 640px; width: 100%;
16
- background: #13161f; border: 1px solid rgba(255,255,255,0.08);
17
- border-radius: 20px; padding: 40px;
18
- box-shadow: 0 24px 80px rgba(0,0,0,0.45);
19
- }
20
- h1 {
21
- font-size: 2rem; margin-bottom: 8px;
22
- background: linear-gradient(135deg, #6366f1, #a78bfa);
23
- -webkit-background-clip: text; -webkit-text-fill-color: transparent;
24
- }
25
- .sub { color: #94a3b8; margin-bottom: 28px; line-height: 1.6; }
26
- .links { display: flex; flex-direction: column; gap: 12px; }
27
- a.link {
28
- display: flex; align-items: center; justify-content: space-between;
29
- padding: 14px 18px; border-radius: 12px;
30
- background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08);
31
- color: #e2e8f0; text-decoration: none; transition: border-color 0.2s, background 0.2s;
32
- }
33
- a.link:hover { border-color: #6366f1; background: rgba(99,102,241,0.12); }
34
- a.link strong { font-size: 15px; }
35
- a.link span { color: #64748b; font-size: 13px; }
36
- .badge {
37
- display: inline-block; margin-top: 24px; padding: 6px 12px;
38
- border-radius: 999px; font-size: 12px; font-weight: 600;
39
- background: rgba(34,197,94,0.15); color: #4ade80; border: 1px solid rgba(34,197,94,0.3);
40
- }
41
- .note { margin-top: 20px; font-size: 13px; color: #64748b; line-height: 1.5; }
42
- code { background: rgba(255,255,255,0.06); padding: 2px 6px; border-radius: 4px; }
43
- </style>
44
- </head>
45
- <body>
46
- <div class="card">
47
- <h1>AuthorBot RAG</h1>
48
- <p class="sub">Your AI book advisor API is running on Hugging Face Spaces.</p>
49
- <div class="links">
50
- <a class="link" href="/demo">
51
- <strong>💬 Try the chatbot</strong>
52
- <span>Open demo →</span>
53
- </a>
54
- <a class="link" href="/health">
55
- <strong>❤️ Health check</strong>
56
- <span>API status →</span>
57
- </a>
58
- <a class="link" href="/docs">
59
- <strong>📖 API docs</strong>
60
- <span>Swagger UI →</span>
61
- </a>
62
- </div>
63
- <div class="badge">● Online</div>
64
- <p class="note">
65
- To use the chatbot, open <code>/demo?token=YOUR_SUBSCRIPTION_TOKEN</code>.<br>
66
- Register via <code>POST /api/v1/auth/register</code> in the API docs, then grant a subscription as SuperAdmin.
67
- </p>
68
- </div>
69
- </body>
70
- </html>