vivekvish2004 commited on
Commit
cfb0d65
Β·
1 Parent(s): 9fcca57

fix: restore frontend-backend compatibility with dual response keys

Browse files

- server/app.py: return both 'state' and 'observation' for broad frontend support
- frontend/src/app/page.tsx: clean rewrite with robust boot logic and multi-field support
- Verified local connectivity and dual-key compliance

Files changed (2) hide show
  1. frontend/src/app/page.tsx +70 -183
  2. server/app.py +10 -3
frontend/src/app/page.tsx CHANGED
@@ -2,7 +2,11 @@
2
 
3
  import { useState, useEffect } from 'react';
4
 
5
- const API_URL = typeof window !== 'undefined' ? (window.location.origin.includes('localhost') ? 'http://127.0.0.1:7860' : window.location.origin) : "http://127.0.0.1:7860";
 
 
 
 
6
 
7
  export default function Home() {
8
  const [state, setState] = useState<any>(null);
@@ -28,8 +32,8 @@ export default function Home() {
28
  const res = await fetch(`${API_URL}/state`);
29
  if (res.ok) {
30
  const data = await res.json();
31
- const obs = data.observation || data;
32
- if (obs && obs.ticket_text) {
33
  setState(obs);
34
  setBooting(false);
35
  return true;
@@ -47,7 +51,7 @@ export default function Home() {
47
  try {
48
  const res = await fetch(`${API_URL}/reset`, { method: 'POST' });
49
  const data = await res.json();
50
- const obs = data.observation || data;
51
  setState(obs);
52
  setLogs([{ role: 'system', message: 'Environment Reset Successfully' }]);
53
  showStatus("Enterprise session initialized.", "success");
@@ -68,15 +72,15 @@ export default function Home() {
68
  const res = await fetch(`${API_URL}/reset`, { method: 'POST' });
69
  if (res.ok) {
70
  const data = await res.json();
71
- const obs = data.observation || data;
72
  setState(obs);
73
  setBooting(false);
74
  addLog('Backend connected β€” session started', 'system');
75
  } else {
76
- setTimeout(boot, 1500);
77
  }
78
  } catch {
79
- setTimeout(boot, 1500);
80
  }
81
  }
82
  };
@@ -88,7 +92,7 @@ export default function Home() {
88
  try {
89
  actionObj = JSON.parse(actionInput.trim());
90
  } catch (e) {
91
- throw new Error("Invalid JSON: Fix brackets or commas.");
92
  }
93
 
94
  const res = await fetch(`${API_URL}/step`, {
@@ -97,24 +101,20 @@ export default function Home() {
97
  body: JSON.stringify(actionObj)
98
  });
99
 
100
- if (!res.ok) {
101
- const errorData = await res.json().catch(() => ({ detail: "Server error" }));
102
- throw new Error(errorData.detail || "Step failed.");
103
- }
104
-
105
  const data = await res.json();
106
- const obs = data.observation;
107
-
 
 
 
108
  if (obs.status === "session_complete") {
109
  addLog('πŸŽ‰ Session Complete!', 'system');
110
- setState(obs);
111
- showStatus("Session finished successfully!", "success");
112
  } else {
113
- setState(obs);
114
  const reward = data.reward ?? 0;
115
  const msg = data.info?.message || "Action processed";
116
  addLog(`Action: ${actionObj.action_type}`, 'agent', `${msg} | Reward: ${reward.toFixed(3)}`, reward >= 0 ? "success" : "failed");
117
- showStatus(`Action executed: ${reward >= 0 ? 'SUCCESS' : 'DEDUCTION'}`, reward >= 0 ? "success" : "error");
118
  }
119
  } catch (e: any) {
120
  showStatus(e.message || "Network Error.", "error");
@@ -127,13 +127,12 @@ export default function Home() {
127
  setScore(null);
128
  try {
129
  const res = await fetch(`${API_URL}/grader?task_id=task_hard_1`);
130
- if (!res.ok) throw new Error("Evaluation engine unreachable.");
131
  const data = await res.json();
132
  setScore(data.score ?? 0);
133
- addLog(`Evaluation: ${(data.score * 100).toFixed(0)}%`, 'system');
134
- showStatus("Model evaluation complete.", "success");
135
  } catch (e: any) {
136
- showStatus(e.message || "Grader connection failed.", "error");
137
  }
138
  setLoading(false);
139
  };
@@ -152,221 +151,109 @@ export default function Home() {
152
  <p style={{ color: 'var(--muted)', fontSize: '1.1rem', margin: '0.25rem 0 0 0' }}>AI Decision Monitoring System</p>
153
  </div>
154
  <div style={{ textAlign: 'right' }}>
155
- <button className="btn btn-outline" onClick={resetEnv} disabled={loading}>
156
  {loading ? 'Processing...' : 'New Session'}
157
  </button>
158
  </div>
159
  </header>
160
 
161
  {statusMsg && (
162
- <div className={`animate-slide`} style={{
163
- position: 'fixed',
164
- top: '20px',
165
- right: '20px',
166
- zIndex: 1000,
167
- padding: '1rem 2rem',
168
- borderRadius: '12px',
169
  background: statusMsg.type === 'success' ? '#10b981' : '#ef4444',
170
- color: 'white',
171
- fontWeight: 700,
172
- boxShadow: '0 20px 25px -5px rgba(0,0,0,0.1)',
173
- display: 'flex',
174
- alignItems: 'center',
175
- gap: '10px'
176
  }}>
177
  {statusMsg.type === 'success' ? 'βœ…' : '❌'} {statusMsg.text}
178
  </div>
179
  )}
180
 
181
- <div className="layout-grid" style={{ gridTemplateColumns: 'minmax(0, 1fr) 350px', gap: '2rem' }}>
182
  <section style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
183
-
184
  <div className="glass-card">
185
  {booting ? (
186
  <div style={{ textAlign: 'center', padding: '5rem' }}>
187
  <div style={{ fontSize: '3rem', marginBottom: '1.5rem', animation: 'spin 2s linear infinite', display: 'inline-block' }}>βš™οΈ</div>
188
- <h2 style={{ fontSize: '1.5rem', fontWeight: 800, marginBottom: '0.5rem' }}>Starting Engine</h2>
189
  <p style={{ color: 'var(--muted)' }}>Connecting to backend... Attempt {bootAttempt}</p>
190
- <div style={{ marginTop: '1rem', fontSize: '0.8rem', opacity: 0.6 }}>Waiting for high-performance simulation environment</div>
191
  </div>
192
  ) : state && state.status !== "session_complete" ? (
193
  <div style={{ display: 'grid', gap: '1.5rem' }}>
194
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
195
  <div style={{ flex: 1 }}>
196
- <span style={{ fontSize: '0.7rem', fontWeight: 800, color: 'var(--primary)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Current Instruction</span>
197
- <p style={{ marginTop: '0.5rem', fontSize: '1.5rem', fontWeight: 600, lineHeight: 1.4 }}>"{state.ticket_text}"</p>
198
  </div>
199
- <div style={{ marginLeft: '2rem', textAlign: 'right', minWidth: '100px' }}>
200
- <div style={{ fontSize: '0.7rem', fontWeight: 800, color: state.sla_warning ? 'var(--error)' : 'var(--muted)', textTransform: 'uppercase' }}>SLA Health</div>
201
- <div style={{ fontSize: '1.5rem', fontWeight: 800, color: state.sla_warning ? 'var(--error)' : 'var(--foreground)' }}>
202
- {state.steps_taken || 0} / {state.sla_limit || 10}
203
- </div>
204
  </div>
205
  </div>
206
 
207
  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '1rem' }}>
208
- <div className="glass-card" style={{ padding: '0.75rem' }}>
209
- <div style={{ fontSize: '0.65rem', fontWeight: 700, color: 'var(--muted)', marginBottom: '0.25rem', textTransform: 'uppercase' }}>Sentiment</div>
210
- <div className={`badge badge-${state.sentiment}`} style={{ fontSize: '0.75rem', width: '100%', textAlign: 'center' }}>{state.sentiment || 'neutral'}</div>
211
- </div>
212
- <div className="glass-card" style={{ padding: '0.75rem' }}>
213
- <div style={{ fontSize: '0.65rem', fontWeight: 700, color: 'var(--muted)', marginBottom: '0.25rem', textTransform: 'uppercase' }}>Priority</div>
214
- <div className={`badge ${state.priority ? `badge-${state.priority}` : 'badge-unassigned'}`} style={{ fontSize: '0.75rem', width: '100%', textAlign: 'center' }}>{state.priority || 'PENDING'}</div>
215
- </div>
216
- <div className="glass-card" style={{ padding: '0.75rem' }}>
217
- <div style={{ fontSize: '0.65rem', fontWeight: 700, color: 'var(--muted)', marginBottom: '0.25rem', textTransform: 'uppercase' }}>Status</div>
218
- <div className={`badge badge-${state.status}`} style={{ fontSize: '0.75rem', width: '100%', textAlign: 'center' }}>{state.status}</div>
219
- </div>
220
- <div className="glass-card" style={{ padding: '0.75rem' }}>
221
- <div style={{ fontSize: '0.65rem', fontWeight: 700, color: 'var(--muted)', marginBottom: '0.25rem', textTransform: 'uppercase' }}>Reward</div>
222
- <div style={{ fontSize: '0.8rem', fontWeight: 900, textAlign: 'center', color: 'var(--primary)' }}>+{(state.total_reward || 0).toFixed(2)}</div>
223
  </div>
224
  </div>
225
  </div>
226
  ) : state?.status === "session_complete" ? (
227
  <div style={{ textAlign: 'center', padding: '4rem' }}>
228
- <div style={{ fontSize: '4rem', marginBottom: '1rem' }}>πŸŽ‰</div>
229
- <h2 style={{ fontSize: '2rem', fontWeight: 800 }}>Evaluation Finished</h2>
230
  <div style={{ display: 'flex', justifyContent: 'center', gap: '3rem', marginTop: '2rem' }}>
231
- <div>
232
- <div style={{ color: 'var(--muted)', fontSize: '0.8rem', fontWeight: 700 }}>RESOLVED</div>
233
- <div style={{ fontSize: '2rem', fontWeight: 900 }}>{state.resolved}</div>
234
- </div>
235
- <div>
236
- <div style={{ color: 'var(--muted)', fontSize: '0.8rem', fontWeight: 700 }}>TOTAL REWARD</div>
237
- <div style={{ fontSize: '2rem', fontWeight: 900, color: 'var(--primary)' }}>{state.total_reward?.toFixed(2) || '0.00'}</div>
238
- </div>
239
  </div>
240
  <button className="btn" style={{ marginTop: '2rem' }} onClick={resetEnv}>Start New Session</button>
241
  </div>
242
  ) : (
243
- <div style={{ textAlign: 'center', padding: '5rem', color: 'var(--muted)' }}>
244
- Waiting for engine state...
245
- </div>
246
  )}
247
  </div>
248
 
249
  <div className="glass-card">
250
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
251
- <h2 style={{ fontSize: '1.25rem', fontWeight: 700, margin: 0, color: 'var(--foreground)' }}>Control Center</h2>
252
- <div style={{ display: 'flex', gap: '0.5rem' }}>
253
- <button className="btn btn-outline" style={{ fontSize: '0.7rem', padding: '0.25rem 0.5rem' }} onClick={() => setActionInput('{\n "action_type": "classify_ticket",\n "payload": { "classification": "refund" }\n}')}>🏷️ Classify</button>
254
- <button className="btn btn-outline" style={{ fontSize: '0.7rem', padding: '0.25rem 0.5rem' }} onClick={() => setActionInput('{\n "action_type": "assign_priority",\n "payload": { "priority": "high" }\n}')}>⚑ Priority</button>
255
- <button className="btn btn-outline" style={{ fontSize: '0.7rem', padding: '0.25rem 0.5rem' }} onClick={() => setActionInput('{\n "action_type": "generate_response",\n "payload": { "response": "I apologize for the delay, we are fixing this now." }\n}')}>✍️ Reply</button>
256
- <button className="btn btn-outline" style={{ fontSize: '0.7rem', padding: '0.25rem 0.5rem' }} onClick={() => setActionInput('{\n "action_type": "resolve",\n "payload": {}\n}')}>βœ… Resolve</button>
257
- </div>
258
- </div>
259
-
260
- <div style={{ marginBottom: '1.5rem' }}>
261
- <textarea
262
- value={actionInput}
263
- onChange={(e) => setActionInput(e.target.value)}
264
- rows={5}
265
- style={{ fontSize: '0.9rem', fontFamily: 'monospace', padding: '1rem', background: '#f8fafc', borderRadius: '12px' }}
266
- placeholder="Enter AI Action JSON..."
267
- />
268
- </div>
269
  <div style={{ display: 'flex', gap: '1rem' }}>
270
- <button className="btn" onClick={sendAction} disabled={loading || booting || !state || state.status === 'session_complete'} style={{ flex: 2 }}>
271
  {loading ? 'Executing...' : 'Execute Action'}
272
  </button>
273
- <button className="btn btn-outline" onClick={runHardGrader} disabled={loading || booting || !state} style={{ flex: 1 }}>
274
- Grade Model
275
- </button>
276
  </div>
277
  </div>
278
-
279
- {score !== null && (
280
- <div className="glass-card animate-slide" style={{ border: '2px solid #10b981', background: '#ecfdf5' }}>
281
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
282
- <div>
283
- <h3 style={{ margin: 0, fontSize: '0.9rem', color: '#065f46', fontWeight: 800, textTransform: 'uppercase' }}>Benchmark Result</h3>
284
- <p style={{ margin: '0.25rem 0 0 0', color: '#047857', fontSize: '0.8rem' }}>Hard Task Evaluation Suite</p>
285
- </div>
286
- <div style={{ fontSize: '3rem', fontWeight: 950, color: '#059669', letterSpacing: '-0.05em' }}>
287
- {(score * 100).toFixed(0)}%
288
- </div>
289
- </div>
290
- </div>
291
- )}
292
-
293
  </section>
294
 
295
  <section style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
296
- <div className="glass-card" style={{ flex: 1, maxHeight: '450px', display: 'flex', flexDirection: 'column' }}>
297
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
298
- <h2 style={{ fontSize: '1.1rem', fontWeight: 700, margin: 0 }}>Queue Management</h2>
299
- <span className="badge" style={{ background: 'var(--primary)', color: 'white', padding: '2px 10px' }}>{state?.info?.queue?.length || 0} Pending</span>
300
- </div>
301
-
302
- {/* Progress Tracker */}
303
- <div style={{ marginBottom: '1.5rem' }}>
304
- <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.7rem', fontWeight: 700, marginBottom: '0.4rem', color: 'var(--muted)' }}>
305
- <span>SESSION PROGRESS</span>
306
- <span>{((state?.info?.resolved || 0) / 3 * 100).toFixed(0)}%</span>
307
- </div>
308
- <div style={{ width: '100%', height: '6px', background: '#f1f5f9', borderRadius: '10px', overflow: 'hidden' }}>
309
- <div style={{
310
- width: `${((state?.info?.resolved || 0) / 3 * 100)}%`,
311
- height: '100%',
312
- background: 'var(--primary)',
313
- transition: 'width 0.5s ease-out'
314
- }}></div>
315
- </div>
316
- </div>
317
-
318
- <div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
319
- {state?.info?.queue && state.info.queue.length > 0 ? state.info.queue.map((q: string, i: number) => {
320
- const isFirst = i === 0;
321
- const difficulty = q.length > 25 ? "HARD" : q.length > 20 ? "MEDIUM" : "EASY";
322
- return (
323
- <div key={i} className="glass-card" style={{
324
- padding: '0.75rem',
325
- fontSize: '0.85rem',
326
- background: isFirst ? '#f0f9ff' : 'white',
327
- borderLeft: isFirst ? '4px solid var(--primary)' : '1px solid var(--card-border)',
328
- position: 'relative',
329
- opacity: isFirst ? 1 : 0.8,
330
- animation: isFirst ? 'pulse 2s infinite' : 'none'
331
- }}>
332
- <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.4rem' }}>
333
- <span style={{ fontSize: '0.6rem', fontWeight: 800, color: isFirst ? 'var(--primary)' : 'var(--muted)' }}>
334
- {isFirst ? '● ACTIVE NOW' : `UPCOMING #${i + 1}`}
335
- </span>
336
- <span style={{ fontSize: '0.55rem', fontWeight: 900, color: difficulty === 'HARD' ? 'var(--error)' : 'var(--muted)' }}>
337
- {difficulty}
338
- </span>
339
- </div>
340
- <div style={{ fontWeight: 600, color: 'var(--foreground)' }}>{q}</div>
341
- <div style={{ marginTop: '0.5rem', fontSize: '0.65rem', color: 'var(--muted)', display: 'flex', gap: '10px' }}>
342
- <span>⏱️ {isFirst ? 'Est. 2m' : `${(i+1)*3}m wait`}</span>
343
- <span>🏒 Tier 1</span>
344
- </div>
345
- </div>
346
- );
347
- }) : (
348
- <div style={{ textAlign: 'center', padding: '2rem', color: 'var(--muted)' }}>
349
- <div style={{ fontSize: '2rem', marginBottom: '0.5rem' }}>🎯</div>
350
- <p style={{ fontSize: '0.8rem' }}>Queue Cleared</p>
351
  </div>
352
- )}
353
  </div>
354
  </div>
355
 
356
  <div className="glass-card" style={{ height: '350px', display: 'flex', flexDirection: 'column' }}>
357
- <h2 style={{ fontSize: '1.1rem', fontWeight: 700, marginBottom: '1rem' }}>Activity Logs</h2>
358
- <div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column-reverse', gap: '0.75rem' }}>
359
  {logs.map((log, i) => (
360
- <div key={i} className={`log-entry ${log.role === 'agent' ? 'log-agent' : 'log-customer'}`} style={{
361
- padding: '0.75rem',
362
- borderLeft: log.status === 'success' ? '4px solid #10b981' : log.status === 'failed' ? '4px solid #ef4444' : 'none'
363
- }}>
364
- <div style={{ fontSize: '0.65rem', fontWeight: 900, textTransform: 'uppercase', marginBottom: '0.25rem', opacity: 0.6, display: 'flex', justifyContent: 'space-between' }}>
365
- <span>{log.role}</span>
366
- {log.status && <span style={{ color: log.status === 'success' ? '#10b981' : '#ef4444' }}>{log.status.toUpperCase()}</span>}
367
- </div>
368
- <div style={{ fontSize: '0.9rem', fontWeight: 600 }}>{log.message}</div>
369
- {log.info && <div style={{ fontSize: '0.7rem', color: 'var(--primary)', marginTop: '0.25rem', fontWeight: 700 }}>{log.info}</div>}
370
  </div>
371
  ))}
372
  </div>
 
2
 
3
  import { useState, useEffect } from 'react';
4
 
5
+ const API_URL = typeof window !== 'undefined'
6
+ ? (window.location.origin.includes('localhost') || window.location.origin.includes('127.0.0.1')
7
+ ? 'http://127.0.0.1:7860'
8
+ : window.location.origin)
9
+ : "http://127.0.0.1:7860";
10
 
11
  export default function Home() {
12
  const [state, setState] = useState<any>(null);
 
32
  const res = await fetch(`${API_URL}/state`);
33
  if (res.ok) {
34
  const data = await res.json();
35
+ const obs = data.observation || data.state || data;
36
+ if (obs && obs.status !== "session_complete") {
37
  setState(obs);
38
  setBooting(false);
39
  return true;
 
51
  try {
52
  const res = await fetch(`${API_URL}/reset`, { method: 'POST' });
53
  const data = await res.json();
54
+ const obs = data.observation || data.state || data;
55
  setState(obs);
56
  setLogs([{ role: 'system', message: 'Environment Reset Successfully' }]);
57
  showStatus("Enterprise session initialized.", "success");
 
72
  const res = await fetch(`${API_URL}/reset`, { method: 'POST' });
73
  if (res.ok) {
74
  const data = await res.json();
75
+ const obs = data.observation || data.state || data;
76
  setState(obs);
77
  setBooting(false);
78
  addLog('Backend connected β€” session started', 'system');
79
  } else {
80
+ setTimeout(boot, 2000);
81
  }
82
  } catch {
83
+ setTimeout(boot, 2000);
84
  }
85
  }
86
  };
 
92
  try {
93
  actionObj = JSON.parse(actionInput.trim());
94
  } catch (e) {
95
+ throw new Error("Invalid JSON format.");
96
  }
97
 
98
  const res = await fetch(`${API_URL}/step`, {
 
101
  body: JSON.stringify(actionObj)
102
  });
103
 
 
 
 
 
 
104
  const data = await res.json();
105
+ if (!res.ok) throw new Error(data.detail || "Step failed.");
106
+
107
+ const obs = data.observation || data.state;
108
+ setState(obs);
109
+
110
  if (obs.status === "session_complete") {
111
  addLog('πŸŽ‰ Session Complete!', 'system');
112
+ showStatus("Session finished!", "success");
 
113
  } else {
 
114
  const reward = data.reward ?? 0;
115
  const msg = data.info?.message || "Action processed";
116
  addLog(`Action: ${actionObj.action_type}`, 'agent', `${msg} | Reward: ${reward.toFixed(3)}`, reward >= 0 ? "success" : "failed");
117
+ showStatus(`Action: ${reward >= 0 ? 'SUCCESS' : 'DEDUCTION'}`, reward >= 0 ? "success" : "error");
118
  }
119
  } catch (e: any) {
120
  showStatus(e.message || "Network Error.", "error");
 
127
  setScore(null);
128
  try {
129
  const res = await fetch(`${API_URL}/grader?task_id=task_hard_1`);
 
130
  const data = await res.json();
131
  setScore(data.score ?? 0);
132
+ addLog(`Evaluation Score: ${(data.score * 100).toFixed(0)}%`, 'system');
133
+ showStatus("Evaluation complete.", "success");
134
  } catch (e: any) {
135
+ showStatus("Grader unavailable.", "error");
136
  }
137
  setLoading(false);
138
  };
 
151
  <p style={{ color: 'var(--muted)', fontSize: '1.1rem', margin: '0.25rem 0 0 0' }}>AI Decision Monitoring System</p>
152
  </div>
153
  <div style={{ textAlign: 'right' }}>
154
+ <button className="btn btn-outline" onClick={resetEnv} disabled={loading || booting}>
155
  {loading ? 'Processing...' : 'New Session'}
156
  </button>
157
  </div>
158
  </header>
159
 
160
  {statusMsg && (
161
+ <div style={{
162
+ position: 'fixed', top: '20px', right: '20px', zIndex: 1000,
163
+ padding: '1rem 2rem', borderRadius: '12px',
 
 
 
 
164
  background: statusMsg.type === 'success' ? '#10b981' : '#ef4444',
165
+ color: 'white', fontWeight: 700, boxShadow: '0 20px 25px -5px rgba(0,0,0,0.1)',
166
+ display: 'flex', alignItems: 'center', gap: '10px'
 
 
 
 
167
  }}>
168
  {statusMsg.type === 'success' ? 'βœ…' : '❌'} {statusMsg.text}
169
  </div>
170
  )}
171
 
172
+ <div className="layout-grid">
173
  <section style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
 
174
  <div className="glass-card">
175
  {booting ? (
176
  <div style={{ textAlign: 'center', padding: '5rem' }}>
177
  <div style={{ fontSize: '3rem', marginBottom: '1.5rem', animation: 'spin 2s linear infinite', display: 'inline-block' }}>βš™οΈ</div>
178
+ <h2 style={{ fontSize: '1.5rem', fontWeight: 800 }}>Starting Engine</h2>
179
  <p style={{ color: 'var(--muted)' }}>Connecting to backend... Attempt {bootAttempt}</p>
 
180
  </div>
181
  ) : state && state.status !== "session_complete" ? (
182
  <div style={{ display: 'grid', gap: '1.5rem' }}>
183
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
184
  <div style={{ flex: 1 }}>
185
+ <span style={{ fontSize: '0.7rem', fontWeight: 800, color: 'var(--primary)', textTransform: 'uppercase' }}>Current Ticket</span>
186
+ <p style={{ marginTop: '0.5rem', fontSize: '1.4rem', fontWeight: 600 }}>"{state.ticket_text}"</p>
187
  </div>
188
+ <div style={{ textAlign: 'right', minWidth: '100px' }}>
189
+ <div style={{ fontSize: '0.7rem', fontWeight: 800, color: 'var(--muted)' }}>SLA</div>
190
+ <div style={{ fontSize: '1.5rem', fontWeight: 800 }}>{state.steps_taken || 0} / {state.sla_limit || 10}</div>
 
 
191
  </div>
192
  </div>
193
 
194
  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '1rem' }}>
195
+ {['sentiment', 'priority', 'status'].map(key => (
196
+ <div key={key} className="glass-card" style={{ padding: '0.75rem', textAlign: 'center' }}>
197
+ <div style={{ fontSize: '0.6rem', fontWeight: 700, color: 'var(--muted)', textTransform: 'uppercase' }}>{key}</div>
198
+ <div className={`badge badge-${state[key] || 'neutral'}`} style={{ fontSize: '0.7rem', marginTop: '0.25rem' }}>{state[key] || 'OPEN'}</div>
199
+ </div>
200
+ ))}
201
+ <div className="glass-card" style={{ padding: '0.75rem', textAlign: 'center' }}>
202
+ <div style={{ fontSize: '0.6rem', fontWeight: 700, color: 'var(--muted)', textTransform: 'uppercase' }}>Reward</div>
203
+ <div style={{ fontSize: '0.8rem', fontWeight: 900, color: 'var(--primary)' }}>+{(state.total_reward || 0).toFixed(2)}</div>
 
 
 
 
 
 
204
  </div>
205
  </div>
206
  </div>
207
  ) : state?.status === "session_complete" ? (
208
  <div style={{ textAlign: 'center', padding: '4rem' }}>
209
+ <div style={{ fontSize: '4rem' }}>πŸŽ‰</div>
210
+ <h2 style={{ fontSize: '2rem', fontWeight: 800 }}>Queue Completed</h2>
211
  <div style={{ display: 'flex', justifyContent: 'center', gap: '3rem', marginTop: '2rem' }}>
212
+ <div><div style={{ color: 'var(--muted)', fontWeight: 700 }}>RESOLVED</div><div style={{ fontSize: '2rem', fontWeight: 900 }}>{state.resolved}</div></div>
213
+ <div><div style={{ color: 'var(--muted)', fontWeight: 700 }}>TOTAL REWARD</div><div style={{ fontSize: '2rem', fontWeight: 900, color: 'var(--primary)' }}>{state.total_reward?.toFixed(2)}</div></div>
 
 
 
 
 
 
214
  </div>
215
  <button className="btn" style={{ marginTop: '2rem' }} onClick={resetEnv}>Start New Session</button>
216
  </div>
217
  ) : (
218
+ <div style={{ textAlign: 'center', padding: '5rem', color: 'var(--muted)' }}>Initializing Enterprise Service...</div>
 
 
219
  )}
220
  </div>
221
 
222
  <div className="glass-card">
223
+ <h2 style={{ fontSize: '1.2rem', fontWeight: 700, marginBottom: '1rem' }}>Control Center</h2>
224
+ <textarea
225
+ value={actionInput} onChange={(e) => setActionInput(e.target.value)}
226
+ rows={5} style={{ fontSize: '0.9rem', fontFamily: 'monospace', marginBottom: '1.5rem' }}
227
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  <div style={{ display: 'flex', gap: '1rem' }}>
229
+ <button className="btn" onClick={sendAction} disabled={loading || booting || !state} style={{ flex: 2 }}>
230
  {loading ? 'Executing...' : 'Execute Action'}
231
  </button>
232
+ <button className="btn btn-outline" onClick={runHardGrader} disabled={loading || booting || !state} style={{ flex: 1 }}>Grade</button>
 
 
233
  </div>
234
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  </section>
236
 
237
  <section style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
238
+ <div className="glass-card" style={{ flex: 1, maxHeight: '400px', display: 'flex', flexDirection: 'column' }}>
239
+ <h2 style={{ fontSize: '1rem', fontWeight: 700, marginBottom: '1rem' }}>Support Queue</h2>
240
+ <div style={{ flex: 1, overflowY: 'auto' }}>
241
+ {state?.info?.queue?.map((q: string, i: number) => (
242
+ <div key={i} style={{ padding: '0.75rem', borderBottom: '1px solid var(--card-border)', fontSize: '0.85rem', opacity: i === 0 ? 1 : 0.6 }}>
243
+ #{i+1}: {q}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  </div>
245
+ )) || <div style={{ color: 'var(--muted)' }}>Queue is empty</div>}
246
  </div>
247
  </div>
248
 
249
  <div className="glass-card" style={{ height: '350px', display: 'flex', flexDirection: 'column' }}>
250
+ <h2 style={{ fontSize: '1rem', fontWeight: 700, marginBottom: '1rem' }}>Activity Logs</h2>
251
+ <div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column-reverse', gap: '0.5rem' }}>
252
  {logs.map((log, i) => (
253
+ <div key={i} className={`log-entry ${log.role === 'agent' ? 'log-agent' : 'log-customer'}`}>
254
+ <div style={{ fontSize: '0.6rem', fontWeight: 900, opacity: 0.5 }}>{log.role.toUpperCase()}</div>
255
+ <div style={{ fontSize: '0.85rem', fontWeight: 600 }}>{log.message}</div>
256
+ {log.info && <div style={{ fontSize: '0.7rem', color: 'var(--primary)' }}>{log.info}</div>}
 
 
 
 
 
 
257
  </div>
258
  ))}
259
  </div>
server/app.py CHANGED
@@ -112,8 +112,10 @@ def get_schema():
112
  def reset_env():
113
  """Reset the environment and yield the initial observation (GET or POST)."""
114
  obs = env_instance.reset()
 
115
  return {
116
- "observation": obs.state,
 
117
  "reward": 0.0,
118
  "done": False
119
  }
@@ -127,8 +129,10 @@ def step_env(action: Action):
127
  env_instance.reset()
128
 
129
  obs, reward, done, info = env_instance.step(action)
 
130
  return {
131
- "observation": obs.state,
 
132
  "reward": float(reward.value),
133
  "done": bool(done),
134
  "info": info
@@ -144,7 +148,10 @@ def get_state():
144
  if state.get("status") == "session_complete":
145
  obs = env_instance.reset()
146
  state = obs.state
147
- return {"observation": state}
 
 
 
148
 
149
 
150
  # ───────────────────────────────────────────────────────────────────────────────
 
112
  def reset_env():
113
  """Reset the environment and yield the initial observation (GET or POST)."""
114
  obs = env_instance.reset()
115
+ state = obs.state
116
  return {
117
+ "observation": state,
118
+ "state": state, # Legacy compatibility
119
  "reward": 0.0,
120
  "done": False
121
  }
 
129
  env_instance.reset()
130
 
131
  obs, reward, done, info = env_instance.step(action)
132
+ state = obs.state
133
  return {
134
+ "observation": state,
135
+ "state": state, # Legacy compatibility
136
  "reward": float(reward.value),
137
  "done": bool(done),
138
  "info": info
 
148
  if state.get("status") == "session_complete":
149
  obs = env_instance.reset()
150
  state = obs.state
151
+ return {
152
+ "observation": state,
153
+ "state": state # Legacy compatibility
154
+ }
155
 
156
 
157
  # ───────────────────────────────────────────────────────────────────────────────