GitHub Actions commited on
Commit
2c76b35
·
1 Parent(s): 9e4203c

Sync from screenase@a9b2f49a40611ed7a10f2844bd6deca51a968047

Browse files
Files changed (3) hide show
  1. .gitattributes +0 -35
  2. .streamlit/config.toml +23 -6
  3. app.py +189 -87
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.streamlit/config.toml CHANGED
@@ -1,14 +1,31 @@
1
- # Benchling-inspired: white-dominant, near-black text, a single saturated
2
- # blue (#3d65fb) reserved for CTAs (download buttons, active tabs, sliders).
3
- # Streamlit doesn't expose a separate sidebar color, so we don't emulate
4
- # Benchling's dark nav we just stop bleeding blue everywhere.
 
 
 
 
 
5
  [theme]
 
 
 
 
6
  primaryColor = "#3d65fb"
7
  backgroundColor = "#ffffff"
8
  secondaryBackgroundColor = "#f5f6f8"
9
  textColor = "#1f2937"
10
- font = "sans serif"
11
- base = "light"
 
 
 
 
 
 
 
 
12
 
13
  [server]
14
  headless = true
 
1
+ # Benchling-inspired light theme (white-dominant, CTA-blue as the only
2
+ # accent) paired with a pure-black dark theme. Streamlit matches the user's
3
+ # browser preference on first load and exposes a light/dark switch in the
4
+ # settings menu (three-dot Settings Choose app theme).
5
+ #
6
+ # `dataframeHeaderBackgroundColor` is what finally makes st.dataframe /
7
+ # st.data_editor render with dark headers — glide-data-grid reads Streamlit's
8
+ # theme config at page load and won't honor runtime CSS variable flips.
9
+
10
  [theme]
11
+ font = "sans serif"
12
+ base = "light"
13
+
14
+ [theme.light]
15
  primaryColor = "#3d65fb"
16
  backgroundColor = "#ffffff"
17
  secondaryBackgroundColor = "#f5f6f8"
18
  textColor = "#1f2937"
19
+ borderColor = "#e5e7eb"
20
+
21
+ [theme.dark]
22
+ primaryColor = "#3d65fb"
23
+ backgroundColor = "#000000"
24
+ secondaryBackgroundColor = "#0a0a0a"
25
+ textColor = "#ffffff"
26
+ borderColor = "#2a2a2a"
27
+ linkColor = "#7aa0ff"
28
+ dataframeHeaderBackgroundColor = "#0a0a0a"
29
 
30
  [server]
31
  headless = true
app.py CHANGED
@@ -38,6 +38,75 @@ DEMO_RESULTS_PATH = Path(__file__).parent / "examples" / "results_simulated.csv"
38
  POS_COLOR = "#4a6fa5"
39
  NEG_COLOR = "#c05454"
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  def _config_from_url() -> ReactionConfig | None:
43
  """If ?cfg=… is present in the URL, decode it into a ReactionConfig."""
@@ -85,7 +154,7 @@ def build_default_config() -> ReactionConfig:
85
 
