# JustDial-themed Category -> Vertical & Sub-vertical Classifier (Gradio 6.x safe)
try:
import spaces # ZeroGPU runtime on Hugging Face
except ImportError: # local / CPU fallback
class spaces:
@staticmethod
def GPU(fn=None, **kw):
return (lambda f: f)(fn) if fn else (lambda f: f)
import gradio as gr
from inference import classify, VERT_THR, SUB_THR
import os, tempfile
import pandas as pd
JD_BLUE, JD_ORANGE, GREEN, RED = "#00457C", "#F58220", "#16a34a", "#dc2626"
def conf_color(c):
return GREEN if c >= 0.80 else (JD_ORANGE if c >= 0.60 else RED)
def bar(pct, color):
return (f'
')
def render(r):
vc, sc = r["vertical_confidence"], r["sub_vertical_confidence"]
v_flag = ('⚠ Review · low confidence'
if r["vertical_review"] else '✓ Confident')
v_card = (f'VERTICAL'
f'{v_flag}
{r["vertical"]}
'
f'{bar(vc*100, conf_color(vc))}
{vc*100:.1f}% confidence
')
if r["propose_new_sub_vertical"]:
s_flag = '✨ New sub-vertical suggested'
s_extra = (f'Suggested name: '
f'{r["suggested_sub_vertical"]}
')
else:
s_flag = '✓ Fits existing'
s_extra = ''
s_card = (f'SUB-VERTICAL'
f'{s_flag}
{r["sub_vertical"]}
'
f'{bar(sc*100, conf_color(sc))}
{sc*100:.1f}% confidence
{s_extra}
')
rows = ""
for v, c in r["vertical_top3"]:
rows += (f'{v}
'
f'
{bar(c*100, JD_BLUE)}
'
f'
{c*100:.1f}%
')
top = f''
return f'{v_card}{s_card}{top}
'
EMPTY_HTML = ('Enter a category name above and click '
'Classify to see its vertical, sub-vertical and confidence.
')
def _run(name, vthr, sthr):
if not name or not name.strip():
return EMPTY_HTML
return render(classify(name, vert_thr=vthr, sub_thr=sthr))
run = spaces.GPU(_run) # ZeroGPU-detectable; also works on CPU
def process_file(file, vthr, sthr, progress=gr.Progress()):
if file is None:
return None, ('Upload an Excel/CSV file with a '
'category_name column, then click Process file.
')
path = file if isinstance(file, str) else getattr(file, "name", None)
try:
df = pd.read_csv(path, dtype=str) if str(path).lower().endswith(".csv") \
else pd.read_excel(path, dtype=str)
except Exception as e:
return None, f'Could not read the file: {e}
'
cols = {c.strip().lower(): c for c in df.columns}
if "category_name" not in cols:
return None, ('No category_name column found. '
f'Columns present: {", ".join(map(str, df.columns))}
')
names = df[cols["category_name"]].fillna("").astype(str)
R = [classify(n, vert_thr=vthr, sub_thr=sthr)
for n in progress.tqdm(names, desc="Classifying")]
# keep every original column, then append the classification columns
out = df.copy()
out["vertical"] = [r["vertical"] for r in R]
out["vertical_confidence_%"] = [round(r["vertical_confidence"]*100, 1) for r in R]
out["vertical_flag"] = ["review" if r["vertical_review"] else "ok" for r in R]
out["sub_vertical"] = [r["sub_vertical"] for r in R]
out["sub_vertical_confidence_%"] = [round(r["sub_vertical_confidence"]*100, 1) for r in R]
out["sub_vertical_status"] = ["new-suggested" if r["propose_new_sub_vertical"] else "fits-existing" for r in R]
out["suggested_new_sub_vertical"] = [r["suggested_sub_vertical"] or "" for r in R]
out_path = os.path.join(tempfile.gettempdir(), "classified_results.xlsx")
out.to_excel(out_path, index=False)
n = len(out); rev = int((out["vertical_flag"] == "review").sum())
new = int((out["sub_vertical_status"] == "new-suggested").sum())
status = (f'Processed {n} rows · '
f'{rev} flagged for vertical review · {new} with a suggested new '
f'sub-vertical.
Download the results file below.
')
return out_path, status
HEADER = '''
Type a category name to get its vertical (1 of 21) and sub-vertical,
each with a confidence score. Low-confidence verticals are flagged for review; when no existing
sub-vertical fits, a new one is proposed.
'''
CSS = """
gradio-app, body {background:#ffffff !important;}
.gradio-container, .gradio-container.dark, body {
--body-background-fill:#ffffff; --background-fill-primary:#ffffff;
--background-fill-secondary:#f7f8fa; --block-background-fill:#ffffff;
--block-border-color:#eef0f2; --block-label-text-color:#6b7280;
--body-text-color:#1f2937; --body-text-color-subdued:#6b7280;
--input-background-fill:#ffffff; --input-border-color:#dfe3e8;
--border-color-primary:#eef0f2; --color-accent:#F58220;
--link-text-color:#00457C; --neutral-50:#f7f8fa; color-scheme:light !important;}
.jd-blue{color:#00457C !important;} .jd-dial{color:#F58220 !important;}
.jd-just{color:#00457C !important;} .jd-orange{color:#F58220 !important;}
.gradio-container {background:#ffffff !important; max-width:960px !important; margin:0 auto !important;
font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif !important;}
.gradio-container::before {content:""; position:fixed; top:0; left:0; right:0; height:5px; z-index:50;
background:linear-gradient(90deg,#ff5f6d,#ff8a5b,#ffc371);}
footer {display:none !important;}
.jd-header {display:flex; align-items:center; gap:14px; padding:26px 4px 6px;}
.jd-brand {font-size:30px; font-weight:800; letter-spacing:-.5px;}
.jd-just {color:#00457C;} .jd-dial {color:#F58220;}
.jd-divider {width:1px; height:26px; background:#e5e7eb;}
.jd-apptitle {font-size:20px; font-weight:600; color:#1f2937;}
.jd-live {margin-left:auto; color:#16a34a; font-weight:700; font-size:12px; letter-spacing:.5px;
background:#eafaf0; border:1px solid #bbe9ca; padding:4px 10px; border-radius:20px;}
.jd-stats {display:grid; grid-template-columns:repeat(4,1fr); gap:14px; margin:18px 4px 6px;}
.jd-stat {background:#fff; border:1px solid #eef0f2; border-radius:12px; padding:14px 16px;
box-shadow:0 1px 2px rgba(0,0,0,.03);}
.jd-stat-label {font-size:12px; color:#6b7280; margin-bottom:6px;}
.jd-stat-num {font-size:26px; font-weight:800; color:#1f2937;}
.jd-sub {color:#6b7280; font-size:14px; line-height:1.5; margin:12px 4px 8px;}
#jd-input textarea {border:1px solid #dfe3e8 !important; border-radius:10px !important; font-size:16px !important;}
#jd-input textarea:focus {border-color:#F58220 !important; box-shadow:0 0 0 3px rgba(245,130,32,.15) !important;}
#jd-btn {background:#F58220 !important; color:#fff !important; border:none !important; font-weight:700 !important;
border-radius:10px !important; box-shadow:0 2px 6px rgba(245,130,32,.25) !important;}
#jd-btn:hover {background:#e2710f !important;}
.jd-results {display:flex; flex-direction:column; gap:14px; margin-top:6px;}
.jd-card {background:#fff; border:1px solid #eef0f2; border-radius:14px; padding:18px 20px;
box-shadow:0 1px 3px rgba(0,0,0,.04);}
.jd-card-top {display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;}
.jd-eyebrow {font-size:11px; font-weight:700; letter-spacing:1px; color:#9aa2ad;}
.jd-value {font-size:24px; font-weight:800; margin:2px 0 12px; text-transform:capitalize;}
.jd-blue {color:#00457C;} .jd-orange {color:#F58220;}
.jd-track {height:9px; background:#f0f2f5; border-radius:6px; overflow:hidden;}
.jd-fill {height:100%; border-radius:6px;}
.jd-pct {font-size:12px; color:#6b7280; margin-top:6px;}
.jd-badge {font-size:12px; font-weight:700; padding:4px 10px; border-radius:20px;}
.jd-badge-ok {background:#eafaf0; color:#16a34a; border:1px solid #bbe9ca;}
.jd-badge-warn {background:#fff5e6; color:#b45309; border:1px solid #fcd9a3;}
.jd-badge-new {background:#eef4ff; color:#00457C; border:1px solid #c3d7f7;}
.jd-suggest {margin-top:12px; font-size:13px; color:#374151;}
.jd-chip {background:#fff5e6; color:#b45309; border:1px solid #fcd9a3; padding:3px 10px;
border-radius:8px; font-weight:700;}
.jd-top-row {display:flex; align-items:center; gap:12px; margin:9px 0;}
.jd-top-name {width:150px; font-size:13px; color:#374151; text-transform:capitalize;}
.jd-top-bar {flex:1;} .jd-top-pct {width:52px; text-align:right; font-size:12px; color:#6b7280; font-weight:600;}
.jd-empty {background:#fafbfc; border:1px dashed #dfe3e8; border-radius:14px; padding:26px;
text-align:center; color:#6b7280; font-size:14px;}
.jd-section {font-size:16px; font-weight:800; color:#1f2937; margin:26px 4px 2px; padding-top:10px;
border-top:1px solid #eef0f2;}
#jd-btn2 {background:#00457C !important; color:#fff !important; border:none !important; font-weight:700 !important;
border-radius:10px !important; box-shadow:0 2px 6px rgba(0,69,124,.2) !important;}
#jd-btn2:hover {background:#013a68 !important;}
.jd-status {font-size:13px; border-radius:10px; padding:12px 14px; margin:8px 4px;}
.jd-status-info {background:#f7f8fa; border:1px solid #eef0f2; color:#374151;}
.jd-status-ok {background:#eff8f1; border:1px solid #bbe9ca; color:#166534;}
.jd-status-err {background:#fef2f2; border:1px solid #f3c0c0; color:#b91c1c;}
#jd-file, #jd-file *, #jd-fileout, #jd-fileout * {background:#ffffff !important;
color:#374151 !important; -webkit-text-fill-color:#374151 !important;}
#jd-file, #jd-fileout {border:1px solid #dfe3e8 !important; border-radius:10px !important;}
#jd-file a, #jd-fileout a {color:#00457C !important; -webkit-text-fill-color:#00457C !important;}
#jd-file svg, #jd-fileout svg {color:#6b7280 !important; opacity:1 !important;}
/* --- force native components to light (input text, labels, examples) --- */
#jd-input textarea, #jd-input input {background:#ffffff !important; color:#1f2937 !important;
-webkit-text-fill-color:#1f2937 !important; caret-color:#F58220 !important;}
.gradio-container label, .gradio-container .label-wrap, .gradio-container .label-wrap > span,
.gradio-container span[data-testid="block-info"], .gradio-container .head {
color:#374151 !important; opacity:1 !important;}
.gradio-container .label-wrap svg, .gradio-container .icon {color:#374151 !important; opacity:1 !important;}
.gradio-container [class*="sample"] {background:#ffffff !important; color:#1f2937 !important;
-webkit-text-fill-color:#1f2937 !important; border:1px solid #dfe3e8 !important;}
.gradio-container [class*="sample"]:hover {background:#fff5e6 !important; color:#b45309 !important;
-webkit-text-fill-color:#b45309 !important; border-color:#fcd9a3 !important;}
.gradio-container .accordion, .gradio-container .accordion * {opacity:1 !important;}
"""
FORCE_LIGHT = """
() => {
const u = new URL(window.location.href);
if (u.searchParams.get('__theme') !== 'light') {
u.searchParams.set('__theme', 'light');
window.location.replace(u.href);
}
}
"""
with gr.Blocks(title="JustDial AI Category Classifier", js=FORCE_LIGHT) as demo:
gr.HTML(HEADER)
with gr.Row():
inp = gr.Textbox(show_label=False, placeholder="e.g. Bridal Makeup Artists",
elem_id="jd-input", scale=5, lines=1)
btn = gr.Button("Classify", elem_id="jd-btn", scale=1)
with gr.Accordion("Thresholds (advanced)", open=False):
vthr = gr.Slider(0.0, 1.0, value=VERT_THR, step=0.05, label="Vertical review threshold")
sthr = gr.Slider(0.0, 1.0, value=SUB_THR, step=0.05, label="New sub-vertical threshold")
out = gr.HTML(EMPTY_HTML)
btn.click(run, [inp, vthr, sthr], out)
inp.submit(run, [inp, vthr, sthr], out)
gr.HTML('Bulk classify — upload an Excel/CSV
'
'File must have a category_name column. '
'You get back vertical, sub-vertical and both confidence scores as a downloadable Excel.
')
with gr.Row():
fin = gr.File(label="Upload .xlsx / .csv", file_types=[".xlsx", ".xls", ".csv"],
type="filepath", elem_id="jd-file", scale=4)
pbtn = gr.Button("Process file", elem_id="jd-btn2", scale=1)
fstatus = gr.HTML('Upload a file with a '
'category_name column, then click Process file.
')
fout = gr.File(label="Download results (.xlsx)", elem_id="jd-fileout")
pbtn.click(process_file, [fin, vthr, sthr], [fout, fstatus])
if __name__ == "__main__":
demo.launch(css=CSS)