Skip to content

Commit 3c9a9c5

Browse files
committed
feat(app): polished dark mode, per-problem solution viz, no flash
Summary - Dark mode was rendering most text invisible: Streamlit's theme config fixes ``textColor`` globally to dark slate, so our dark background overlay left sidebar nav, widget labels, radio toggle, and section headings unreadable. The dark CSS branch in ``app/_common.py`` now explicitly recolours every Streamlit test-id (sidebar ``p``/``li``/``label``, ``[data-testid="stSidebarNav"] a``, ``[data-testid="stRadio"] label *``, markdown containers, captions) while excluding our intentionally coloured ``.qqa-*`` classes so the score-card / badge palette still reads correctly. - The sidebar header "streamlit app" was the Streamlit-auto nav label derived from the entry filename. Added ``sidebar_brand()`` which injects a QQA brand block at the top of every page and hides the auto heading via CSS. Paired with ``paper_link_footer()`` and ``hero_badges()`` so the OpenReview paper, GitHub repo, and live demo are one click away from anywhere in the app. - The live Plotly dynamics / population / diversity charts flashed on every callback update. Added stable ``key=`` arguments (``qqa_solve_dynamics``, ``qqa_solve_population``, ``qqa_solve_diversity``) so Streamlit updates the existing chart in place rather than re-mounting the DOM element. - New ``app/_solution_viz.py`` module dispatches on ``cfg["kind"]`` to professional per-problem renderers: MIS / MaxClique / VertexCover (graph with highlighted set), MaxCut (edges coloured by cut), GraphBisection, Coloring (node colours + conflict edges), TSP (tour path with visit-order annotations), QAP (assignment matrix), NQueens (chessboard with queens + conflict detection), Knapsack (value bars + capacity budget), NumberPartitioning (subset-sum balance), MaxSAT3 (satisfaction pie), Ising1D / SK / EA (spin strip or 2-D grid), Perceptron (signed-margin bars), Hopfield (overlap with stored patterns). Every renderer uses ``plotly_layout()`` so the theme stays coherent. - Solve page renders the new solution view inline after a successful run; Visualize page's Solution tab leads with the new view and keeps the generic heatmap inside an expander. - ``tests/test_gui_apptest.py`` gains ``test_solution_viz_smoke_across_problem_kinds`` which drives every registered renderer end-to-end via AppTest. Verified locally - ``uv run ruff check src tests scripts app`` — clean. - ``uv run ruff format --check src tests scripts app`` — clean. - ``uv run pytest -q`` — 53 passed, 1 skipped (plotly-fallback path).
1 parent 7284532 commit 3c9a9c5

7 files changed

Lines changed: 1239 additions & 14 deletions

File tree

app/_common.py

