pasternake commited on
Commit
b90345a
·
verified ·
1 Parent(s): d8c39de

feat(ui): light/dark theme toggle (PR #7)

Browse files
Files changed (3) hide show
  1. README.md +5 -0
  2. app.py +47 -0
  3. 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
- # Warm latency is ~50s, but a cold container must download + load the ~16 GB
21
- # model first, which pushes the first request past 2 min. 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. 'gemma3:4b'); enables the local-LLM path."""
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