From fb9674fcdb05234ed5ad69ce922cbc2d9c517382 Mon Sep 17 00:00:00 2001 From: Pawel Date: Fri, 5 Jun 2026 06:13:33 -0400 Subject: [PATCH] Web-demo styling --- webapp/app.py | 13 +++- webapp/static/app.css | 44 +++++++++++- webapp/static/app.js | 12 ++++ webapp/static/index.html | 141 +++++++++++++++++---------------------- 4 files changed, 125 insertions(+), 85 deletions(-) diff --git a/webapp/app.py b/webapp/app.py index d8a0921..9b4584e 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -14,6 +14,7 @@ from __future__ import annotations import hashlib import os +import re import sys import time from contextlib import asynccontextmanager @@ -23,7 +24,7 @@ from typing import Any import pandas as pd import torch from fastapi import FastAPI, HTTPException -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, HTMLResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field @@ -58,6 +59,7 @@ KNOWN_ROUTES_PATH = Path( os.getenv("CBGPT_KNOWN_ROUTES_PATH", TOKENIZED_DIR / "route_sequences.csv") ) +STATIC_DIR = REPO_ROOT / "webapp" / "static" GENERATOR_PATH = MODEL_DIR / "joint_route_gpt_generator.pth" GRADE_MODEL_PATH = MODEL_DIR / "joint_transformer_grade_predictor.pth" @@ -547,8 +549,13 @@ app.mount("/board-images", StaticFiles(directory=REPO_ROOT / "images"), name="bo @app.get("/") def index(): - """Serve the single-page web UI.""" - return FileResponse(REPO_ROOT / "webapp" / "static" / "index.html") + """Serve the single-page web UI with auto-versioned static asset URLs.""" + html = (STATIC_DIR / "index.html").read_text(encoding="utf-8") + css_v = _file_version(STATIC_DIR / "app.css") + js_v = _file_version(STATIC_DIR / "app.js") + html = re.sub(r'app\.css\?v=[^\s"\']+', f"app.css?v={css_v}", html) + html = re.sub(r'app\.js\?v=[^\s"\']+', f"app.js?v={js_v}", html) + return HTMLResponse(content=html) @app.get("/api/health") diff --git a/webapp/static/app.css b/webapp/static/app.css index 3182661..165c20c 100644 --- a/webapp/static/app.css +++ b/webapp/static/app.css @@ -86,11 +86,18 @@ body { .layout { display: grid; grid-template-columns: 22rem minmax(0, 1fr); + grid-template-rows: auto 1fr; + grid-template-areas: + "col-top col-viewer" + "col-info col-viewer"; gap: 2rem; padding: 2rem 1rem 1rem; max-width: 78rem; margin: 0 auto; } +#col-top { grid-area: col-top; } +#col-viewer { grid-area: col-viewer; } +#col-info { grid-area: col-info; } /* Control cards, form fields, and action buttons. */ .controls { @@ -181,6 +188,32 @@ button.secondary:hover { gap: 0.55rem; } +/* Collapsible cards: clicking the h2 toggles visibility of card body. + Only cards with class .collapsible get this behavior. */ +.card.collapsible > h2 { + display: flex; + align-items: center; + cursor: pointer; + user-select: none; +} +.card.collapsible > h2::after { + content: "▾"; + font-size: 2rem; + color: var(--muted); + margin-left: auto; + padding-left: 0.5rem; + flex-shrink: 0; +} +.card.collapsible.collapsed > h2::after { + content: "▸"; +} +.card.collapsible > h2:hover::after { + color: var(--off-fg); +} +.card.collapsible.collapsed > *:not(h2) { + display: none; +} + .note p, .small { color: var(--off-fg); font-size: 0.82rem; @@ -284,9 +317,16 @@ button.secondary:hover { font-size: 0.76rem; } -/* Stack controls above the board on narrower screens. */ +/* On narrower screens, stack controls → viewer → info. */ @media (max-width: 900px) { - .layout { grid-template-columns: 1fr; } + .layout { + grid-template-columns: 1fr; + grid-template-rows: auto auto auto; + grid-template-areas: + "col-top" + "col-viewer" + "col-info"; + } .site-header { flex-direction: column; } } diff --git a/webapp/static/app.js b/webapp/static/app.js index 28b63e6..6701785 100644 --- a/webapp/static/app.js +++ b/webapp/static/app.js @@ -690,6 +690,16 @@ async function predict() { } +/** Initialize collapsible cards: clicking the h2 toggles the .collapsed class. */ +function initCollapsibleCards() { + document.querySelectorAll(".card.collapsible > h2").forEach((heading) => { + const card = heading.parentElement; + heading.addEventListener("click", () => { + card.classList.toggle("collapsed"); + }); + }); +} + /** Attach a listener only when optional markup exists. */ function addListenerIfPresent(id, eventName, handler) { const element = $(id); @@ -717,6 +727,8 @@ async function init() { await setBoardBackground("tb2"); syncAngleSelectors(40); + initCollapsibleCards(); + addListenerIfPresent("gen-board", "change", async (e) => { syncBoardSelectors(e.target.value); await setBoardBackground(e.target.value); diff --git a/webapp/static/index.html b/webapp/static/index.html index 7f8a610..c462f11 100644 --- a/webapp/static/index.html +++ b/webapp/static/index.html @@ -4,10 +4,9 @@ ClimbingBoardGPT - + -
- -
-
+ +
+

Generate a climb

-
+

Click holds to build a climb