- Simplified DOMAIN_SECTIONS: only 'title' + 'questions'
- Removed separate 'About Attendance' label and 'Chronic absenteeism' blurb
- Each domain card now has one title line + one dropdown
- Domain row uses equal-height columns with flex alignment
- 'Your question' textarea now explicitly white background
- Dropdown popup/menu has solid #F3F4F6 background (100% opacity, no overlap)
- Only interactive elements (input, dropdown trigger, button, dataframe) keep visible borders
- Non-interactive wrappers are borderless
- No more gr.themes.Soft; custom CSS now drives every element
- Resets Gradio theme variables to light Catalyst palette
- Forces white input backgrounds with dark text
- Primary CTA: near-black bg with white text (visible)
- Domain cards: white bg, 5% border; dropdowns white/light
- Domain blurbs: plain muted text, no badge/pill background
- 8px radius, 14px type, Inter/sans-serif, no shadows
- Tables, footer, nudge, explainer all match light Catalyst
Redesign: full Catalyst design system from rendered DOM tokens
- Page bg: F3F4F6 (zinc-50), text: 111827 (zinc-900)
- Borders: rgba(0,0,0,0.08) nearly invisible everywhere
- No dark interactive surfaces — all white bg, dark text
- 8px border-radius, 14px dense body typography
- Emerald green accent (#059669) for positive states
- gr.themes.Soft zinc scale, matched CSS tokens
- Fixed broken CSS hover rule on sql-disclosure
- Data tables: white bg, dark text, subtle row separators
Was positioned mid-page between domain columns and first-visit nudge.
Now sits between 'Read the full explainer' button and the wiring section,
so it expands at the very bottom of the page.
Gradio wraps the HTML component in nested block divs, so
.explainer-content is never :empty even when value=''. The
:has(:empty) rule never fired. The wrapper forced white bg
+ border while the inner Gradio block rendered dark, creating
a visible rectangle.
Fix: make .explainer-wrap transparent/borderless/paddingless.
Inner block handles its own bg when content loads.
Fix dark mode at root: body_text_color #1A1A1A -> #F0F0F0
The real problem was at the theme level. Gradio's Soft theme generates
dark component backgrounds from the neutral color scale:
--background-fill-primary: var(--neutral-950) → #0b0f19 (near black)
--input-background-fill: var(--neutral-700) → #374151 (dark gray)
--block-background-fill: var(--neutral-800) → #1f2937
But body_text_color was set to #1A1A1A. Svelte-rendered components
(dataframe cells, dropdown items) read color: var(--body-text-color)
directly via scoped class names that our .gr-* selectors can't match.
Dark component backgrounds + dark body text = unreadable.
Fix: set body_text_color to off-white (#F0F0F0) at the theme level.
Light surfaces (headers, prose, footer) already have explicit CSS rules
with color: var(--ink), so they stay dark.
User CSS fixes for dark mode + virtual DOM results styling
The broad selector at line 306 already set all inputs/textareas/dropdowns to
off-white text. Two later selectors with their own !important were reversing
it back to dark ink (#1A1A1A):
- .ask-block textarea: color: var(--ink) -> var(--light-ink)
- Domain dropdown: color: var(--ink) + forced white bg -> var(--light-ink), no bg
Also removed the forced white bg from the input border rule — Gradio's dark
mode wins for backgrounds, so forcing white there was either dead or could
create white-on-white if it ever started winning.
fix: dark text on dark dataframe cells, dropdowns clear after submit, valid Gradio outputs
Dark text on dark surface in dataframe
- The 'data tables' CSS block was forcing --ink (dark) on .gr-dataframe th
and td, which is invisible against Gradio's dark-mode surface. Switched
to --light-ink (#F0F0F0) for table cells and headers, with hairline
borders adjusted to be visible on dark.
'Bring it back' InvalidComponentError
- gr.Row and gr.Group are layout primitives, not valid Gradio outputs
(Components). Returning gr.update(visible=...) for them throws
'Component not a valid output component'. Switched the ribbon row to
be always-visible, with the inner Button (bring_back_btn) toggling
visibility. Same pattern for the explainer wrap: always-present, the
inner HTML holds the visible content (empty when closed, full when
open), the close button toggles itself.
Dropdowns now clear after a query
- on_submit was yielding nothing for the three domain dropdowns, so the
prior selection persisted visually. Now yields gr.update(value=None)
for all three, so they s…
fix: off-white text in dark interactive boxes; explainer close button; uniform domain columns
Text contrast
- Add --light-ink (#F0F0F0) and --light-ink-muted (#A0A0A0) tokens.
Interactive surfaces (input, textarea, dropdown, code, dataframe) keep
Gradio's dark-mode styling when the OS is in dark mode, but their
text is now off-white instead of being forced to dark ink.
- Placeholders in input/textarea use the muted light token at full
opacity (browsers default to a dimmer rendering).
- All contrast pairs verified WCAG AA (>=4.5:1 for normal text, >=3:1
for large text) against several plausible Gradio dark-mode
backgrounds (#1A1A1A, #1F1F1F, #2A2A2A).
Domain columns
- Three columns now stretch to equal height (align-items: stretch +
min-height: 9rem).
- Content is top-aligned with a flex column: label on top, blurb
middle, dropdown pushed to the bottom.
Explainer close
- Explainer panel is now a Column wrapping the HTML content + a real
'Close' button. The 'Read the full explainer' footer button opens
it; the Close button inside hides it. Both go through th…
tighten: contrast fixes, compact header, dropdown columns, number formatting
Contrast
- Force dark text on every Gradio surface (input, dataframe, code,
status, prose) — the theme's default text color was too light against
the soft-monochrome surfaces and the body text was barely legible.
- Explicit ink-strong for the result summary, ink-muted for secondary
text (tagline, status, footer, ribbon).
Layout
- Drop the 'How this works' link from the header (it was duplicating
the footer button and bloating the top of the page).
- Header is now just title + one-line tagline.
- Domain sections moved below the input box, presented as three
columns side by side (collapses to single column on mobile). Each
column has a label, a one-line blurb, and a Gradio Dropdown of
starter questions. Selecting an option fills the input (no
auto-submit, so the user can review/edit before sending).
Result formatting
- New format_result_df() rounds floats to 2dp. Columns named with
'percent', '%', or ending in '_pct' / '_percentage' get a 'NN.NN%'
format. Columns en…
redesign: soft monochrome Query tab with instructions, domain starters, and one-step prior-answer memory
- New soft monochrome palette (#1A1A1A on #FAFAFA, 10px corners) replacing
the ResearchMono/IBM-blue theme. 10px radius on all surfaces.
- Header rewritten for non-technical users: 'Ask your school a question.' +
tagline + trust line. Removed SQL-as-centerpiece framing.
- Starter questions reorganized into three domain sections (Attendance,
Discipline, Grades) rendered as real clickable Gradio Button components
grouped by domain. Clicking a chip fills the input (no auto-submit).
- All user-facing copy extracted to ui_strings.py: title, tagline, trust
line, domain sections, input helpers, summary templates, error
rephrasings, footer copy.
- Plain-English error rephraser maps validation/timeout/model errors to
user-friendly messages ending with a suggested starter question.
- One-step prior-answer memory: the most recent successful answer is kept
in a thin ribbon above the new result, with a 'Bring it back' button
that restores the prior table. Helps users chain related…
fix: deterministic seed data — commit all 5 parquet files (LFS)
The Space only had enrollment+attendance parquet committed (.gitignore
excluded data/*.parquet), so seed_database() fell through to the Python
generator on EVERY request, and module-level random.seed() meant each
call produced different data (e.g. 1411 vs 1446 discipline rows).
- un-ignore data/*.parquet; commit all 5 via LFS, regenerated together
for cross-table consistency (verified byte-deterministic)
- app.py startup check now requires all 5 parquet files
- generate_seed_data() re-seeds the RNG per call as defense-in-depth
- handle_query is now a generator: tokens stream into the SQL panel
within seconds instead of a blank wait
- prompts.py: drop 3 redundant few-shot examples (simple patterns
covered by remaining 4) to cut bnb-4bit prefill time
- spaces.GPU(duration=30): shorter durations get ZeroGPU queue priority
- max_tokens 192 (observed outputs are ~140-170 chars)
fix: load LoRA adapter weights to CPU for ZeroGPU startup compat
llama-cpp-python pre-built CUDA 12 wheels already support SM 7.5 (T4)
and SM 8.6 (A10G). Source compilation exhausted the build VM's memory
(exit code 137, OOMKilled).
Day 1 expanded schema: 5 tables, 14B fine-tuned model
- Expanded seed data from 2 tables to 5 (enrollment, attendance, students,
discipline, grades) for cross-table analytics
- Swapped model from 7B base to lfed-qwen2.5-coder-14b-sql-gguf (v2 fine-tune)
- Updated prompts.py: chatml assistant turn format, 3 new few-shot examples
- Updated data_engine.py: dynamic 5-table parquet loading with fallback generator
- Added demographics (gender, race/ethnicity, ELL, SpEd, FRL) to students
- Added discipline incident records with severity/actions
- Added course grades with term GPA calculation
- Updated tests for 5-table schema and chatml prompt format
fix: add llama-cpp-python to requirements.txt, simplify Dockerfile
- Added llama-cpp-python to requirements.txt (was missing entirely,
causing ModuleNotFoundError on Space startup)
- Simplified Dockerfile to single-phase pip install instead of fragile
two-phase approach (install llama-cpp first, then append to reqs)
- Added --no-binary llama-cpp-python to force CUDA source build,
ensuring CMAKE_ARGS actually applies (pre-built wheels ignore it)
fix: install CUDA llama-cpp-python BEFORE spaces, add verification step