Download api.py from Adityashinde0/customer-support-openenv: direct link, hf CLI and curl.
- Browser
- Download file 52.2 kB
-
https://huggingface.co/spaces/Adityashinde0/customer-support-openenv/resolve/main/api.py
- Command line
-
hf download hf://spaces/Adityashinde0/customer-support-openenv/api.py
-
curl -L -o api.py https://huggingface.co/spaces/Adityashinde0/customer-support-openenv/resolve/main/api.py
52.2 kB
| from fastapi import FastAPI, HTTPException, Request | |
| from fastapi.responses import HTMLResponse | |
| from environment import CustomerSupportEnv | |
| from models import Action, Observation | |
| app = FastAPI(title="Customer Support OpenEnv API") | |
| env_instance = CustomerSupportEnv() | |
| def reset_environment(): | |
| try: | |
| obs = env_instance.reset() | |
| return {"observation": obs} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| def step_environment(action: Action): | |
| if env_instance.obs is None: | |
| raise HTTPException(status_code=400, detail="You must call /reset before calling /step.") | |
| try: | |
| obs, reward, done, info = env_instance.step(action) | |
| return {"observation": obs, "reward": reward, "done": done, "info": info} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| def get_current_state(): | |
| if env_instance.obs is None: | |
| raise HTTPException(status_code=400, detail="Environment has not been initialized. Call /reset.") | |
| return {"observation": env_instance.state()} | |
| # ========================================== | |
| # OPENENV VALIDATION ENDPOINTS (HACKATHON) | |
| # ========================================== | |
| def health_check(): | |
| """Simple health check for the validator.""" | |
| return {"status": "healthy"} | |
| def get_metadata(): | |
| """Returns environment metadata.""" | |
| return { | |
| "name": "customer-support-openenv", | |
| "description": "A real-world Customer Support OpenEnv simulation", | |
| "version": "1.0.0", | |
| "tags": ["customer-support", "nlp", "openenv"] | |
| } | |
| def get_schema(): | |
| """Returns the JSON schemas for the environment's Pydantic models.""" | |
| return { | |
| "action": Action.model_json_schema(), | |
| "observation": Observation.model_json_schema(), | |
| "state": Observation.model_json_schema() | |
| } | |
| async def mcp_endpoint(request: Request): | |
| """Minimal JSON-RPC 2.0 response to satisfy validator MCP checks.""" | |
| try: | |
| body = await request.json() | |
| req_id = body.get("id") | |
| except Exception: | |
| req_id = None | |
| return { | |
| "jsonrpc": "2.0", | |
| "id": req_id, | |
| "result": {} | |
| } | |
| def home(): | |
| return """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Customer Support Command Center — OpenEnv Hackathon</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Outfit:wght@400;500;600;700;800;900&display=swap" rel="stylesheet"> | |
| <style> | |
| *,*::before,*::after{margin:0;padding:0;box-sizing:border-box} | |
| :root{ | |
| --bg:#ffffff; | |
| --black:#0a0a0a; | |
| --gray:#f2f2f2; | |
| --gray2:#e8e8e8; | |
| --gray3:#999; | |
| --lime:#c8ff00; | |
| /* GenZ Vibe Colors */ | |
| --purple:#a855f7; | |
| --cyan:#00f3ff; | |
| --blue:#3b82f6; | |
| --orange:#f97316; | |
| --text:#0a0a0a; | |
| /* Bouncy animation timing */ | |
| --bounce: cubic-bezier(0.34, 1.56, 0.64, 1); | |
| } | |
| html{scroll-behavior:smooth} | |
| body{ | |
| font-family:'Outfit',sans-serif; | |
| background:var(--bg); | |
| color:var(--text); | |
| overflow-x:hidden; | |
| } | |
| /* ── NOISE TEXTURE OVERLAY ── */ | |
| body::after{ | |
| content:'';position:fixed;inset:0;pointer-events:none;z-index:9999; | |
| opacity:.025; | |
| background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); | |
| } | |
| /* ── NAV ── */ | |
| nav{ | |
| position:fixed;top:0;left:0;right:0;z-index:100; | |
| display:flex;align-items:center;justify-content:space-between; | |
| padding:18px 48px; | |
| background:rgba(255,255,255,.92); | |
| backdrop-filter:blur(12px); | |
| border-bottom:2px solid var(--black); | |
| } | |
| .nav-logo{ | |
| font-family:'Bebas Neue',sans-serif; | |
| font-size:26px;letter-spacing:2px;color:var(--black); | |
| } | |
| .nav-logo span{color:var(--purple)} | |
| .nav-right{display:flex;align-items:center;gap:12px} | |
| /* DEVELOPER CREDITS ANIMATIONS */ | |
| .nav-devs { | |
| font-size: 11px; | |
| font-weight: 800; | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| padding: 6px 14px; | |
| border-radius: 100px; | |
| background: var(--black); | |
| color: #fff; | |
| border: 2px solid var(--cyan); | |
| animation: pulseNeon 3s infinite alternate, floatDev 4s ease-in-out infinite; | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| .nav-devs span { | |
| color: var(--cyan); | |
| animation: textShift 3s infinite alternate; | |
| } | |
| @keyframes pulseNeon { | |
| 0% { box-shadow: 0 0 5px var(--cyan); border-color: var(--cyan); } | |
| 100% { box-shadow: 0 0 15px var(--purple); border-color: var(--purple); } | |
| } | |
| @keyframes floatDev { | |
| 0%, 100% { transform: translateY(0px); } | |
| 50% { transform: translateY(-3px); } | |
| } | |
| @keyframes textShift { | |
| 0% { color: var(--cyan); } | |
| 100% { color: var(--purple); } | |
| } | |
| .nav-tag{ | |
| font-size:11px;font-weight:700;letter-spacing:2px;text-transform:uppercase; | |
| padding:6px 14px;border-radius:100px; | |
| background:var(--lime);color:var(--black); | |
| border:2px solid var(--black); | |
| } | |
| .nav-docs{ | |
| font-size:11px;font-weight:700;letter-spacing:1px;text-transform:uppercase; | |
| padding:6px 16px;border-radius:100px; | |
| background:var(--black);color:#fff; | |
| text-decoration:none;transition:all 0.3s var(--bounce); | |
| border:2px solid var(--black); | |
| } | |
| .nav-docs:hover{ | |
| background:var(--purple); | |
| border-color:var(--purple); | |
| box-shadow: 0 0 15px rgba(168, 85, 247, 0.6); | |
| transform: translateY(-2px); | |
| } | |
| /* ── HERO ── */ | |
| .hero{ | |
| min-height:100vh; | |
| display:flex;flex-direction:column; | |
| align-items:center;justify-content:center; | |
| padding:120px 48px 80px; | |
| text-align:center; | |
| position:relative; | |
| overflow:hidden; | |
| } | |
| .hero-bg-blob{ | |
| position:absolute; | |
| border-radius:50%; | |
| filter:blur(80px); | |
| pointer-events:none; | |
| z-index:0; | |
| } | |
| .blob1{width:500px;height:500px;background:rgba(200,255,0,.25);top:-100px;right:-80px;animation:drift 8s ease-in-out infinite alternate} | |
| .blob2{width:400px;height:400px;background:rgba(168, 85, 247,.15);bottom:-80px;left:-60px;animation:drift 10s ease-in-out infinite alternate-reverse} | |
| .blob3{width:300px;height:300px;background:rgba(0, 243, 255,.1);top:40%;left:60%;animation:drift 12s ease-in-out infinite alternate} | |
| @keyframes drift{from{transform:translate(0,0) scale(1)}to{transform:translate(30px,20px) scale(1.05)}} | |
| .hero-inner{position:relative;z-index:1;max-width:900px} | |
| .hero-badge{ | |
| display:inline-flex;align-items:center;gap:8px; | |
| background:var(--black);color:#fff; | |
| font-size:12px;font-weight:700;letter-spacing:2px;text-transform:uppercase; | |
| padding:8px 18px;border-radius:100px;margin-bottom:32px; | |
| animation:fadeUp .5s ease both; | |
| } | |
| .hero-badge-dot{ | |
| width:7px;height:7px;border-radius:50%;background:var(--cyan); | |
| animation:blink 1.5s ease infinite; | |
| box-shadow: 0 0 10px var(--cyan); | |
| } | |
| @keyframes blink{0%,100%{opacity:1}50%{opacity:.3}} | |
| .hero-title{ | |
| font-family:'Bebas Neue',sans-serif; | |
| font-size:clamp(80px,14vw,160px); | |
| line-height:.92;letter-spacing:2px; | |
| color:var(--black); | |
| animation:fadeUp .6s .1s ease both; | |
| } | |
| .hero-title .stroke{ | |
| -webkit-text-stroke:3px var(--black); | |
| color:transparent; | |
| } | |
| .hero-title .fill-cyan{color:var(--cyan);-webkit-text-stroke:0} | |
| .hero-title .fill-purple{color:var(--purple)} | |
| .hero-sub{ | |
| margin:28px auto 0;max-width:580px; | |
| font-size:18px;font-weight:500;line-height:1.6; | |
| color:#444; | |
| animation:fadeUp .6s .2s ease both; | |
| } | |
| .hero-sub strong{color:var(--black);font-weight:800} | |
| .hero-actions{ | |
| margin-top:40px;display:flex;gap:14px;justify-content:center;flex-wrap:wrap; | |
| animation:fadeUp .6s .3s ease both; | |
| } | |
| .btn-hero{ | |
| font-family:'Outfit',sans-serif;font-size:15px;font-weight:800; | |
| letter-spacing:.5px;text-transform:uppercase; | |
| padding:16px 36px;border-radius:100px; | |
| border:3px solid var(--black);cursor:pointer; | |
| transition:all 0.3s var(--bounce); | |
| } | |
| .btn-hero.primary{background:var(--black);color:#fff} | |
| .btn-hero.primary:hover{ | |
| background:var(--purple); | |
| border-color:var(--purple); | |
| transform:translateY(-5px) scale(1.05); | |
| box-shadow:0 12px 24px rgba(168, 85, 247,.4); | |
| } | |
| .btn-hero.secondary{background:var(--lime);color:var(--black)} | |
| .btn-hero.secondary:hover{ | |
| background:#d4ff1a; | |
| transform:translateY(-5px) scale(1.05); | |
| box-shadow:0 12px 24px rgba(200,255,0,.4); | |
| } | |
| /* ── MARQUEE ── */ | |
| .marquee-wrap{ | |
| border-top:3px solid var(--black); | |
| border-bottom:3px solid var(--black); | |
| background:var(--black); | |
| padding:14px 0;overflow:hidden;white-space:nowrap; | |
| } | |
| .marquee-track{ | |
| display:inline-flex;gap:0; | |
| animation:marquee 18s linear infinite; | |
| } | |
| .marquee-track span{ | |
| font-family:'Bebas Neue',sans-serif; | |
| font-size:22px;letter-spacing:3px;color:var(--cyan); | |
| padding:0 32px; | |
| } | |
| .marquee-track span.sep{color:#fff;padding:0} | |
| @keyframes marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}} | |
| /* ── TASKS SECTION ── */ | |
| .tasks-section{padding:80px 48px;border-bottom:3px solid var(--black)} | |
| .section-header{text-align:center;margin-bottom:56px} | |
| .section-eyebrow{ | |
| font-size:12px;font-weight:800;letter-spacing:3px;text-transform:uppercase; | |
| color:var(--purple);margin-bottom:12px; | |
| } | |
| .section-title{ | |
| font-family:'Bebas Neue',sans-serif; | |
| font-size:clamp(52px,7vw,96px); | |
| letter-spacing:2px;line-height:.95; | |
| color:var(--black); | |
| } | |
| /* Highlight Title matches the screenshot */ | |
| .highlight-bg { | |
| background: #2563eb; | |
| color: #fff; | |
| padding: 0 16px; | |
| display: inline-block; | |
| margin-bottom: 4px; | |
| } | |
| /* Updated Task Grid for 6 Items */ | |
| .tasks-grid{ | |
| display:flex; | |
| flex-wrap:wrap; | |
| gap:16px; | |
| } | |
| .task-card{ | |
| flex: 1 1 calc(33.333% - 16px); | |
| min-width: 280px; | |
| padding:36px 32px; | |
| border:3px solid var(--black); | |
| border-radius:12px; | |
| position:relative;transition:all 0.3s var(--bounce);cursor:pointer; | |
| background: var(--bg); | |
| } | |
| .task-card:hover{background:var(--black);color:#fff; transform:scale(1.02); z-index:10; box-shadow:0 10px 30px rgba(0,0,0,0.15);} | |
| .task-card:hover .task-num{color:var(--cyan)} | |
| .task-card:hover .task-name{color:#fff} | |
| .task-card:hover .task-desc{color:#aaa} | |
| .task-card:hover .task-tag{background:var(--cyan);color:var(--black);border-color:var(--cyan);box-shadow:0 0 10px var(--cyan);} | |
| .task-num{ | |
| font-family:'Bebas Neue',sans-serif; | |
| font-size:64px;line-height:1;color:var(--gray2); | |
| margin-bottom:12px;transition:color .2s; | |
| } | |
| .task-tag{ | |
| display:inline-block;font-size:10px;font-weight:800;letter-spacing:2px;text-transform:uppercase; | |
| padding:4px 12px;border-radius:100px;margin-bottom:16px; | |
| border:2px solid var(--black);transition:all .2s; | |
| } | |
| .tag-easy{background:rgba(200,255,0,.3);color:var(--black)} | |
| .tag-med{background:rgba(249, 115, 22,.15);color:var(--orange)} | |
| .tag-hard{background:rgba(168, 85, 247,.12);color:var(--purple)} | |
| .task-name{font-size:22px;font-weight:800;margin-bottom:10px;transition:color .2s} | |
| .task-desc{font-size:13px;font-weight:500;line-height:1.6;color:#555;transition:color .2s} | |
| /* ── ACTIONS SECTION ── */ | |
| .actions-section{padding:80px 48px;background:var(--black);border-bottom:3px solid var(--black)} | |
| .actions-section .section-eyebrow{color:var(--cyan)} | |
| .actions-section .section-title{color:#fff} | |
| .actions-grid{ | |
| display:grid;grid-template-columns:repeat(3,1fr);gap:3px; | |
| margin-top:48px; | |
| } | |
| .action-card{ | |
| background:#111;padding:28px 24px; | |
| transition:all 0.3s var(--bounce);cursor:default; | |
| border-radius:4px; | |
| } | |
| .action-card:hover{background:var(--purple);transform:translateY(-6px); box-shadow:0 10px 20px rgba(168, 85, 247, 0.2);} | |
| .action-card:hover .ac-name{color:#fff} | |
| .action-card:hover .ac-desc{color:rgba(255,255,255,0.8)} | |
| .action-card:hover .ac-icon{background:var(--black);color:var(--cyan);box-shadow:0 0 15px rgba(0, 243, 255, 0.5);} | |
| .ac-icon{ | |
| width:44px;height:44px;border-radius:10px; | |
| background:rgba(255,255,255,.06); | |
| display:flex;align-items:center;justify-content:center; | |
| font-size:20px;margin-bottom:16px;transition:all .3s; | |
| } | |
| .ac-name{font-size:14px;font-weight:800;color:#fff;letter-spacing:.3px;margin-bottom:6px;transition:color .2s;font-family:'Outfit',monospace} | |
| .ac-desc{font-size:12px;color:#666;line-height:1.6;transition:color .2s} | |
| /* ── PLAYGROUND SECTION ── */ | |
| .playground{padding:80px 48px;border-bottom:3px solid var(--black)} | |
| .playground .section-header{text-align:center;margin-bottom:48px} | |
| .pg-grid{display:grid;grid-template-columns:380px 1fr;gap:3px;border:3px solid var(--black);border-radius:12px;overflow:hidden} | |
| /* LEFT CONTROLS */ | |
| .pg-left{background:var(--gray);border-right:3px solid var(--black);display:flex;flex-direction:column} | |
| .pg-block{padding:24px;border-bottom:3px solid var(--black)} | |
| .pg-block:last-child{border-bottom:none;flex:1} | |
| .pg-label{font-size:10px;font-weight:800;letter-spacing:2.5px;text-transform:uppercase;color:#888;margin-bottom:14px} | |
| /* TICKET CARD */ | |
| .tkt{background:#fff;border:3px solid var(--black);border-radius:8px;padding:16px} | |
| .tkt-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px} | |
| .tkt-id{font-size:11px;font-weight:700;letter-spacing:1px;color:#888} | |
| .tkt-badge{font-size:9px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;padding:4px 10px;border-radius:100px;border:2px solid var(--black)} | |
| .tkt-badge.idle{background:var(--gray2);color:#888} | |
| .tkt-badge.active{background:var(--cyan);color:var(--black);box-shadow:0 0 8px rgba(0,243,255,0.4);} | |
| .tkt-badge.done{background:var(--purple);color:#fff;border-color:var(--purple)} | |
| .tkt-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;font-size:13px} | |
| .tkt-row:last-of-type{margin-bottom:0} | |
| .tkt-k{font-weight:600;color:#888} | |
| .tkt-v{font-weight:800;color:var(--black)} | |
| .tkt-v.vip{color:var(--orange)}.tkt-v.ent{color:var(--blue)}.tkt-v.std{color:var(--black)} | |
| .prog-bar{margin-top:14px} | |
| .prog-labels{display:flex;justify-content:space-between;font-size:11px;font-weight:700;color:#888;margin-bottom:6px} | |
| .prog-track{height:6px;background:var(--gray2);border-radius:100px;overflow:hidden;border:2px solid var(--black)} | |
| .prog-fill{height:100%;background:var(--black);border-radius:100px;transition:width .5s ease;width:0%} | |
| /* SENTIMENT */ | |
| .sent-pills{display:flex;gap:6px} | |
| .sp{ | |
| flex:1;padding:6px 4px;border-radius:100px; | |
| font-size:9px;font-weight:800;letter-spacing:1px;text-transform:uppercase; | |
| text-align:center;border:2px solid var(--black); | |
| background:#fff;color:#888;transition:all .3s var(--bounce); | |
| } | |
| .sp.a-happy{background:var(--lime);color:var(--black);transform:scale(1.05);box-shadow:0 4px 10px rgba(200,255,0,0.3);} | |
| .sp.a-neutral{background:var(--black);color:#fff;transform:scale(1.05);} | |
| .sp.a-frustrated{background:var(--orange);color:#fff;border-color:var(--orange);transform:scale(1.05);box-shadow:0 4px 10px rgba(249,115,22,0.3);} | |
| .sp.a-angry{background:var(--purple);color:#fff;border-color:var(--purple);transform:scale(1.05);box-shadow:0 4px 10px rgba(168,85,247,0.3);} | |
| /* KB */ | |
| .kb-result{ | |
| background:#fff;border:3px solid var(--black);border-left:6px solid var(--cyan); | |
| border-radius:0 8px 8px 0;padding:12px; | |
| font-size:12px;font-weight:500;line-height:1.7;color:#333; | |
| display:none; | |
| } | |
| .kb-result.on{display:block; animation: msgPop 0.4s var(--bounce) both;} | |
| .kb-ph{ | |
| font-size:12px;font-weight:600;color:#888; | |
| padding:10px;background:#fff;border:2px dashed var(--gray2);border-radius:8px;text-align:center; | |
| } | |
| .kb-ph span{color:var(--purple)} | |
| /* FORM */ | |
| .fg{margin-bottom:14px} | |
| .fl{font-size:10px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:#888;margin-bottom:6px;display:block} | |
| .fl.select-label::after{content:" ▾";color:var(--purple);font-size:11px} | |
| select,input[type=text]{ | |
| width:100%;padding:11px 14px; | |
| background:#fff;border:2px solid var(--black); | |
| border-radius:8px;color:var(--black); | |
| font-family:'Outfit',sans-serif;font-size:13px;font-weight:600; | |
| outline:none;transition:all 0.3s; | |
| appearance:none;-webkit-appearance:none; | |
| } | |
| select{ | |
| background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%230a0a0a' stroke-width='2' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); | |
| background-repeat:no-repeat;background-position:right 12px center;padding-right:36px; | |
| } | |
| select:focus,input:focus{border-color:var(--cyan);box-shadow:0 0 0 4px rgba(0, 243, 255,.15); transform:scale(1.01);} | |
| .fhide{display:none} | |
| .btn-submit{ | |
| width:100%;padding:14px; | |
| background:var(--black);color:#fff; | |
| border:3px solid var(--black);border-radius:100px; | |
| font-family:'Outfit',sans-serif;font-size:14px;font-weight:800; | |
| letter-spacing:1px;text-transform:uppercase; | |
| cursor:pointer;transition:all 0.3s var(--bounce); | |
| } | |
| .btn-submit:hover{ | |
| background:var(--purple); | |
| border-color:var(--purple); | |
| transform:translateY(-3px); | |
| box-shadow:0 8px 20px rgba(168, 85, 247,.4); | |
| } | |
| .btn-submit:active{transform:translateY(0)} | |
| .btn-submit:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none;} | |
| .btn-row{display:flex;gap:8px;margin-top:10px} | |
| .bsm{ | |
| flex:1;padding:10px; | |
| background:#fff;border:2px solid var(--black);border-radius:100px; | |
| font-family:'Outfit',sans-serif;font-size:11px;font-weight:800; | |
| letter-spacing:1px;text-transform:uppercase; | |
| cursor:pointer;transition:all 0.3s var(--bounce);color:var(--black); | |
| } | |
| .bsm:hover{background:var(--black);color:#fff;transform:translateY(-2px);} | |
| .bsm.new:hover{ | |
| background:var(--cyan); | |
| color:var(--black); | |
| border-color:var(--cyan); | |
| box-shadow: 0 5px 15px rgba(0,243,255,0.4); | |
| } | |
| /* RIGHT CONVO */ | |
| .pg-right{display:flex;flex-direction:column;background:#fff;height:700px;} | |
| .convo-area{ | |
| flex:1;overflow-y:scroll;padding:28px;display:flex;flex-direction:column;gap:14px;min-height:0; | |
| } | |
| /* Cool custom scrollbar for GenZ vibe */ | |
| .convo-area::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .convo-area::-webkit-scrollbar-track { | |
| background: var(--gray); | |
| border-radius: 8px; | |
| } | |
| .convo-area::-webkit-scrollbar-thumb { | |
| background: var(--purple); | |
| border-radius: 8px; | |
| border: 2px solid var(--gray); | |
| } | |
| .convo-area::-webkit-scrollbar-thumb:hover { | |
| background: var(--cyan); | |
| } | |
| .convo-empty{ | |
| flex:1;display:flex;flex-direction:column; | |
| align-items:center;justify-content:center; | |
| text-align:center;gap:12px;padding:48px; | |
| } | |
| .ce-emoji{font-size:56px; animation: bounce 2s infinite;} | |
| @keyframes bounce { 0%, 100% {transform: translateY(0);} 50% {transform: translateY(-10px);} } | |
| .ce-title{font-family:'Bebas Neue',sans-serif;font-size:36px;letter-spacing:1px;color:var(--black)} | |
| .ce-sub{font-size:14px;font-weight:500;color:#888;max-width:280px;line-height:1.6} | |
| .msg{display:flex;gap:12px;animation:msgPop .3s var(--bounce) both} | |
| @keyframes msgPop{from{opacity:0;transform:translateY(12px) scale(0.95)}to{opacity:1;transform:translateY(0) scale(1)}} | |
| .mavatar{ | |
| width:32px;height:32px;border-radius:8px; | |
| border:2px solid var(--black); | |
| display:flex;align-items:center;justify-content:center; | |
| font-size:14px;flex-shrink:0; | |
| } | |
| .mavatar.cu{background:var(--black);color:#fff} | |
| .mavatar.ag{background:var(--cyan);color:var(--black)} | |
| .mavatar.sy{background:var(--gray2)} | |
| .mbody{flex:1} | |
| .msender{font-size:10px;font-weight:800;letter-spacing:2px;text-transform:uppercase;margin-bottom:5px} | |
| .msender.cu{color:var(--black)}.msender.ag{color:var(--purple)}.msender.sy{color:#888} | |
| .mtext{ | |
| font-size:13px;font-weight:500;line-height:1.6;color:var(--black); | |
| background:var(--gray);border:2px solid var(--black); | |
| border-radius:0 10px 10px 10px;padding:10px 14px; | |
| display:inline-block;max-width:90%; | |
| } | |
| .mtext.sys{background:var(--gray2);border-color:var(--gray2);color:#555;font-size:12px} | |
| /* REWARD LOG */ | |
| .rlog{border-top:3px solid var(--black);background:var(--gray);height:310px;min-height:200px;overflow-y:scroll;width:100%;box-sizing:border-box;flex-shrink:0;} | |
| .rlog::-webkit-scrollbar { width: 8px; display:block; } | |
| .rlog::-webkit-scrollbar-track { background: var(--gray); border-radius: 8px; } | |
| .rlog::-webkit-scrollbar-thumb { background: var(--purple); border-radius: 8px; border: 2px solid var(--gray); } | |
| .rlog::-webkit-scrollbar-thumb:hover { background: var(--cyan); } | |
| .rlog-head{ | |
| padding:12px 24px;border-bottom:2px solid var(--black); | |
| display:flex;justify-content:space-between;align-items:center; | |
| position:sticky;top:0;background:var(--gray);z-index:5; | |
| } | |
| .rlog-title{font-size:10px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:#888} | |
| .rtotal{font-size:15px;font-weight:900;color:var(--black)} | |
| /* Custom History Log Entry based on Image */ | |
| .history-log { | |
| background: #fff; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .log-entry { | |
| padding: 14px 24px; | |
| border-bottom: 1px solid var(--gray2); | |
| font-family: 'Outfit', sans-serif; | |
| font-size: 13px; | |
| animation: msgPop .3s var(--bounce) both; | |
| background: #fff; | |
| } | |
| .log-entry:last-child { | |
| border-bottom: none; | |
| } | |
| .log-action { | |
| font-size: 14px; | |
| margin-bottom: 6px; | |
| color: #38bdf8; /* Light blue */ | |
| font-weight: 500; | |
| } | |
| .log-action strong { font-weight: 700; } | |
| .log-reward { | |
| font-weight: 700; | |
| color: #475569; | |
| } | |
| .score-hint{ | |
| margin-top:8px; | |
| font-size:11px; | |
| line-height:1.45; | |
| color:var(--muted); | |
| } | |
| /* SETUP SECTION */ | |
| .setup-section{padding:80px 48px;background:var(--gray);border-bottom:3px solid var(--black)} | |
| .setup-section .section-header{text-align:center;margin-bottom:48px} | |
| .setup-grid{display:grid;grid-template-columns:1fr 1fr;gap:24px;max-width:900px;margin:0 auto} | |
| .code-card{background:#fff;border:3px solid var(--black);border-radius:12px;overflow:hidden; transition:transform 0.3s var(--bounce);} | |
| .code-card:hover { transform: translateY(-4px); box-shadow: 0 10px 25px rgba(0,0,0,0.1); } | |
| .code-head{ | |
| background:var(--black);padding:14px 20px; | |
| display:flex;justify-content:space-between;align-items:center; | |
| } | |
| .code-head-title{font-size:11px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:var(--cyan)} | |
| .copy-btn{ | |
| font-size:10px;font-weight:800;letter-spacing:1px;text-transform:uppercase; | |
| padding:4px 12px;border-radius:100px; | |
| background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2); | |
| color:#fff;cursor:pointer;transition:all 0.3s var(--bounce); | |
| font-family:'Outfit',sans-serif; | |
| } | |
| .copy-btn:hover{background:var(--purple);color:#fff;border-color:var(--purple);box-shadow:0 0 10px var(--purple);} | |
| .copy-btn.done{background:var(--cyan);color:var(--black); border-color:var(--cyan);} | |
| .code-body{padding:20px;font-size:12px;line-height:1.9;font-family:'Outfit',monospace;background:#fff} | |
| .c-cmd{color:var(--purple);font-weight:700} | |
| .c-val{color:var(--blue)} | |
| .c-cmt{color:#aaa} | |
| /* FOOTER */ | |
| footer{ | |
| padding:48px; | |
| background:var(--black); | |
| border-top:3px solid var(--black); | |
| display:flex;justify-content:space-between;align-items:center; | |
| flex-wrap:wrap;gap:20px; | |
| } | |
| .foot-logo{font-family:'Bebas Neue',sans-serif;font-size:28px;letter-spacing:2px;color:var(--cyan)} | |
| .foot-text{font-size:13px;font-weight:500;color:#555} | |
| /* FOOTER DEV ANIMATION */ | |
| .foot-devs { | |
| font-size: 14px; | |
| font-weight: 700; | |
| color: #888; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .foot-devs span { | |
| background: linear-gradient(90deg, var(--cyan), var(--purple), var(--cyan)); | |
| background-size: 200% auto; | |
| color: transparent; | |
| -webkit-background-clip: text; | |
| animation: shine 3s linear infinite; | |
| font-weight: 900; | |
| font-size: 16px; | |
| } | |
| @keyframes shine { | |
| to { background-position: 200% center; } | |
| } | |
| .foot-links{display:flex;gap:20px} | |
| .foot-links a{font-size:11px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:#555;text-decoration:none;transition:color .3s} | |
| .foot-links a:hover{color:var(--purple)} | |
| /* TOAST */ | |
| .toast{ | |
| position:fixed;bottom:24px;right:24px;z-index:999; | |
| background:var(--black);color:#fff;border:3px solid var(--black); | |
| border-radius:100px;padding:12px 22px; | |
| font-size:13px;font-weight:700; | |
| box-shadow:0 8px 28px rgba(0,0,0,.2); | |
| transform:translateY(16px);opacity:0; | |
| transition:all .3s var(--bounce);pointer-events:none; | |
| } | |
| .toast.show{transform:translateY(0);opacity:1} | |
| .toast.ok{background:var(--cyan);color:var(--black)} | |
| .toast.err{background:var(--purple); color:#fff; border-color:var(--purple);} | |
| /* ── MODERN HACKATHON THEME OVERRIDES ── */ | |
| :root{ | |
| --bg:#070b14; | |
| --panel:#0f172a; | |
| --panel2:#111c33; | |
| --line:#21314f; | |
| --text:#e5edf8; | |
| --muted:#9fb0ca; | |
| --accent:#22d3ee; | |
| --accent2:#a78bfa; | |
| --good:#4ade80; | |
| } | |
| body{ | |
| background: | |
| radial-gradient(circle at 20% 20%, rgba(34,211,238,.12), transparent 35%), | |
| radial-gradient(circle at 80% 0%, rgba(167,139,250,.14), transparent 40%), | |
| var(--bg); | |
| color:var(--text); | |
| } | |
| body::after{opacity:.02} | |
| nav{ | |
| background:rgba(7,11,20,.88); | |
| border-bottom:1px solid var(--line); | |
| } | |
| .nav-logo,.nav-tag,.hero-sub strong,.section-title,.task-name,.ac-name,.ce-title,.rtotal{color:var(--text)} | |
| .nav-logo span,.section-eyebrow,.hero-badge-dot,.rlog-title{color:var(--accent)} | |
| .nav-devs{ | |
| background:rgba(17,28,51,.85); | |
| border-color:var(--accent2); | |
| } | |
| .nav-tag{ | |
| background:rgba(34,211,238,.14); | |
| color:var(--accent); | |
| border:1px solid rgba(34,211,238,.45); | |
| } | |
| .nav-docs{ | |
| background:rgba(167,139,250,.15); | |
| border:1px solid rgba(167,139,250,.45); | |
| color:#ddd6fe; | |
| } | |
| .hero{ | |
| min-height:88vh; | |
| padding-top:130px; | |
| } | |
| .hero-badge{ | |
| background:rgba(17,28,51,.75); | |
| border:1px solid var(--line); | |
| color:#c7d8f5; | |
| } | |
| .hero-title{ | |
| font-size:clamp(54px,10vw,118px); | |
| text-shadow:0 10px 24px rgba(0,0,0,.4); | |
| } | |
| .hero-title .stroke{ | |
| -webkit-text-stroke:2px #93c5fd; | |
| } | |
| .hero-title .fill-purple{color:#c4b5fd} | |
| .hero-sub{color:#b7c7df;max-width:760px} | |
| .hero-actions .btn-hero{ | |
| border-radius:12px; | |
| border:1px solid var(--line); | |
| } | |
| .btn-hero.primary{ | |
| background:linear-gradient(90deg,#0ea5e9,#7c3aed); | |
| color:#fff; | |
| } | |
| .btn-hero.secondary{ | |
| background:rgba(15,23,42,.82); | |
| color:#dbe7ff; | |
| } | |
| .hero-metrics{ | |
| margin:34px auto 0; | |
| display:grid; | |
| grid-template-columns:repeat(4,minmax(130px,1fr)); | |
| gap:10px; | |
| max-width:760px; | |
| } | |
| .metric{ | |
| background:linear-gradient(180deg,rgba(15,23,42,.86),rgba(17,28,51,.9)); | |
| border:1px solid var(--line); | |
| border-radius:12px; | |
| padding:14px 12px; | |
| } | |
| .metric-k{ | |
| font-size:11px; | |
| letter-spacing:1.5px; | |
| text-transform:uppercase; | |
| color:var(--muted); | |
| } | |
| .metric-v{ | |
| margin-top:6px; | |
| font-size:20px; | |
| font-weight:800; | |
| color:#f8fbff; | |
| } | |
| .marquee-wrap,.tasks-section,.actions-section,.playground,.setup-section,footer{ | |
| border-color:var(--line); | |
| } | |
| .tasks-section,.playground{ | |
| background:transparent; | |
| } | |
| .task-card,.code-card,.pg-grid,.tkt,.kb-result,.action-card,.log-entry{ | |
| background:var(--panel); | |
| border-color:var(--line); | |
| color:var(--text); | |
| } | |
| .task-desc,.ac-desc,.ce-sub,.foot-text,.tkt-k,.prog-labels,.log-reward,.kb-ph,.c-cmt{color:var(--muted)} | |
| .task-tag{ | |
| border:1px solid var(--line); | |
| color:#dbeafe; | |
| } | |
| .tag-easy,.tag-med,.tag-hard{ | |
| background:rgba(34,211,238,.12); | |
| } | |
| .actions-section{ | |
| background:transparent; | |
| } | |
| .ac-icon{ | |
| background:rgba(34,211,238,.14); | |
| } | |
| .pg-grid{ | |
| border-radius:16px; | |
| } | |
| .pg-left{ | |
| background:var(--panel2); | |
| border-right:1px solid var(--line); | |
| } | |
| .pg-block{ | |
| border-bottom:1px solid var(--line); | |
| } | |
| .pg-right{ | |
| background:var(--panel); | |
| } | |
| .convo-area,.rlog,.history-log{ | |
| background:var(--panel); | |
| } | |
| .mtext{ | |
| background:rgba(17,28,51,.9); | |
| border:1px solid var(--line); | |
| color:#eaf2ff; | |
| } | |
| .mtext.sys{ | |
| background:rgba(148,163,184,.14); | |
| color:#cbd5e1; | |
| } | |
| .rlog-head{ | |
| background:var(--panel2); | |
| border-bottom:1px solid var(--line); | |
| } | |
| select,input[type=text]{ | |
| background:#0a1326; | |
| border:1px solid var(--line); | |
| color:#e6effc; | |
| } | |
| .btn-submit{ | |
| background:linear-gradient(90deg,#0ea5e9,#7c3aed); | |
| border:0; | |
| } | |
| .bsm{ | |
| background:#0a1326; | |
| border:1px solid var(--line); | |
| color:#dbeafe; | |
| } | |
| .bsm.new{ | |
| background:rgba(74,222,128,.2); | |
| border-color:rgba(74,222,128,.5); | |
| color:#bbf7d0; | |
| } | |
| .setup-section{ | |
| background:transparent; | |
| } | |
| .code-head,footer{ | |
| background:var(--panel2); | |
| } | |
| .foot-logo{ | |
| color:#c4b5fd; | |
| } | |
| .foot-links a{color:#a5b4cc} | |
| @media(max-width:900px){ | |
| .hero-metrics{grid-template-columns:repeat(2,minmax(130px,1fr))} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <nav> | |
| <div class="nav-logo">Customer Support <span>Command Center</span></div> | |
| <div class="nav-right"> | |
| <div class="nav-devs">Developed by <span>Hrushikesh Sarode</span> & <span>Aditya Shinde</span></div> | |
| <span class="nav-tag">🟢 Live</span> | |
| <a href="/docs" class="nav-docs" target="_blank">API Docs ↗</a> | |
| </div> | |
| </nav> | |
| <section class="hero"> | |
| <div class="hero-bg-blob blob1"></div> | |
| <div class="hero-bg-blob blob2"></div> | |
| <div class="hero-bg-blob blob3"></div> | |
| <div class="hero-inner"> | |
| <div class="hero-badge"><div class="hero-badge-dot"></div> OpenEnv Hackathon · Live Environment</div> | |
| <h1 class="hero-title"> | |
| <span class="fill-purple">CUSTOMER</span><br> | |
| <span class="stroke">SUPPORT</span><br> | |
| <span class="fill-cyan">COMMAND CENTER</span> | |
| </h1> | |
| <p class="hero-sub"> | |
| A clean, modern mission dashboard to train AI on <strong>real customer tickets</strong> with clear actions, live reward tracking, and policy-safe outcomes. | |
| </p> | |
| <div class="hero-actions"> | |
| <button class="btn-hero primary" onclick="document.getElementById('playground').scrollIntoView({behavior:'smooth'})">Launch Demo</button> | |
| <button class="btn-hero secondary" onclick="document.getElementById('tasks').scrollIntoView({behavior:'smooth'})">View Mission Tasks</button> | |
| </div> | |
| <div class="hero-metrics"> | |
| <div class="metric"><div class="metric-k">Tasks</div><div class="metric-v">6 Scenarios</div></div> | |
| <div class="metric"><div class="metric-k">Actions</div><div class="metric-v">5 Controls</div></div> | |
| <div class="metric"><div class="metric-k">Step Limit</div><div class="metric-v">10/Episode</div></div> | |
| <div class="metric"><div class="metric-k">Target Score</div><div class="metric-v">0.90+</div></div> | |
| </div> | |
| </div> | |
| </section> | |
| <div class="marquee-wrap"> | |
| <div class="marquee-track"> | |
| <span>CLASSIFY ISSUE</span><span class="sep">·</span> | |
| <span>SEARCH KB</span><span class="sep">·</span> | |
| <span>CHECK LOGS</span><span class="sep">·</span> | |
| <span>RESOLVE TICKET</span><span class="sep">·</span> | |
| <span>ESCALATE TO HUMAN</span><span class="sep">·</span> | |
| <span>SENTIMENT SCORE</span><span class="sep">·</span> | |
| <span>REWARD SIGNAL</span><span class="sep">·</span> | |
| <span>CLASSIFY ISSUE</span><span class="sep">·</span> | |
| <span>SEARCH KB</span><span class="sep">·</span> | |
| <span>CHECK LOGS</span><span class="sep">·</span> | |
| <span>RESOLVE TICKET</span><span class="sep">·</span> | |
| <span>ESCALATE TO HUMAN</span><span class="sep">·</span> | |
| <span>SENTIMENT SCORE</span><span class="sep">·</span> | |
| <span>REWARD SIGNAL</span><span class="sep">·</span> | |
| </div> | |
| </div> | |
| <section class="tasks-section" id="tasks"> | |
| <div class="section-header"> | |
| <div class="section-eyebrow">Mission Library</div> | |
| <div class="section-title"> | |
| <span class="">6 TICKET MISSIONS</span><br> | |
| </div> | |
| </div> | |
| <div class="tasks-grid"> | |
| <div class="task-card"> | |
| <div class="task-num">01</div> | |
| <div class="task-tag tag-easy">Easy</div> | |
| <div class="task-name">Billing Receipt Request</div> | |
| <div class="task-desc">Classify as Billing, optionally verify policy via search_kb, then resolve clearly.</div> | |
| </div> | |
| <div class="task-card"> | |
| <div class="task-num">02</div> | |
| <div class="task-tag tag-med">Medium</div> | |
| <div class="task-name">Dashboard Error 404</div> | |
| <div class="task-desc">Identify Technical issue, pull KB guidance, and provide the correct fix path.</div> | |
| </div> | |
| <div class="task-card"> | |
| <div class="task-num">03</div> | |
| <div class="task-tag tag-hard">Hard</div> | |
| <div class="task-name">Digital Item Refund Demand</div> | |
| <div class="task-desc">For non-refundable digital downloads, policy-safe handling usually means escalation.</div> | |
| </div> | |
| <div class="task-card"> | |
| <div class="task-num">04</div> | |
| <div class="task-tag tag-hard">Hard</div> | |
| <div class="task-name">VIP Outage</div> | |
| <div class="task-desc">Tests if the agent can correctly classify high-urgency technical issues without being distracted by account status keywords.</div> | |
| </div> | |
| <div class="task-card"> | |
| <div class="task-num">05</div> | |
| <div class="task-tag tag-med">Medium</div> | |
| <div class="task-name">Vague Complaint</div> | |
| <div class="task-desc">The user says "It is broken." Ask one clarifying question, then classify/search/resolve efficiently.</div> | |
| </div> | |
| <div class="task-card"> | |
| <div class="task-num">06</div> | |
| <div class="task-tag tag-hard">Hard</div> | |
| <div class="task-name">Subscription Plan Refund Difference</div> | |
| <div class="task-desc">Reason over refund policy edge case and complete with policy-consistent action.</div> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="actions-section"> | |
| <div class="section-header"> | |
| <div class="section-eyebrow">Control Panel</div> | |
| <div class="section-title">5 CORE ACTIONS.<br>POLICY-FIRST EXECUTION.</div> | |
| </div> | |
| <div class="actions-grid"> | |
| <div class="action-card"> | |
| <div class="ac-icon">🏷️</div> | |
| <div class="ac-name">classify_issue</div> | |
| <div class="ac-desc">Tag the ticket type first: Billing, Technical, or Refund_Request. Correct classification earns +0.03; incorrect earns +0.01.</div> | |
| </div> | |
| <div class="action-card"> | |
| <div class="ac-icon">📚</div> | |
| <div class="ac-name">search_kb</div> | |
| <div class="ac-desc">Query the knowledge base with keywords. Returns the exact policy the agent needs to follow. Relevant query earns +0.05; off-target earns +0.02.</div> | |
| </div> | |
| <div class="action-card"> | |
| <div class="ac-icon">💬</div> | |
| <div class="ac-name">ask_clarifying_question</div> | |
| <div class="ac-desc">Talk to the customer. Warning: ask too many times and their patience runs out fast.</div> | |
| </div> | |
| <div class="action-card"> | |
| <div class="ac-icon">✅</div> | |
| <div class="ac-name">resolve_ticket</div> | |
| <div class="ac-desc">Close the ticket with a final solution. Resolved tickets get the terminal success score.</div> | |
| </div> | |
| <div class="action-card"> | |
| <div class="ac-icon">🚨</div> | |
| <div class="ac-name">escalate_to_human</div> | |
| <div class="ac-desc">Hand off to a real person when policy requires it. For some refund tasks, this is the successful terminal action.</div> | |
| </div> | |
| <div class="action-card" style="background:rgba(168, 85, 247,.08)"> | |
| <div class="ac-icon">⚡</div> | |
| <div class="ac-name">Reward Signal</div> | |
| <div class="ac-desc">Graduated rewards: ask_clarifying_question +0.01 · classify_issue +0.01/+0.03 · search_kb +0.02/+0.05 · correct terminal action +0.90 before step 10.</div> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="playground" id="playground"> | |
| <div class="section-header"> | |
| <div class="section-eyebrow">Interactive Demo</div> | |
| <div class="section-title">PLAY THE<br>ENVIRONMENT.</div> | |
| </div> | |
| <div class="pg-grid"> | |
| <div class="pg-left"> | |
| <div class="pg-block"> | |
| <div class="pg-label">Ticket Status</div> | |
| <div class="tkt"> | |
| <div class="tkt-top"> | |
| <span class="tkt-id" id="ticketId">NO TICKET</span> | |
| <span class="tkt-badge idle" id="ticketStatus">IDLE</span> | |
| </div> | |
| <div class="tkt-row"><span class="tkt-k">Tier</span><span class="tkt-v" id="tierVal">—</span></div> | |
| <div class="tkt-row"><span class="tkt-k">Category</span><span class="tkt-v" id="catVal" style="color:#888">Not classified</span></div> | |
| <div class="tkt-row"><span class="tkt-k">Steps</span><span class="tkt-v" id="stepVal">0 / 10</span></div> | |
| <div class="tkt-row"><span class="tkt-k">Terminal Score</span><span class="tkt-v" id="terminalScore">—</span></div> | |
| <div class="score-hint" id="scoreHint">Use a terminal action before step 10 to reach 0.90.</div> | |
| <div class="prog-bar"> | |
| <div class="prog-labels"><span>Progress</span><span id="progPct">0%</span></div> | |
| <div class="prog-track"><div class="prog-fill" id="progFill"></div></div> | |
| </div> | |
| </div> | |
| <div style="margin-top:16px"> | |
| <div class="pg-label" style="margin-bottom:8px">Sentiment</div> | |
| <div class="sent-pills"> | |
| <div class="sp" id="sp-happy">😊 Happy</div> | |
| <div class="sp a-neutral" id="sp-neutral">😐 Neutral</div> | |
| <div class="sp" id="sp-frustrated">😤 Upset</div> | |
| <div class="sp" id="sp-angry">😡 Angry</div> | |
| </div> | |
| </div> | |
| <div style="margin-top:16px"> | |
| <div class="pg-label" style="margin-bottom:8px">Knowledge Base</div> | |
| <div class="kb-result" id="kbResult"></div> | |
| <div class="kb-ph" id="kbPh">Use <span>search_kb</span> to pull policy</div> | |
| </div> | |
| </div> | |
| <div class="pg-block" style="flex:1"> | |
| <div class="pg-label">Submit Action</div> | |
| <div class="fg"> | |
| <label class="fl select-label">Action Type</label> | |
| <select id="actionType" onchange="toggleFields()"> | |
| <option value="classify_issue">classify_issue</option> | |
| <option value="search_kb">search_kb</option> | |
| <option value="ask_clarifying_question">ask_clarifying_question</option> | |
| <option value="resolve_ticket">resolve_ticket</option> | |
| <option value="escalate_to_human">escalate_to_human</option> | |
| </select> | |
| </div> | |
| <div class="fg fhide" id="f-cat"> | |
| <label class="fl select-label">Category</label> | |
| <select id="catGuess"> | |
| <option value="Billing">Billing</option> | |
| <option value="Technical">Technical</option> | |
| <option value="Refund_Request">Refund_Request</option> | |
| </select> | |
| </div> | |
| <div class="fg fhide" id="f-search"> | |
| <label class="fl">Search Query</label> | |
| <input type="text" id="searchQ" placeholder="e.g. billing receipt"> | |
| </div> | |
| <div class="fg fhide" id="f-msg"> | |
| <label class="fl">Message</label> | |
| <input type="text" id="msgInput" placeholder="Type your message…"> | |
| </div> | |
| <button class="btn-submit" id="submitBtn" onclick="submitAction()">→ Submit Action</button> | |
| <div class="btn-row"> | |
| <button class="bsm new" onclick="resetEnv()">⟳ New Episode</button> | |
| <button class="bsm" onclick="refreshState()">↺ Refresh</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="pg-right"> | |
| <div class="convo-area" id="convoWrap"> | |
| <div class="convo-empty" id="convoEmpty"> | |
| <div class="ce-emoji">🎧</div> | |
| <div class="ce-title">No Ticket Yet</div> | |
| <div class="ce-sub">Hit "New Episode" to load a customer scenario and start playing.</div> | |
| </div> | |
| </div> | |
| <div class="rlog"> | |
| <div class="rlog-head"> | |
| <span class="rlog-title">Reward Log</span> | |
| <span class="rtotal" id="rtotal">+0.00</span> | |
| </div> | |
| <div class="history-log" id="historyLog"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="setup-section" id="setup"> | |
| <div class="section-header"> | |
| <div class="section-eyebrow">Get Running</div> | |
| <div class="section-title">SETUP IN<br>60 SECONDS.</div> | |
| </div> | |
| <div class="setup-grid"> | |
| <div class="code-card"> | |
| <div class="code-head"> | |
| <span class="code-head-title">Install & Run</span> | |
| <button class="copy-btn" onclick="copyCode(this,'install')">Copy</button> | |
| </div> | |
| <div class="code-body" id="install"> | |
| <span class="c-cmt"># Install dependencies</span><br> | |
| <span class="c-cmd">pip install</span> fastapi uvicorn openai pydantic<br><br> | |
| <span class="c-cmt"># Start the server</span><br> | |
| <span class="c-cmd">uvicorn</span> api:app --reload | |
| </div> | |
| </div> | |
| <div class="code-card"> | |
| <div class="code-head"> | |
| <span class="code-head-title">Run Inference</span> | |
| <button class="copy-btn" onclick="copyCode(this,'inference')">Copy</button> | |
| </div> | |
| <div class="code-body" id="inference"> | |
| <span class="c-cmd">set</span> <span class="c-val">HF_TOKEN</span>=your_token<br> | |
| <span class="c-cmd">set</span> <span class="c-val">API_BASE_URL</span>=https://router.huggingface.co/v1<br> | |
| <span class="c-cmd">set</span> <span class="c-val">MODEL_NAME</span>=Qwen/Qwen2.5-72B-Instruct<br><br> | |
| <span class="c-cmd">python</span> inference.py | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <footer> | |
| <div class="foot-logo">Customer Support Openenv</div> | |
| <div class="foot-text">Built for the OpenEnv Hackathon 2026</div> | |
| <div class="foot-devs">Developed by <span>Hrushikesh Sarode</span> & <span>Aditya Shinde</span></div> | |
| <div class="foot-links"> | |
| <a href="/docs" target="_blank">API Docs</a> | |
| <a href="/reset" onclick="event.preventDefault();fetch('/reset',{method:'POST'})">Reset Env</a> | |
| </div> | |
| </footer> | |
| <div class="toast" id="toast"></div> | |
| <script> | |
| let totalRew=0,stepNum=0,lastHistLen=0; | |
| function toggleFields(){ | |
| const a=document.getElementById('actionType').value; | |
| ['f-cat','f-search','f-msg'].forEach(id=>document.getElementById(id).classList.add('fhide')); | |
| if(a==='classify_issue') document.getElementById('f-cat').classList.remove('fhide'); | |
| if(a==='search_kb') document.getElementById('f-search').classList.remove('fhide'); | |
| if(a==='ask_clarifying_question'||a==='resolve_ticket') document.getElementById('f-msg').classList.remove('fhide'); | |
| } | |
| function getTerminalHint(obs){ | |
| // Show the most useful terminal scoring tip from current state. | |
| const category=(obs&&typeof obs==='object'&&typeof obs.issue_category==='string')?obs.issue_category:''; | |
| if(category==='Refund_Request'){ | |
| return 'Refund_Request tasks usually need escalate_to_human for a 0.90 terminal score.'; | |
| } | |
| return 'classify_issue +0.01/+0.03 · search_kb +0.02/+0.05 · ask +0.01. Use a correct terminal action before step 10 for +0.90.'; | |
| } | |
| function showToast(msg,type=''){ | |
| const t=document.getElementById('toast'); | |
| t.textContent=msg;t.className='toast show '+type; | |
| setTimeout(()=>t.className='toast',2600); | |
| } | |
| function addMsg(sender,text,cls){ | |
| const wrap=document.getElementById('convoWrap'); | |
| const em=document.getElementById('convoEmpty');if(em)em.remove(); | |
| const icons={cu:'👤',ag:'🤖',sy:'⚙'}; | |
| const d=document.createElement('div');d.className='msg'; | |
| d.innerHTML=`<div class="mavatar ${cls}">${icons[cls]||'•'}</div> | |
| <div class="mbody"> | |
| <div class="msender ${cls}">${sender}</div> | |
| <div class="mtext ${cls==='sy'?'sys':''}">${text}</div> | |
| </div>`; | |
| wrap.appendChild(d);wrap.scrollTop=wrap.scrollHeight; | |
| } | |
| // Replaced addReward with logHistory from your prompt, fully integrated to update total points | |
| function logHistory(actionPayload, reward, done) { | |
| const logDiv = document.getElementById('historyLog'); | |
| const entry = document.createElement('div'); | |
| entry.className = 'log-entry'; | |
| // Extract the reward value and reason beautifully | |
| let rewardText = reward; | |
| let rewardValNum = 0; | |
| if (typeof reward === 'object' && reward !== null) { | |
| rewardValNum = parseFloat(reward.value) || 0; | |
| if (reward.value !== undefined && reward.reason !== undefined) { | |
| let color = reward.value >= 0 ? '#22c55e' : '#ef4444'; | |
| rewardText = `<span style="color:${color}; font-weight:bold;">Reward: ${rewardValNum.toFixed(2)}</span> | <span style="color:#94a3b8; font-weight:normal;">Reason: ${reward.reason}</span>`; | |
| } else { | |
| rewardText = JSON.stringify(reward); | |
| } | |
| } else { | |
| rewardValNum = parseFloat(reward) || 0; | |
| let color = rewardValNum >= 0 ? '#22c55e' : '#ef4444'; | |
| rewardText = `<span style="color:${color}; font-weight:bold;">Reward: ${rewardValNum.toFixed(2)}</span>`; | |
| } | |
| // Translate the raw JSON Action into beautiful English Badges! | |
| let formattedAction = ""; | |
| if (actionPayload.action_type === 'ask_clarifying_question') { | |
| formattedAction = `🗣️ <strong>Asked Question:</strong> "${actionPayload.message_to_customer}"`; | |
| } else if (actionPayload.action_type === 'search_kb') { | |
| formattedAction = `🔍 <strong>Searched KB:</strong> "${actionPayload.search_query}"`; | |
| } else if (actionPayload.action_type === 'classify_issue') { | |
| formattedAction = `🗂️ <strong>Classified Issue:</strong> ${actionPayload.category_guess}`; | |
| } else if (actionPayload.action_type === 'resolve_ticket') { | |
| formattedAction = `✅ <strong>Resolved Ticket:</strong> "${actionPayload.message_to_customer}"`; | |
| } else if (actionPayload.action_type === 'escalate_to_human') { | |
| formattedAction = `🚨 <strong>Escalated to Human</strong>`; | |
| } else if (actionPayload.command) { | |
| formattedAction = `🔄 <strong>${actionPayload.command}</strong>`; | |
| } else { | |
| formattedAction = JSON.stringify(actionPayload); | |
| } | |
| entry.innerHTML = ` | |
| <div class="log-action">${formattedAction}</div> | |
| <div class="log-reward">${rewardText !== null ? rewardText : '0'} ${done ? '<br><span style="color:#ef4444; font-size:12px; margin-top:6px; display:inline-block; font-weight:bold;">[EPISODE FINISHED]</span>' : ''}</div> | |
| `; | |
| logDiv.prepend(entry); | |
| // Update total score UI | |
| totalRew += rewardValNum; | |
| const col = totalRew >= 0 ? '#16a34a' : '#ef4444'; | |
| const s = totalRew >= 0 ? '+' : ''; | |
| document.getElementById('rtotal').innerHTML = `<span style="color:${col};font-weight:900">${s}${totalRew.toFixed(2)}</span>`; | |
| } | |
| function updateStatus(obs,done){ | |
| if(!obs||typeof obs!=='object')return; | |
| document.getElementById('ticketId').textContent=obs.ticket_id||'N/A'; | |
| const st=document.getElementById('ticketStatus'); | |
| if(done){st.textContent='DONE';st.className='tkt-badge done';} | |
| else{st.textContent='ACTIVE';st.className='tkt-badge active';} | |
| const tier=obs.customer_tier||'—'; | |
| const te=document.getElementById('tierVal'); | |
| te.textContent=tier; | |
| te.className='tkt-v '+(tier==='VIP'?'vip':tier==='Enterprise'?'ent':'std'); | |
| const ce=document.getElementById('catVal'); | |
| ce.textContent=obs.issue_category||'Not classified'; | |
| ce.style.color=obs.issue_category?'var(--black)':'#888'; | |
| const sc=obs.step_count||0; | |
| document.getElementById('stepVal').textContent=`${sc} / 10`; | |
| document.getElementById('scoreHint').textContent=getTerminalHint(obs); | |
| const pct=Math.min((sc/10)*100,100); | |
| document.getElementById('progFill').style.width=pct+'%'; | |
| document.getElementById('progPct').textContent=Math.round(pct)+'%'; | |
| const sent=(obs.user_sentiment||'Neutral').toLowerCase(); | |
| ['happy','neutral','frustrated','angry'].forEach(s=>{ | |
| const el=document.getElementById('sp-'+s); | |
| el.className='sp'; | |
| if(sent===s)el.classList.add('a-'+s); | |
| }); | |
| if(obs.knowledge_base_result){ | |
| document.getElementById('kbResult').textContent=obs.knowledge_base_result; | |
| document.getElementById('kbResult').classList.add('on'); | |
| document.getElementById('kbPh').style.display='none'; | |
| } | |
| } | |
| function renderNewLines(history){ | |
| if(!Array.isArray(history))return; | |
| const newLines=history.slice(lastHistLen); | |
| lastHistLen=history.length; | |
| newLines.forEach(line=>{ | |
| if(typeof line!=='string')return; | |
| if(line.startsWith('Customer:')) addMsg('Customer',line.replace('Customer: ',''),'cu'); | |
| else if(line.startsWith('Agent:')) addMsg('Agent',line.replace('Agent: ',''),'ag'); | |
| else addMsg('System',line,'sy'); | |
| }); | |
| } | |
| async function resetEnv(){ | |
| document.getElementById('historyLog').innerHTML=''; | |
| document.getElementById('kbResult').classList.remove('on'); | |
| document.getElementById('kbPh').style.display=''; | |
| totalRew=0;stepNum=0;lastHistLen=0; | |
| document.getElementById('rtotal').innerHTML='+0.00'; | |
| document.getElementById('terminalScore').textContent='—'; | |
| document.getElementById('convoWrap').innerHTML=`<div class="convo-empty" id="convoEmpty"> | |
| <div class="ce-emoji">⏳</div> | |
| <div class="ce-title">Loading…</div></div>`; | |
| try{ | |
| const res=await fetch('/reset',{method:'POST'}); | |
| const data=await res.json(); | |
| const obs=data.observation||data; | |
| document.getElementById('convoWrap').innerHTML=''; | |
| updateStatus(obs,false); | |
| lastHistLen=0; | |
| renderNewLines(obs.conversation_history||[]); | |
| // Log initial reset state | |
| logHistory({command: 'System Reset - New Episode Started'}, {value: 0, reason: 'New episode started'}, false); | |
| stepNum=0; | |
| showToast('Episode started — '+(obs.ticket_id||''),'ok'); | |
| document.getElementById('playground').scrollIntoView({behavior:'smooth'}); | |
| }catch(e){showToast('Reset failed: '+e.message,'err');} | |
| } | |
| async function refreshState(){ | |
| try{ | |
| const res=await fetch('/state'); | |
| if(!res.ok){showToast('No active state. Start a new episode first.','err');return;} | |
| const data=await res.json(); | |
| const payload=data&&typeof data==='object'?data:{}; | |
| const obs=payload.observation||payload; | |
| updateStatus(obs,Boolean(obs&&obs.is_resolved)); | |
| showToast('State refreshed'); | |
| }catch(e){showToast('Error: '+e.message,'err');} | |
| } | |
| async function submitAction(){ | |
| const a=document.getElementById('actionType').value; | |
| const payload={action_type:a}; | |
| const categoryGuess=(document.getElementById('catGuess').value||'').trim(); | |
| const searchQuery=(document.getElementById('searchQ').value||'').trim(); | |
| const messageToCustomer=(document.getElementById('msgInput').value||'').trim(); | |
| if(a==='classify_issue') payload.category_guess=categoryGuess; | |
| if(a==='search_kb'){ | |
| if(!searchQuery){showToast('search_kb needs a query.','err');return;} | |
| payload.search_query=searchQuery; | |
| } | |
| if(a==='ask_clarifying_question'||a==='resolve_ticket'){ | |
| if(!messageToCustomer){showToast(a+' needs a message.','err');return;} | |
| payload.message_to_customer=messageToCustomer; | |
| } | |
| const btn=document.getElementById('submitBtn'); | |
| btn.disabled=true;btn.textContent='⟳ Processing…'; | |
| try{ | |
| const res=await fetch('/step',{ | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body:JSON.stringify(payload) | |
| }); | |
| if(!res.ok){ | |
| const err=await res.json(); | |
| showToast(err.detail||'Error','err'); | |
| return; | |
| } | |
| const data=await res.json(); | |
| const obs=data.observation||{}; | |
| updateStatus(obs,data.done); | |
| renderNewLines(obs.conversation_history||[]); | |
| // Using the custom logHistory format requested | |
| logHistory(payload, data.reward, data.done); | |
| if(data.done){ | |
| const v=typeof data.reward==='object'?data.reward.value:(data.reward||0); | |
| const terminalScore=parseFloat(v)||0; | |
| document.getElementById('terminalScore').textContent=terminalScore.toFixed(2); | |
| document.getElementById('terminalScore').style.color=terminalScore>=0.9?'#16a34a':(terminalScore>=0.5?'#f59e0b':'#ef4444'); | |
| document.getElementById('scoreHint').textContent=terminalScore>=0.9 | |
| ? 'Great run: terminal success score achieved.' | |
| : 'Episode ended without terminal success. Finish with the right terminal action before step 10.'; | |
| addMsg('System',`Episode finished. Reward: ${parseFloat(v).toFixed(2)}`,'sy'); | |
| showToast(v>=0.5?'🎉 Episode complete!':'❌ Episode ended — review policy',''); | |
| } | |
| }catch(e){showToast('Network error: '+e.message,'err');} | |
| finally{btn.disabled=false;btn.textContent='→ Submit Action';} | |
| } | |
| function copyCode(btn,id){ | |
| const el=document.getElementById(id); | |
| const text=el.innerText; | |
| navigator.clipboard.writeText(text).then(()=>{ | |
| btn.textContent='Copied!';btn.classList.add('done'); | |
| setTimeout(()=>{btn.textContent='Copy';btn.classList.remove('done');},2000); | |
| }); | |
| } | |
| toggleFields(); | |
| </script> | |
| </body> | |
| </html>""" |