buglens / app.py
vatche.thorossian
Fix JSX MIME type on Spaces
e7f005d
Raw
History Blame Contribute Delete
4.01 kB
"""BugLens entrypoint — a custom frontend served on Gradio's Server (FastAPI).
`gradio.Server` is a FastAPI subclass with Gradio's API engine (queue, SSE,
concurrency) layered on top. We use it to serve the custom `buglens-ui` React
prototype as the frontend (the Off-Brand path) while exposing the analysis as
a backend endpoint.
Example screenshots are always served from the built-in mock reports (so the
app is fully explorable without a GPU). Uploaded screenshots are analyzed for
real by POSTing to the Modal backend when BUGLENS_BACKEND=modal.
"""
from __future__ import annotations
import base64
import binascii
import mimetypes
from pathlib import Path
from fastapi import HTTPException
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from gradio import Server
from pydantic import BaseModel
from buglens import backend
from buglens.config import load_settings
from buglens.examples import get_example, list_examples
from buglens.render import (
regression_tests_to_csv,
to_github_issue,
to_jira_markdown,
)
from buglens.schema import BugReport
UI_DIR = Path(__file__).parent / "buglens-ui"
INDEX_FILE = UI_DIR / "BugLens.html"
mimetypes.add_type("text/babel", ".jsx")
server = Server(title="BugLens")
def analyze(example_id: str | None = None) -> BugReport:
"""Return a built-in example report for gallery clicks and exports."""
report = get_example(example_id) if example_id else None
if report is None:
raise HTTPException(status_code=404, detail="Unknown example id.")
return report
@server.get("/api/examples")
def api_examples() -> list[dict]:
"""Gallery metadata for the example screenshots."""
return list_examples()
@server.get("/api/analyze")
def api_analyze(id: str | None = None) -> dict:
"""Return a built-in example's report payload (gallery clicks)."""
return analyze(id).to_ui_payload()
class AnalyzeUpload(BaseModel):
"""JSON body for analyzing an uploaded screenshot."""
image_b64: str
note: str = ""
@server.post("/api/analyze")
def api_analyze_upload(payload: AnalyzeUpload) -> dict:
"""Analyze an uploaded screenshot via the configured inference backend."""
settings = load_settings()
if settings.backend.value != "modal":
raise HTTPException(
status_code=501,
detail=(
"Live screenshot analysis needs the Modal backend "
"(set BUGLENS_BACKEND=modal). Pick an example to preview the app."
),
)
try:
image_bytes = base64.b64decode(payload.image_b64, validate=True)
except (ValueError, binascii.Error) as exc:
raise HTTPException(status_code=400, detail="Invalid image data.") from exc
try:
return backend.analyze_via_modal(settings.modal_endpoint, image_bytes, payload.note)
except backend.BackendError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
@server.get("/api/export/{fmt}")
def api_export(fmt: str, id: str | None = None):
"""Return a text export (jira | github | csv) for an example."""
report = analyze(id)
if fmt == "jira":
return {"format": "jira", "text": to_jira_markdown(report)}
if fmt == "github":
return {"format": "github", "text": to_github_issue(report)}
if fmt == "csv":
return {"format": "csv", "text": regression_tests_to_csv(report)}
raise HTTPException(status_code=404, detail="Unknown export format.")
@server.get("/")
def index() -> FileResponse:
"""Serve the custom BugLens frontend."""
return FileResponse(INDEX_FILE)
# Frontend assets (the jsx modules) under a dedicated prefix so we never shadow
# Gradio's own /gradio_api/* routes or our /api/* endpoints.
server.mount("/static", StaticFiles(directory=UI_DIR), name="static")
def main() -> None:
"""Launch the BugLens server."""
server.launch(server_name="0.0.0.0", server_port=7860)
if __name__ == "__main__":
main()