Benny-Tang commited on
Commit
09c247d
·
verified ·
1 Parent(s): a2da065

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +628 -0
app.py ADDED
@@ -0,0 +1,628 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Sloth — AI Workforce Intelligence Demo
3
+ Hugging Face Spaces deployment
4
+
5
+ Works in two modes:
6
+ 1. STANDALONE — if no SLOTH_BACKEND_URL is set, uses built-in demo data (always live)
7
+ 2. CONNECTED — if SLOTH_BACKEND_URL is set, calls your private backend (real engine)
8
+ """
9
+
10
+ import gradio as gr
11
+ import requests
12
+ import os
13
+ import json
14
+ import datetime
15
+ import random
16
+
17
+ BACKEND_URL = os.environ.get("SLOTH_BACKEND_URL", "").strip()
18
+ API_TOKEN = os.environ.get("SLOTH_API_TOKEN", "sloth-demo-token-2025")
19
+ HEADERS = {"Authorization": f"Bearer {API_TOKEN}", "Content-Type": "application/json"}
20
+
21
+ # ── Built-in demo data (used when no backend is connected) ─────────────────
22
+ DEMO_DB = {
23
+ "malaysia": {
24
+ "country": "Malaysia", "flag": "🇲🇾",
25
+ "min_wage": {"amount": "MYR 1,700/month", "effective": "2023-02-01", "status": "current"},
26
+ "tax": {"corporate": "24%", "personal_top": "30%", "last_updated": "2024-01-15"},
27
+ "social_security": {"epf_employer": "13%", "epf_employee": "11%", "socso": "1.75%"},
28
+ "contractor_rules": "Contractors treated as employees if engagement > 3 months or exclusivity clause present.",
29
+ "recent_changes": [
30
+ {"date": "2024-03-01", "type": "Payroll", "summary": "New mandatory e-PCB tax filing effective Q1 2024", "severity": "high"},
31
+ {"date": "2024-01-15", "type": "Tax", "summary": "Personal income tax band adjusted for mid-range earners", "severity": "medium"},
32
+ ],
33
+ "expansion_score": 87, "eor_available": True,
34
+ },
35
+ "germany": {
36
+ "country": "Germany", "flag": "🇩🇪",
37
+ "min_wage": {"amount": "EUR 12.41/hour", "effective": "2024-01-01", "status": "current"},
38
+ "tax": {"corporate": "15% + solidarity", "personal_top": "45%", "last_updated": "2024-01-01"},
39
+ "social_security": {"employer_total": "~20%", "employee_total": "~20%", "health": "14.6%"},
40
+ "contractor_rules": "Strict bogus self-employment (Scheinselbständigkeit) laws. High misclassification risk.",
41
+ "recent_changes": [
42
+ {"date": "2024-01-01", "type": "Payroll", "summary": "Minimum wage increased to EUR 12.41/hour", "severity": "high"},
43
+ {"date": "2024-02-15", "type": "Compliance", "summary": "New Works Council AI consultation requirements", "severity": "medium"},
44
+ ],
45
+ "expansion_score": 72, "eor_available": True,
46
+ },
47
+ "brazil": {
48
+ "country": "Brazil", "flag": "🇧🇷",
49
+ "min_wage": {"amount": "BRL 1,412/month", "effective": "2024-01-01", "status": "current"},
50
+ "tax": {"corporate": "15-25%", "personal_top": "27.5%", "last_updated": "2024-01-01"},
51
+ "social_security": {"inss_employer": "20%", "inss_employee": "7.5-14%", "fgts": "8%"},
52
+ "contractor_rules": "CLT employment strongly preferred by courts. PJ contractor arrangements under scrutiny.",
53
+ "recent_changes": [
54
+ {"date": "2024-01-01", "type": "Payroll", "summary": "Minimum wage raised to BRL 1,412", "severity": "high"},
55
+ {"date": "2024-02-01", "type": "Compliance", "summary": "Updated eSocial reporting v3.1 mandatory", "severity": "high"},
56
+ {"date": "2024-03-10", "type": "Tax", "summary": "New Simples Nacional thresholds for 2024", "severity": "medium"},
57
+ ],
58
+ "expansion_score": 61, "eor_available": True,
59
+ },
60
+ "singapore": {
61
+ "country": "Singapore", "flag": "🇸🇬",
62
+ "min_wage": {"amount": "No statutory minimum wage", "effective": "N/A", "status": "info"},
63
+ "tax": {"corporate": "17%", "personal_top": "24%", "last_updated": "2024-04-01"},
64
+ "social_security": {"cpf_employer": "17%", "cpf_employee": "20%", "applies_to": "Citizens/PRs only"},
65
+ "contractor_rules": "Clear contractor framework. Employment Act amended 2021 covers most workers.",
66
+ "recent_changes": [
67
+ {"date": "2024-04-01", "type": "Tax", "summary": "GST increased to 9% from 8%", "severity": "medium"},
68
+ {"date": "2024-01-01", "type": "Compliance", "summary": "Flexible work arrangement guidelines effective", "severity": "low"},
69
+ ],
70
+ "expansion_score": 94, "eor_available": True,
71
+ },
72
+ "united states": {
73
+ "country": "United States", "flag": "🇺🇸",
74
+ "min_wage": {"amount": "USD 7.25/hour federal (state rates vary)", "effective": "2009-07-24", "status": "outdated"},
75
+ "tax": {"corporate": "21%", "personal_top": "37%", "last_updated": "2024-01-01"},
76
+ "social_security": {"fica_employer": "7.65%", "fica_employee": "7.65%", "futa": "0.6%"},
77
+ "contractor_rules": "ABC test or economic realities test varies by state. California AB5 most restrictive.",
78
+ "recent_changes": [
79
+ {"date": "2024-01-01", "type": "Compliance", "summary": "Corporate Transparency Act BOI reporting effective", "severity": "high"},
80
+ {"date": "2024-04-23", "type": "Labor", "summary": "DOL new overtime threshold rule finalized", "severity": "high"},
81
+ ],
82
+ "expansion_score": 79, "eor_available": True,
83
+ },
84
+ "india": {
85
+ "country": "India", "flag": "🇮🇳",
86
+ "min_wage": {"amount": "INR 176–423/day (varies by state & skill)", "effective": "2024-04-01", "status": "current"},
87
+ "tax": {"corporate": "22% (domestic)", "personal_top": "30%", "last_updated": "2024-04-01"},
88
+ "social_security": {"epf_employer": "12%", "epf_employee": "12%", "esi": "3.25% employer"},
89
+ "contractor_rules": "Contract Labour Act regulates use of contractors. Principal employer liability applies.",
90
+ "recent_changes": [
91
+ {"date": "2024-04-01", "type": "Tax", "summary": "New TDS provisions for online gaming / crypto gains", "severity": "medium"},
92
+ {"date": "2024-01-15", "type": "Compliance", "summary": "EPFO UAN autogeneration now mandatory for new hires", "severity": "medium"},
93
+ ],
94
+ "expansion_score": 68, "eor_available": True,
95
+ },
96
+ "japan": {
97
+ "country": "Japan", "flag": "🇯🇵",
98
+ "min_wage": {"amount": "JPY 1,004/hour (national avg)", "effective": "2023-10-01", "status": "current"},
99
+ "tax": {"corporate": "23.2%", "personal_top": "45%", "last_updated": "2024-01-01"},
100
+ "social_security": {"health_employer": "5.0%", "pension_employer": "9.15%", "employment_insurance": "0.95%"},
101
+ "contractor_rules": "Worker dispatch laws strictly regulate contractor use. Misclassification fines are heavy.",
102
+ "recent_changes": [
103
+ {"date": "2024-04-01", "type": "Labor", "summary": "Overtime disclosure rules for large firms effective", "severity": "medium"},
104
+ {"date": "2024-01-01", "type": "Compliance", "summary": "Childcare leave promotion requirements strengthened", "severity": "low"},
105
+ ],
106
+ "expansion_score": 78, "eor_available": True,
107
+ },
108
+ "australia": {
109
+ "country": "Australia", "flag": "🇦🇺",
110
+ "min_wage": {"amount": "AUD 23.23/hour", "effective": "2023-07-01", "status": "current"},
111
+ "tax": {"corporate": "30% (25% for small business)", "personal_top": "45%", "last_updated": "2024-07-01"},
112
+ "social_security": {"superannuation_employer": "11%", "medicare_levy": "2%"},
113
+ "contractor_rules": "High misclassification risk post-2022 High Court rulings. Substance over form test applies.",
114
+ "recent_changes": [
115
+ {"date": "2024-07-01", "type": "Payroll", "summary": "Superannuation rate increases to 11.5%", "severity": "high"},
116
+ {"date": "2024-01-01", "type": "Labor", "summary": "Right to disconnect legislation effective", "severity": "medium"},
117
+ ],
118
+ "expansion_score": 82, "eor_available": True,
119
+ },
120
+ }
121
+
122
+ EXAMPLE_QUERIES = [
123
+ "Malaysia payroll compliance 2024",
124
+ "Germany minimum wage and recent changes",
125
+ "Brazil contractor classification rules",
126
+ "Singapore expansion readiness",
127
+ "United States overtime compliance",
128
+ "India EPF social security rates",
129
+ "Japan worker dispatch laws",
130
+ "Australia superannuation requirements",
131
+ ]
132
+
133
+ SEVERITY_ICON = {"high": "🔴", "medium": "🟡", "low": "🟢"}
134
+ RATING_ICON = {"High": "🟢", "Medium": "🟡", "Low": "🔴"}
135
+
136
+
137
+ def query_standalone(query: str) -> dict:
138
+ """Query the built-in demo database."""
139
+ raw = query.lower()
140
+ for key, data in DEMO_DB.items():
141
+ if key in raw:
142
+ return {
143
+ "status": "success",
144
+ "queried_at": datetime.datetime.utcnow().isoformat() + "Z",
145
+ "source": "Sloth Intelligence Engine v1.0-MVP (Demo)",
146
+ "country": data["country"], "flag": data["flag"],
147
+ "summary": {
148
+ "minimum_wage": data["min_wage"],
149
+ "tax_rates": data["tax"],
150
+ "social_security": data["social_security"],
151
+ "contractor_classification": data["contractor_rules"],
152
+ },
153
+ "recent_regulatory_changes": data["recent_changes"],
154
+ "expansion_readiness": {
155
+ "score": data["expansion_score"],
156
+ "eor_available": data["eor_available"],
157
+ "rating": "High" if data["expansion_score"] >= 80 else "Medium" if data["expansion_score"] >= 60 else "Low",
158
+ },
159
+ "alert_count": len(data["recent_changes"]),
160
+ "monitored_sources": random.randint(18, 34),
161
+ }
162
+ return {
163
+ "status": "no_match",
164
+ "message": f"No data found for: '{query}'",
165
+ "available_countries": [v["country"] for v in DEMO_DB.values()],
166
+ }
167
+
168
+
169
+ def call_backend(query: str) -> dict:
170
+ """Call the connected private backend."""
171
+ try:
172
+ resp = requests.post(
173
+ f"{BACKEND_URL}/query",
174
+ headers=HEADERS,
175
+ json={"query": query},
176
+ timeout=15,
177
+ )
178
+ resp.raise_for_status()
179
+ return resp.json()
180
+ except requests.exceptions.ConnectionError:
181
+ return {"status": "error", "message": "⚠ Backend unreachable. Falling back to demo data.", "fallback": True}
182
+ except Exception as e:
183
+ return {"status": "error", "message": f"⚠ Backend error: {str(e)}", "fallback": True}
184
+
185
+
186
+ def get_data(query: str) -> dict:
187
+ if BACKEND_URL:
188
+ result = call_backend(query)
189
+ if result.get("fallback"):
190
+ return query_standalone(query)
191
+ return result
192
+ return query_standalone(query)
193
+
194
+
195
+ def format_result(data: dict) -> tuple:
196
+ if data.get("status") == "error":
197
+ return data.get("message", "Unknown error"), "", "", ""
198
+
199
+ if data.get("status") == "no_match":
200
+ avail = ", ".join(data.get("available_countries", []))
201
+ return f"**No match for that query.**\n\nCurrently monitoring: {avail}", "", "", ""
202
+
203
+ if data.get("status") != "success":
204
+ return "Unexpected response.", "", "", ""
205
+
206
+ flag = data.get("flag", "")
207
+ country = data.get("country", "")
208
+ ts = data.get("queried_at", "")[:19].replace("T", " ")
209
+ alerts = data.get("alert_count", 0)
210
+ sources = data.get("monitored_sources", 0)
211
+ mode = " · Demo mode" if "Demo" in data.get("source", "") else ""
212
+
213
+ header = f"# {flag} {country}\n*Queried {ts} UTC · {sources} sources monitored · {alerts} recent alerts{mode}*\n---"
214
+
215
+ s = data.get("summary", {})
216
+ mw = s.get("minimum_wage", {})
217
+ tx = s.get("tax_rates", {})
218
+ ss = s.get("social_security", {})
219
+ cr = s.get("contractor_classification", "N/A")
220
+ wage_status = {"current": "✅ Current", "outdated": "⚠ Outdated", "info": "ℹ Info"}.get(mw.get("status", ""), "")
221
+
222
+ summary = (
223
+ f"## 📋 Compliance Summary\n\n"
224
+ f"**Minimum Wage** \n{mw.get('amount','N/A')} — effective {mw.get('effective','N/A')} {wage_status}\n\n"
225
+ f"**Tax Rates** \n- Corporate: `{tx.get('corporate','N/A')}`\n- Personal (top): `{tx.get('personal_top','N/A')}`\n- Last updated: {tx.get('last_updated','N/A')}\n\n"
226
+ f"**Social Security** \n" + "\n".join(f"- {k.replace('_',' ').title()}: `{v}`" for k, v in ss.items()) +
227
+ f"\n\n**Contractor Classification** \n{cr}"
228
+ )
229
+
230
+ changes = data.get("recent_regulatory_changes", [])
231
+ if changes:
232
+ rows = "\n".join(
233
+ f"| {SEVERITY_ICON.get(c.get('severity','low'),'⚪')} | {c.get('date','')} | {c.get('type','')} | {c.get('summary','')} |"
234
+ for c in changes
235
+ )
236
+ changes_md = f"## ⚡ Recent Regulatory Changes\n\n| Sev | Date | Type | Summary |\n|---|---|---|---|\n{rows}"
237
+ else:
238
+ changes_md = "## ⚡ Recent Regulatory Changes\n\nNo recent changes detected."
239
+
240
+ er = data.get("expansion_readiness", {})
241
+ score = er.get("score", 0)
242
+ rating = er.get("rating", "N/A")
243
+ eor = "✅ Available" if er.get("eor_available") else "❌ Not available"
244
+ bar = "█" * (score // 10) + "░" * (10 - score // 10)
245
+ expansion = (
246
+ f"## 🌍 Expansion Readiness\n\n"
247
+ f"**Score: {score}/100** {RATING_ICON.get(rating,'⚪')} {rating}\n\n"
248
+ f"`{bar}` {score}%\n\n**EOR:** {eor}"
249
+ )
250
+
251
+ return header, summary, changes_md, expansion
252
+
253
+
254
+ def run_query(query: str):
255
+ if not query.strip():
256
+ return "Enter a country or compliance question above.", "", "", ""
257
+ return format_result(get_data(query))
258
+
259
+
260
+ CSS = """
261
+ body, .gradio-container { font-family: 'Inter', system-ui, sans-serif !important; background: #F8FAFC !important; }
262
+ #header-banner { background: linear-gradient(135deg, #0B1F3A 0%, #12294D 60%, #0D9488 100%); border-radius: 12px; padding: 28px 32px 20px; margin-bottom: 16px; }
263
+ #header-banner h1 { font-size: 2.4rem; font-weight: 700; letter-spacing: 0.1em; color: white !important; margin: 0 0 4px; }
264
+ #header-banner p { color: #5DCAA5; font-size: 1rem; margin: 0; }
265
+ textarea, input[type=text] { border: 1.5px solid #CBD5E1 !important; border-radius: 8px !important; background: white !important; }
266
+ textarea:focus, input[type=text]:focus { border-color: #0D9488 !important; box-shadow: 0 0 0 3px rgba(13,148,136,0.15) !important; }
267
+ button.primary { background: #0D9488 !important; border: none !important; border-radius: 8px !important; font-weight: 600 !important; color: white !important; }
268
+ button.primary:hover { background: #0F6E56 !important; }
269
+ .result-panel { background: white; border: 1px solid #E2E8F0; border-radius: 10px; padding: 20px; margin-top: 12px; }
270
+ .markdown-body h1 { color: #0B1F3A !important; font-size: 1.5rem !important; }
271
+ .markdown-body h2 { color: #0D9488 !important; font-size: 1.15rem !important; border-bottom: 1px solid #E2E8F0; padding-bottom: 4px; }
272
+ .markdown-body code { background: #F0FDFA !important; color: #0B1F3A !important; border-radius: 4px; padding: 1px 5px; }
273
+ .markdown-body table { border-collapse: collapse; width: 100%; }
274
+ .markdown-body td, .markdown-body th { border: 1px solid #E2E8F0 !important; padding: 6px 12px !important; font-size: 0.9rem; }
275
+ .markdown-body th { background: #F8FAFC !important; }
276
+ """
277
+
278
+ HEADER_HTML = """
279
+ <div id="header-banner">
280
+ <h1>🦥 SLOTH</h1>
281
+ <p>AI Workforce Intelligence · Real-time compliance monitoring across 150+ countries</p>
282
+ </div>
283
+ """
284
+
285
+ with gr.Blocks(css=CSS, title="Sloth — AI Workforce Intelligence") as demo:
286
+ gr.HTML(HEADER_HTML)
287
+
288
+ with gr.Row():
289
+ with gr.Column(scale=5):
290
+ query_box = gr.Textbox(
291
+ placeholder="Try: 'Malaysia payroll compliance' or 'Germany minimum wage changes'",
292
+ label="Ask Sloth", lines=2, max_lines=4,
293
+ )
294
+ with gr.Column(scale=1, min_width=120):
295
+ gr.Markdown(" ")
296
+ submit_btn = gr.Button("Run Query →", variant="primary")
297
+
298
+ gr.Markdown("<div style='color:#64748B;font-size:0.85rem;margin-bottom:6px'>Quick examples:</div>")
299
+ gr.Examples(examples=EXAMPLE_QUERIES, inputs=query_box, label="")
300
+
301
+ with gr.Row():
302
+ header_out = gr.Markdown(label="", elem_classes=["result-panel"])
303
+ with gr.Row():
304
+ with gr.Column(scale=1):
305
+ summary_out = gr.Markdown(label="", elem_classes=["result-panel"])
306
+ with gr.Column(scale=1):
307
+ changes_out = gr.Markdown(label="", elem_classes=["result-panel"])
308
+ with gr.Row():
309
+ expansion_out = gr.Markdown(label="", elem_classes=["result-panel"])
310
+
311
+ gr.Markdown(
312
+ "<br><center style='color:#94A3B8;font-size:0.8rem'>"
313
+ "Sloth MVP · 8 countries in demo · Full 150-country coverage in production · "
314
+ "<a href='mailto:hello@sloth.ai' style='color:#0D9488'>hello@sloth.ai</a>"
315
+ "</center>"
316
+ )
317
+
318
+ submit_btn.click(fn=run_query, inputs=query_box, outputs=[header_out, summary_out, changes_out, expansion_out])
319
+ query_box.submit(fn=run_query, inputs=query_box, outputs=[header_out, summary_out, changes_out, expansion_out])
320
+
321
+ if __name__ == "__main__":
322
+ demo.launch(server_name="0.0.0.0", server_port=7860)
323
+
324
+ HEADERS = {
325
+ "Authorization": f"Bearer {API_TOKEN}",
326
+ "Content-Type": "application/json",
327
+ }
328
+
329
+ EXAMPLE_QUERIES = [
330
+ "What are Malaysia's payroll compliance requirements?",
331
+ "Show me Germany's minimum wage and recent changes",
332
+ "Brazil contractor classification rules",
333
+ "Singapore expansion readiness report",
334
+ "United States overtime compliance 2024",
335
+ "India EPF and social security rates",
336
+ ]
337
+
338
+ SEVERITY_ICON = {"high": "🔴", "medium": "🟡", "low": "🟢"}
339
+ RATING_ICON = {"High": "🟢", "Medium": "🟡", "Low": "🔴"}
340
+
341
+
342
+ # ── API call ───────────────────────────────────────────────────────────────
343
+
344
+ def call_backend(query: str) -> dict:
345
+ try:
346
+ resp = requests.post(
347
+ f"{BACKEND_URL}/query",
348
+ headers=HEADERS,
349
+ json={"query": query},
350
+ timeout=15,
351
+ )
352
+ resp.raise_for_status()
353
+ return resp.json()
354
+ except requests.exceptions.ConnectionError:
355
+ return {"status": "error", "message": "⚠ Backend offline. Start your local server and set SLOTH_BACKEND_URL."}
356
+ except requests.exceptions.Timeout:
357
+ return {"status": "error", "message": "⚠ Request timed out. Check your backend URL."}
358
+ except Exception as e:
359
+ return {"status": "error", "message": f"⚠ Error: {str(e)}"}
360
+
361
+
362
+ # ── Result formatter ───────────────────────────────────────────────────────
363
+
364
+ def format_result(data: dict) -> tuple[str, str, str, str]:
365
+ """Returns (header_md, summary_md, changes_md, expansion_md)"""
366
+
367
+ if data.get("status") == "error":
368
+ err = data.get("message", "Unknown error")
369
+ return err, "", "", ""
370
+
371
+ if data.get("status") == "no_match":
372
+ msg = data.get("message", "No data found.")
373
+ avail = ", ".join(c.title() for c in data.get("available_countries", []))
374
+ return f"**{msg}**\n\nAvailable: {avail}", "", "", ""
375
+
376
+ if data.get("status") != "success":
377
+ return "Unexpected response from backend.", "", "", ""
378
+
379
+ flag = data.get("flag", "")
380
+ country = data.get("country", "")
381
+ ts = data.get("queried_at", "")[:19].replace("T", " ")
382
+ alerts = data.get("alert_count", 0)
383
+ sources = data.get("monitored_sources", 0)
384
+
385
+ # ── Header ──
386
+ header = (
387
+ f"# {flag} {country}\n"
388
+ f"*Queried {ts} UTC · {sources} sources monitored · {alerts} recent alerts*\n"
389
+ f"---"
390
+ )
391
+
392
+ # ── Summary ──
393
+ s = data.get("summary", {})
394
+ mw = s.get("minimum_wage", {})
395
+ tx = s.get("tax_rates", {})
396
+ ss = s.get("social_security", {})
397
+ cr = s.get("contractor_classification", "N/A")
398
+
399
+ wage_status = {"current": "✅ Current", "outdated": "⚠ Outdated", "info": "ℹ Info"}.get(mw.get("status", ""), "")
400
+
401
+ summary = (
402
+ f"## 📋 Compliance Summary\n\n"
403
+ f"**Minimum Wage** \n"
404
+ f"{mw.get('amount','N/A')} — effective {mw.get('effective','N/A')} {wage_status}\n\n"
405
+ f"**Tax Rates** \n"
406
+ f"- Corporate: `{tx.get('corporate','N/A')}`\n"
407
+ f"- Personal (top): `{tx.get('personal_top','N/A')}`\n"
408
+ f"- Last updated: {tx.get('last_updated','N/A')}\n\n"
409
+ f"**Social Security** \n"
410
+ + "\n".join(f"- {k.replace('_',' ').title()}: `{v}`" for k, v in ss.items()) +
411
+ f"\n\n**Contractor Classification** \n{cr}"
412
+ )
413
+
414
+ # ── Recent changes ──
415
+ changes = data.get("recent_regulatory_changes", [])
416
+ if changes:
417
+ rows = "\n".join(
418
+ f"| {SEVERITY_ICON.get(c.get('severity','low'), '⚪')} {c.get('date','')} "
419
+ f"| {c.get('type','')} | {c.get('summary','')} |"
420
+ for c in changes
421
+ )
422
+ changes_md = (
423
+ f"## ⚡ Recent Regulatory Changes\n\n"
424
+ f"| Severity | Date | Type | Summary |\n"
425
+ f"|---|---|---|---|\n"
426
+ f"{rows}"
427
+ )
428
+ else:
429
+ changes_md = "## ⚡ Recent Regulatory Changes\n\nNo recent changes detected."
430
+
431
+ # ── Expansion readiness ──
432
+ er = data.get("expansion_readiness", {})
433
+ score = er.get("score", 0)
434
+ rating = er.get("rating", "N/A")
435
+ eor = "✅ Available" if er.get("eor_available") else "❌ Not available"
436
+ bar = "█" * (score // 10) + "░" * (10 - score // 10)
437
+
438
+ expansion = (
439
+ f"## 🌍 Expansion Readiness\n\n"
440
+ f"**Score: {score}/100** {RATING_ICON.get(rating,'⚪')} {rating}\n\n"
441
+ f"`{bar}` {score}%\n\n"
442
+ f"**EOR (Employer of Record):** {eor}"
443
+ )
444
+
445
+ return header, summary, changes_md, expansion
446
+
447
+
448
+ # ── Main query function ────────────────────────────────────────────────────
449
+
450
+ def run_query(query: str):
451
+ if not query.strip():
452
+ return "Enter a country or compliance question above.", "", "", ""
453
+
454
+ data = call_backend(query)
455
+ return format_result(data)
456
+
457
+
458
+ # ── CSS theme — navy/teal matching pitch deck ──────────────────────────────
459
+
460
+ CSS = """
461
+ :root {
462
+ --navy: #0B1F3A;
463
+ --teal: #0D9488;
464
+ --light: #F0FDFA;
465
+ }
466
+
467
+ body, .gradio-container {
468
+ font-family: 'Inter', system-ui, sans-serif !important;
469
+ background: #F8FAFC !important;
470
+ }
471
+
472
+ .gradio-container > .main > .wrap {
473
+ max-width: 900px;
474
+ margin: 0 auto;
475
+ }
476
+
477
+ /* Header banner */
478
+ #header-banner {
479
+ background: linear-gradient(135deg, #0B1F3A 0%, #12294D 60%, #0D9488 100%);
480
+ border-radius: 12px;
481
+ padding: 28px 32px 20px;
482
+ margin-bottom: 16px;
483
+ color: white;
484
+ }
485
+
486
+ #header-banner h1 {
487
+ font-size: 2.4rem;
488
+ font-weight: 700;
489
+ letter-spacing: 0.1em;
490
+ color: white !important;
491
+ margin: 0 0 4px;
492
+ }
493
+
494
+ #header-banner p {
495
+ color: #5DCAA5;
496
+ font-size: 1rem;
497
+ margin: 0;
498
+ }
499
+
500
+ /* Search box */
501
+ textarea, input[type=text] {
502
+ border: 1.5px solid #CBD5E1 !important;
503
+ border-radius: 8px !important;
504
+ font-size: 1rem !important;
505
+ background: white !important;
506
+ }
507
+
508
+ textarea:focus, input[type=text]:focus {
509
+ border-color: #0D9488 !important;
510
+ box-shadow: 0 0 0 3px rgba(13,148,136,0.15) !important;
511
+ }
512
+
513
+ /* Buttons */
514
+ button.primary {
515
+ background: #0D9488 !important;
516
+ border: none !important;
517
+ border-radius: 8px !important;
518
+ font-weight: 600 !important;
519
+ font-size: 1rem !important;
520
+ color: white !important;
521
+ padding: 10px 28px !important;
522
+ cursor: pointer !important;
523
+ transition: background 0.2s !important;
524
+ }
525
+
526
+ button.primary:hover {
527
+ background: #0F6E56 !important;
528
+ }
529
+
530
+ button.secondary {
531
+ background: transparent !important;
532
+ border: 1.5px solid #CBD5E1 !important;
533
+ border-radius: 8px !important;
534
+ color: #64748B !important;
535
+ font-size: 0.9rem !important;
536
+ cursor: pointer !important;
537
+ }
538
+
539
+ /* Result panels */
540
+ .result-panel {
541
+ background: white;
542
+ border: 1px solid #E2E8F0;
543
+ border-radius: 10px;
544
+ padding: 20px;
545
+ margin-top: 12px;
546
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
547
+ }
548
+
549
+ /* Markdown output */
550
+ .markdown-body h1 { color: #0B1F3A !important; font-size: 1.5rem !important; }
551
+ .markdown-body h2 { color: #0D9488 !important; font-size: 1.15rem !important; border-bottom: 1px solid #E2E8F0; padding-bottom: 4px; }
552
+ .markdown-body code { background: #F0FDFA !important; color: #0B1F3A !important; border-radius: 4px; padding: 1px 5px; }
553
+ .markdown-body table { border-collapse: collapse; width: 100%; }
554
+ .markdown-body td, .markdown-body th { border: 1px solid #E2E8F0 !important; padding: 6px 12px !important; font-size: 0.9rem; }
555
+ .markdown-body th { background: #F8FAFC !important; }
556
+
557
+ /* Example pills */
558
+ .example-label { color: #64748B; font-size: 0.85rem; margin-bottom: 6px; }
559
+ """
560
+
561
+ HEADER_HTML = """
562
+ <div id="header-banner">
563
+ <h1>🦥 SLOTH</h1>
564
+ <p>AI Workforce Intelligence · Real-time compliance monitoring across 150+ countries</p>
565
+ </div>
566
+ """
567
+
568
+ # ── Build Gradio UI ──────────────���─────────────────────────────────────────
569
+
570
+ with gr.Blocks(css=CSS, title="Sloth — AI Workforce Intelligence") as demo:
571
+
572
+ gr.HTML(HEADER_HTML)
573
+
574
+ with gr.Row():
575
+ with gr.Column(scale=5):
576
+ query_box = gr.Textbox(
577
+ placeholder="Try: 'Malaysia payroll compliance' or 'Germany minimum wage changes'",
578
+ label="Ask Sloth",
579
+ lines=2,
580
+ max_lines=4,
581
+ )
582
+ with gr.Column(scale=1, min_width=120):
583
+ gr.Markdown(" ")
584
+ submit_btn = gr.Button("Run Query →", variant="primary")
585
+
586
+ # Example queries
587
+ gr.Markdown("<div class='example-label'>Quick examples:</div>")
588
+ gr.Examples(
589
+ examples=EXAMPLE_QUERIES,
590
+ inputs=query_box,
591
+ label="",
592
+ )
593
+
594
+ # Output panels
595
+ with gr.Row():
596
+ header_out = gr.Markdown(label="", elem_classes=["result-panel"])
597
+
598
+ with gr.Row():
599
+ with gr.Column(scale=1):
600
+ summary_out = gr.Markdown(label="", elem_classes=["result-panel"])
601
+ with gr.Column(scale=1):
602
+ changes_out = gr.Markdown(label="", elem_classes=["result-panel"])
603
+
604
+ with gr.Row():
605
+ expansion_out = gr.Markdown(label="", elem_classes=["result-panel"])
606
+
607
+ # Footer
608
+ gr.Markdown(
609
+ "<br><center style='color:#94A3B8;font-size:0.8rem'>"
610
+ "Sloth MVP · Data sourced from 5,000+ government portals, labor law sites, and talent platforms · "
611
+ "<a href='https://sloth.ai' style='color:#0D9488'>sloth.ai</a>"
612
+ "</center>"
613
+ )
614
+
615
+ # Wire up
616
+ submit_btn.click(
617
+ fn=run_query,
618
+ inputs=query_box,
619
+ outputs=[header_out, summary_out, changes_out, expansion_out],
620
+ )
621
+ query_box.submit(
622
+ fn=run_query,
623
+ inputs=query_box,
624
+ outputs=[header_out, summary_out, changes_out, expansion_out],
625
+ )
626
+
627
+ if __name__ == "__main__":
628
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=False)