86
  def generate_from_ui(
87
  cfg: ReactionConfig,
88
- min_pipet_uL: float = 0.5,
89
  *,
90
  design_kind: str = "full",
91
  alpha: str = "face",
@@ -151,8 +220,11 @@ def generate_from_ui(
151
  # ---------- sidebar ----------
152
 
153
  def _sidebar(default_cfg: ReactionConfig) -> tuple[ReactionConfig, float, dict]:
 
 
 
154
  with st.sidebar:
155
- st.markdown("### Reaction")
156
  c1, c2 = st.columns(2)
157
  vol = c1.number_input(
158
  "Volume (µL)", value=float(default_cfg.reaction_volume_uL),
@@ -167,76 +239,104 @@ def _sidebar(default_cfg: ReactionConfig) -> tuple[ReactionConfig, float, dict]:
167
  "Center pts", min_value=0, max_value=10,
168
  value=int(default_cfg.center_points), step=1, key="cps",
169
  )
170
- seed = c2.number_input(
171
- "Seed", value=int(default_cfg.seed), step=1, key="seed",
172
- )
173
-
174
- st.markdown("### Design")
175
- design_kind = st.radio(
176
- "Type",
177
- options=["full", "ccd", "pb"],
178
- index=0,
179
- format_func=lambda k: {
180
- "full": "Full factorial (2 + centers)",
181
- "ccd": "Central-composite (CCD follow-up)",
182
- "pb": "Plackett-Burman (screening, k > 5)",
183
- }[k],
184
- key="design_kind",
185
- horizontal=False,
186
- )
187
- alpha = "face"
188
- if design_kind == "ccd":
189
- alpha = st.select_slider(
190
- "Axial α",
191
- options=["face", "rotatable"],
192
- value="face",
193
- help=(
194
- "`face` (α=1) stays within low/high; `rotatable` extends "
195
- "axial setpoints beyond the range — use only if your "
196
- "stocks allow the wider span."
197
- ),
198
- key="ccd_alpha",
199
  )
200
 
201
- st.markdown("### Plate layout")
202
- plate_choice = st.radio(
203
- "Plate",
204
- options=["none", "96", "384"],
205
- horizontal=True,
206
- index=0,
207
- key="plate_choice",
208
- )
209
- plate_layout = "column-major"
210
- if plate_choice != "none":
211
- plate_layout = st.radio(
212
- "Fill order",
213
- options=["column-major", "row-major", "randomized"],
 
214
  index=0,
215
- horizontal=True,
216
- key="plate_layout",
 
 
 
 
 
 
217
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
- st.markdown("### Factors")
220
- st.caption("Edit low / high setpoints.")
221
- factor_rows = [
222
- {
223
- "factor": f.display or f.name,
224
- "low": float(f.low),
225
- "high": float(f.high),
226
- }
227
- for f in default_cfg.factors
228
- ]
229
- edited = st.data_editor(
230
- pd.DataFrame(factor_rows),
231
- hide_index=True,
232
- disabled=["factor"],
233
- key="factors_editor",
234
- column_config={
235
- "factor": st.column_config.TextColumn("Factor", width="medium"),
236
- "low": st.column_config.NumberColumn("Low", format="%.3g"),
237
- "high": st.column_config.NumberColumn("High", format="%.3g"),
238
- },
239
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  new_factors: list[Factor] = []
241
  for orig, row in zip(default_cfg.factors, edited.itertuples(index=False),
242
  strict=True):
@@ -244,7 +344,7 @@ def _sidebar(default_cfg: ReactionConfig) -> tuple[ReactionConfig, float, dict]:
244
  "low": float(row.low), "high": float(row.high),
245
  }))
246
 
247
- with st.expander("Stock concentrations"):
248
  stock_rows = [
249
  {
250
  "key": k,
@@ -273,7 +373,7 @@ def _sidebar(default_cfg: ReactionConfig) -> tuple[ReactionConfig, float, dict]:
273
  for row in stock_edit.itertuples(index=False)
274
  }
275
 
276
- with st.expander("Fixed reagents"):
277
  st.caption("Reagents with a constant volume per run (not swept).")
278
  fixed_rows = [
279
  {"reagent": k, "volume_uL": float(v)}
@@ -296,10 +396,10 @@ def _sidebar(default_cfg: ReactionConfig) -> tuple[ReactionConfig, float, dict]:
296
  for row in fixed_edit.itertuples(index=False)
297
  }
298
 
299
- with st.expander("Advanced"):
300
  min_pipet = st.number_input(
301
  "Min pipetting volume (µL)",
302
- value=0.5, min_value=0.0, step=0.1, key="min_pipet",
303
  help="Volumes below this threshold emit a warning on the bench sheet.",
304
  )
305
 
@@ -387,22 +487,23 @@ def _render_generate_tab(
387
  "sees them at the bench."
388
  )
389
 
390
- left, right = st.columns([1, 1], gap="medium")
391
- with left:
392
- st.markdown("#### Design")
393
- display_cols = [f.name for f in cfg.factors] + ["is_center"]
394
- st.dataframe(
395
- design[display_cols],
396
- height=540,
397
- column_config={
398
- "is_center": st.column_config.CheckboxColumn(
399
- "center?", help="Center-point replicate",
400
- ),
401
- },
402
- )
403
- with right:
404
- st.markdown("#### Bench sheet preview")
405
- st.iframe(art["html"], height=540)
 
406
 
407
  if art.get("plate_df") is not None:
408
  st.markdown("#### Plate layout")
@@ -706,6 +807,7 @@ def main() -> None:
706
  "About": "Screenase — DoE planner for in-vitro transcription.",
707
  },
708
  )
 
