vivekvish2004 commited on
Commit
38752d2
Β·
1 Parent(s): 62fbd09

feat: replace broken Next.js frontend with clean static HTML dashboard

Browse files

- Add static/index.html: fully working dark-mode dashboard
- All API bugs fixed (correct response field parsing)
- /reset, /step, /state, /grader, /tasks, /baseline connected
- Per-task grader buttons in Tasks panel
- Live queue visualization, SLA progress bar, reward tracking
- Activity log with reward chips and timestamps
- Ctrl+Enter shortcut to execute actions
- Toast notifications for all actions
- Simplify Dockerfile: remove 2-stage Node.js build entirely
- Faster Docker builds (no npm install)
- Serve static/index.html directly via FastAPI StaticFiles
- Update .dockerignore: exclude frontend/, include static/

Files changed (3) hide show
  1. .dockerignore +5 -5
  2. Dockerfile +6 -19
  3. static/index.html +694 -0
.dockerignore CHANGED
@@ -7,11 +7,8 @@ venv/
7
  env/
8
  .env
9
 
10
- # Node/Frontend
11
- frontend/node_modules/
12
- frontend/.next/
13
- frontend/out/
14
- static/
15
 
16
  # Git
17
  .git/
@@ -20,3 +17,6 @@ static/
20
  # OS
21
  .DS_Store
22
  Thumbs.db
 
 
 
 
7
  env/
8
  .env
9
 
10
+ # Frontend (Next.js source β€” not needed in Docker image)
11
+ frontend/
 
 
 
12
 
13
  # Git
14
  .git/
 
17
  # OS
18
  .DS_Store
19
  Thumbs.db
20
+
21
+ # Dev artifacts
22
+ *.egg-info/
Dockerfile CHANGED
@@ -1,38 +1,25 @@
1
- # --- Stage 1: Build Frontend ---
2
- FROM node:18-slim AS frontend-builder
3
- WORKDIR /build
4
 
5
- # Cache dependencies
6
- COPY frontend/package*.json ./
7
- RUN npm install
8
-
9
- # Build frontend
10
- COPY frontend/ ./
11
- RUN npm run build
12
-
13
- # --- Stage 2: Final Image ---
14
  FROM python:3.10-slim
15
 
16
- # Create user for Hugging Face compatibility
17
  RUN useradd -m -u 1000 user
18
  USER user
 
19
  ENV HOME=/home/user \
20
  PATH=/home/user/.local/bin:$PATH \
21
  PYTHONUNBUFFERED=1
22
 
23
  WORKDIR $HOME/app
24
 
25
- # Cache backend dependencies
26
  COPY --chown=user requirements.txt .
27
  RUN pip install --no-cache-dir --user -r requirements.txt
28
 
29
- # Copy essential project files
30
  COPY --chown=user . $HOME/app/
31
 
32
- # Copy built frontend to the static directory
33
- COPY --chown=user --from=frontend-builder /build/out $HOME/app/static
34
-
35
  EXPOSE 7860
36
 
