File size: 15,427 Bytes
20b15f3 d7dd595 20b15f3 d7dd595 20b15f3 d7dd595 20b15f3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | """
ui/theme.py
-----------
NOVA's look: a Gradio theme object for the component primitives, plus the CSS
that carries the actual identity.
Ported from the Streamlit build's utils/styles.py — same palette, same brand
header, same speech bubbles, same cluster/paper cards, same live checklist.
Gradio hands us a real DOM instead of Streamlit's opaque `data-testid` tree, so
everything here hooks `elem_classes`/`elem_id` we set ourselves rather than
selectors that break on the next framework release.
FORCE_DARK is a js hook, not CSS: Gradio decides light/dark from a URL param
before our stylesheet loads, and the palette below only exists in dark.
"""
import gradio as gr
NOVA_THEME = gr.themes.Base(
primary_hue=gr.themes.Color(
c50="#f2efff", c100="#e5dfff", c200="#cabfff", c300="#b09fff",
c400="#957fff", c500="#7c5cff", c600="#6344e6", c700="#4a2fbf",
c800="#331f99", c900="#1f1273", c950="#0f0847",
),
secondary_hue=gr.themes.Color(
c50="#ecfeff", c100="#cffafe", c200="#a5f3fc", c300="#67e8f9",
c400="#22d3ee", c500="#06b6d4", c600="#0891b2", c700="#0e7490",
c800="#155e75", c900="#164e63", c950="#083344",
),
neutral_hue=gr.themes.Color(
c50="#f2f4fb", c100="#e6e9f2", c200="#cbd0e0", c300="#8b93a7",
c400="#5b6377", c500="#3a4157", c600="#252c40", c700="#111828",
c800="#0d1322", c900="#080b16", c950="#05070f",
),
font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
).set(
body_background_fill="#080b16",
body_background_fill_dark="#080b16",
block_background_fill="transparent",
block_background_fill_dark="transparent",
block_border_width="0px",
block_shadow="none",
panel_background_fill="transparent",
panel_background_fill_dark="transparent",
border_color_primary="rgba(255,255,255,0.08)",
border_color_primary_dark="rgba(255,255,255,0.08)",
input_background_fill="rgba(8,11,22,0.7)",
input_background_fill_dark="rgba(8,11,22,0.7)",
input_border_color="rgba(255,255,255,0.08)",
input_border_color_dark="rgba(255,255,255,0.08)",
)
# Put Gradio into its dark mode WITHOUT reloading the page.
#
# The previous version redirected to ?__theme=dark. That's a page reload racing
# SSR: sometimes the `.dark` class landed (dark, correct), sometimes it didn't
# (light — near-black text on our dark ground, invisible). Two devices, two race
# outcomes, one "why does it look different?" bug. Adding the class directly on
# load is deterministic and flash-free. The CSS below is the real backstop, so
# even if this never ran the page would still be readable.
FORCE_DARK = """
function() {
const mark = (el) => el && el.classList.add('dark');
mark(document.documentElement);
mark(document.querySelector('gradio-app'));
}
"""
CSS = """
:root {
--nova-bg: #080b16;
--nova-card: #111828;
--nova-card-2: #0d1322;
--nova-border: rgba(255,255,255,0.08);
--nova-text: #e6e9f2;
--nova-muted: #8b93a7;
--nova-accent: #7c5cff;
--nova-accent-2: #22d3ee;
}
/* ---- pin Gradio's own theme variables to NOVA's dark palette ----
Gradio flips these between light and dark via a `.dark` class on its root.
In a light-mode browser that class can be absent, and then Gradio resolves
--body-text-color to near-black and --background-fill-primary to literal
`white` -- unreadable on our dark ground (this was THE light/dark-browser
bug). NOVA is deliberately a single dark console, so force the handful of
Gradio variables that would otherwise leak light. `!important` on a custom
property wins the cascade no matter the class or the injection order, so the
look no longer depends on the `.dark` class landing at all. */
:root {
--body-text-color: #e6e9f2 !important;
--body-text-color-subdued: #8b93a7 !important;
--background-fill-primary: #080b16 !important;
--background-fill-secondary: #0d1322 !important;
--border-color-primary: rgba(255,255,255,0.08) !important;
--input-placeholder-color: #8b93a7 !important;
--code-background-fill: #0d1322 !important;
--table-odd-background-fill: #0d1322 !important;
--table-even-background-fill: #111828 !important;
--table-border-color: rgba(255,255,255,0.08) !important;
}
/* ---- page shell: cosmic gradient + a slow drifting aurora ---- */
gradio-app, .gradio-container {
background:
radial-gradient(1200px 600px at 12% -10%, rgba(124,92,255,0.18), transparent 55%),
radial-gradient(1000px 500px at 100% 0%, rgba(34,211,238,0.12), transparent 50%),
var(--nova-bg) !important;
max-width: 100% !important;
color: var(--nova-text);
}
.gradio-container { padding-top: 1.6rem !important; }
/* starfield + aurora live behind everything and never eat clicks */
gradio-app::before {
content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 0;
background-image:
radial-gradient(1.4px 1.4px at 12% 18%, rgba(255,255,255,.55), transparent),
radial-gradient(1.2px 1.2px at 72% 12%, rgba(255,255,255,.42), transparent),
radial-gradient(1.6px 1.6px at 38% 62%, rgba(255,255,255,.35), transparent),
radial-gradient(1.1px 1.1px at 88% 74%, rgba(255,255,255,.40), transparent),
radial-gradient(1.3px 1.3px at 24% 88%, rgba(255,255,255,.30), transparent),
radial-gradient(1.2px 1.2px at 58% 34%, rgba(255,255,255,.28), transparent);
animation: twinkle 7s ease-in-out infinite alternate;
}
@keyframes twinkle { from { opacity: .35; } to { opacity: .9; } }
#nova-root { position: relative; z-index: 1; max-width: 1180px; margin: 0 auto; }
/* hide Gradio's own chrome */
footer, .built-with, .show-api { display: none !important; }
/* ---- NOVA brand header ---- */
.nova-brand { display:flex; align-items:center; gap:14px; margin-bottom: 2px; }
.nova-mark {
font-size: 2.1rem; font-weight: 800; letter-spacing: .06em;
background: linear-gradient(120deg, #b8a7ff, #7c5cff 45%, #22d3ee);
background-size: 200% auto;
-webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
animation: shimmer 6s linear infinite;
}
@keyframes shimmer { to { background-position: 200% center; } }
.nova-star { font-size: 1.7rem; filter: drop-shadow(0 0 8px rgba(124,92,255,.7)); animation: pulse-star 3s ease-in-out infinite; }
@keyframes pulse-star { 50% { filter: drop-shadow(0 0 16px rgba(124,92,255,1)); transform: scale(1.08); } }
.nova-sub { color: var(--nova-muted); font-size: .82rem; letter-spacing:.14em; text-transform: uppercase; }
.sonic-chip {
margin-left:auto; display:flex; align-items:center; gap:8px;
background: rgba(124,92,255,0.1); border:1px solid rgba(124,92,255,0.35);
color:#cdbcff; padding:6px 14px; border-radius:999px; font-size:.78rem; font-weight:600;
}
.sonic-dot { width:8px; height:8px; border-radius:50%; background:#22d3ee; box-shadow:0 0 8px #22d3ee; animation: blink 2s ease-in-out infinite; }
@keyframes blink { 50% { opacity:.35; } }
/* ---- SONIC speech bubble ---- */
.sonic-row { display:flex; gap:14px; align-items:flex-start; margin: 20px 0 8px; }
.sonic-avatar {
flex:0 0 48px; width:48px; height:48px; border-radius:14px; overflow:hidden;
box-shadow: 0 0 22px rgba(124,92,255,.45);
}
.sonic-avatar img { width:100%; height:100%; object-fit:cover; display:block; }
.sonic-bubble {
background: var(--nova-card); border:1px solid var(--nova-border);
border-radius: 4px 16px 16px 16px; padding: 14px 18px; color: var(--nova-text);
font-size: 1.02rem; line-height:1.5; max-width: 760px;
animation: bubble-in .32s cubic-bezier(.2,.9,.3,1.2);
}
@keyframes bubble-in { from { opacity:0; transform: translateY(6px) scale(.98); } }
.sonic-name { color:#9d8cff; font-weight:700; font-size:.72rem; letter-spacing:.1em; text-transform:uppercase; margin-bottom:4px; }
/* ---- cluster section header ---- */
.cluster-head { display:flex; align-items:center; gap:12px; margin: 26px 0 6px; }
.cluster-bar { width:5px; height:30px; border-radius:3px; }
.cluster-title { font-size:1.22rem; font-weight:700; color:#f2f4fb; }
.cluster-why { color:var(--nova-muted); font-size:.88rem; line-height:1.5; margin: 0 0 6px 17px; max-width: 900px; }
/* ---- paper card ---- */
.paper-card {
background: linear-gradient(180deg, var(--nova-card), var(--nova-card-2));
border: 1px solid var(--nova-border) !important; border-radius: 16px !important;
padding: 16px !important;
transition: transform .15s ease, border-color .15s ease, box-shadow .15s ease;
}
.paper-card:hover {
transform: translateY(-3px); border-color: rgba(124,92,255,0.5) !important;
box-shadow: 0 12px 30px rgba(0,0,0,0.4);
}
.paper-title { font-size: 1.02rem; font-weight: 700; color:#f2f4fb; line-height:1.35; margin-bottom:6px; }
.paper-meta { color: var(--nova-muted); font-size: .82rem; margin-bottom: 10px; }
.src-badge { display:inline-block; padding:2px 9px; border-radius:999px; font-size:.68rem; font-weight:700;
margin-right:6px; border:1px solid transparent; }
/* ---- buttons ---- */
#nova-root button.secondary, #nova-root a.button {
border-radius: 10px !important; font-weight: 600 !important; font-size: .84rem !important;
border: 1px solid var(--nova-border) !important; background: rgba(255,255,255,0.04) !important;
color: #dfe3ee !important; transition: all .14s ease;
}
#nova-root button.secondary:hover, #nova-root a.button:hover {
border-color: rgba(124,92,255,0.6) !important; background: rgba(124,92,255,0.12) !important; color:#fff !important;
}
#nova-root button.primary {
background: linear-gradient(120deg, #7c5cff, #22d3ee) !important; border: none !important;
color: #0a0e1a !important; font-weight: 800 !important; border-radius: 10px !important;
box-shadow: 0 6px 18px rgba(124,92,255,.4) !important; transition: filter .14s ease, transform .14s ease;
}
#nova-root button.primary:hover { filter: brightness(1.08); transform: translateY(-1px); }
#nova-root button:disabled { opacity:.4 !important; cursor: not-allowed !important; }
/* link-buttons rendered as anchors inside cards */
.card-link {
display:inline-flex; align-items:center; justify-content:center; gap:6px;
padding:7px 12px; border-radius:10px; font-weight:600; font-size:.8rem; text-decoration:none !important;
border:1px solid var(--nova-border); background:rgba(255,255,255,.04); color:#dfe3ee !important;
transition: all .14s ease; flex:1; text-align:center;
}
.card-link:hover { border-color:rgba(124,92,255,.6); background:rgba(124,92,255,.12); color:#fff !important; }
.card-link.dead { opacity:.35; pointer-events:none; }
.card-links { display:flex; gap:8px; margin-top:2px; }
/* ---- inputs ---- */
#nova-root textarea, #nova-root input[type="text"] {
background: rgba(8,11,22,0.7) !important; border:1px solid var(--nova-border) !important;
color: var(--nova-text) !important; border-radius: 12px !important; font-size:.95rem !important;
}
#nova-root textarea:focus, #nova-root input[type="text"]:focus {
border-color: rgba(124,92,255,.6) !important; box-shadow: 0 0 0 3px rgba(124,92,255,.15) !important;
}
.field-label { font-weight:700; color:#cbd0e0; margin: 8px 0 2px; font-size:.9rem; }
/* ---- welcome hero ---- */
.hero-figure { display:flex; flex-direction:column; align-items:center; justify-content:center; }
.hero-figure img { height:52vh; max-height:540px; width:auto; filter: drop-shadow(0 20px 40px rgba(124,92,231,.35)); animation: float 6s ease-in-out infinite; }
@keyframes float { 50% { transform: translateY(-12px); } }
.hero-name { margin-top:14px; font-weight:800; letter-spacing:.12em; color:#c9c3ff; font-size:.9rem; }
.hero-speech {
background: var(--nova-card); border:1px solid var(--nova-border);
border-radius: 18px 18px 18px 4px; padding: 20px 22px; color: var(--nova-text);
font-size: 1.18rem; line-height:1.55; position:relative; margin-bottom: 18px;
}
.hero-speech .sonic-name { color:#9d8cff; font-weight:700; font-size:.72rem; letter-spacing:.12em; text-transform:uppercase; margin-bottom:8px; }
.hero-answer-label { color:var(--nova-muted); font-size:.82rem; text-transform:uppercase; letter-spacing:.1em; margin: 6px 0 8px; }
/* ---- live search checklist ---- */
.search-wrap { max-width: 620px; margin: 6px auto 0; }
.step-row { display:flex; align-items:center; gap:14px; padding:11px 4px; font-size:1rem; color:var(--nova-muted); border-bottom:1px solid rgba(255,255,255,.05); }
.step-ic { width:26px; height:26px; border-radius:50%; flex:0 0 26px; display:flex; align-items:center; justify-content:center; font-size:.8rem; border:2px solid rgba(255,255,255,.14); }
.step-row.done { color:#dfe3ee; }
.step-row.done .step-ic { background:linear-gradient(120deg,#7c5cff,#22d3ee); border-color:transparent; color:#0a0e1a; font-weight:800; }
.step-row.active { color:#fff; }
.step-row.active .step-ic { border-color:#7c5cff; border-top-color:transparent; animation: spin .8s linear infinite; }
.step-count { margin-left:auto; font-size:.8rem; color:var(--nova-muted); }
@keyframes spin { to { transform: rotate(360deg); } }
/* ---- boot splash + vectorize loading ---- */
.vec-wrap { max-width: 640px; margin: 10px auto; text-align:center; }
.vec-figure img { height:150px; width:auto; filter: drop-shadow(0 10px 26px rgba(124,92,231,.4)); animation: float 6s ease-in-out infinite; }
.vec-quote { color:#cbd0e6; font-size:1.12rem; line-height:1.55; font-style:italic; margin:18px auto 6px; max-width:520px; min-height:3em; }
.vec-quote .q { color:#9d8cff; font-weight:700; }
.vec-phase { color:var(--nova-muted); font-size:.85rem; margin-top:10px; }
.vec-bar { height:6px; border-radius:999px; background:rgba(255,255,255,.07); overflow:hidden; margin:16px auto 0; max-width:520px; }
.vec-fill { height:100%; border-radius:999px; background:linear-gradient(90deg,#7c5cff,#22d3ee); transition: width .35s ease; }
/* ---- per-source status chips ---- */
.src-stat-row { display:flex; flex-wrap:wrap; gap:8px; margin: 2px 0 18px; }
.src-stat { font-size:.74rem; font-weight:600; padding:4px 11px; border-radius:999px; border:1px solid transparent; }
.src-stat.ok { color:#5eead4; background:rgba(45,212,191,.10); border-color:rgba(45,212,191,.35); }
.src-stat.warn { color:#fbbf24; background:rgba(251,191,36,.12); border-color:rgba(251,191,36,.45); }
.src-stat.err { color:#f87171; background:rgba(248,113,113,.12); border-color:rgba(248,113,113,.45); }
.src-stat.muted { color:#8b93a7; background:rgba(255,255,255,.04); border-color:var(--nova-border); }
/* ---- chat ---- */
#nova-chat { background: transparent !important; border: none !important; }
#nova-chat .message-wrap { gap: 14px !important; }
#nova-chat .message {
border-radius: 4px 16px 16px 16px !important; border:1px solid var(--nova-border) !important;
background: var(--nova-card) !important; color: var(--nova-text) !important; line-height:1.55 !important;
}
#nova-chat .message.user {
border-radius: 16px 4px 16px 16px !important;
background: rgba(124,92,231,.12) !important; border-color: rgba(124,92,231,.3) !important;
}
#nova-chat .avatar-container img { border-radius: 12px !important; box-shadow: 0 0 14px rgba(124,92,255,.35); }
.nova-error {
border:1px solid rgba(251,191,36,.45); background:rgba(251,191,36,.10); color:#fbbf24;
border-radius:12px; padding:12px 16px; margin:10px 0; font-size:.9rem;
}
@media (max-width: 860px) {
.hero-figure img { height:32vh; }
.card-links { flex-wrap: wrap; }
}
"""
|