709
 
710
  header_l, header_r = st.columns([3, 1])
711
  with header_l:
 
38
  POS_COLOR = "#4a6fa5"
39
  NEG_COLOR = "#c05454"
40
 
41
+ # Streamlit's native dual-theme support (`.streamlit/config.toml` with
42
+ # [theme.light] + [theme.dark]) drives dark mode — the user toggles via the
43
+ # three-dot menu → Settings → Choose app theme. glide-data-grid reads that
44
+ # config at page load, which is the only way to get dark dataframes.
45
+ #
46
+ # The only CSS we still inject ourselves is a white backing under iframes:
47
+ # the plate-map HTML has no body bg of its own, so on a dark Streamlit page
48
+ # its light-gray cell borders would render against a dark frame.
49
+ _IFRAME_SAFETY_CSS = """
50
+ <style>
51
+ .stApp iframe { background: #ffffff !important; }
52
+ [data-testid="stDeployButton"] { display: none !important; }
53
+ .stApp [data-testid="stMainBlockContainer"] { padding-top: 2rem; }
54
+
55
+ /* Swap Streamlit's default sidebar toggle (a Material Symbols ligature
56
+ `keyboard_double_arrow_left/right` that renders as << / >>) for
57
+ sidekickicons' sidebar-left outline icon — https://sidekickicons.com/.
58
+ We hide the ligature text via font-size:0 on the icon span, then paint
59
+ the sidekickicons glyph on a ::before using currentColor so it inherits
60
+ the span's theme-driven color. */
61
+ [data-testid="stSidebarCollapseButton"] [data-testid="stIconMaterial"],
62
+ [data-testid="stExpandSidebarButton"] [data-testid="stIconMaterial"] {
63
+ font-size: 0 !important;
64
+ line-height: 0 !important;
65
+ width: 1.25rem;
66
+ height: 1.25rem;
67
+ display: inline-block;
68
+ position: relative;
69
+ }
70
+ [data-testid="stSidebarCollapseButton"] [data-testid="stIconMaterial"]::before,
71
+ [data-testid="stExpandSidebarButton"] [data-testid="stIconMaterial"]::before {
72
+ content: "";
73
+ position: absolute;
74
+ inset: 0;
75
+ background-color: currentColor;
76
+ -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><path d='M9 4.5v15M4.125 19.5h15.75c.621 0 1.125-.504 1.125-1.125V5.625c0-.621-.504-1.125-1.125-1.125H4.125C3.504 4.5 3 5.004 3 5.625v12.75c0 .621.504 1.125 1.125 1.125z'/></svg>") no-repeat center / contain;
77
+ mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><path d='M9 4.5v15M4.125 19.5h15.75c.621 0 1.125-.504 1.125-1.125V5.625c0-.621-.504-1.125-1.125-1.125H4.125C3.504 4.5 3 5.004 3 5.625v12.75c0 .621.504 1.125 1.125 1.125z'/></svg>") no-repeat center / contain;
78
+ }
79
+
80
+ .sn-label-with-info {
81
+ display: flex; align-items: center; gap: 0.3rem;
82
+ font-size: 0.875rem; line-height: 1.6; margin-bottom: 0.25rem;
83
+ }
84
+ .sn-info-icon {
85
+ position: relative; cursor: help; color: #888;
86
+ display: inline-flex; align-items: center; line-height: 1;
87
+ }
88
+ .sn-info-icon::after {
89
+ content: attr(data-tooltip);
90
+ position: absolute; bottom: calc(100% + 6px);
91
+ left: 50%; transform: translateX(-50%);
92
+ background: #262730; color: #fafafa;
93
+ padding: 0.4rem 0.6rem; border-radius: 0.3rem;
94
+ font-size: 0.78rem; font-weight: 400;
95
+ white-space: normal; width: max-content; max-width: 14rem;
96
+ box-shadow: 0 2px 8px rgba(0,0,0,0.18);
97
+ opacity: 0; pointer-events: none; transition: opacity 120ms;
98
+ z-index: 1000;
99
+ }
100
+ .sn-info-icon:hover::after,
101
+ .sn-info-icon:focus::after { opacity: 1; }
102
+ </style>
103
+ """
104
+
105
+
106
+ def _inject_safety_css() -> None:
107
+ """Inject the minimal CSS that's not covered by Streamlit's theme config."""
108
+ st.markdown(_IFRAME_SAFETY_CSS, unsafe_allow_html=True)
109
+
110
 