Lines changed: 199 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,25 @@ def apply_theme() -> None:
179179
background: {p["bg_sidebar"]};
180180
border-right: 1px solid var(--qqa-border);
181181
}}
182+
/* Hide the auto-generated multipage heading ("streamlit app")
183+
rendered above our brand block. */
184+
[data-testid="stSidebarNav"]::before {{ display: none; }}
185+
section[data-testid="stSidebar"] [data-testid="stSidebarNav"] > div:first-child,
186+
section[data-testid="stSidebar"] [data-testid="stSidebarNav"] > ul + div:has(>h1),
187+
section[data-testid="stSidebar"] [data-testid="stSidebarNav"] h1:first-of-type,
188+
section[data-testid="stSidebar"] [data-testid="stSidebarNav"] h2:first-of-type {{
189+
display: none !important;
190+
}}
191+
[data-testid="stSidebarNav"] a {{
192+
color: var(--qqa-text) !important;
193+
font-weight: 500;
194+
border-radius: 6px;
195+
}}
196+
[data-testid="stSidebarNav"] a:hover {{
197+
background: rgba(15,118,110,0.10) !important;
198+
color: var(--qqa-accent) !important;
199+
}}
200+
[data-testid="stSidebarNav"] a span {{ color: inherit !important; }}
182201
h1, h2, h3, h4 {{
183202
font-family: 'Source Serif 4', Georgia, serif;
184203
color: var(--qqa-text);
@@ -345,6 +364,37 @@ def apply_theme() -> None:
345364
--qqa-accent2: {p["accent2"]};
346365
}}
347366
html, body, [class*="css"] {{ font-family: 'Inter', sans-serif; }}
367+
/* Dark-mode text contrast. The Streamlit theme is declared ``light``
368+
in config.toml so that ``textColor`` defaults to a dark tone; the
369+
rules below re-colour every bit of Streamlit-managed text so the
370+
dark background stays readable. Our intentionally-coloured
371+
classes (.qqa-badge, .qqa-score .value, ...) are excluded. */
372+
.stApp div[data-testid="stMarkdownContainer"] p,
373+
.stApp div[data-testid="stMarkdownContainer"] li,
374+
.stApp div[data-testid="stMarkdownContainer"] span:not([class*="qqa-"]),
375+
.stApp div[data-testid="stMarkdownContainer"] strong,
376+
.stApp div[data-testid="stMarkdownContainer"] em {{
377+
color: #e8edf7 !important;
378+
}}
379+
.stApp a {{ color: #93c5fd !important; }}
380+
.stApp a:hover {{ color: #bfdbfe !important; }}
381+
section[data-testid="stSidebar"] p,
382+
section[data-testid="stSidebar"] li,
383+
section[data-testid="stSidebar"] label,
384+
section[data-testid="stSidebar"] h1,
385+
section[data-testid="stSidebar"] h2,
386+
section[data-testid="stSidebar"] h3,
387+
section[data-testid="stSidebar"] h4,
388+
section[data-testid="stSidebar"] [data-testid="stMarkdownContainer"] *,
389+
section[data-testid="stSidebar"] [data-testid="stSidebarNav"] span,
390+
section[data-testid="stSidebar"] [data-testid="stRadio"] label,
391+
section[data-testid="stSidebar"] [data-testid="stSelectbox"] label,
392+
section[data-testid="stSidebar"] [data-testid="stNumberInput"] label,
393+
section[data-testid="stSidebar"] [data-testid="stSlider"] label,
394+
section[data-testid="stSidebar"] [data-testid="stCaptionContainer"] * {{
395+
color: #e8edf7 !important;
396+
}}
397+
section[data-testid="stSidebar"] svg {{ fill: #e8edf7 !important; }}
348398
.stApp {{
349399
background:
350400
radial-gradient(900px 500px at 10% -10%, rgba(56,189,248,0.18), transparent 60%),
@@ -357,9 +407,27 @@ def apply_theme() -> None:
357407
backdrop-filter: blur(14px);
358408
border-right: 1px solid var(--qqa-border);
359409
}}
360-
h1, h2, h3, h4 {{
410+
/* Same nav-header suppression as in light theme. */
411+
[data-testid="stSidebarNav"]::before {{ display: none; }}
412+
section[data-testid="stSidebar"] [data-testid="stSidebarNav"] > div:first-child,
413+
section[data-testid="stSidebar"] [data-testid="stSidebarNav"] > ul + div:has(>h1),
414+
section[data-testid="stSidebar"] [data-testid="stSidebarNav"] h1:first-of-type,
415+
section[data-testid="stSidebar"] [data-testid="stSidebarNav"] h2:first-of-type {{
416+
display: none !important;
417+
}}
418+
[data-testid="stSidebarNav"] a {{
419+
color: #e8edf7 !important;
420+
font-weight: 500;
421+
border-radius: 6px;
422+
}}
423+
[data-testid="stSidebarNav"] a:hover {{
424+
background: rgba(56,189,248,0.14) !important;
425+
color: #bae6fd !important;
426+
}}
427+
[data-testid="stSidebarNav"] a span {{ color: inherit !important; }}
428+
h1, h2, h3, h4, h5, h6 {{
361429
font-family: 'Source Serif 4', serif;
362-
color: #f8fafc;
430+
color: #f8fafc !important;
363431
letter-spacing: -0.01em;
364432
}}
365433
h1 {{
@@ -368,6 +436,16 @@ def apply_theme() -> None:
368436
background-clip: text;
369437
-webkit-text-fill-color: transparent;
370438
}}
439+
/* Caption / muted text in dark mode stays readable but softer. */
440+
.stCaption, [data-testid="stCaptionContainer"] p {{
441+
color: #94a3b8 !important;
442+
font-variant: small-caps;
443+
letter-spacing: 0.04em;
444+
}}
445+
/* Radio-toggle label ("light" / "dark") on the sidebar. */
446+
section[data-testid="stSidebar"] [data-testid="stRadio"] label p {{
447+
color: #e8edf7 !important;
448+
}}
371449
div[data-testid="stMetric"] {{
372450
background: rgba(15,23,42,0.55);
373451
padding: 0.7rem 1rem;
@@ -472,6 +550,125 @@ def apply_theme() -> None:
472550
st.markdown(css, unsafe_allow_html=True)
473551

474552

553+
# ---------------------------------------------------------------------------
554+
# Sidebar brand + paper-link footer (used by every page)
555+
# ---------------------------------------------------------------------------
556+
557+
# URLs surfaced in the UI. Keep in sync with [project.urls] in pyproject.toml.
558+
_PAPER_URL = "https://openreview.net/forum?id=9EfBeXaXf0"
559+
_GITHUB_URL = "https://github.com/Yuma-Ichikawa/QQA4CO"
560+
_DEMO_URL = "https://parallelquasiquantum4co.streamlit.app/"
561+
562+
563+
def sidebar_brand() -> None:
564+
"""Render the QQA brand block at the top of the sidebar.
565+
566+
Also hides the auto ``streamlit app`` heading injected by Streamlit's
567+
built-in multipage navigator (CSS takes care of that — this function
568+
just adds the branded block above the nav).
569+
"""
570+
theme = get_theme()
571+
accent = "#0f766e" if theme == "light" else "#38bdf8"
572+
accent2 = "#be5a3c" if theme == "light" else "#a855f7"
573+
muted = "#64748b" if theme == "light" else "#94a3b8"
574+
with st.sidebar:
575+
st.markdown(
576+
f"""
577+
<div class="qqa-brand" style="
578+
padding: 0.75rem 0.4rem 0.9rem 0.4rem;
579+
margin-bottom: 0.4rem;
580+
border-bottom: 1px solid rgba(148,163,184,0.2);
581+
">
582+
<div style="display:flex;align-items:center;gap:0.55rem;">
583+
<div style="
584+
width:32px;height:32px;border-radius:8px;
585+
background:linear-gradient(135deg,{accent} 0%,{accent2} 100%);
586+
display:flex;align-items:center;justify-content:center;
587+
color:#fff;font-weight:700;font-family:'Source Serif 4',serif;
588+
box-shadow:0 2px 6px rgba(15,23,42,0.18);
589+
">Q</div>
590+
<div>
591+
<div style="
592+
font-family:'Source Serif 4',Georgia,serif;
593+
font-weight:700;font-size:1.05rem;line-height:1.1;
594+
">QQA4CO</div>
595+
<div style="font-size:0.7rem;color:{muted};letter-spacing:0.04em;">
596+
Quasi-Quantum Annealing
597+
</div>
598+
</div>
599+
</div>
600+
</div>
601+
""",
602+
unsafe_allow_html=True,
603+
)
604+
605+
606+
def paper_link_footer() -> None:
607+
"""Compact link row rendered at the very bottom of the sidebar."""
608+
theme = get_theme()
609+
muted = "#64748b" if theme == "light" else "#94a3b8"
610+
link = "#0f766e" if theme == "light" else "#93c5fd"
611+
with st.sidebar:
612+
st.markdown(
613+
f"""
614+
<div style="
615+
margin-top: 1.2rem;
616+
padding-top: 0.8rem;
617+
border-top: 1px solid rgba(148,163,184,0.2);
618+
font-size: 0.78rem;
619+
color: {muted};
620+
">
621+
<div style="margin-bottom:0.3rem;letter-spacing:0.04em;
622+
text-transform:uppercase;font-size:0.66rem;">
623+
References
624+
</div>
625+
<div style="display:flex;flex-direction:column;gap:0.3rem;">
626+
<a href="{_PAPER_URL}" target="_blank" style="color:{link};
627+
text-decoration:none;">📄 Paper (OpenReview)</a>
628+
<a href="{_GITHUB_URL}" target="_blank" style="color:{link};
629+
text-decoration:none;">⎇ GitHub repository</a>
630+
<a href="{_DEMO_URL}" target="_blank" style="color:{link};
631+
text-decoration:none;">🚀 Hosted live demo</a>
632+
</div>
633+
</div>
634+
""",
635+
unsafe_allow_html=True,
636+
)
637+
638+
639+
def hero_badges() -> None:
640+
"""Horizontal badge row for the Home hero (paper / repo / demo)."""
641+
theme = get_theme()
642+
accent = "#0f766e" if theme == "light" else "#38bdf8"
643+
border = "#d4d0c3" if theme == "light" else "rgba(148,163,184,0.25)"
644+
bg = "#ffffff" if theme == "light" else "rgba(15,23,42,0.55)"
645+
st.markdown(
646+
f"""
647+
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;margin:0.3rem 0 0.9rem 0;">
648+
<a href="{_PAPER_URL}" target="_blank" style="
649+
display:inline-flex;align-items:center;gap:0.35rem;
650+
padding:0.26rem 0.7rem;border:1px solid {accent};
651+
background:{bg};color:{accent};border-radius:999px;
652+
text-decoration:none;font-size:0.8rem;font-weight:600;
653+
">📄 OpenReview paper</a>
654+
<a href="{_GITHUB_URL}" target="_blank" style="
655+
display:inline-flex;align-items:center;gap:0.35rem;
656+
padding:0.26rem 0.7rem;border:1px solid {border};
657+
background:{bg};color:inherit;border-radius:999px;
658+
text-decoration:none;font-size:0.8rem;font-weight:600;
659+
">⎇ Source on GitHub</a>
660+
<a href="{_DEMO_URL}" target="_blank" style="
661+
display:inline-flex;align-items:center;gap:0.35rem;
662+
padding:0.26rem 0.7rem;border:1px solid {border};
663+
background:{bg};color:inherit;border-radius:999px;
664+
text-decoration:none;font-size:0.8rem;font-weight:600;
665+
">🚀 Live demo</a>
666+
</div>
667+
""",
668+
unsafe_allow_html=True,
669+
)
670+
671+
475672
def render_score_card(score: dict, raw_loss: float | None = None) -> None:
476673
"""Render the big problem-specific score tile used by the Solve page."""
477674
if not score:

0 commit comments

Comments
 (0)