Moha2266 commited on
Commit
09fbe36
·
verified ·
1 Parent(s): b1282e3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +61 -137
app.py CHANGED
@@ -53,9 +53,7 @@ CUSTOM_CSS = """
53
  --bg1: #18004f;
54
  --bg2: #2a0a89;
55
  --panel: rgba(255,255,255,0.88);
56
- --panel-2: rgba(255,255,255,0.75);
57
  --text: #20114f;
58
- --muted: #6958a6;
59
  --gold: #f3c544;
60
  --orange: #ff8b1a;
61
  --line: rgba(255,255,255,0.20);
@@ -78,6 +76,7 @@ html, body, .gradio-container {
78
  padding-bottom: 32px !important;
79
  }
80
 
 
81
  .gr-tab-nav {
82
  background: rgba(34, 9, 110, 0.70) !important;
83
  border: 1px solid rgba(255,255,255,0.14) !important;
@@ -89,7 +88,9 @@ html, body, .gradio-container {
89
  .gr-tab-nav button span,
90
  .gr-tab-nav button p,
91
  .gr-tab-nav button div,
92
- .gr-tab-nav button label {
 
 
93
  color: #ffffff !important;
94
  opacity: 1 !important;
95
  font-weight: 800 !important;
@@ -98,46 +99,34 @@ html, body, .gradio-container {
98
  .gr-tab-nav button {
99
  border-radius: 14px !important;
100
  transition: background 0.2s ease, color 0.2s ease !important;
 
101
  }
102
 
103
- .gr-tab-nav button:hover {
 
104
  background: #000000 !important;
 
 
105
  }
106
 
107
- .gr-tab-nav button:hover,
108
- .gr-tab-nav button:hover span,
109
- .gr-tab-nav button:hover p,
110
- .gr-tab-nav button:hover div,
111
- .gr-tab-nav button:hover label {
112
  color: #ffffff !important;
113
  }
114
 
115
- .gr-tab-nav button.selected {
 
116
  background: transparent !important;
117
  border-bottom: 3px solid var(--orange) !important;
 
118
  }
119
 
120
- .gr-tab-nav button.selected,
121
- .gr-tab-nav button.selected span,
122
- .gr-tab-nav button.selected p,
123
- .gr-tab-nav button.selected div,
124
- .gr-tab-nav button.selected label {
125
- color: var(--orange) !important;
126
- }
127
-
128
- button[aria-selected="false"],
129
- button[aria-selected="false"] * {
130
- color: #ffffff !important;
131
- }
132
- button[aria-selected="false"]:hover,
133
- button[aria-selected="false"]:hover * {
134
- color: #ffffff !important;
135
- }
136
- button[aria-selected="true"],
137
  button[aria-selected="true"] * {
138
  color: var(--orange) !important;
139
  }
140
 
 
141
  .app-shell {
142
  background: rgba(28, 8, 94, 0.58);
143
  border: 1px solid var(--line);
@@ -270,7 +259,6 @@ button[aria-selected="true"] * {
270
  .section-title {
271
  color: var(--text) !important;
272
  font-weight: 900 !important;
273
- letter-spacing: 0.2px;
274
  }
275
 
276
  .section-title-white,
@@ -288,12 +276,6 @@ label, .gr-form > div > label, .gr-box, .gr-panel {
288
  border: none !important;
289
  }
290
 
291
- .gr-button-secondary {
292
- background: rgba(43, 29, 125, 0.14) !important;
293
- color: var(--text) !important;
294
- border: 1px solid rgba(43, 29, 125, 0.15) !important;
295
- }
296
-
297
  .gradio-container .block {
298
  border-radius: 16px !important;
299
  }
@@ -312,16 +294,6 @@ label, .gr-form > div > label, .gr-box, .gr-panel {
312
  color: white !important;
313
  }
314
 
315
- .dashboard-white-text h1,
316
- .dashboard-white-text h2,
317
- .dashboard-white-text h3,
318
- .dashboard-white-text h4,
319
- .dashboard-white-text strong,
320
- .dashboard-white-text li,
321
- .dashboard-white-text p {
322
- color: white !important;
323
- }
324
-
325
  .dashboard-white-text {
326
  padding-bottom: 22px !important;
327
  }
@@ -442,29 +414,13 @@ def safe_df_from_records(records):
442
  return pd.DataFrame(records)
443
 
444
 
445
- def rename_for_coworking(df: pd.DataFrame) -> pd.DataFrame:
446
- if df is None or df.empty:
447
- return df
448
-
449
- rename_map = {
450
- "hotel_name": "location_name",
451
- "room_type": "space_type",
452
- "avg_price": "avg_space_price",
453
- }
454
- out = df.copy()
455
- for old, new in rename_map.items():
456
- if old in out.columns:
457
- out = out.rename(columns={old: new})
458
- return out
459
-
460
-
461
  def build_kpi_cards(pricing_df: pd.DataFrame, risk_alerts_df: pd.DataFrame) -> str:
462
  pricing_df = pricing_df.copy() if pricing_df is not None else pd.DataFrame()
463
  risk_alerts_df = risk_alerts_df.copy() if risk_alerts_df is not None else pd.DataFrame()
464
 
465
- avg_price = pricing_df["avg_price"].mean() if "avg_price" in pricing_df.columns and not pricing_df.empty else None
466
- avg_occ = pricing_df["avg_occupancy"].mean() if "avg_occupancy" in pricing_df.columns and not pricing_df.empty else None
467
- avg_cancel = pricing_df["avg_cancellation"].mean() if "avg_cancellation" in pricing_df.columns and not pricing_df.empty else None
468
  total_revenue = pricing_df["total_revenue"].sum() if "total_revenue" in pricing_df.columns and not pricing_df.empty else None
469
  raise_count = int((pricing_df["pricing_action"] == "Raise price").sum()) if "pricing_action" in pricing_df.columns and not pricing_df.empty else 0
470
  alert_count = len(risk_alerts_df)
@@ -472,8 +428,8 @@ def build_kpi_cards(pricing_df: pd.DataFrame, risk_alerts_df: pd.DataFrame) -> s
472
  cards = [
473
  ("Locations/Segments", len(pricing_df), "#5f44cc"),
474
  ("Avg Space Price", fmt_num(avg_price), "#2fbf9f"),
475
- ("Avg Occupancy", fmt_pct(avg_occ), "#f3c544"),
476
- ("Avg Cancellation", fmt_pct(avg_cancel), "#e05b77"),
477
  ("Total Revenue", fmt_num(total_revenue), "#3ba0ff"),
478
  ("Raise Opportunities", raise_count, "#8a5cff"),
479
  ("Risk Alerts", alert_count, "#ff7a5c"),
@@ -541,12 +497,7 @@ def chart_action_distribution(pricing_df: pd.DataFrame) -> go.Figure:
541
  title="Pricing Action Distribution",
542
  color_discrete_sequence=["#5f44cc", "#2fbf9f", "#f3c544", "#e05b77", "#3ba0ff"],
543
  )
544
- fig.update_layout(
545
- template="plotly_white",
546
- paper_bgcolor="rgba(255,255,255,0.95)",
547
- height=420,
548
- showlegend=False,
549
- )
550
  return fig
551
 
552
 
@@ -554,10 +505,8 @@ def chart_theme_counts(theme_counts: dict) -> go.Figure:
554
  if not theme_counts:
555
  return empty_figure("Top Complaint / Satisfaction Themes")
556
 
557
- df = pd.DataFrame({
558
- "theme": list(theme_counts.keys()),
559
- "count": list(theme_counts.values())
560
- }).sort_values("count", ascending=True).tail(10)
561
 
562
  fig = px.bar(
563
  df,
@@ -568,53 +517,41 @@ def chart_theme_counts(theme_counts: dict) -> go.Figure:
568
  color="count",
569
  color_continuous_scale=["#d8cdfa", "#5f44cc"],
570
  )
571
- fig.update_layout(
572
- template="plotly_white",
573
- paper_bgcolor="rgba(255,255,255,0.95)",
574
- height=420,
575
- )
576
  return fig
577
 
578
 
579
  def chart_avg_price_by_city(pricing_df: pd.DataFrame) -> go.Figure:
580
- if pricing_df is None or pricing_df.empty or "city" not in pricing_df.columns or "avg_price" not in pricing_df.columns:
581
  return empty_figure("Average Space Price by City")
582
 
583
- df = pricing_df.groupby("city", dropna=False)["avg_price"].mean().reset_index()
584
  fig = px.bar(
585
- df.sort_values("avg_price", ascending=False),
586
  x="city",
587
- y="avg_price",
588
  title="Average Space Price by City",
589
- color="avg_price",
590
  color_continuous_scale=["#d4d0ff", "#4320b5"],
591
  )
592
- fig.update_layout(
593
- template="plotly_white",
594
- paper_bgcolor="rgba(255,255,255,0.95)",
595
- height=420,
596
- )
597
  return fig
598
 
599
 
600
- def chart_avg_occupancy_by_space(pricing_df: pd.DataFrame) -> go.Figure:
601
- if pricing_df is None or pricing_df.empty or "room_type" not in pricing_df.columns or "avg_occupancy" not in pricing_df.columns:
602
  return empty_figure("Average Utilization by Space Type")
603
 
604
- df = pricing_df.groupby("room_type", dropna=False)["avg_occupancy"].mean().reset_index()
605
  fig = px.bar(
606
- df.sort_values("avg_occupancy", ascending=False),
607
- x="room_type",
608
- y="avg_occupancy",
609
  title="Average Utilization by Space Type",
610
- color="avg_occupancy",
611
  color_continuous_scale=["#d4fff2", "#2fbf9f"],
612
  )
613
- fig.update_layout(
614
- template="plotly_white",
615
- paper_bgcolor="rgba(255,255,255,0.95)",
616
- height=420,
617
- )
618
  fig.update_yaxes(tickformat=".0%")
619
  return fig
620
 
@@ -632,11 +569,7 @@ def chart_revenue_by_city(pricing_df: pd.DataFrame) -> go.Figure:
632
  color="total_revenue",
633
  color_continuous_scale=["#f9ddb0", "#f3c544"],
634
  )
635
- fig.update_layout(
636
- template="plotly_white",
637
- paper_bgcolor="rgba(255,255,255,0.95)",
638
- height=420,
639
- )
640
  return fig
641
 
642
 
@@ -655,12 +588,7 @@ def chart_alert_levels(risk_alerts_df: pd.DataFrame) -> go.Figure:
655
  title="Risk Alert Levels",
656
  color_discrete_map={"High": "#e05b77", "Medium": "#f3c544", "Low": "#2fbf9f"},
657
  )
658
- fig.update_layout(
659
- template="plotly_white",
660
- paper_bgcolor="rgba(255,255,255,0.95)",
661
- height=420,
662
- showlegend=False,
663
- )
664
  return fig
665
 
666
 
@@ -737,9 +665,7 @@ def run_pipeline(merged_file):
737
  risk_alerts_df = safe_df_from_records(risk_alerts)
738
 
739
  dashboard_kpis = build_kpi_cards(pricing_df, risk_alerts_df)
740
- preview_df = merged_df.head(MAX_PREVIEW_ROWS)
741
- display_pricing_df = rename_for_coworking(pricing_df.head(20))
742
- display_alerts_df = rename_for_coworking(risk_alerts_df.head(20))
743
 
744
  coworking_summary_md = f"""
745
  ### Coworking Pricing Summary
@@ -749,7 +675,7 @@ def run_pipeline(merged_file):
749
  ### Automation Notes
750
  - Workflow 1 cleaned and standardized the uploaded merged dataset.
751
  - Workflow 2 generated pricing actions and chart-ready outputs.
752
- - Workflow 3 flagged risky locations and space segments for management review.
753
  """
754
 
755
  risk_summary_md = f"""
@@ -783,11 +709,11 @@ def run_pipeline(merged_file):
783
  chart_action_distribution(pricing_df),
784
  chart_theme_counts(theme_counts),
785
  chart_avg_price_by_city(pricing_df),
786
- chart_avg_occupancy_by_space(pricing_df),
787
  chart_revenue_by_city(pricing_df),
788
  chart_alert_levels(risk_alerts_df),
789
- display_pricing_df,
790
- display_alerts_df,
791
  analysis_state,
792
  )
793
 
@@ -823,8 +749,8 @@ def fallback_ai_answer(question: str, analysis_state: dict) -> str:
823
  if not candidates.empty:
824
  top = candidates.iloc[0]
825
  return (
826
- f"The strongest current raise-price opportunity is {top.get('hotel_name', 'Unknown location')} "
827
- f"in {top.get('city', 'Unknown city')} for {top.get('room_type', 'Unknown space type')}. "
828
  f"Rationale: {top.get('rationale', 'No rationale returned.')}"
829
  )
830
  return "No raise-price opportunity was returned by the automation."
@@ -834,8 +760,8 @@ def fallback_ai_answer(question: str, analysis_state: dict) -> str:
834
  first = risk_alerts[0]
835
  return (
836
  f"{alerts_summary}\n\n"
837
- f"One flagged segment is {first.get('hotel_name', 'Unknown location')} in "
838
- f"{first.get('city', 'Unknown city')} for {first.get('room_type', 'Unknown space type')}. "
839
  f"Reason: {first.get('reasons', 'No reason returned.')}"
840
  )
841
  return "No active risk alerts were returned by the automation."
@@ -844,13 +770,13 @@ def fallback_ai_answer(question: str, analysis_state: dict) -> str:
844
  return management_summary or "No management summary is currently available."
845
 
846
  if "occupancy" in q or "utilization" in q:
847
- if "avg_occupancy" in pricing_df.columns and not pricing_df.empty:
848
- avg_occ = pricing_df["avg_occupancy"].mean()
849
- return f"The average utilization across coworking segments is {fmt_pct(avg_occ)}."
850
  return "Utilization data is not currently available."
851
 
852
  return (
853
- "I can answer questions about pricing actions, coworking themes, risk alerts, utilization, and overall management summary. "
854
  "Try asking: 'Where should prices be raised?' or 'What are the main complaint themes?'"
855
  )
856
 
@@ -860,7 +786,7 @@ def build_llm_prompt(question: str, analysis_state: dict) -> str:
860
  You are an AI assistant for a coworking space pricing and satisfaction dashboard.
861
 
862
  You must answer as a concise business analyst.
863
- Use the automation outputs below.
864
 
865
  Management summary:
866
  {analysis_state.get("management_summary", "")}
@@ -882,7 +808,7 @@ User question:
882
 
883
  Instructions:
884
  - Answer directly.
885
- - Use coworking language, not hotel language.
886
  - Mention pricing implications when relevant.
887
  - Keep it clear and business-focused.
888
  """
@@ -925,10 +851,10 @@ def ask_ai(question, history, analysis_state):
925
  else:
926
  answer = fallback_ai_answer(question, analysis_state)
927
 
928
- history.append({"role": "user", "content": question})
929
- history.append({"role": "assistant", "content": answer})
930
  return history, ""
931
 
 
932
  # =========================================================
933
  # UI
934
  # =========================================================
@@ -1012,7 +938,7 @@ with gr.Blocks(title="AI Coworking Space Pricing Optimizer") as demo:
1012
 
1013
  with gr.Row():
1014
  price_city_chart = gr.Plot(label="Average Space Price by City")
1015
- occupancy_space_chart = gr.Plot(label="Average Utilization by Space Type")
1016
 
1017
  with gr.Row():
1018
  revenue_city_chart = gr.Plot(label="Revenue by City")
@@ -1026,9 +952,7 @@ with gr.Blocks(title="AI Coworking Space Pricing Optimizer") as demo:
1026
 
1027
  with gr.Tab('"AI" Dashboard'):
1028
  with gr.Group(elem_classes=["panel-card", "ai-panel"]):
1029
- llm_status = gr.HTML(
1030
- f'<div class="status-card"><p>{get_llm_status_text()}</p></div>'
1031
- )
1032
 
1033
  gr.Markdown(
1034
  """
@@ -1068,7 +992,7 @@ Example questions:
1068
  action_chart,
1069
  theme_chart,
1070
  price_city_chart,
1071
- occupancy_space_chart,
1072
  revenue_city_chart,
1073
  alerts_level_chart,
1074
  pricing_table,
 
53
  --bg1: #18004f;
54
  --bg2: #2a0a89;
55
  --panel: rgba(255,255,255,0.88);
 
56
  --text: #20114f;
 
57
  --gold: #f3c544;
58
  --orange: #ff8b1a;
59
  --line: rgba(255,255,255,0.20);
 
76
  padding-bottom: 32px !important;
77
  }
78
 
79
+ /* tabs */
80
  .gr-tab-nav {
81
  background: rgba(34, 9, 110, 0.70) !important;
82
  border: 1px solid rgba(255,255,255,0.14) !important;
 
88
  .gr-tab-nav button span,
89
  .gr-tab-nav button p,
90
  .gr-tab-nav button div,
91
+ .gr-tab-nav button label,
92
+ button[aria-selected="false"],
93
+ button[aria-selected="false"] * {
94
  color: #ffffff !important;
95
  opacity: 1 !important;
96
  font-weight: 800 !important;
 
99
  .gr-tab-nav button {
100
  border-radius: 14px !important;
101
  transition: background 0.2s ease, color 0.2s ease !important;
102
+ box-shadow: none !important;
103
  }
104
 
105
+ .gr-tab-nav button:hover,
106
+ button[aria-selected="false"]:hover {
107
  background: #000000 !important;
108
+ background-color: #000000 !important;
109
+ box-shadow: none !important;
110
  }
111
 
112
+ .gr-tab-nav button:hover *,
113
+ button[aria-selected="false"]:hover * {
 
 
 
114
  color: #ffffff !important;
115
  }
116
 
117
+ .gr-tab-nav button.selected,
118
+ button[aria-selected="true"] {
119
  background: transparent !important;
120
  border-bottom: 3px solid var(--orange) !important;
121
+ box-shadow: none !important;
122
  }
123
 
124
+ .gr-tab-nav button.selected *,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  button[aria-selected="true"] * {
126
  color: var(--orange) !important;
127
  }
128
 
129
+ /* layout */
130
  .app-shell {
131
  background: rgba(28, 8, 94, 0.58);
132
  border: 1px solid var(--line);
 
259
  .section-title {
260
  color: var(--text) !important;
261
  font-weight: 900 !important;
 
262
  }
263
 
264
  .section-title-white,
 
276
  border: none !important;
277
  }
278
 
 
 
 
 
 
 
279
  .gradio-container .block {
280
  border-radius: 16px !important;
281
  }
 
294
  color: white !important;
295
  }
296
 
 
 
 
 
 
 
 
 
 
 
297
  .dashboard-white-text {
298
  padding-bottom: 22px !important;
299
  }
 
414
  return pd.DataFrame(records)
415
 
416
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  def build_kpi_cards(pricing_df: pd.DataFrame, risk_alerts_df: pd.DataFrame) -> str:
418
  pricing_df = pricing_df.copy() if pricing_df is not None else pd.DataFrame()
419
  risk_alerts_df = risk_alerts_df.copy() if risk_alerts_df is not None else pd.DataFrame()
420
 
421
+ avg_price = pricing_df["avg_space_price"].mean() if "avg_space_price" in pricing_df.columns and not pricing_df.empty else None
422
+ avg_util = pricing_df["avg_utilization"].mean() if "avg_utilization" in pricing_df.columns and not pricing_df.empty else None
423
+ avg_cancel = pricing_df["avg_member_cancellation"].mean() if "avg_member_cancellation" in pricing_df.columns and not pricing_df.empty else None
424
  total_revenue = pricing_df["total_revenue"].sum() if "total_revenue" in pricing_df.columns and not pricing_df.empty else None
425
  raise_count = int((pricing_df["pricing_action"] == "Raise price").sum()) if "pricing_action" in pricing_df.columns and not pricing_df.empty else 0
426
  alert_count = len(risk_alerts_df)
 
428
  cards = [
429
  ("Locations/Segments", len(pricing_df), "#5f44cc"),
430
  ("Avg Space Price", fmt_num(avg_price), "#2fbf9f"),
431
+ ("Avg Utilization", fmt_pct(avg_util), "#f3c544"),
432
+ ("Avg Member Cancellation", fmt_pct(avg_cancel), "#e05b77"),
433
  ("Total Revenue", fmt_num(total_revenue), "#3ba0ff"),
434
  ("Raise Opportunities", raise_count, "#8a5cff"),
435
  ("Risk Alerts", alert_count, "#ff7a5c"),
 
497
  title="Pricing Action Distribution",
498
  color_discrete_sequence=["#5f44cc", "#2fbf9f", "#f3c544", "#e05b77", "#3ba0ff"],
499
  )
500
+ fig.update_layout(template="plotly_white", paper_bgcolor="rgba(255,255,255,0.95)", height=420, showlegend=False)
 
 
 
 
 
501
  return fig
502
 
503
 
 
505
  if not theme_counts:
506
  return empty_figure("Top Complaint / Satisfaction Themes")
507
 
508
+ df = pd.DataFrame({"theme": list(theme_counts.keys()), "count": list(theme_counts.values())})
509
+ df = df.sort_values("count", ascending=True).tail(10)
 
 
510
 
511
  fig = px.bar(
512
  df,
 
517
  color="count",
518
  color_continuous_scale=["#d8cdfa", "#5f44cc"],
519
  )
520
+ fig.update_layout(template="plotly_white", paper_bgcolor="rgba(255,255,255,0.95)", height=420)
 
 
 
 
521
  return fig
522
 
523
 
524
  def chart_avg_price_by_city(pricing_df: pd.DataFrame) -> go.Figure:
525
+ if pricing_df is None or pricing_df.empty or "city" not in pricing_df.columns or "avg_space_price" not in pricing_df.columns:
526
  return empty_figure("Average Space Price by City")
527
 
528
+ df = pricing_df.groupby("city", dropna=False)["avg_space_price"].mean().reset_index()
529
  fig = px.bar(
530
+ df.sort_values("avg_space_price", ascending=False),
531
  x="city",
532
+ y="avg_space_price",
533
  title="Average Space Price by City",
534
+ color="avg_space_price",
535
  color_continuous_scale=["#d4d0ff", "#4320b5"],
536
  )
537
+ fig.update_layout(template="plotly_white", paper_bgcolor="rgba(255,255,255,0.95)", height=420)
 
 
 
 
538
  return fig
539
 
540
 
541
+ def chart_avg_utilization_by_space(pricing_df: pd.DataFrame) -> go.Figure:
542
+ if pricing_df is None or pricing_df.empty or "space_type" not in pricing_df.columns or "avg_utilization" not in pricing_df.columns:
543
  return empty_figure("Average Utilization by Space Type")
544
 
545
+ df = pricing_df.groupby("space_type", dropna=False)["avg_utilization"].mean().reset_index()
546
  fig = px.bar(
547
+ df.sort_values("avg_utilization", ascending=False),
548
+ x="space_type",
549
+ y="avg_utilization",
550
  title="Average Utilization by Space Type",
551
+ color="avg_utilization",
552
  color_continuous_scale=["#d4fff2", "#2fbf9f"],
553
  )
554
+ fig.update_layout(template="plotly_white", paper_bgcolor="rgba(255,255,255,0.95)", height=420)
 
 
 
 
555
  fig.update_yaxes(tickformat=".0%")
556
  return fig
557
 
 
569
  color="total_revenue",
570
  color_continuous_scale=["#f9ddb0", "#f3c544"],
571
  )
572
+ fig.update_layout(template="plotly_white", paper_bgcolor="rgba(255,255,255,0.95)", height=420)
 
 
 
 
573
  return fig
574
 
575
 
 
588
  title="Risk Alert Levels",
589
  color_discrete_map={"High": "#e05b77", "Medium": "#f3c544", "Low": "#2fbf9f"},
590
  )
591
+ fig.update_layout(template="plotly_white", paper_bgcolor="rgba(255,255,255,0.95)", height=420, showlegend=False)
 
 
 
 
 
592
  return fig
593
 
594
 
 
665
  risk_alerts_df = safe_df_from_records(risk_alerts)
666
 
667
  dashboard_kpis = build_kpi_cards(pricing_df, risk_alerts_df)
668
+ preview_df = merged_df.head(MAX_PREVIEW_ROWS).copy()
 
 
669
 
670
  coworking_summary_md = f"""
671
  ### Coworking Pricing Summary
 
675
  ### Automation Notes
676
  - Workflow 1 cleaned and standardized the uploaded merged dataset.
677
  - Workflow 2 generated pricing actions and chart-ready outputs.
678
+ - Workflow 3 flagged risky coworking segments for management review.
679
  """
680
 
681
  risk_summary_md = f"""
 
709
  chart_action_distribution(pricing_df),
710
  chart_theme_counts(theme_counts),
711
  chart_avg_price_by_city(pricing_df),
712
+ chart_avg_utilization_by_space(pricing_df),
713
  chart_revenue_by_city(pricing_df),
714
  chart_alert_levels(risk_alerts_df),
715
+ pricing_df.head(20),
716
+ risk_alerts_df.head(20),
717
  analysis_state,
718
  )
719
 
 
749
  if not candidates.empty:
750
  top = candidates.iloc[0]
751
  return (
752
+ f"The strongest current raise-price opportunity is {top.get('coworking_space_name', 'Unknown location')} "
753
+ f"in {top.get('city', 'Unknown city')} for {top.get('space_type', 'Unknown space type')}. "
754
  f"Rationale: {top.get('rationale', 'No rationale returned.')}"
755
  )
756
  return "No raise-price opportunity was returned by the automation."
 
760
  first = risk_alerts[0]
761
  return (
762
  f"{alerts_summary}\n\n"
763
+ f"One flagged segment is {first.get('coworking_space_name', 'Unknown location')} in "
764
+ f"{first.get('city', 'Unknown city')} for {first.get('space_type', 'Unknown space type')}. "
765
  f"Reason: {first.get('reasons', 'No reason returned.')}"
766
  )
767
  return "No active risk alerts were returned by the automation."
 
770
  return management_summary or "No management summary is currently available."
771
 
772
  if "occupancy" in q or "utilization" in q:
773
+ if "avg_utilization" in pricing_df.columns and not pricing_df.empty:
774
+ avg_util = pricing_df["avg_utilization"].mean()
775
+ return f"The average utilization across coworking segments is {fmt_pct(avg_util)}."
776
  return "Utilization data is not currently available."
777
 
778
  return (
779
+ "I can answer questions about pricing actions, coworking themes, risk alerts, utilization, and the overall management summary. "
780
  "Try asking: 'Where should prices be raised?' or 'What are the main complaint themes?'"
781
  )
782
 
 
786
  You are an AI assistant for a coworking space pricing and satisfaction dashboard.
787
 
788
  You must answer as a concise business analyst.
789
+ Use coworking language only. Never refer to hotels, guests, or rooms.
790
 
791
  Management summary:
792
  {analysis_state.get("management_summary", "")}
 
808
 
809
  Instructions:
810
  - Answer directly.
811
+ - Use coworking language only.
812
  - Mention pricing implications when relevant.
813
  - Keep it clear and business-focused.
814
  """
 
851
  else:
852
  answer = fallback_ai_answer(question, analysis_state)
853
 
854
+ history.append((question, answer))
 
855
  return history, ""
856
 
857
+
858
  # =========================================================
859
  # UI
860
  # =========================================================
 
938
 
939
  with gr.Row():
940
  price_city_chart = gr.Plot(label="Average Space Price by City")
941
+ utilization_space_chart = gr.Plot(label="Average Utilization by Space Type")
942
 
943
  with gr.Row():
944
  revenue_city_chart = gr.Plot(label="Revenue by City")
 
952
 
953
  with gr.Tab('"AI" Dashboard'):
954
  with gr.Group(elem_classes=["panel-card", "ai-panel"]):
955
+ gr.HTML(f'<div class="status-card"><p>{get_llm_status_text()}</p></div>')
 
 
956
 
957
  gr.Markdown(
958
  """
 
992
  action_chart,
993
  theme_chart,
994
  price_city_chart,
995
+ utilization_space_chart,
996
  revenue_city_chart,
997
  alerts_level_chart,
998
  pricing_table,