riazmo commited on
Commit
a2bd6e4
Β·
verified Β·
1 Parent(s): e2a2bc6

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +398 -13
app.py CHANGED
@@ -709,7 +709,11 @@ async def run_stage2_analysis(competitors_str: str = "", progress=gr.Progress())
709
  state.log("")
710
  state.log("🎨 Generating visual previews...")
711
 
712
- from core.preview_generator import generate_typography_preview_html, generate_color_ramps_preview_html
 
 
 
 
713
 
714
  primary_font = fonts.get("primary", "Open Sans")
715
 
@@ -737,24 +741,54 @@ async def run_stage2_analysis(competitors_str: str = "", progress=gr.Progress())
737
  sample_text="The quick brown fox jumps over the lazy dog",
738
  )
739
 
740
- color_ramps_preview_html = generate_color_ramps_preview_html(
741
- color_tokens=color_dict,
742
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
743
 
744
- state.log(" βœ… Visual previews generated")
 
 
 
 
 
 
745
 
746
  progress(1.0, desc="βœ… Analysis complete!")
747
 
748
  return (status, state.get_logs(), brand_md, font_families_md,
749
  typography_desktop_data, typography_mobile_data, spacing_data,
750
  base_colors_md, color_ramps_md, radius_md, shadows_md,
751
- typography_preview_html, color_ramps_preview_html)
 
752
 
753
  except Exception as e:
754
  import traceback
755
  state.log(f"❌ Error: {str(e)}")
756
  state.log(traceback.format_exc())
757
- return (f"❌ Analysis failed: {str(e)}", state.get_logs(), "", "", None, None, None, "", "", "", "", "", "")
758
 
759
 
760
  def normalized_to_dict(normalized) -> dict:
@@ -1052,6 +1086,303 @@ def format_font_families_display(fonts: dict) -> str:
1052
  return "\n".join(lines)
1053
 
1054
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1055
  def format_typography_comparison_viewport(normalized_tokens, base_size: int, viewport: str) -> list:
1056
  """Format typography comparison for a specific viewport."""
1057
  if not normalized_tokens:
@@ -1304,8 +1635,8 @@ def snap_to_grid(value: float, base: int) -> int:
1304
  return round(value / base) * base
1305
 
1306
 
1307
- def apply_selected_upgrades(type_choice: str, spacing_choice: str, apply_ramps: bool):
1308
- """Apply selected upgrade options."""
1309
  if not state.upgrade_recommendations:
1310
  return "❌ Run analysis first", ""
1311
 
@@ -1322,6 +1653,39 @@ def apply_selected_upgrades(type_choice: str, spacing_choice: str, apply_ramps:
1322
  state.log(f" Spacing: {spacing_choice}")
1323
  state.log(f" Color Ramps: {'Yes' if apply_ramps else 'No'}")
1324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1325
  state.log("βœ… Upgrades applied! Proceed to Stage 3 for export.")
1326
 
1327
  return "βœ… Upgrades applied! Proceed to Stage 3 to export.", state.get_logs()
@@ -2021,13 +2385,33 @@ def create_ui():
2021
  gr.Markdown("*Font family will be preserved. Sizes rounded to even numbers.*")
2022
 
2023
  # =============================================================
2024
- # COLORS SECTION - Base Colors + Ramps
2025
  # =============================================================
2026
  gr.Markdown("---")
2027
  gr.Markdown("## 🎨 Colors")
2028
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2029
  # Visual Preview
2030
- with gr.Accordion("πŸ‘οΈ Color Ramps Visual Preview", open=True):
2031
  stage2_color_ramps_preview = gr.HTML(
2032
  value="<div style='padding: 20px; background: #f5f5f5; border-radius: 8px; color: #666;'>Color ramps preview will appear after analysis...</div>",
2033
  label="Color Ramps Preview"
@@ -2164,13 +2548,14 @@ def create_ui():
2164
  outputs=[stage2_status, stage2_log, brand_comparison, font_families_display,
2165
  typography_desktop, typography_mobile, spacing_comparison,
2166
  base_colors_display, color_ramps_display, radius_display, shadows_display,
2167
- stage2_typography_preview, stage2_color_ramps_preview],
 
2168
  )
2169
 
2170
  # Stage 2: Apply upgrades
2171
  apply_upgrades_btn.click(
2172
  fn=apply_selected_upgrades,
2173
- inputs=[type_scale_radio, spacing_radio, color_ramps_checkbox],
2174
  outputs=[apply_status, stage2_log],
2175
  )
2176
 
 
709
  state.log("")
710
  state.log("🎨 Generating visual previews...")
711
 
712
+ from core.preview_generator import (
713
+ generate_typography_preview_html,
714
+ generate_color_ramps_preview_html,
715
+ generate_semantic_color_ramps_html
716
+ )
717
 
718
  primary_font = fonts.get("primary", "Open Sans")
719
 
 
741
  sample_text="The quick brown fox jumps over the lazy dog",
742
  )
743
 
744
+ # Use semantic color ramps if available, otherwise fallback to regular
745
+ semantic_analysis = getattr(state, 'semantic_analysis', None)
746
+ if semantic_analysis:
747
+ # Extract LLM color recommendations
748
+ llm_color_recs = {}
749
+ if final_recs and isinstance(final_recs, dict):
750
+ llm_color_recs = final_recs.get("color_recommendations", {})
751
+ # Also add accessibility fixes
752
+ aa_fixes = final_recs.get("accessibility_fixes", [])
753
+ if aa_fixes:
754
+ llm_color_recs["changes_made"] = [
755
+ f"AA fix suggested for {f.get('color', '?')}"
756
+ for f in aa_fixes if isinstance(f, dict)
757
+ ][:5]
758
+
759
+ color_ramps_preview_html = generate_semantic_color_ramps_html(
760
+ semantic_analysis=semantic_analysis,
761
+ color_tokens=color_dict,
762
+ llm_recommendations={"color_recommendations": llm_color_recs} if llm_color_recs else None,
763
+ )
764
+ state.log(" βœ… Semantic color ramps preview generated (with LLM recommendations)")
765
+ else:
766
+ color_ramps_preview_html = generate_color_ramps_preview_html(
767
+ color_tokens=color_dict,
768
+ )
769
+ state.log(" βœ… Color ramps preview generated (no semantic data)")
770
 
771
+ state.log(" βœ… Typography preview generated")
772
+
773
+ # Generate LLM recommendations display
774
+ llm_recs_html = format_llm_color_recommendations_html(final_recs, semantic_analysis)
775
+ llm_recs_table = format_llm_color_recommendations_table(final_recs, semantic_analysis)
776
+
777
+ state.log(" βœ… LLM recommendations formatted")
778
 
779
  progress(1.0, desc="βœ… Analysis complete!")
780
 
781
  return (status, state.get_logs(), brand_md, font_families_md,
782
  typography_desktop_data, typography_mobile_data, spacing_data,
783
  base_colors_md, color_ramps_md, radius_md, shadows_md,
784
+ typography_preview_html, color_ramps_preview_html,
785
+ llm_recs_html, llm_recs_table)
786
 
787
  except Exception as e:
788
  import traceback
789
  state.log(f"❌ Error: {str(e)}")
790
  state.log(traceback.format_exc())
791
+ return (f"❌ Analysis failed: {str(e)}", state.get_logs(), "", "", None, None, None, "", "", "", "", "", "", "", [])
792
 
793
 
794
  def normalized_to_dict(normalized) -> dict:
 
1086
  return "\n".join(lines)
1087
 
1088
 
1089
+ def format_llm_color_recommendations_html(final_recs: dict, semantic_analysis: dict) -> str:
1090
+ """Generate HTML showing LLM color recommendations with before/after comparison."""
1091
+
1092
+ if not final_recs:
1093
+ return '''
1094
+ <div style="padding: 20px; background: #f0f0f0 !important; border-radius: 8px; text-align: center;">
1095
+ <p style="color: #666 !important;">No LLM recommendations available yet. Run analysis first.</p>
1096
+ </div>
1097
+ '''
1098
+
1099
+ color_recs = final_recs.get("color_recommendations", {})
1100
+ aa_fixes = final_recs.get("accessibility_fixes", [])
1101
+
1102
+ if not color_recs and not aa_fixes:
1103
+ return '''
1104
+ <div style="padding: 20px; background: #d4edda !important; border-radius: 8px; border: 1px solid #28a745;">
1105
+ <p style="color: #155724 !important; margin: 0;">βœ… No color changes recommended. Your colors look good!</p>
1106
+ </div>
1107
+ '''
1108
+
1109
+ # Build recommendations HTML
1110
+ recs_html = ""
1111
+
1112
+ # Process color recommendations
1113
+ for role, rec in color_recs.items():
1114
+ if not isinstance(rec, dict):
1115
+ continue
1116
+ if role in ["generate_ramps_for", "changes_made"]:
1117
+ continue
1118
+
1119
+ current = rec.get("current", "?")
1120
+ suggested = rec.get("suggested", current)
1121
+ action = rec.get("action", "keep")
1122
+ rationale = rec.get("rationale", "")
1123
+
1124
+ if action == "keep" or suggested == current:
1125
+ # No change needed
1126
+ recs_html += f'''
1127
+ <div class="llm-rec-row keep">
1128
+ <div class="rec-color-box" style="background: {current};"></div>
1129
+ <div class="rec-details">
1130
+ <span class="rec-role">{role}</span>
1131
+ <span class="rec-current">{current}</span>
1132
+ <span class="rec-action keep">βœ“ Keep</span>
1133
+ </div>
1134
+ </div>
1135
+ '''
1136
+ else:
1137
+ # Change suggested
1138
+ recs_html += f'''
1139
+ <div class="llm-rec-row change">
1140
+ <div class="rec-comparison">
1141
+ <div class="rec-before">
1142
+ <div class="rec-color-box" style="background: {current};"></div>
1143
+ <span class="rec-label">Before</span>
1144
+ <span class="rec-hex">{current}</span>
1145
+ </div>
1146
+ <span class="rec-arrow">β†’</span>
1147
+ <div class="rec-after">
1148
+ <div class="rec-color-box" style="background: {suggested};"></div>
1149
+ <span class="rec-label">After</span>
1150
+ <span class="rec-hex">{suggested}</span>
1151
+ </div>
1152
+ </div>
1153
+ <div class="rec-details">
1154
+ <span class="rec-role">{role}</span>
1155
+ <span class="rec-rationale">{rationale[:80]}...</span>
1156
+ </div>
1157
+ </div>
1158
+ '''
1159
+
1160
+ # Process accessibility fixes
1161
+ for fix in aa_fixes:
1162
+ if not isinstance(fix, dict):
1163
+ continue
1164
+
1165
+ color = fix.get("color", "?")
1166
+ role = fix.get("role", "unknown")
1167
+ issue = fix.get("issue", "contrast issue")
1168
+ fix_color = fix.get("fix", color)
1169
+ current_contrast = fix.get("current_contrast", "?")
1170
+ fixed_contrast = fix.get("fixed_contrast", "?")
1171
+
1172
+ if fix_color and fix_color != color:
1173
+ recs_html += f'''
1174
+ <div class="llm-rec-row aa-fix">
1175
+ <div class="rec-comparison">
1176
+ <div class="rec-before">
1177
+ <div class="rec-color-box" style="background: {color};"></div>
1178
+ <span class="rec-label">⚠️ {current_contrast}:1</span>
1179
+ <span class="rec-hex">{color}</span>
1180
+ </div>
1181
+ <span class="rec-arrow">β†’</span>
1182
+ <div class="rec-after">
1183
+ <div class="rec-color-box" style="background: {fix_color};"></div>
1184
+ <span class="rec-label">βœ“ {fixed_contrast}:1</span>
1185
+ <span class="rec-hex">{fix_color}</span>
1186
+ </div>
1187
+ </div>
1188
+ <div class="rec-details">
1189
+ <span class="rec-role">{role}</span>
1190
+ <span class="rec-issue">πŸ”΄ {issue}</span>
1191
+ </div>
1192
+ </div>
1193
+ '''
1194
+
1195
+ if not recs_html:
1196
+ return '''
1197
+ <div style="padding: 20px; background: #d4edda !important; border-radius: 8px; border: 1px solid #28a745;">
1198
+ <p style="color: #155724 !important; margin: 0;">βœ… No color changes recommended. Your colors look good!</p>
1199
+ </div>
1200
+ '''
1201
+
1202
+ html = f'''
1203
+ <style>
1204
+ .llm-recs-container {{
1205
+ font-family: system-ui, -apple-system, sans-serif;
1206
+ background: #f5f5f5 !important;
1207
+ border-radius: 12px;
1208
+ padding: 16px;
1209
+ }}
1210
+
1211
+ .llm-rec-row {{
1212
+ display: flex;
1213
+ align-items: center;
1214
+ padding: 12px;
1215
+ margin-bottom: 12px;
1216
+ border-radius: 8px;
1217
+ background: #ffffff !important;
1218
+ border: 1px solid #e0e0e0 !important;
1219
+ }}
1220
+
1221
+ .llm-rec-row.change {{
1222
+ border-left: 4px solid #f59e0b !important;
1223
+ }}
1224
+
1225
+ .llm-rec-row.aa-fix {{
1226
+ border-left: 4px solid #dc2626 !important;
1227
+ background: #fef2f2 !important;
1228
+ }}
1229
+
1230
+ .llm-rec-row.keep {{
1231
+ border-left: 4px solid #22c55e !important;
1232
+ background: #f0fdf4 !important;
1233
+ }}
1234
+
1235
+ .rec-comparison {{
1236
+ display: flex;
1237
+ align-items: center;
1238
+ gap: 12px;
1239
+ margin-right: 20px;
1240
+ }}
1241
+
1242
+ .rec-before, .rec-after {{
1243
+ display: flex;
1244
+ flex-direction: column;
1245
+ align-items: center;
1246
+ gap: 4px;
1247
+ }}
1248
+
1249
+ .rec-color-box {{
1250
+ width: 48px;
1251
+ height: 48px;
1252
+ border-radius: 8px;
1253
+ border: 2px solid rgba(0,0,0,0.15) !important;
1254
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1255
+ }}
1256
+
1257
+ .rec-label {{
1258
+ font-size: 11px;
1259
+ font-weight: 600;
1260
+ color: #666 !important;
1261
+ }}
1262
+
1263
+ .rec-hex {{
1264
+ font-family: 'SF Mono', Monaco, monospace;
1265
+ font-size: 11px;
1266
+ color: #333 !important;
1267
+ }}
1268
+
1269
+ .rec-arrow {{
1270
+ font-size: 20px;
1271
+ color: #666 !important;
1272
+ font-weight: bold;
1273
+ }}
1274
+
1275
+ .rec-details {{
1276
+ flex: 1;
1277
+ display: flex;
1278
+ flex-direction: column;
1279
+ gap: 4px;
1280
+ }}
1281
+
1282
+ .rec-role {{
1283
+ font-weight: 700;
1284
+ font-size: 14px;
1285
+ color: #1a1a1a !important;
1286
+ }}
1287
+
1288
+ .rec-action {{
1289
+ font-size: 12px;
1290
+ padding: 2px 8px;
1291
+ border-radius: 4px;
1292
+ }}
1293
+
1294
+ .rec-action.keep {{
1295
+ background: #dcfce7 !important;
1296
+ color: #166534 !important;
1297
+ }}
1298
+
1299
+ .rec-rationale {{
1300
+ font-size: 12px;
1301
+ color: #666 !important;
1302
+ }}
1303
+
1304
+ .rec-issue {{
1305
+ font-size: 12px;
1306
+ color: #991b1b !important;
1307
+ font-weight: 500;
1308
+ }}
1309
+ </style>
1310
+
1311
+ <div class="llm-recs-container">
1312
+ {recs_html}
1313
+ </div>
1314
+ '''
1315
+
1316
+ return html
1317
+
1318
+
1319
+ def format_llm_color_recommendations_table(final_recs: dict, semantic_analysis: dict) -> list:
1320
+ """Generate table data for LLM color recommendations with accept/reject checkboxes."""
1321
+
1322
+ rows = []
1323
+
1324
+ if not final_recs:
1325
+ return rows
1326
+
1327
+ color_recs = final_recs.get("color_recommendations", {})
1328
+ aa_fixes = final_recs.get("accessibility_fixes", [])
1329
+
1330
+ # Process color recommendations
1331
+ for role, rec in color_recs.items():
1332
+ if not isinstance(rec, dict):
1333
+ continue
1334
+ if role in ["generate_ramps_for", "changes_made"]:
1335
+ continue
1336
+
1337
+ current = rec.get("current", "?")
1338
+ suggested = rec.get("suggested", current)
1339
+ action = rec.get("action", "keep")
1340
+ rationale = rec.get("rationale", "")[:50]
1341
+
1342
+ if action != "keep" and suggested != current:
1343
+ # Calculate contrast improvement
1344
+ try:
1345
+ from core.color_utils import get_contrast_with_white
1346
+ old_contrast = get_contrast_with_white(current)
1347
+ new_contrast = get_contrast_with_white(suggested)
1348
+ contrast_str = f"{old_contrast:.1f} β†’ {new_contrast:.1f}"
1349
+ except:
1350
+ contrast_str = "?"
1351
+
1352
+ rows.append([
1353
+ True, # Accept checkbox (default True)
1354
+ role,
1355
+ current,
1356
+ rationale or action,
1357
+ suggested,
1358
+ contrast_str,
1359
+ ])
1360
+
1361
+ # Process accessibility fixes
1362
+ for fix in aa_fixes:
1363
+ if not isinstance(fix, dict):
1364
+ continue
1365
+
1366
+ color = fix.get("color", "?")
1367
+ role = fix.get("role", "unknown")
1368
+ issue = fix.get("issue", "contrast")[:40]
1369
+ fix_color = fix.get("fix", color)
1370
+ current_contrast = fix.get("current_contrast", "?")
1371
+ fixed_contrast = fix.get("fixed_contrast", "?")
1372
+
1373
+ if fix_color and fix_color != color:
1374
+ rows.append([
1375
+ True, # Accept checkbox
1376
+ f"{role} (AA fix)",
1377
+ color,
1378
+ issue,
1379
+ fix_color,
1380
+ f"{current_contrast}:1 β†’ {fixed_contrast}:1",
1381
+ ])
1382
+
1383
+ return rows
1384
+
1385
+
1386
  def format_typography_comparison_viewport(normalized_tokens, base_size: int, viewport: str) -> list:
1387
  """Format typography comparison for a specific viewport."""
1388
  if not normalized_tokens:
 
1635
  return round(value / base) * base
1636
 
1637
 
1638
+ def apply_selected_upgrades(type_choice: str, spacing_choice: str, apply_ramps: bool, color_recs_table: list = None):
1639
+ """Apply selected upgrade options including LLM color recommendations."""
1640
  if not state.upgrade_recommendations:
1641
  return "❌ Run analysis first", ""
1642
 
 
1653
  state.log(f" Spacing: {spacing_choice}")
1654
  state.log(f" Color Ramps: {'Yes' if apply_ramps else 'No'}")
1655
 
1656
+ # Process accepted color recommendations
1657
+ accepted_color_changes = []
1658
+ if color_recs_table:
1659
+ state.log("")
1660
+ state.log(" 🎨 LLM Color Recommendations:")
1661
+ for row in color_recs_table:
1662
+ if len(row) >= 5:
1663
+ accept = row[0] # Boolean checkbox
1664
+ role = row[1] # Role name
1665
+ current = row[2] # Current color
1666
+ issue = row[3] # Issue description
1667
+ suggested = row[4] # Suggested color
1668
+
1669
+ if accept and suggested and current != suggested:
1670
+ accepted_color_changes.append({
1671
+ "role": role,
1672
+ "from": current,
1673
+ "to": suggested,
1674
+ "reason": issue,
1675
+ })
1676
+ state.log(f" β”œβ”€ βœ… ACCEPTED: {role}")
1677
+ state.log(f" β”‚ └─ {current} β†’ {suggested}")
1678
+ elif not accept:
1679
+ state.log(f" β”œβ”€ ❌ REJECTED: {role} (keeping {current})")
1680
+
1681
+ # Store accepted changes
1682
+ state.selected_upgrades["color_changes"] = accepted_color_changes
1683
+
1684
+ if accepted_color_changes:
1685
+ state.log("")
1686
+ state.log(f" πŸ“Š {len(accepted_color_changes)} color change(s) will be applied to export")
1687
+
1688
+ state.log("")
1689
  state.log("βœ… Upgrades applied! Proceed to Stage 3 for export.")
1690
 
1691
  return "βœ… Upgrades applied! Proceed to Stage 3 to export.", state.get_logs()
 
2385
  gr.Markdown("*Font family will be preserved. Sizes rounded to even numbers.*")
2386
 
2387
  # =============================================================
2388
+ # COLORS SECTION - Base Colors + Ramps + LLM Recommendations
2389
  # =============================================================
2390
  gr.Markdown("---")
2391
  gr.Markdown("## 🎨 Colors")
2392
 
2393
+ # LLM Recommendations Section (NEW)
2394
+ with gr.Accordion("πŸ€– LLM Color Recommendations", open=True):
2395
+ gr.Markdown("""
2396
+ *The LLMs analyzed your colors and made these suggestions. Accept or reject each one.*
2397
+ """)
2398
+
2399
+ llm_color_recommendations = gr.HTML(
2400
+ value="<div style='padding: 20px; background: #f5f5f5; border-radius: 8px; color: #666;'>LLM recommendations will appear after analysis...</div>",
2401
+ label="LLM Recommendations"
2402
+ )
2403
+
2404
+ # Accept/Reject table for color recommendations
2405
+ color_recommendations_table = gr.Dataframe(
2406
+ headers=["Accept", "Role", "Current", "Issue", "Suggested", "Contrast"],
2407
+ datatype=["bool", "str", "str", "str", "str", "str"],
2408
+ label="Color Recommendations",
2409
+ interactive=True,
2410
+ col_count=(6, "fixed"),
2411
+ )
2412
+
2413
  # Visual Preview
2414
+ with gr.Accordion("πŸ‘οΈ Color Ramps Visual Preview (Semantic Groups)", open=True):
2415
  stage2_color_ramps_preview = gr.HTML(
2416
  value="<div style='padding: 20px; background: #f5f5f5; border-radius: 8px; color: #666;'>Color ramps preview will appear after analysis...</div>",
2417
  label="Color Ramps Preview"
 
2548
  outputs=[stage2_status, stage2_log, brand_comparison, font_families_display,
2549
  typography_desktop, typography_mobile, spacing_comparison,
2550
  base_colors_display, color_ramps_display, radius_display, shadows_display,
2551
+ stage2_typography_preview, stage2_color_ramps_preview,
2552
+ llm_color_recommendations, color_recommendations_table],
2553
  )
2554
 
2555
  # Stage 2: Apply upgrades
2556
  apply_upgrades_btn.click(
2557
  fn=apply_selected_upgrades,
2558
+ inputs=[type_scale_radio, spacing_radio, color_ramps_checkbox, color_recommendations_table],
2559
  outputs=[apply_status, stage2_log],
2560
  )
2561