| """Screen Companion — Gradio Server frontend. |
| |
| Serves the custom HTML landing page and proxies /api/analyze requests to |
| the Modal backend. Run with SCREEN_COMPANION_MOCK=1 for local testing. |
| """ |
| import os |
| from pathlib import Path |
|
|
| import httpx |
| from fastapi.responses import HTMLResponse |
| from fastapi.staticfiles import StaticFiles |
| from gradio import Server |
| from pydantic import BaseModel |
|
|
| app = Server() |
|
|
| BACKEND_URL = os.environ.get( |
| "SCREEN_COMPANION_API_URL", |
| "https://rafalbogusdxc--screen-companion-api.modal.run", |
| ).rstrip("/") |
|
|
| MOCK = os.environ.get("SCREEN_COMPANION_MOCK", "").strip().lower() in ("1", "true", "yes") |
|
|
| STATIC_DIR = Path(__file__).parent / "static" |
|
|
| http = httpx.AsyncClient(base_url=BACKEND_URL, timeout=120, follow_redirects=True) |
|
|
|
|
| class AnalyzeRequest(BaseModel): |
| image_b64: str |
| memories: str = "" |
|
|
|
|
| @app.get("/", response_class=HTMLResponse) |
| async def homepage(): |
| return (Path(__file__).parent / "index.html").read_text() |
|
|
|
|
| app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") |
|
|
|
|
| @app.post("/api/analyze") |
| async def proxy_analyze(req: AnalyzeRequest): |
| if MOCK: |
| import random |
| observations = [ |
| "user editing code in IDE", |
| "browser with documentation open", |
| "terminal with test output", |
| "same as before", |
| "user reading article", |
| ] |
| msgs = [ |
| ("Nice progress! That code looks clean.", "happy"), |
| ("Hmm, looks like you're deep in thought...", "neutral"), |
| ("You've been at this a while - remember to stretch!", "happy"), |
| ("Ooh, interesting problem you're working on!", "excited"), |
| ("Errors happen to everyone. You'll figure it out!", "sad"), |
| ("That's a great approach, keep going!", "excited"), |
| ("I see you're reading docs - smart move!", "happy"), |
| ("Take a deep breath, you've got this!", "neutral"), |
| (None, "neutral"), |
| (None, "happy"), |
| (None, "neutral"), |
| ] |
| msg, emotion = random.choice(msgs) |
| obs = random.choice(observations) |
| return {"message": msg, "emotion": emotion, "observation": obs} |
|
|
| try: |
| r = await http.post("/analyze-screen", json={ |
| "image_b64": req.image_b64, |
| "memories": req.memories, |
| }) |
| r.raise_for_status() |
| return r.json() |
| except httpx.ConnectError: |
| return {"message": "Waking up... one moment.", "emotion": "neutral", "observation": "backend cold starting"} |
| except httpx.ReadTimeout: |
| return {"message": "Hmm, lost my train of thought.", "emotion": "neutral", "observation": "response timed out"} |
| except Exception as e: |
| print(f"[screen-companion] proxy error: {type(e).__name__}: {e}") |
| return {"message": None, "emotion": "neutral", "observation": f"error: {type(e).__name__}"} |
|
|
|
|
| @app.get("/api/health") |
| async def proxy_health(): |
| if MOCK: |
| return {"status": "mock", "model": "SmolVLM-2B (mock mode)"} |
| try: |
| r = await http.get("/health", timeout=10) |
| r.raise_for_status() |
| return r.json() |
| except Exception: |
| return {"status": "unreachable", "url": BACKEND_URL} |
|
|
|
|
| if __name__ == "__main__": |
| app.launch(server_name="0.0.0.0", server_port=7860) |
|
|