111
  def _config_from_url() -> ReactionConfig | None:
112
  """If ?cfg=… is present in the URL, decode it into a ReactionConfig."""
 
154
 
155
  def generate_from_ui(
156
  cfg: ReactionConfig,
157
+ min_pipet_uL: float = 0.1,
158
  *,
159
  design_kind: str = "full",
160
  alpha: str = "face",
 
220
  # ---------- sidebar ----------
221
 
222
  def _sidebar(default_cfg: ReactionConfig) -> tuple[ReactionConfig, float, dict]:
223
+ if "expand_all" not in st.session_state:
224
+ st.session_state["expand_all"] = False
225
+
226
  with st.sidebar:
227
+ st.markdown("### Reaction Parameters")
228
  c1, c2 = st.columns(2)
229
  vol = c1.number_input(
230
  "Volume (µL)", value=float(default_cfg.reaction_volume_uL),
 
239
  "Center pts", min_value=0, max_value=10,
240
  value=int(default_cfg.center_points), step=1, key="cps",
241
  )
242
+ with c2:
243
+ st.markdown(
244
+ '<div class="sn-label-with-info">'
245
+ '<span>Seed</span>'
246
+ '<span class="sn-info-icon" tabindex="0" '
247
+ 'data-tooltip="Randomizes run order. Same seed yields identical output.">'
248
+ '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" '
249
+ 'viewBox="0 0 24 24" fill="none" stroke="currentColor" '
250
+ 'stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'
251
+ '<circle cx="12" cy="12" r="10"/>'
252
+ '<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>'
253
+ '<line x1="12" y1="17" x2="12.01" y2="17"/>'
254
+ '</svg></span>'
255
+ '</div>',
256
+ unsafe_allow_html=True,
257
+ )
258
+ seed = st.number_input(
259
+ "Seed", value=int(default_cfg.seed), step=1, key="seed",
260
+ label_visibility="collapsed",
 
 
 
 
 
 
 
 
 
 
261
  )
262
 
263
+ ec1, ec2 = st.columns(2)
264
+ if ec1.button("Expand all", width="stretch", key="expand_all_btn"):
265
+ st.session_state["expand_all"] = True
266
+ st.rerun()
267
+ if ec2.button("Collapse all", width="stretch", key="collapse_all_btn"):
268
+ st.session_state["expand_all"] = False
269
+ st.rerun()
270
+
271
+ expanded = st.session_state["expand_all"]
272
+
273
+ with st.expander("Design Type", expanded=expanded):
274
+ design_kind = st.radio(
275
+ "Design type",
276
+ options=["full", "ccd", "pb"],
277
  index=0,
278
+ format_func=lambda k: {
279
+ "full": "Full factorial (2ᵏ + centers)",
280
+ "ccd": "Central-composite (CCD follow-up)",
281
+ "pb": "Plackett-Burman (screening, k > 5)",
282
+ }[k],
283
+ key="design_kind",
284
+ horizontal=False,
285
+ label_visibility="collapsed",
286
  )
287
+ alpha = "face"
288
+ if design_kind == "ccd":
289
+ alpha = st.select_slider(
290
+ "Axial α",
291
+ options=["face", "rotatable"],
292
+ value="face",
293
+ help=(
294
+ "`face` (α=1) stays within low/high; `rotatable` extends "
295
+ "axial setpoints beyond the range — use only if your "
296
+ "stocks allow the wider span."
297
+ ),
298
+ key="ccd_alpha",
299
+ )
300
 
301
+ with st.expander("Plate Layout", expanded=expanded):
302
+ plate_choice = st.radio(
303
+ "Plate",
304
+ options=["none", "96", "384"],
305
+ horizontal=True,
306
+ index=0,
307
+ key="plate_choice",
308
+ label_visibility="collapsed",
309
+ )
310
+ plate_layout = "column-major"
311
+ if plate_choice != "none":
312
+ plate_layout = st.radio(
313
+ "Fill order",
314
+ options=["column-major", "row-major", "randomized"],
315
+ index=0,
316
+ horizontal=True,
317
+ key="plate_layout",
318
+ )
319
+
320
+ with st.expander("High / Low Setpoints", expanded=expanded):
321
+ factor_rows = [
322
+ {
323
+ "factor": f.display or f.name,
324
+ "low": float(f.low),
325
+ "high": float(f.high),
326
+ }
327
+ for f in default_cfg.factors
328
+ ]
329
+ edited = st.data_editor(
330
+ pd.DataFrame(factor_rows),
331
+ hide_index=True,
332
+ disabled=["factor"],
333
+ key="factors_editor",
334
+ column_config={
335
+ "factor": st.column_config.TextColumn("Factor", width="medium"),
336
+ "low": st.column_config.NumberColumn("Low", format="%.3g"),
337
+ "high": st.column_config.NumberColumn("High", format="%.3g"),
338
+ },
339
+ )
340
  new_factors: list[Factor] = []
341
  for orig, row in zip(default_cfg.factors, edited.itertuples(index=False),
342
  strict=True):
 
344
  "low": float(row.low), "high": float(row.high),
345
  }))
