nihalaninihal Claude Opus 4.6 commited on
Commit
e85e584
·
1 Parent(s): f20603d

Improve Gradio UI layout with sidebar controls, sub-tabs, and styled score widgets

Browse files

- Add sidebar/main content layout with controls on left, content on right
- Replace JSON score outputs with styled HTML score cards
- Add format_scores_html and format_comparison_scores_html to chart_helpers
- Use CSS variables consistently across inspector and verdict HTML
- Add tooltips to all plots, interactive=False on DataFrames
- Widen container to 1600px, hide Gradio footer
- Use sub-tabs within each main tab for better organization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (4) hide show
  1. app.py +152 -118
  2. chart_helpers.py +77 -13
  3. inspector.py +10 -10
  4. sentinel_theme.py +12 -1
app.py CHANGED
@@ -22,6 +22,8 @@ from chart_helpers import (
22
  build_attack_timeline_df,
23
  build_comparison_df,
24
  build_verdict_html,
 
 
25
  )
26
  from inspector import (
27
  get_all_customers,
@@ -41,12 +43,13 @@ def run_single_episode(seed, trained):
41
  """Run a single episode and return formatted replay + charts."""
42
  log, scores = run_episode(trained=bool(trained), seed=int(seed))
43
  html = format_replay_html(log, scores)
44
- scores_text = json.dumps(scores, indent=2)
45
-
 
46
  score_df = build_score_progression_df(log)
47
  attack_df = build_attack_timeline_df(log)
48
 
49
- return html, scores_text, score_df, attack_df
50
 
51
 
52
  def run_before_after(seed):
@@ -71,18 +74,9 @@ def run_before_after(seed):
71
  untrained_score_df = build_score_progression_df(result["untrained"]["log"])
72
  trained_score_df = build_score_progression_df(result["trained"]["log"])
73
 
74
- comparison_json = {
75
- "untrained_scores": result["untrained"]["scores"],
76
- "trained_scores": result["trained"]["scores"],
77
- "improvement": {
78
- agent: round(
79
- result["trained"]["scores"][agent]
80
- - result["untrained"]["scores"][agent],
81
- 2,
82
- )
83
- for agent in result["trained"]["scores"]
84
- },
85
- }
86
 
87
  return (
88
  untrained_html,
@@ -91,7 +85,7 @@ def run_before_after(seed):
91
  comparison_df,
92
  untrained_score_df,
93
  trained_score_df,
94
- json.dumps(comparison_json, indent=2),
95
  )
96
 
97
 
@@ -113,7 +107,7 @@ def inspect_state(seed):
113
  # Gradio UI
114
  # -------------------------------------------------------------------
115
 
116
- with gr.Blocks(title="SentinelOps Arena") as demo:
117
 
118
  # Header banner
119
  gr.HTML(HEADER_HTML)
@@ -124,38 +118,47 @@ with gr.Blocks(title="SentinelOps Arena") as demo:
124
  # ============================================================
125
  with gr.TabItem("Run Episode"):
126
  with gr.Row():
127
- seed_input = gr.Number(
128
- value=42, label="Random Seed", precision=0
129
- )
130
- trained_toggle = gr.Checkbox(
131
- value=False, label="Use Trained Worker"
132
- )
133
- run_btn = gr.Button("Run Episode", variant="primary")
134
-
135
- with gr.Row():
136
- with gr.Column(scale=2):
137
- replay_output = gr.HTML(label="Episode Replay")
138
- with gr.Column(scale=1):
139
- scores_output = gr.Code(
140
- label="Final Scores", language="json"
141
- )
142
-
143
- with gr.Accordion("Score Progression & Attack Timeline", open=True):
144
- with gr.Row():
145
- score_plot = gr.LinePlot(
146
- x="tick",
147
- y="score",
148
- color="agent",
149
- label="Cumulative Score Progression",
150
- height=300,
151
  )
152
- attack_plot = gr.BarPlot(
153
- x="attack_type",
154
- y="count",
155
- color="attack_type",
156
- label="Attack Timeline",
157
- height=300,
158
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
  run_btn.click(
161
  run_single_episode,
@@ -167,50 +170,64 @@ with gr.Blocks(title="SentinelOps Arena") as demo:
167
  # Tab 2: Before/After Comparison
168
  # ============================================================
169
  with gr.TabItem("Untrained vs Trained"):
170
- gr.Markdown(
171
- "Compare how an **untrained** worker vs a **trained** worker "
172
- "handles the same attack sequence."
173
- )
174
- with gr.Row():
175
- comp_seed = gr.Number(
176
- value=42, label="Random Seed", precision=0
177
- )
178
- comp_btn = gr.Button("Run Comparison", variant="primary")
179
-
180
- # Verdict stats
181
- verdict_output = gr.HTML(label="Training Impact")
182
-
183
  with gr.Row():
184
- untrained_output = gr.HTML(label="Untrained Worker")
185
- trained_output = gr.HTML(label="Trained Worker")
186
-
187
- with gr.Accordion("Score Comparison Charts", open=True):
188
- comparison_bar = gr.BarPlot(
189
- x="agent",
190
- y="score",
191
- color="type",
192
- label="Score Comparison: Untrained vs Trained",
193
- height=300,
194
- )
195
- with gr.Row():
196
- untrained_score_plot = gr.LinePlot(
197
- x="tick",
198
- y="score",
199
- color="agent",
200
- label="Untrained Score Progression",
201
- height=250,
202
  )
203
- trained_score_plot = gr.LinePlot(
204
- x="tick",
205
- y="score",
206
- color="agent",
207
- label="Trained Score Progression",
208
- height=250,
209
  )
210
-
211
- comparison_output = gr.Code(
212
- label="Score Details", language="json"
213
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
  comp_btn.click(
216
  run_before_after,
@@ -231,36 +248,53 @@ with gr.Blocks(title="SentinelOps Arena") as demo:
231
  # ============================================================
232
  with gr.TabItem("Environment Inspector"):
233
  with gr.Row():
234
- inspect_seed = gr.Number(
235
- value=42, label="Random Seed", precision=0
236
- )
237
- inspect_btn = gr.Button("Inspect", variant="primary")
238
-
239
- config_output = gr.HTML(label="Environment Configuration")
240
-
241
- with gr.Accordion("Customers (CRM)", open=False):
242
- customers_table = gr.Dataframe(
243
- label="All Customers",
244
- headers=["customer_id", "name", "tier", "region", "lifetime_value"],
245
- )
246
-
247
- with gr.Accordion("Invoices (Billing)", open=False):
248
- invoices_table = gr.Dataframe(
249
- label="All Invoices",
250
- headers=["invoice_id", "customer_id", "amount", "status"],
251
- )
252
-
253
- with gr.Accordion("Tickets (Support)", open=False):
254
- tickets_table = gr.Dataframe(
255
- label="All Tickets",
256
- headers=["ticket_id", "customer_id", "subject", "priority", "status", "sla_deadline_tick"],
257
- )
258
-
259
- with gr.Accordion("Task Queue", open=False):
260
- tasks_table = gr.Dataframe(
261
- label="Task Queue",
262
- headers=["task_id", "customer_id", "task_type", "message", "arrival_tick"],
263
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
  inspect_btn.click(
266
  inspect_state,
 
22
  build_attack_timeline_df,
23
  build_comparison_df,
24
  build_verdict_html,
25
+ format_scores_html,
26
+ format_comparison_scores_html,
27
  )
28
  from inspector import (
29
  get_all_customers,
 
43
  """Run a single episode and return formatted replay + charts."""
44
  log, scores = run_episode(trained=bool(trained), seed=int(seed))
45
  html = format_replay_html(log, scores)
46
+
47
+ scores_html = format_scores_html(scores)
48
+
49
  score_df = build_score_progression_df(log)
50
  attack_df = build_attack_timeline_df(log)
51
 
52
+ return html, scores_html, score_df, attack_df
53
 
54
 
55
  def run_before_after(seed):
 
74
  untrained_score_df = build_score_progression_df(result["untrained"]["log"])
75
  trained_score_df = build_score_progression_df(result["trained"]["log"])
76
 
77
+ comparison_html = format_comparison_scores_html(
78
+ result["untrained"]["scores"], result["trained"]["scores"]
79
+ )
 
 
 
 
 
 
 
 
 
80
 
81
  return (
82
  untrained_html,
 
85
  comparison_df,
86
  untrained_score_df,
87
  trained_score_df,
88
+ comparison_html,
89
  )
90
 
91
 
 
107
  # Gradio UI
108
  # -------------------------------------------------------------------
109
 
110
+ with gr.Blocks(title="SentinelOps Arena", fill_width=True) as demo:
111
 
112
  # Header banner
113
  gr.HTML(HEADER_HTML)
 
118
  # ============================================================
119
  with gr.TabItem("Run Episode"):
120
  with gr.Row():
121
+ # Left sidebar for controls
122
+ with gr.Column(scale=1, min_width=300):
123
+ gr.Markdown("### Episode Configuration")
124
+ seed_input = gr.Number(
125
+ value=42, label="Random Seed", precision=0,
126
+ info="Seed for generating customer scenarios and attack patterns."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  )
128
+ trained_toggle = gr.Checkbox(
129
+ value=False, label="Use Trained Worker",
130
+ info="Toggle to use a worker trained via GRPO instead of a naive heuristic worker."
 
 
 
131
  )
132
+ run_btn = gr.Button("▶ Run Episode", variant="primary", size="lg")
133
+
134
+ gr.Markdown("---")
135
+ gr.Markdown("### Final Scores")
136
+ scores_output = gr.HTML(elem_classes=["glow-card"])
137
+
138
+ # Main content area
139
+ with gr.Column(scale=3):
140
+ with gr.Tabs():
141
+ with gr.TabItem("Execution Replay"):
142
+ replay_output = gr.HTML(elem_classes=["glow-card"])
143
+ with gr.TabItem("Analytics & Timeline"):
144
+ with gr.Row():
145
+ score_plot = gr.LinePlot(
146
+ x="tick",
147
+ y="score",
148
+ color="agent",
149
+ title="Cumulative Score Progression",
150
+ tooltip=["tick", "score", "agent"],
151
+ height=350,
152
+ )
153
+ with gr.Row():
154
+ attack_plot = gr.BarPlot(
155
+ x="attack_type",
156
+ y="count",
157
+ color="attack_type",
158
+ title="Attack Timeline",
159
+ tooltip=["attack_type", "count"],
160
+ height=350,
161
+ )
162
 
163
  run_btn.click(
164
  run_single_episode,
 
170
  # Tab 2: Before/After Comparison
171
  # ============================================================
172
  with gr.TabItem("Untrained vs Trained"):
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  with gr.Row():
174
+ with gr.Column(scale=1, min_width=300):
175
+ gr.Markdown(
176
+ "### Benchmarking Mode\n"
177
+ "Compare how an **untrained** worker vs a **trained** worker "
178
+ "handles the same attack sequence."
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  )
180
+ comp_seed = gr.Number(
181
+ value=42, label="Random Seed", precision=0,
182
+ info="Ensures identical attack sequence for fair comparison."
 
 
 
183
  )
184
+ comp_btn = gr.Button("▶ Run Comparison", variant="primary", size="lg")
185
+
186
+ gr.Markdown("---")
187
+ gr.Markdown("### Training Impact")
188
+ verdict_output = gr.HTML(elem_classes=["glow-card"])
189
+ comparison_output = gr.HTML(elem_classes=["glow-card"])
190
+
191
+ with gr.Column(scale=3):
192
+ with gr.Tabs():
193
+ with gr.TabItem("Execution Replays"):
194
+ with gr.Row():
195
+ with gr.Column():
196
+ gr.Markdown("#### 🛑 Untrained Worker")
197
+ untrained_output = gr.HTML(elem_classes=["glow-card"])
198
+ with gr.Column():
199
+ gr.Markdown("#### 🚀 Trained Worker")
200
+ trained_output = gr.HTML(elem_classes=["glow-card"])
201
+
202
+ with gr.TabItem("Score Analytics"):
203
+ with gr.Row():
204
+ comparison_bar = gr.BarPlot(
205
+ x="agent",
206
+ y="score",
207
+ color="type",
208
+ title="Score Comparison: Untrained vs Trained",
209
+ tooltip=["agent", "score", "type"],
210
+ height=350,
211
+ )
212
+ with gr.Row():
213
+ with gr.Column():
214
+ untrained_score_plot = gr.LinePlot(
215
+ x="tick",
216
+ y="score",
217
+ color="agent",
218
+ title="Untrained Score Progression",
219
+ tooltip=["tick", "score", "agent"],
220
+ height=300,
221
+ )
222
+ with gr.Column():
223
+ trained_score_plot = gr.LinePlot(
224
+ x="tick",
225
+ y="score",
226
+ color="agent",
227
+ title="Trained Score Progression",
228
+ tooltip=["tick", "score", "agent"],
229
+ height=300,
230
+ )
231
 
232
  comp_btn.click(
233
  run_before_after,
 
248
  # ============================================================
249
  with gr.TabItem("Environment Inspector"):
250
  with gr.Row():
251
+ with gr.Column(scale=1, min_width=300):
252
+ gr.Markdown(
253
+ "### System Databases\n"
254
+ "Inspect the initial state of the simulated enterprise."
255
+ )
256
+ inspect_seed = gr.Number(
257
+ value=42, label="Random Seed", precision=0,
258
+ info="Seed used for procedural generation of records."
259
+ )
260
+ inspect_btn = gr.Button("🔍 Inspect Databases", variant="primary", size="lg")
261
+
262
+ gr.Markdown("---")
263
+ config_output = gr.HTML(elem_classes=["glow-card"])
264
+
265
+ with gr.Column(scale=3):
266
+ with gr.Tabs():
267
+ with gr.TabItem("CRM System (Customers)"):
268
+ customers_table = gr.Dataframe(
269
+ label="Customer Database",
270
+ headers=["customer_id", "name", "tier", "region", "lifetime_value"],
271
+ interactive=False,
272
+ elem_classes=["glow-card"]
273
+ )
274
+
275
+ with gr.TabItem("Billing System (Invoices)"):
276
+ invoices_table = gr.Dataframe(
277
+ label="Invoice Database",
278
+ headers=["invoice_id", "customer_id", "amount", "status"],
279
+ interactive=False,
280
+ elem_classes=["glow-card"]
281
+ )
282
+
283
+ with gr.TabItem("Ticketing System (Support)"):
284
+ tickets_table = gr.Dataframe(
285
+ label="Active Tickets",
286
+ headers=["ticket_id", "customer_id", "subject", "priority", "status", "sla_deadline_tick"],
287
+ interactive=False,
288
+ elem_classes=["glow-card"]
289
+ )
290
+
291
+ with gr.TabItem("Live Task Queue"):
292
+ tasks_table = gr.Dataframe(
293
+ label="Tasks to Process",
294
+ headers=["task_id", "customer_id", "task_type", "message", "arrival_tick"],
295
+ interactive=False,
296
+ elem_classes=["glow-card"]
297
+ )
298
 
299
  inspect_btn.click(
300
  inspect_state,
chart_helpers.py CHANGED
@@ -9,6 +9,72 @@ from __future__ import annotations
9
  import pandas as pd
10
 
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  def build_score_progression_df(log: list[dict]) -> pd.DataFrame:
13
  """Track cumulative scores for each agent at each tick.
14
 
@@ -122,29 +188,27 @@ def build_verdict_html(untrained_log: list, trained_log: list) -> str:
122
  diff_sign = "+" if diff > 0 else ""
123
  return (
124
  f"<div style='flex:1; text-align:center; padding:16px; "
125
- f"background:#111827; border-radius:12px; margin:4px;'>"
126
- f"<div style='font-size:12px; color:#888; text-transform:uppercase; "
127
  f"letter-spacing:1px;'>{label}</div>"
128
- f"<div style='display:flex; justify-content:center; gap:24px; margin-top:8px;'>"
129
  f"<div>"
130
- f"<div style='font-size:28px; font-weight:bold; color:#ff4444;'>{untrained_val}</div>"
131
- f"<div style='font-size:10px; color:#888;'>Untrained</div>"
132
  f"</div>"
133
  f"<div>"
134
- f"<div style='font-size:28px; font-weight:bold; color:#00ff41;'>{trained_val}</div>"
135
- f"<div style='font-size:10px; color:#888;'>Trained</div>"
136
  f"</div>"
137
  f"</div>"
138
- f"<div style='font-size:14px; color:{diff_color}; margin-top:6px; "
139
- f"font-weight:bold;'>{diff_sign}{diff}</div>"
140
  f"</div>"
141
  )
142
 
143
  html = (
144
- "<div style='font-family:system-ui,sans-serif; padding:12px;'>"
145
- "<h3 style='text-align:center; color:#e0e0e0; margin-bottom:12px;'>"
146
- "Training Impact Verdict</h3>"
147
- "<div style='display:flex; gap:8px;'>"
148
  )
149
  html += _stat_card(
150
  "Attacks Launched",
 
9
  import pandas as pd
10
 
11
 
12
+ def format_comparison_scores_html(untrained: dict, trained: dict) -> str:
13
+ """Format comparative scores for untrained vs trained."""
14
+ colors = {
15
+ "attacker": "var(--sentinel-red)",
16
+ "worker": "var(--sentinel-blue)",
17
+ "oversight": "var(--sentinel-green)",
18
+ }
19
+
20
+ html = "<div style='display:flex; flex-direction:column; gap:8px;'>"
21
+ for agent in untrained.keys():
22
+ color = colors.get(agent, "#888")
23
+ u_score = untrained[agent]
24
+ t_score = trained[agent]
25
+ diff = t_score - u_score
26
+
27
+ diff_color = "#44bb44" if diff > 0 else ("#ff4444" if diff < 0 else "#888")
28
+ diff_sign = "+" if diff > 0 else ""
29
+
30
+ html += (
31
+ f"<div style='display:flex; flex-direction:column; padding:12px 16px; "
32
+ f"background:var(--sentinel-surface); border:1px solid var(--sentinel-border); "
33
+ f"border-radius:6px; border-left:4px solid {color};'>"
34
+ f"<div style='font-family:\"IBM Plex Mono\", monospace; font-weight:bold; "
35
+ f"text-transform:uppercase; letter-spacing:1px; margin-bottom:8px;'>{agent}</div>"
36
+ f"<div style='display:flex; justify-content:space-between; align-items:center;'>"
37
+ f"<div style='font-family:\"IBM Plex Mono\", monospace;'>"
38
+ f"<span style='color:#888; font-size:12px; margin-right:8px;'>UNTRAINED:</span>"
39
+ f"<span style='font-weight:bold;'>{u_score:.1f}</span>"
40
+ f"</div>"
41
+ f"<div style='font-family:\"IBM Plex Mono\", monospace;'>"
42
+ f"<span style='color:#888; font-size:12px; margin-right:8px;'>TRAINED:</span>"
43
+ f"<span style='font-weight:bold; color:{color};'>{t_score:.1f}</span>"
44
+ f"</div>"
45
+ f"<div style='font-family:\"IBM Plex Mono\", monospace; font-weight:bold; color:{diff_color};'>"
46
+ f"{diff_sign}{diff:.1f}"
47
+ f"</div>"
48
+ f"</div>"
49
+ f"</div>"
50
+ )
51
+ html += "</div>"
52
+ return html
53
+
54
+ def format_scores_html(scores: dict) -> str:
55
+ """Format final scores as a styled HTML widget."""
56
+ colors = {
57
+ "attacker": "var(--sentinel-red)",
58
+ "worker": "var(--sentinel-blue)",
59
+ "oversight": "var(--sentinel-green)",
60
+ }
61
+
62
+ html = "<div style='display:flex; flex-direction:column; gap:8px;'>"
63
+ for agent, score in scores.items():
64
+ color = colors.get(agent, "#888")
65
+ html += (
66
+ f"<div style='display:flex; justify-content:space-between; align-items:center; "
67
+ f"padding:12px 16px; background:var(--sentinel-surface); border:1px solid var(--sentinel-border); "
68
+ f"border-radius:6px; border-left:4px solid {color};'>"
69
+ f"<span style='font-family:\"IBM Plex Mono\", monospace; font-weight:bold; "
70
+ f"text-transform:uppercase; letter-spacing:1px;'>{agent}</span>"
71
+ f"<span style='font-family:\"IBM Plex Mono\", monospace; font-size:18px; "
72
+ f"font-weight:bold; color:{color};'>{score:.1f}</span>"
73
+ f"</div>"
74
+ )
75
+ html += "</div>"
76
+ return html
77
+
78
  def build_score_progression_df(log: list[dict]) -> pd.DataFrame:
79
  """Track cumulative scores for each agent at each tick.
80
 
 
188
  diff_sign = "+" if diff > 0 else ""
189
  return (
190
  f"<div style='flex:1; text-align:center; padding:16px; "
191
+ f"background:var(--sentinel-surface); border-radius:8px; border:1px solid var(--sentinel-border); margin:4px;'>"
192
+ f"<div style='font-size:11px; color:var(--sentinel-text); text-transform:uppercase; "
193
  f"letter-spacing:1px;'>{label}</div>"
194
+ f"<div style='display:flex; justify-content:center; align-items:center; gap:24px; margin-top:12px;'>"
195
  f"<div>"
196
+ f"<div style='font-size:28px; font-weight:bold; color:var(--sentinel-red);'>{untrained_val}</div>"
197
+ f"<div style='font-size:10px; color:#888; text-transform:uppercase;'>Untrained</div>"
198
  f"</div>"
199
  f"<div>"
200
+ f"<div style='font-size:28px; font-weight:bold; color:var(--sentinel-green);'>{trained_val}</div>"
201
+ f"<div style='font-size:10px; color:#888; text-transform:uppercase;'>Trained</div>"
202
  f"</div>"
203
  f"</div>"
204
+ f"<div style='font-size:14px; color:{diff_color}; margin-top:12px; "
205
+ f"font-weight:bold;'>Difference: {diff_sign}{diff}</div>"
206
  f"</div>"
207
  )
208
 
209
  html = (
210
+ "<div style='font-family:\"IBM Plex Mono\", monospace; padding:12px;'>"
211
+ "<div style='display:flex; gap:16px;'>"
 
 
212
  )
213
  html += _stat_card(
214
  "Attacks Launched",
inspector.py CHANGED
@@ -82,15 +82,15 @@ def get_env_config_html(env: SentinelOpsArena) -> str:
82
  sla = env.ticketing.sla_rules.model_dump()
83
 
84
  css = (
85
- "font-family: 'Courier New', monospace;"
86
- "background: #0d1117;"
87
- "color: #c9d1d9;"
88
  "padding: 20px;"
89
- "border-radius: 10px;"
90
- "border: 1px solid #30363d;"
91
  )
92
  heading_css = (
93
- "color: #39ff14;"
94
  "font-size: 14px;"
95
  "font-weight: bold;"
96
  "margin: 16px 0 8px 0;"
@@ -105,17 +105,17 @@ def get_env_config_html(env: SentinelOpsArena) -> str:
105
  th_css = (
106
  "text-align: left;"
107
  "padding: 6px 12px;"
108
- "border-bottom: 1px solid #30363d;"
109
- "color: #58a6ff;"
110
  "font-size: 12px;"
111
  )
112
  td_css = (
113
  "padding: 6px 12px;"
114
- "border-bottom: 1px solid #21262d;"
115
  "font-size: 13px;"
116
  )
117
  val_css = (
118
- "color: #39ff14;"
119
  "font-weight: bold;"
120
  )
121
 
 
82
  sla = env.ticketing.sla_rules.model_dump()
83
 
84
  css = (
85
+ "font-family: 'IBM Plex Mono', monospace;"
86
+ "background: var(--sentinel-surface);"
87
+ "color: var(--sentinel-text);"
88
  "padding: 20px;"
89
+ "border-radius: 8px;"
90
+ "border: 1px solid var(--sentinel-border);"
91
  )
92
  heading_css = (
93
+ "color: var(--sentinel-green);"
94
  "font-size: 14px;"
95
  "font-weight: bold;"
96
  "margin: 16px 0 8px 0;"
 
105
  th_css = (
106
  "text-align: left;"
107
  "padding: 6px 12px;"
108
+ "border-bottom: 1px solid var(--sentinel-border);"
109
+ "color: var(--sentinel-blue);"
110
  "font-size: 12px;"
111
  )
112
  td_css = (
113
  "padding: 6px 12px;"
114
+ "border-bottom: 1px solid rgba(201, 209, 217, 0.1);"
115
  "font-size: 13px;"
116
  )
117
  val_css = (
118
+ "color: var(--sentinel-green);"
119
  "font-weight: bold;"
120
  )
121
 
sentinel_theme.py CHANGED
@@ -153,10 +153,21 @@ CUSTOM_CSS = """
153
  --sentinel-text: #c9d1d9;
154
  }
155
 
 
 
 
 
156
  /* ====================== GLOBAL ====================== */
157
  .gradio-container {
158
  background: var(--sentinel-bg) !important;
159
- max-width: 1200px !important;
 
 
 
 
 
 
 
160
  }
161
 
162
  /* ====================== TAB HEADERS ====================== */
 
153
  --sentinel-text: #c9d1d9;
154
  }
155
 
156
+ footer {
157
+ display: none !important;
158
+ }
159
+
160
  /* ====================== GLOBAL ====================== */
161
  .gradio-container {
162
  background: var(--sentinel-bg) !important;
163
+ max-width: 100% !important;
164
+ padding: 0 !important;
165
+ }
166
+
167
+ .gradio-container > .main {
168
+ max-width: 1600px;
169
+ margin: 0 auto;
170
+ padding: 0 20px;
171
  }
172
 
173
  /* ====================== TAB HEADERS ====================== */