Spaces:
Sleeping
Sleeping
feat(ui): light/dark theme toggle (PR #7)
Browse files- README.md +5 -0
- app.py +47 -0
- audit_client.py +3 -3
README.md
CHANGED
|
@@ -20,3 +20,8 @@ Brief-aware social post audit for the [build-small-hackathon](https://huggingfac
|
|
| 20 |
**Host:** Rule linters + deterministic score recomputation
|
| 21 |
|
| 22 |
Set Space secret `MODAL_AUDIT_URL` to your deployed Modal web endpoint base URL (without trailing `/audit`).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
**Host:** Rule linters + deterministic score recomputation
|
| 21 |
|
| 22 |
Set Space secret `MODAL_AUDIT_URL` to your deployed Modal web endpoint base URL (without trailing `/audit`).
|
| 23 |
+
|
| 24 |
+
## Authors
|
| 25 |
+
|
| 26 |
+
- Eugene Pasternak — [`pasternake`](https://huggingface.co/pasternake)
|
| 27 |
+
- Pavel Trubin — [`agsagds`](https://huggingface.co/agsagds)
|
app.py
CHANGED
|
@@ -113,6 +113,49 @@ def load_example(example: dict):
|
|
| 113 |
return example["platform"], example["goal"], example["audience"], example["post"]
|
| 114 |
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
# Page identity matches the report (render.py): cool slate instrument panel,
|
| 117 |
# Space Grotesk display, IBM Plex Sans body, IBM Plex Mono labels/codes.
|
| 118 |
_THEME = gr.themes.Base(
|
|
@@ -204,6 +247,7 @@ with gr.Blocks(title="Post Audit", theme=_THEME, css=_PAGE_CSS) as demo:
|
|
| 204 |
audit_btn = gr.Button("Run audit", variant="primary")
|
| 205 |
ex_webinar = gr.Button("Load example: weak webinar CTA")
|
| 206 |
ex_chat = gr.Button("Load example: chat dump")
|
|
|
|
| 207 |
|
| 208 |
status_report = gr.HTML()
|
| 209 |
|
|
@@ -224,6 +268,9 @@ with gr.Blocks(title="Post Audit", theme=_THEME, css=_PAGE_CSS) as demo:
|
|
| 224 |
fn=lambda: load_example(EXAMPLE_CHAT_DUMP),
|
| 225 |
outputs=[platform, goal, audience, post],
|
| 226 |
)
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
if __name__ == "__main__":
|
| 229 |
demo.launch(theme=_THEME, css=_PAGE_CSS)
|
|
|
|
| 113 |
return example["platform"], example["goal"], example["audience"], example["post"]
|
| 114 |
|
| 115 |
|
| 116 |
+
# Dark mode in Gradio is just a `dark` class on <body> (the served bundle does
|
| 117 |
+
# `document.body.classList.add("dark")`); toggling it flips every theme CSS var to
|
| 118 |
+
# its `_dark` variant. The report/status cards pin their own light colors
|
| 119 |
+
# (color-scheme:light) on purpose, so they stay legible either way.
|
| 120 |
+
#
|
| 121 |
+
# Standalone-only by design: this matches Gradio's *standalone* mode (a hosted HF
|
| 122 |
+
# Space — our deploy target). If this app is ever embedded as a <gradio-app> on
|
| 123 |
+
# another site, Gradio puts `dark` on the host's parentElement and renders into a
|
| 124 |
+
# shadow root, so both `document.body` and `getElementById` below would miss and
|
| 125 |
+
# the toggle would silently no-op.
|
| 126 |
+
#
|
| 127 |
+
# We persist the choice to localStorage so a manual toggle survives a reload;
|
| 128 |
+
# `_THEME_INIT_JS` re-applies it on load (otherwise refresh resets to the system
|
| 129 |
+
# preference / ?__theme). The imperative `textContent` write is safe only because
|
| 130 |
+
# `theme_btn` is never wired as an event *output* — Svelte never re-renders the
|
| 131 |
+
# button, so it won't clobber our label. Don't add it as an output without moving
|
| 132 |
+
# the labeling into the handler's return value.
|
| 133 |
+
_THEME_TOGGLE_JS = """
|
| 134 |
+
() => {
|
| 135 |
+
const dark = document.body.classList.toggle('dark');
|
| 136 |
+
try { localStorage.setItem('pa-theme', dark ? 'dark' : 'light'); } catch (e) {}
|
| 137 |
+
const b = document.getElementById('pa-theme-toggle');
|
| 138 |
+
if (b) b.textContent = dark ? '☀ Light mode' : '☾ Dark mode';
|
| 139 |
+
}
|
| 140 |
+
"""
|
| 141 |
+
|
| 142 |
+
# Re-apply the saved choice and label the button for the *current* state on load.
|
| 143 |
+
# Without the saved choice the app falls back to whatever Gradio picked (system
|
| 144 |
+
# preference or ?__theme=dark) before the user ever clicks.
|
| 145 |
+
_THEME_INIT_JS = """
|
| 146 |
+
() => {
|
| 147 |
+
let dark = document.body.classList.contains('dark');
|
| 148 |
+
try {
|
| 149 |
+
const saved = localStorage.getItem('pa-theme');
|
| 150 |
+
if (saved === 'dark' && !dark) { document.body.classList.add('dark'); dark = true; }
|
| 151 |
+
else if (saved === 'light' && dark) { document.body.classList.remove('dark'); dark = false; }
|
| 152 |
+
} catch (e) {}
|
| 153 |
+
const b = document.getElementById('pa-theme-toggle');
|
| 154 |
+
if (b) b.textContent = dark ? '☀ Light mode' : '☾ Dark mode';
|
| 155 |
+
}
|
| 156 |
+
"""
|
| 157 |
+
|
| 158 |
+
|
| 159 |
# Page identity matches the report (render.py): cool slate instrument panel,
|
| 160 |
# Space Grotesk display, IBM Plex Sans body, IBM Plex Mono labels/codes.
|
| 161 |
_THEME = gr.themes.Base(
|
|
|
|
| 247 |
audit_btn = gr.Button("Run audit", variant="primary")
|
| 248 |
ex_webinar = gr.Button("Load example: weak webinar CTA")
|
| 249 |
ex_chat = gr.Button("Load example: chat dump")
|
| 250 |
+
theme_btn = gr.Button("☾ Dark mode", elem_id="pa-theme-toggle", scale=0)
|
| 251 |
|
| 252 |
status_report = gr.HTML()
|
| 253 |
|
|
|
|
| 268 |
fn=lambda: load_example(EXAMPLE_CHAT_DUMP),
|
| 269 |
outputs=[platform, goal, audience, post],
|
| 270 |
)
|
| 271 |
+
theme_btn.click(fn=None, inputs=None, outputs=None, js=_THEME_TOGGLE_JS)
|
| 272 |
+
|
| 273 |
+
demo.load(fn=None, inputs=None, outputs=None, js=_THEME_INIT_JS)
|
| 274 |
|
| 275 |
if __name__ == "__main__":
|
| 276 |
demo.launch(theme=_THEME, css=_PAGE_CSS)
|
audit_client.py
CHANGED
|
@@ -17,13 +17,13 @@ def get_modal_url() -> str | None:
|
|
| 17 |
|
| 18 |
|
| 19 |
def get_modal_timeout() -> float:
|
| 20 |
-
#
|
| 21 |
-
#
|
| 22 |
return float(os.environ.get("MODAL_AUDIT_TIMEOUT", "300"))
|
| 23 |
|
| 24 |
|
| 25 |
def get_ollama_model() -> str | None:
|
| 26 |
-
"""Local Ollama model tag (e.g. '
|
| 27 |
return os.environ.get("OLLAMA_MODEL")
|
| 28 |
|
| 29 |
|
|
|
|
| 17 |
|
| 18 |
|
| 19 |
def get_modal_timeout() -> float:
|
| 20 |
+
# A cold Modal container loads the quantized model (and on a fresh deploy
|
| 21 |
+
# pulls the GGUF), which can push the first request past a minute. Generous default.
|
| 22 |
return float(os.environ.get("MODAL_AUDIT_TIMEOUT", "300"))
|
| 23 |
|
| 24 |
|
| 25 |
def get_ollama_model() -> str | None:
|
| 26 |
+
"""Local Ollama model tag (e.g. 'gemma4:e4b'); enables the local-LLM path."""
|
| 27 |
return os.environ.get("OLLAMA_MODEL")
|
| 28 |
|
| 29 |
|