346
 
347
+ with st.expander("Stock concentrations", expanded=expanded):
348
  stock_rows = [
349
  {
350
  "key": k,
 
373
  for row in stock_edit.itertuples(index=False)
374
  }
375
 
376
+ with st.expander("Fixed reagents", expanded=expanded):
377
  st.caption("Reagents with a constant volume per run (not swept).")
378
  fixed_rows = [
379
  {"reagent": k, "volume_uL": float(v)}
 
396
  for row in fixed_edit.itertuples(index=False)
397
  }
398
 
399
+ with st.expander("Advanced", expanded=expanded):
400
  min_pipet = st.number_input(
401
  "Min pipetting volume (µL)",
402
+ value=0.1, min_value=0.0, step=0.1, key="min_pipet",
403
  help="Volumes below this threshold emit a warning on the bench sheet.",
404
  )
405
 
 
487
  "sees them at the bench."
488
  )
489
 
490
+ st.markdown("#### Design")
491
+ display_cols = [f.name for f in cfg.factors] + ["is_center"]
492
+ st.dataframe(
493
+ design[display_cols],
494
+ height=540,
495
+ width="stretch",
496
+ column_config={
497
+ "is_center": st.column_config.CheckboxColumn(
498
+ "center?", help="Center-point replicate",
499
+ ),
500
+ },
501
+ )
502
+
503
+ with st.expander("Bench sheet preview", expanded=False):
504
+ # Render inside a white-background iframe so the template's black
505
+ # text stays legible regardless of the surrounding theme.
506
+ st.components.v1.html(art["html"], height=540, scrolling=True)
507
 
508
  if art.get("plate_df") is not None:
509
  st.markdown("#### Plate layout")
 
807
  "About": "Screenase — DoE planner for in-vitro transcription.",
808
  },
809
  )
810
+ _inject_safety_css()
811
 
812
  header_l, header_r = st.columns([3, 1])
813
  with header_l: