Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
| 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["
|
| 466 |
-
|
| 467 |
-
avg_cancel = pricing_df["
|
| 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
|
| 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 |
-
|
| 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 "
|
| 581 |
return empty_figure("Average Space Price by City")
|
| 582 |
|
| 583 |
-
df = pricing_df.groupby("city", dropna=False)["
|
| 584 |
fig = px.bar(
|
| 585 |
-
df.sort_values("
|
| 586 |
x="city",
|
| 587 |
-
y="
|
| 588 |
title="Average Space Price by City",
|
| 589 |
-
color="
|
| 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
|
| 601 |
-
if pricing_df is None or pricing_df.empty or "
|
| 602 |
return empty_figure("Average Utilization by Space Type")
|
| 603 |
|
| 604 |
-
df = pricing_df.groupby("
|
| 605 |
fig = px.bar(
|
| 606 |
-
df.sort_values("
|
| 607 |
-
x="
|
| 608 |
-
y="
|
| 609 |
title="Average Utilization by Space Type",
|
| 610 |
-
color="
|
| 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
|
| 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 |
-
|
| 787 |
chart_revenue_by_city(pricing_df),
|
| 788 |
chart_alert_levels(risk_alerts_df),
|
| 789 |
-
|
| 790 |
-
|
| 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('
|
| 827 |
-
f"in {top.get('city', 'Unknown city')} for {top.get('
|
| 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('
|
| 838 |
-
f"{first.get('city', 'Unknown city')} for {first.get('
|
| 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 "
|
| 848 |
-
|
| 849 |
-
return f"The average utilization across coworking segments is {fmt_pct(
|
| 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
|
| 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
|
| 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(
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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,
|