37
- # Use python -m uvicorn for direct execution
38
  CMD ["python3", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860", "--log-level", "info"]
 
1
+ # Single-stage Python image β€” no Node.js build needed
2
+ # Frontend is served as a pre-built static/index.html
 
3
 
 
 
 
 
 
 
 
 
 
4
  FROM python:3.10-slim
5
 
6
+ # Create non-root user (required for Hugging Face Spaces)
7
  RUN useradd -m -u 1000 user
8
  USER user
9
+
10
  ENV HOME=/home/user \
11
  PATH=/home/user/.local/bin:$PATH \
12
  PYTHONUNBUFFERED=1
13
 
14
  WORKDIR $HOME/app
15
 
16
+ # Install Python dependencies first (better layer caching)
17
  COPY --chown=user requirements.txt .
18
  RUN pip install --no-cache-dir --user -r requirements.txt
19
 
20
+ # Copy entire project
21
  COPY --chown=user . $HOME/app/
22
 
 
 
 
23
  EXPOSE 7860
24
 
 
25
  CMD ["python3", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860", "--log-level", "info"]
static/index.html ADDED
@@ -0,0 +1,694 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>OpenEnv Β· Customer Support AI</title>
7
+ <meta name="description" content="Enterprise AI Customer Support simulation environment β€” classify, prioritize, respond and resolve tickets using OpenEnv." />
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet" />
11
+ <style>
12
+ /* ── Reset & Tokens ──────────────────────────────────────────── */
13
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
14
+
15
+ :root {
16
+ --bg: #0a0f1e;
17
+ --surface: #111827;
18
+ --surface2: #1a2235;
19
+ --border: #1e2d45;
20
+ --primary: #6366f1;
21
+ --primary-d: #4f46e5;
22
+ --accent: #06b6d4;
23
+ --success: #10b981;
24
+ --warning: #f59e0b;
25
+ --danger: #ef4444;
26
+ --text: #f1f5f9;
27
+ --muted: #64748b;
28
+ --card-glow: rgba(99,102,241,0.06);
29
+ }
30
+
31
+ html, body {
32
+ background: var(--bg);
33
+ color: var(--text);
34
+ font-family: 'Inter', sans-serif;
35
+ min-height: 100vh;
36
+ -webkit-font-smoothing: antialiased;
37
+ }
38
+
39
+ /* ── Layout ──────────────────────────────────────────────────── */
40
+ .app { max-width: 1400px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
41
+
42
+ .grid { display: grid; grid-template-columns: 1fr 380px; gap: 1.5rem; }
43
+ @media (max-width: 960px) { .grid { grid-template-columns: 1fr; } }
44
+
45
+ /* ── Cards ───────────────────────────────────────────────────── */
46
+ .card {
47
+ background: var(--surface);
48
+ border: 1px solid var(--border);
49
+ border-radius: 16px;
50
+ padding: 1.5rem;
51
+ transition: border-color .25s, box-shadow .25s;
52
+ }
53
+ .card:hover { border-color: rgba(99,102,241,0.4); box-shadow: 0 0 30px var(--card-glow); }
54
+ .card-title { font-size: .7rem; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); margin-bottom: 1rem; }
55
+
56
+ /* ── Header ──────────────────────────────────────────────────── */
57
+ header {
58
+ display: flex; justify-content: space-between; align-items: center;
59
+ padding: 1.25rem 0 1.75rem;
60
+ border-bottom: 1px solid var(--border);
61
+ margin-bottom: 1.75rem;
62
+ }
63
+ .logo { display: flex; align-items: center; gap: .75rem; }
64
+ .logo-icon {
65
+ width: 42px; height: 42px; border-radius: 12px;
66
+ background: linear-gradient(135deg, var(--primary), var(--accent));
67
+ display: grid; place-items: center; font-size: 1.2rem;
68
+ }
69
+ .logo h1 { font-size: 1.5rem; font-weight: 800; letter-spacing: -.03em; }
70
+ .logo span { color: var(--primary); }
71
+ .logo sub { display: block; font-size: .75rem; color: var(--muted); font-weight: 500; margin-top: .1rem; }
72
+
73
+ .header-right { display: flex; align-items: center; gap: .75rem; }
74
+ .status-pill {
75
+ display: flex; align-items: center; gap: .5rem;
76
+ background: var(--surface2); border: 1px solid var(--border);
77
+ border-radius: 999px; padding: .4rem 1rem; font-size: .8rem; font-weight: 600;
78
+ }
79
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--success); animation: pulse-dot 1.8s infinite; }
80
+ @keyframes pulse-dot { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.5;transform:scale(.8)} }
81
+
82
+ /* ── Buttons ─────────────────────────────────────────────────── */
83
+ .btn {
84
+ display: inline-flex; align-items: center; gap: .5rem;
85
+ padding: .6rem 1.25rem; border-radius: 10px; font-size: .85rem; font-weight: 700;
86
+ cursor: pointer; border: none; transition: all .2s; white-space: nowrap;
87
+ }
88
+ .btn-primary { background: var(--primary); color: #fff; }
89
+ .btn-primary:hover { background: var(--primary-d); transform: translateY(-1px); box-shadow: 0 8px 20px rgba(99,102,241,.35); }
90
+ .btn-ghost { background: transparent; border: 1px solid var(--border); color: var(--text); }
91
+ .btn-ghost:hover { background: var(--surface2); border-color: var(--muted); }
92
+ .btn-sm { padding: .4rem .85rem; font-size: .78rem; border-radius: 8px; }
93
+ .btn-danger { background: var(--danger); color: #fff; }
94
+ .btn:disabled { opacity: .45; cursor: not-allowed; pointer-events: none; }
95
+
96
+ /* ── Ticket Card ─────────────────────────────────────────────── */
97
+ .ticket-text {
98
+ font-size: 1.2rem; font-weight: 600; line-height: 1.6;
99
+ color: var(--text); border-left: 3px solid var(--primary);
100
+ padding-left: 1rem; margin: .5rem 0 1.25rem;
101
+ }
102
+
103
+ .meta-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: .75rem; }
104
+ @media (max-width: 600px) { .meta-grid { grid-template-columns: repeat(2,1fr); } }
105
+
106
+ .meta-item {
107
+ background: var(--surface2); border: 1px solid var(--border);
108
+ border-radius: 12px; padding: .75rem;
109
+ display: flex; flex-direction: column; gap: .25rem;
110
+ }
111
+ .meta-label { font-size: .6rem; font-weight: 800; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); }
112
+ .meta-value { font-size: .9rem; font-weight: 700; }
113
+
114
+ /* ── SLA Bar ─────────────────────────────────────────────────── */
115
+ .sla-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: .4rem; }
116
+ .progress { height: 6px; border-radius: 999px; background: var(--border); overflow: hidden; }
117
+ .progress-bar { height: 100%; border-radius: 999px; transition: width .5s ease; background: var(--primary); }
118
+ .progress-bar.warn { background: var(--danger); }
119
+
120
+ /* ── Badges ──────────────────────────────────────────────────── */
121
+ .badge {
122
+ display: inline-block; padding: .25rem .65rem; border-radius: 6px;
123
+ font-size: .72rem; font-weight: 800; text-transform: uppercase; letter-spacing: .05em;
124
+ }
125
+ .badge-angry { background: rgba(239,68,68,.15); color: #fca5a5; }
126
+ .badge-neutral { background: rgba(100,116,139,.15);color: var(--muted); }
127
+ .badge-happy { background: rgba(16,185,129,.15); color: #6ee7b7; }
128
+ .badge-panicked { background: rgba(239,68,68,.2); color: #f87171; border:1px dashed #f87171; }
129
+ .badge-concerned{ background: rgba(245,158,11,.15); color: #fcd34d; }
130
+ .badge-curious { background: rgba(139,92,246,.15); color: #c4b5fd; }
131
+ .badge-low { background: rgba(16,185,129,.15); color: #6ee7b7; }
132
+ .badge-medium { background: rgba(245,158,11,.15); color: #fcd34d; }
133
+ .badge-high { background: rgba(239,68,68,.15); color: #fca5a5; }
134
+ .badge-open { background: rgba(99,102,241,.15); color: #a5b4fc; }
135
+ .badge-closed { background: rgba(16,185,129,.15); color: #6ee7b7; }
136
+ .badge-pending { background: rgba(100,116,139,.15);color: var(--muted); }
137
+
138
+ /* ── Action Panel ────────────────────────────────────────────── */
139
+ .action-chips { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: 1rem; }
140
+
141
+ .code-area {
142
+ width: 100%; background: #0d1526; border: 1px solid var(--border);
143
+ border-radius: 12px; padding: 1rem; font-family: 'JetBrains Mono', monospace;
144
+ font-size: .85rem; color: #a5b4fc; resize: vertical; min-height: 120px;
145
+ outline: none; transition: border-color .2s;
146
+ line-height: 1.6;
147
+ }
148
+ .code-area:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(99,102,241,.15); }
149
+
150
+ .action-bar { display: flex; gap: .75rem; margin-top: 1rem; }
151
+
152
+ /* ── Score card ──────────────────────────────────────────────── */
153
+ .score-card {
154
+ background: linear-gradient(135deg, rgba(16,185,129,.12), rgba(6,182,212,.08));
155
+ border: 1px solid rgba(16,185,129,.3); border-radius: 16px; padding: 1.5rem;
156
+ display: flex; justify-content: space-between; align-items: center;
157
+ animation: fadein .4s ease;
158
+ }
159
+ .score-num { font-size: 3rem; font-weight: 900; color: var(--success); letter-spacing: -.05em; }
160
+
161
+ /* ── Queue ───────────────────────────────────────────────────── */
162
+ .queue-item {
163
+ background: var(--surface2); border: 1px solid var(--border);
164
+ border-radius: 12px; padding: 1rem; font-size: .85rem;
165
+ transition: border-color .2s;
166
+ }
167
+ .queue-item.active { border-color: var(--primary); background: rgba(99,102,241,.07); }
168
+ .queue-item-top { display: flex; justify-content: space-between; margin-bottom: .4rem; font-size: .65rem; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; }
169
+
170
+ /* ── Logs ────────────────────────────────────────────────────── */
171
+ .log-feed { display: flex; flex-direction: column; gap: .5rem; overflow-y: auto; max-height: 300px; padding-right: .25rem; }
172
+ .log-feed::-webkit-scrollbar { width: 4px; }
173
+ .log-feed::-webkit-scrollbar-track { background: transparent; }
174
+ .log-feed::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
175
+
176
+ .log-item {
177
+ border-radius: 10px; padding: .75rem 1rem;
178
+ font-size: .82rem; animation: fadein .3s ease;
179
+ border-left: 3px solid transparent;
180
+ }
181
+ .log-item.system { background: rgba(6,182,212,.08); border-left-color: var(--accent); }
182
+ .log-item.success { background: rgba(16,185,129,.08); border-left-color: var(--success); }
183
+ .log-item.error { background: rgba(239,68,68,.08); border-left-color: var(--danger); }
184
+ .log-item.action { background: rgba(99,102,241,.08); border-left-color: var(--primary); }
185
+ .log-meta { font-size: .62rem; color: var(--muted); font-weight: 700; text-transform: uppercase; letter-spacing: .08em; margin-bottom: .2rem; display: flex; justify-content: space-between; }
186
+ .log-text { font-weight: 600; color: var(--text); }
187
+ .log-sub { font-size: .75rem; color: var(--muted); margin-top: .2rem; }
188
+
189
+ /* ── Tasks panel ─────────────────────────────────────────────── */
190
+ .task-row {
191
+ display: flex; justify-content: space-between; align-items: center;
192
+ padding: .65rem .85rem; border-radius: 10px; background: var(--surface2);
193
+ border: 1px solid var(--border); font-size: .82rem; cursor: pointer;
194
+ transition: all .2s;
195
+ }
196
+ .task-row:hover { border-color: var(--primary); }
197
+ .task-row .diff { font-size: .65rem; font-weight: 800; text-transform: uppercase; }
198
+ .diff-easy { color: var(--success); }
199
+ .diff-medium { color: var(--warning); }
200
+ .diff-hard { color: var(--danger); }
201
+
202
+ /* ── Toast ───────────────────────────────────────────────────── */
203
+ .toast {
204
+ position: fixed; top: 1.25rem; right: 1.25rem; z-index: 9999;
205
+ padding: .85rem 1.5rem; border-radius: 12px; font-weight: 700; font-size: .9rem;
206
+ display: flex; align-items: center; gap: .6rem;
207
+ box-shadow: 0 20px 40px rgba(0,0,0,.4);
208
+ animation: toastIn .3s ease; pointer-events: none;
209
+ }
210
+ .toast.success { background: var(--success); color: #fff; }
211
+ .toast.error { background: var(--danger); color: #fff; }
212
+ @keyframes toastIn { from{opacity:0;transform:translateX(24px)} to{opacity:1;transform:translateX(0)} }
213
+
214
+ /* ── Empty / Complete states ─────────────────────────────────── */
215
+ .empty { text-align: center; padding: 4rem 2rem; color: var(--muted); }
216
+ .empty .icon { font-size: 3rem; margin-bottom: .75rem; }
217
+ .empty h2 { font-size: 1.25rem; color: var(--text); margin-bottom: .5rem; }
218
+
219
+ /* ── Misc ────────────────────────────────────────────────────── */
220
+ @keyframes fadein { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:translateY(0)} }
221
+ .sep { height: 1px; background: var(--border); margin: 1rem 0; }
222
+ .flex-between { display: flex; justify-content: space-between; align-items: center; }
223
+ .gap-sm { gap: .5rem; }
224
+ .flex { display: flex; align-items: center; }
225
+ .spin { animation: spin .7s linear infinite; }
226
+ @keyframes spin { to { transform: rotate(360deg); } }
227
+
228
+ .reward-chip {
229
+ font-family: 'JetBrains Mono', monospace; font-size: .8rem; font-weight: 600;
230
+ padding: .25rem .6rem; border-radius: 6px;
231
+ }
232
+ .reward-pos { background: rgba(16,185,129,.15); color: #6ee7b7; }
233
+ .reward-neg { background: rgba(239,68,68,.15); color: #fca5a5; }
234
+ </style>
235
+ </head>
236
+ <body>
237
+
238
+ <div class="app">
239
+ <!-- Header -->
240
+ <header>
241
+ <div class="logo">
242
+ <div class="logo-icon">πŸ€–</div>
243
+ <div>
244
+ <h1>OpenEnv <span>Support</span></h1>
245
+ <sub>Enterprise AI Decision Environment</sub>
246
+ </div>
247
+ </div>
248
+ <div class="header-right">
249
+ <div class="status-pill" id="connection-status">
250
+ <span class="status-dot" id="dot"></span>
251
+ <span id="status-text">Connecting...</span>
252
+ </div>
253
+ <button class="btn btn-ghost btn-sm" onclick="resetEnv()">πŸ”„ New Session</button>
254
+ </div>
255
+ </header>
256
+
257
+ <div class="grid" id="main-grid">
258
+ <!-- ─────────────────── LEFT COLUMN ─────────────────── -->
259
+ <div style="display:flex;flex-direction:column;gap:1.25rem;">
260
+
261
+ <!-- Current Ticket -->
262
+ <div class="card" id="ticket-card">
263
+ <div class="card-title">🎫 Current Ticket</div>
264
+ <div id="ticket-body">
265
+ <div class="empty"><div class="icon">⏳</div><p>Loading environment...</p></div>
266
+ </div>
267
+ </div>
268
+
269
+ <!-- Action Center -->
270
+ <div class="card">
271
+ <div class="flex-between" style="margin-bottom:1rem;">
272
+ <div class="card-title" style="margin:0;">⚑ Action Center</div>
273
+ <div class="action-chips">
274
+ <button class="btn btn-ghost btn-sm" onclick="fillAction('classify_ticket',{classification:'refund'})">🏷️ Classify</button>
275
+ <button class="btn btn-ghost btn-sm" onclick="fillAction('assign_priority',{priority:'high'})">⬆️ Priority</button>
276
+ <button class="btn btn-ghost btn-sm" onclick="fillAction('generate_response',{response:'I apologize for the inconvenience. We are working to resolve this immediately and will keep you updated.'})">✍️ Reply</button>
277
+ <button class="btn btn-ghost btn-sm" onclick="fillAction('resolve',{})">βœ… Resolve</button>
278
+ <button class="btn btn-ghost btn-sm" onclick="fillAction('escalate',{})">🚨 Escalate</button>
279
+ </div>
280
+ </div>
281
+
282
+ <textarea id="action-input" class="code-area" rows="5">{
283
+ "action_type": "classify_ticket",
284
+ "payload": { "classification": "refund" }
285
+ }</textarea>
286
+
287
+ <div class="action-bar">
288
+ <button class="btn btn-primary" id="exec-btn" onclick="sendAction()" style="flex:2;">
289
+ <span id="exec-icon">β–Ά</span> Execute Action
290
+ </button>
291
+ <button class="btn btn-ghost" onclick="gradeTask('task_hard_1')" style="flex:1;">πŸ“Š Grade Model</button>
292
+ <button class="btn btn-ghost" onclick="runBaseline()" style="flex:1;">πŸ€– Baseline</button>
293
+ </div>
294
+ </div>
295
+
296
+ <!-- Score card (hidden until grade) -->
297
+ <div id="score-card" style="display:none;"></div>
298
+
299
+ </div>
300
+
301
+ <!-- ─────────────────── RIGHT COLUMN ─────────────────── -->
302
+ <div style="display:flex;flex-direction:column;gap:1.25rem;">
303
+
304
+ <!-- Queue -->
305
+ <div class="card">
306
+ <div class="flex-between" style="margin-bottom:1rem;">
307
+ <div class="card-title" style="margin:0;">πŸ“‹ Ticket Queue</div>
308
+ <span class="badge badge-open" id="queue-count">0 pending</span>
309
+ </div>
310
+ <div class="sep" style="margin-top:0;"></div>
311
+ <!-- Progress -->
312
+ <div style="margin-bottom:1rem;">
313
+ <div class="sla-row">
314
+ <span style="font-size:.7rem;color:var(--muted);font-weight:700;">SESSION PROGRESS</span>
315
+ <span style="font-size:.7rem;color:var(--muted);font-weight:700;" id="progress-pct">0%</span>
316
+ </div>
317
+ <div class="progress">
318
+ <div class="progress-bar" id="progress-bar" style="width:0%"></div>
319
+ </div>
320
+ </div>
321
+ <div id="queue-list" style="display:flex;flex-direction:column;gap:.6rem;">
322
+ <div class="empty" style="padding:2rem;"><div class="icon" style="font-size:2rem;">🎯</div><p>Load a session to see tickets</p></div>
323
+ </div>
324
+ </div>
325
+
326
+ <!-- Tasks & Graders -->
327
+ <div class="card">
328
+ <div class="card-title">🎯 Tasks & Graders</div>
329
+ <div id="tasks-list" style="display:flex;flex-direction:column;gap:.5rem;">
330
+ <div style="font-size:.8rem;color:var(--muted);">Loading tasks...</div>
331
+ </div>
332
+ </div>
333
+
334
+ <!-- Activity Log -->
335
+ <div class="card">
336
+ <div class="flex-between" style="margin-bottom:1rem;">
337
+ <div class="card-title" style="margin:0;">πŸ“œ Activity Log</div>
338
+ <button class="btn btn-ghost btn-sm" onclick="clearLogs()">Clear</button>
339
+ </div>
340
+ <div class="log-feed" id="log-feed">
341
+ <div class="log-item system"><div class="log-text">Waiting for session...</div></div>
342
+ </div>
343
+ </div>
344
+
345
+ </div>
346
+ </div>
347
+ </div>
348
+
349
+ <!-- Toast -->
350
+ <div class="toast" id="toast" style="display:none;"></div>
351
+
352
+ <script>
353
+ // ─── Config ──────────────────────────────────────────────────────────────────
354
+ const API = window.location.origin.includes('localhost') || window.location.origin.includes('127.0.0.1')
355
+ ? 'http://127.0.0.1:7860'
356
+ : window.location.origin;
357
+
358
+ let currentState = null;
359
+ let toastTimer = null;
360
+
361
+ // ─── Toast ────────────────────────────────────────────────────────────────────
362
+ function showToast(msg, type = 'success') {
363
+ const el = document.getElementById('toast');
364
+ el.textContent = (type === 'success' ? 'βœ… ' : '❌ ') + msg;
365
+ el.className = `toast ${type}`;
366
+ el.style.display = 'flex';
367
+ clearTimeout(toastTimer);
368
+ toastTimer = setTimeout(() => el.style.display = 'none', 4000);
369
+ }
370
+
371
+ // ─── Log ─────────────────────────────────────────────────────────────────────
372
+ function addLog(text, type = 'system', sub = '') {
373
+ const feed = document.getElementById('log-feed');
374
+ const ts = new Date().toLocaleTimeString();
375
+ const el = document.createElement('div');
376
+ el.className = `log-item ${type}`;
377
+ el.innerHTML = `
378
+ <div class="log-meta"><span>${type.toUpperCase()}</span><span>${ts}</span></div>
379
+ <div class="log-text">${text}</div>
380
+ ${sub ? `<div class="log-sub">${sub}</div>` : ''}
381
+ `;
382
+ feed.prepend(el);
383
+ }
384
+
385
+ function clearLogs() {
386
+ document.getElementById('log-feed').innerHTML = '';
387
+ }
388
+
389
+ // ─── Connection status ────────────────────────────────────────────────────────
390
+ function setConnected(ok) {
391
+ document.getElementById('dot').style.background = ok ? 'var(--success)' : 'var(--danger)';
392
+ document.getElementById('status-text').textContent = ok ? 'Connected' : 'Disconnected';
393
+ }
394
+
395
+ // ─── Fill action textarea ─────────────────────────────────────────────────────
396
+ function fillAction(type, payload) {
397
+ document.getElementById('action-input').value = JSON.stringify({ action_type: type, payload }, null, 2);
398
+ }
399
+
400
+ // ─── Render ticket ────────────────────────────────────────────────────────────
401
+ function renderTicket(state) {
402
+ const body = document.getElementById('ticket-body');
403
+
404
+ if (!state) {
405
+ body.innerHTML = `<div class="empty"><div class="icon">⏳</div><p>Waiting for backend...</p></div>`;
406
+ return;
407
+ }
408
+
409
+ if (state.status === 'session_complete') {
410
+ const resolved = state.resolved ?? state.info?.resolved ?? 0;
411
+ const reward = (state.total_reward ?? state.info?.total_reward ?? 0).toFixed(2);
412
+ body.innerHTML = `
413
+ <div class="empty">
414
+ <div class="icon">πŸŽ‰</div>
415
+ <h2>Session Complete!</h2>
416
+ <div style="display:flex;justify-content:center;gap:3rem;margin-top:1.5rem;">
417
+ <div><div style="color:var(--muted);font-size:.75rem;font-weight:700;">RESOLVED</div>
418
+ <div style="font-size:2.5rem;font-weight:900;">${resolved}</div></div>
419
+ <div><div style="color:var(--muted);font-size:.75rem;font-weight:700;">TOTAL REWARD</div>
420
+ <div style="font-size:2.5rem;font-weight:900;color:var(--primary);">${reward}</div></div>
421
+ </div>
422
+ <button class="btn btn-primary" style="margin-top:2rem;" onclick="resetEnv()">πŸ”„ Start New Session</button>
423
+ </div>`;
424
+ return;
425
+ }
426
+
427
+ const cls = state.classification || 'β€”';
428
+ const pri = state.priority || null;
429
+ const slaW = state.sla_warning;
430
+ const slaVal = Math.min(100, ((state.steps_taken || 0) / (state.sla_limit || 10)) * 100).toFixed(0);
431
+
432
+ body.innerHTML = `
433
+ <p class="ticket-text">"${state.ticket_text || 'β€”'}"</p>
434
+
435
+ <div style="margin-bottom:1.25rem;">
436
+ <div class="sla-row">
437
+ <span style="font-size:.68rem;color:${slaW?'var(--danger)':'var(--muted)'};font-weight:800;">SLA HEALTH β€” Step ${state.steps_taken||0} of ${state.sla_limit||'?'}</span>
438
+ <span style="font-size:.68rem;font-weight:700;color:${slaW?'var(--danger)':'var(--muted)'};">${slaVal}%</span>
439
+ </div>
440
+ <div class="progress">
441
+ <div class="progress-bar ${slaW?'warn':''}" style="width:${slaVal}%"></div>
442
+ </div>
443
+ </div>
444
+
445
+ <div class="meta-grid">
446
+ <div class="meta-item">
447
+ <div class="meta-label">Sentiment</div>
448
+ <div class="meta-value"><span class="badge badge-${state.sentiment}">${state.sentiment||'β€”'}</span></div>
449
+ </div>
450
+ <div class="meta-item">
451
+ <div class="meta-label">Priority</div>
452
+ <div class="meta-value">${pri ? `<span class="badge badge-${pri}">${pri}</span>` : `<span class="badge badge-pending">Pending</span>`}</div>
453
+ </div>
454
+ <div class="meta-item">
455
+ <div class="meta-label">Status</div>
456
+ <div class="meta-value"><span class="badge badge-${state.status||'open'}">${state.status||'open'}</span></div>
457
+ </div>
458
+ <div class="meta-item">
459
+ <div class="meta-label">Classification</div>
460
+ <div class="meta-value" style="font-size:.8rem;word-break:break-all;">${cls}</div>
461
+ </div>
462
+ </div>
463
+
464
+ ${state.response ? `
465
+ <div style="margin-top:1rem;background:var(--surface2);border-radius:10px;padding:.9rem;border:1px solid var(--border);">
466
+ <div style="font-size:.65rem;font-weight:800;color:var(--muted);text-transform:uppercase;margin-bottom:.4rem;">Draft Response</div>
467
+ <div style="font-size:.85rem;color:var(--text);">"${state.response}"</div>
468
+ </div>` : ''}
469
+
470
+ <div style="margin-top:1.25rem;display:flex;justify-content:space-between;font-size:.78rem;color:var(--muted);">
471
+ <span>Queue: <strong style="color:var(--text)">${state.queue_size||0}</strong> remaining</span>
472
+ <span>Total Reward: <strong style="color:var(--primary)">${(state.total_reward||0).toFixed(2)}</strong></span>
473
+ </div>`;
474
+ }
475
+
476
+ // ─── Render queue ─────────────────────────────────────────────────────────────
477
+ function renderQueue(state) {
478
+ if (!state) return;
479
+ const queue = state.info?.queue || [];
480
+ const res = state.info?.resolved ?? state.resolved ?? 0;
481
+ const total = 3;
482
+ const pct = Math.min(100, Math.round((res / total) * 100));
483
+
484
+ document.getElementById('queue-count').textContent = `${queue.length} pending`;
485
+ document.getElementById('progress-pct').textContent = `${pct}%`;
486
+ document.getElementById('progress-bar').style.width = `${pct}%`;
487
+
488
+ const list = document.getElementById('queue-list');
489
+ if (!queue.length) {
490
+ list.innerHTML = `<div class="empty" style="padding:1.5rem;"><div class="icon" style="font-size:1.5rem;">🎯</div><p>Queue cleared</p></div>`;
491
+ return;
492
+ }
493
+ list.innerHTML = queue.map((q, i) => `
494
+ <div class="queue-item ${i===0?'active':''}">
495
+ <div class="queue-item-top">
496
+ <span style="color:${i===0?'var(--primary)':'var(--muted)'};">${i===0?'● ACTIVE NOW':`UPCOMING #${i+1}`}</span>
497
+ <span>Tier 1</span>
498
+ </div>
499
+ <div style="font-weight:600;">${q}</div>
500
+ </div>`).join('');
501
+ }
502
+
503
+ // ─── Render tasks ─────────────────────────────────────────────────────────────
504
+ function renderTasks(tasks) {
505
+ const el = document.getElementById('tasks-list');
506
+ el.innerHTML = tasks.map(t => `
507
+ <div class="task-row" onclick="gradeTask('${t.id}')">
508
+ <div>
509
+ <div style="font-weight:700;">${t.name}</div>
510
+ <div style="font-size:.72rem;color:var(--muted);margin-top:.15rem;">${t.id}</div>
511
+ </div>
512
+ <div style="display:flex;align-items:center;gap:.6rem;">
513
+ <span class="diff diff-${t.difficulty.toLowerCase()}">${t.difficulty}</span>
514
+ ${t.grader ? '<span class="badge badge-open">Grader βœ“</span>' : ''}
515
+ </div>
516
+ </div>`).join('');
517
+ }
518
+
519
+ // ─── API helpers ──────────────────────────────────────────────────────────────
520
+ async function fetchJSON(url, options = {}) {
521
+ const res = await fetch(API + url, options);
522
+ if (!res.ok) {
523
+ const err = await res.json().catch(() => ({ detail: 'Server error' }));
524
+ throw new Error(err.detail || `HTTP ${res.status}`);
525
+ }
526
+ return res.json();
527
+ }
528
+
529
+ function setLoading(busy) {
530
+ const btn = document.getElementById('exec-btn');
531
+ const icon = document.getElementById('exec-icon');
532
+ btn.disabled = busy;
533
+ icon.textContent = busy ? '⏳' : 'β–Ά';
534
+ }
535
+
536
+ // ─── Reset ────────────────────────────────────────────────────────────────────
537
+ async function resetEnv() {
538
+ setLoading(true);
539
+ document.getElementById('score-card').style.display = 'none';
540
+ try {
541
+ const data = await fetchJSON('/reset');
542
+ // /reset returns { observation, reward, done }
543
+ const obs = data.observation || data;
544
+ currentState = obs;
545
+ renderTicket(obs);
546
+ renderQueue({ info: obs.info, resolved: obs.resolved });
547
+ addLog('Session reset β€” new ticket queue loaded', 'system');
548
+ showToast('New session started!', 'success');
549
+ setConnected(true);
550
+ } catch (e) {
551
+ showToast(e.message, 'error');
552
+ addLog(e.message, 'error');
553
+ setConnected(false);
554
+ }
555
+ setLoading(false);
556
+ }
557
+
558
+ // ─── Fetch state ─────────────────────────────────────────────────────────────
559
+ async function fetchState() {
560
+ try {
561
+ const data = await fetchJSON('/state');
562
+ // /state returns { observation }
563
+ const obs = data.observation || data;
564
+ if (!currentState && obs.ticket_text) {
565
+ currentState = obs;
566
+ renderTicket(obs);
567
+ renderQueue({ info: obs.info, resolved: obs.resolved });
568
+ }
569
+ setConnected(true);
570
+ } catch (e) {
571
+ setConnected(false);
572
+ }
573
+ }
574
+
575
+ // ─── Send action ──────────────────────────────────────────────────────────────
576
+ async function sendAction() {
577
+ if (!currentState || currentState.status === 'session_complete') return;
578
+ let actionObj;
579
+ try {
580
+ actionObj = JSON.parse(document.getElementById('action-input').value.trim());
581
+ } catch {
582
+ showToast('Invalid JSON β€” fix your action payload', 'error');
583
+ return;
584
+ }
585
+
586
+ setLoading(true);
587
+ try {
588
+ const data = await fetchJSON('/step', {
589
+ method: 'POST',
590
+ headers: { 'Content-Type': 'application/json' },
591
+ body: JSON.stringify(actionObj)
592
+ });
593
+
594
+ // response: { observation, reward (float), done, info }
595
+ const obs = data.observation;
596
+ const reward = parseFloat(data.reward ?? 0);
597
+ const info = data.info || {};
598
+
599
+ currentState = obs;
600
+ renderTicket(obs);
601
+ renderQueue({ info: obs.info, resolved: obs.resolved });
602
+
603
+ const rClass = reward >= 0 ? 'reward-pos' : 'reward-neg';
604
+ const rSign = reward >= 0 ? '+' : '';
605
+ addLog(
606
+ `${actionObj.action_type.replace('_',' ')}`,
607
+ reward >= 0 ? 'success' : 'error',
608
+ `${info.message || ''} &nbsp; <span class="reward-chip ${rClass}">${rSign}${reward.toFixed(3)}</span>`
609
+ );
610
+
611
+ if (obs.status === 'session_complete') {
612
+ showToast('Session complete! πŸŽ‰', 'success');
613
+ } else {
614
+ showToast(`Action executed β€” reward ${rSign}${reward.toFixed(3)}`, reward >= 0 ? 'success' : 'error');
615
+ }
616
+ } catch (e) {
617
+ showToast(e.message, 'error');
618
+ addLog(e.message, 'error');
619
+ }
620
+ setLoading(false);
621
+ }
622
+
623
+ // ─── Grade task ───────────────────────────────────────────────────────────────
624
+ async function gradeTask(taskId) {
625
+ try {
626
+ const data = await fetchJSON(`/grader?task_id=${taskId}`);
627
+ const score = parseFloat(data.score ?? 0);
628
+ const pct = (score * 100).toFixed(1);
629
+
630
+ document.getElementById('score-card').style.display = 'block';
631
+ document.getElementById('score-card').innerHTML = `
632
+ <div class="score-card">
633
+ <div>
634
+ <div style="font-size:.7rem;font-weight:800;color:var(--success);text-transform:uppercase;letter-spacing:.1em;margin-bottom:.5rem;">Benchmark Result</div>
635
+ <div style="font-size:1.1rem;font-weight:700;">${data.task_id||taskId}</div>
636
+ <div style="font-size:.8rem;color:var(--muted);margin-top:.25rem;">${data.difficulty||''} difficulty</div>
637
+ </div>
638
+ <div class="score-num">${pct}<span style="font-size:1.5rem;">%</span></div>
639
+ </div>`;
640
+
641
+ addLog(`Grader result for ${taskId}`, 'system', `Score: ${pct}% &nbsp; <span class="reward-chip reward-pos">${score.toFixed(3)}</span>`);
642
+ showToast(`Graded ${taskId}: ${pct}%`, 'success');
643
+ } catch (e) {
644
+ showToast(e.message, 'error');
645
+ addLog(e.message, 'error');
646
+ }
647
+ }
648
+
649
+ // ─── Run Baseline ─────────────────────────────────────────────────────────────
650
+ async function runBaseline() {
651
+ try {
652
+ const data = await fetchJSON('/baseline');
653
+ showToast('Baseline run complete!', 'success');
654
+ addLog('Baseline (perfect) run executed', 'action',
655
+ `Steps: ${data.trace?.length||0} | Final state loaded`);
656
+ if (data.final_state) {
657
+ // refresh state
658
+ await fetchState();
659
+ await resetEnv();
660
+ }
661
+ } catch (e) {
662
+ showToast(e.message, 'error');
663
+ }
664
+ }
665
+
666
+ // ─── Load tasks ───────────────────────────────────────────────────────────────
667
+ async function loadTasks() {
668
+ try {
669
+ const tasks = await fetchJSON('/tasks');
670
+ renderTasks(tasks);
671
+ } catch {
672
+ document.getElementById('tasks-list').innerHTML =
673
+ `<div style="font-size:.8rem;color:var(--danger);">Could not load tasks</div>`;
674
+ }
675
+ }
676
+
677
+ // ─── Init ─────────────────────────────────────────────────────────────────────
678
+ async function init() {
679
+ await fetchState();
680
+ await loadTasks();
681
+ if (!currentState) {
682
+ await resetEnv();
683
+ }
684
+ }
685
+
686
+ init();
687
+
688
+ // keyboard shortcut: Ctrl+Enter to execute action
689
+ document.getElementById('action-input').addEventListener('keydown', e => {
690
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') sendAction();
691
+ });
692
+ </script>
693
+ </body>
694
+ </html>