Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>AI Support Ticket Resolution</title> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { font-family: 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; } | |
| header { | |
| background: linear-gradient(135deg, #1e3a5f, #0ea5e9); | |
| padding: 20px 32px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| box-shadow: 0 2px 12px rgba(0,0,0,0.4); | |
| } | |
| header h1 { font-size: 1.4rem; font-weight: 700; letter-spacing: 0.5px; } | |
| header span { font-size: 0.8rem; background: rgba(255,255,255,0.15); padding: 4px 12px; border-radius: 20px; } | |
| .container { max-width: 1200px; margin: 0 auto; padding: 24px 16px; } | |
| .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 28px; } | |
| .stat-card { background: #1e293b; border-radius: 10px; padding: 16px; text-align: center; border: 1px solid #334155; } | |
| .stat-card .num { font-size: 2rem; font-weight: 700; color: #38bdf8; } | |
| .stat-card .label { font-size: 0.75rem; color: #94a3b8; margin-top: 4px; } | |
| .layout { display: grid; grid-template-columns: 380px 1fr; gap: 20px; } | |
| @media (max-width: 800px) { .layout { grid-template-columns: 1fr; } } | |
| .card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; } | |
| .card h2 { font-size: 1rem; font-weight: 600; margin-bottom: 16px; color: #7dd3fc; } | |
| label { display: block; font-size: 0.8rem; color: #94a3b8; margin-bottom: 4px; margin-top: 12px; } | |
| input, textarea, select { | |
| width: 100%; padding: 9px 12px; border-radius: 7px; | |
| background: #0f172a; border: 1px solid #334155; color: #e2e8f0; | |
| font-size: 0.875rem; outline: none; transition: border 0.2s; | |
| } | |
| input:focus, textarea:focus, select:focus { border-color: #0ea5e9; } | |
| textarea { resize: vertical; min-height: 90px; } | |
| button { | |
| margin-top: 16px; width: 100%; padding: 10px; | |
| background: linear-gradient(135deg, #0ea5e9, #6366f1); | |
| color: white; border: none; border-radius: 8px; | |
| font-size: 0.9rem; font-weight: 600; cursor: pointer; transition: opacity 0.2s; | |
| } | |
| button:hover { opacity: 0.88; } | |
| button:disabled { opacity: 0.5; cursor: not-allowed; } | |
| .filters { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; } | |
| .filters select { width: auto; padding: 6px 10px; font-size: 0.8rem; } | |
| .ticket-list { display: flex; flex-direction: column; gap: 10px; max-height: 600px; overflow-y: auto; } | |
| .ticket-item { | |
| background: #0f172a; border-radius: 9px; padding: 14px 16px; | |
| border-left: 4px solid #334155; cursor: pointer; transition: border-color 0.2s, background 0.2s; | |
| } | |
| .ticket-item:hover { background: #1a2744; } | |
| .ticket-item.critical { border-left-color: #ef4444; } | |
| .ticket-item.high { border-left-color: #f97316; } | |
| .ticket-item.medium { border-left-color: #eab308; } | |
| .ticket-item.low { border-left-color: #22c55e; } | |
| .ticket-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; } | |
| .ticket-subject { font-weight: 600; font-size: 0.9rem; } | |
| .ticket-meta { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; } | |
| .badge { | |
| font-size: 0.7rem; padding: 2px 8px; border-radius: 20px; font-weight: 600; | |
| background: #334155; color: #94a3b8; | |
| } | |
| .badge.open { background: #1d4ed8; color: #bfdbfe; } | |
| .badge.ai_resolved { background: #065f46; color: #6ee7b7; } | |
| .badge.agent_review { background: #78350f; color: #fde68a; } | |
| .badge.closed { background: #374151; color: #9ca3af; } | |
| .badge.critical { background: #7f1d1d; color: #fca5a5; } | |
| .badge.high { background: #7c2d12; color: #fdba74; } | |
| .badge.medium { background: #713f12; color: #fde68a; } | |
| .badge.low { background: #14532d; color: #86efac; } | |
| .modal-overlay { | |
| display: none; position: fixed; inset: 0; | |
| background: rgba(0,0,0,0.7); z-index: 100; align-items: center; justify-content: center; | |
| } | |
| .modal-overlay.active { display: flex; } | |
| .modal { | |
| background: #1e293b; border-radius: 14px; padding: 28px; | |
| width: 90%; max-width: 600px; max-height: 85vh; overflow-y: auto; | |
| border: 1px solid #334155; position: relative; | |
| } | |
| .modal h2 { font-size: 1.1rem; margin-bottom: 16px; color: #7dd3fc; } | |
| .modal-close { | |
| position: absolute; top: 16px; right: 16px; | |
| background: none; border: none; color: #94a3b8; font-size: 1.4rem; | |
| cursor: pointer; width: auto; margin: 0; padding: 0; | |
| } | |
| .ai-box { | |
| background: #0f172a; border: 1px solid #0ea5e9; border-radius: 8px; | |
| padding: 14px; margin: 12px 0; font-size: 0.875rem; line-height: 1.6; | |
| } | |
| .ai-box .ai-label { color: #38bdf8; font-size: 0.75rem; font-weight: 700; margin-bottom: 6px; } | |
| .confidence-bar { height: 6px; background: #334155; border-radius: 3px; margin-top: 8px; } | |
| .confidence-fill { height: 100%; border-radius: 3px; background: linear-gradient(90deg, #0ea5e9, #6366f1); } | |
| .detail-row { display: flex; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; } | |
| .detail-field { flex: 1; min-width: 140px; } | |
| .detail-field .field-label { font-size: 0.72rem; color: #64748b; margin-bottom: 2px; } | |
| .detail-field .field-value { font-size: 0.875rem; } | |
| .spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(255,255,255,0.3); border-top-color: white; border-radius: 50%; animation: spin 0.7s linear infinite; vertical-align: middle; margin-right: 6px; } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| .empty { text-align: center; color: #475569; padding: 40px; font-size: 0.9rem; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>🤖 AI Support Ticket Resolution</h1> | |
| <span id="env-badge">OpenEnv · Development</span> | |
| </header> | |
| <div class="container"> | |
| <div class="stats-grid" id="stats-grid"> | |
| <div class="stat-card"><div class="num" id="s-total">—</div><div class="label">Total Tickets</div></div> | |
| <div class="stat-card"><div class="num" id="s-open">—</div><div class="label">Open</div></div> | |
| <div class="stat-card"><div class="num" id="s-ai">—</div><div class="label">AI Resolved</div></div> | |
| <div class="stat-card"><div class="num" id="s-review">—</div><div class="label">Agent Review</div></div> | |
| <div class="stat-card"><div class="num" id="s-rate">—</div><div class="label">AI Resolution Rate</div></div> | |
| </div> | |
| <div class="layout"> | |
| <!-- Submit Form --> | |
| <div class="card"> | |
| <h2>📝 Submit New Ticket</h2> | |
| <form id="ticket-form"> | |
| <label>Customer Name</label> | |
| <input type="text" id="f-name" placeholder="Jane Doe" required /> | |
| <label>Email</label> | |
| <input type="email" id="f-email" placeholder="jane@example.com" required /> | |
| <label>Subject</label> | |
| <input type="text" id="f-subject" placeholder="Brief description of issue" required /> | |
| <label>Description</label> | |
| <textarea id="f-desc" placeholder="Describe your issue in detail..." required></textarea> | |
| <button type="submit" id="submit-btn">Submit Ticket</button> | |
| </form> | |
| </div> | |
| <!-- Ticket List --> | |
| <div class="card"> | |
| <h2>🎫 Tickets</h2> | |
| <div class="filters"> | |
| <select id="filter-status" onchange="loadTickets()"> | |
| <option value="">All Status</option> | |
| <option value="open">Open</option> | |
| <option value="ai_resolved">AI Resolved</option> | |
| <option value="agent_review">Agent Review</option> | |
| <option value="closed">Closed</option> | |
| </select> | |
| <select id="filter-priority" onchange="loadTickets()"> | |
| <option value="">All Priority</option> | |
| <option value="critical">Critical</option> | |
| <option value="high">High</option> | |
| <option value="medium">Medium</option> | |
| <option value="low">Low</option> | |
| </select> | |
| </div> | |
| <div class="ticket-list" id="ticket-list"> | |
| <div class="empty">No tickets yet. Submit one to get started.</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Ticket Detail Modal --> | |
| <div class="modal-overlay" id="modal"> | |
| <div class="modal"> | |
| <button class="modal-close" onclick="closeModal()">✕</button> | |
| <h2 id="m-subject"></h2> | |
| <div class="detail-row"> | |
| <div class="detail-field"><div class="field-label">Customer</div><div class="field-value" id="m-customer"></div></div> | |
| <div class="detail-field"><div class="field-label">Email</div><div class="field-value" id="m-email"></div></div> | |
| <div class="detail-field"><div class="field-label">Ticket ID</div><div class="field-value" id="m-id"></div></div> | |
| </div> | |
| <div class="detail-row"> | |
| <div class="detail-field"><div class="field-label">Status</div><div class="field-value" id="m-status"></div></div> | |
| <div class="detail-field"><div class="field-label">Priority</div><div class="field-value" id="m-priority"></div></div> | |
| <div class="detail-field"><div class="field-label">Category</div><div class="field-value" id="m-category"></div></div> | |
| </div> | |
| <div style="font-size:0.8rem;color:#94a3b8;margin-bottom:10px;" id="m-desc"></div> | |
| <div class="ai-box"> | |
| <div class="ai-label">🤖 AI RESOLUTION SUGGESTION</div> | |
| <div id="m-ai-res"></div> | |
| <div style="font-size:0.72rem;color:#64748b;margin-top:8px;">Confidence: <span id="m-conf"></span></div> | |
| <div class="confidence-bar"><div class="confidence-fill" id="m-conf-bar"></div></div> | |
| </div> | |
| <label>Agent Notes</label> | |
| <textarea id="m-agent-notes" placeholder="Add notes..."></textarea> | |
| <label>Final Resolution</label> | |
| <textarea id="m-resolution" placeholder="Final resolution message to customer..."></textarea> | |
| <label>Update Status</label> | |
| <select id="m-new-status"> | |
| <option value="open">Open</option> | |
| <option value="ai_resolved">AI Resolved</option> | |
| <option value="agent_review">Agent Review</option> | |
| <option value="closed">Closed</option> | |
| </select> | |
| <button onclick="updateTicket()">💾 Save Changes</button> | |
| </div> | |
| </div> | |
| <script> | |
| let currentTicketId = null; | |
| async function loadStats() { | |
| const r = await fetch('/stats'); | |
| const d = await r.json(); | |
| document.getElementById('s-total').textContent = d.total; | |
| document.getElementById('s-open').textContent = d.by_status.open; | |
| document.getElementById('s-ai').textContent = d.by_status.ai_resolved; | |
| document.getElementById('s-review').textContent = d.by_status.agent_review; | |
| document.getElementById('s-rate').textContent = d.ai_resolution_rate + '%'; | |
| } | |
| async function loadTickets() { | |
| const status = document.getElementById('filter-status').value; | |
| const priority = document.getElementById('filter-priority').value; | |
| let url = '/tickets?'; | |
| if (status) url += `status=${status}&`; | |
| if (priority) url += `priority=${priority}`; | |
| const r = await fetch(url); | |
| const tickets = await r.json(); | |
| const list = document.getElementById('ticket-list'); | |
| if (!tickets.length) { list.innerHTML = '<div class="empty">No tickets found.</div>'; return; } | |
| list.innerHTML = tickets.map(t => ` | |
| <div class="ticket-item ${t.priority}" onclick="openTicket('${t.id}')"> | |
| <div class="ticket-header"> | |
| <span class="ticket-subject">${t.subject}</span> | |
| <span class="badge ${t.status}">${t.status.replace('_', ' ')}</span> | |
| </div> | |
| <div style="font-size:0.78rem;color:#64748b;margin-top:4px;">${t.customer_name} · ${t.customer_email}</div> | |
| <div class="ticket-meta"> | |
| <span class="badge ${t.priority}">${t.priority}</span> | |
| <span class="badge">${t.category}</span> | |
| <span class="badge">#${t.id}</span> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| async function openTicket(id) { | |
| const r = await fetch(`/tickets/${id}`); | |
| const t = await r.json(); | |
| currentTicketId = id; | |
| document.getElementById('m-subject').textContent = t.subject; | |
| document.getElementById('m-customer').textContent = t.customer_name; | |
| document.getElementById('m-email').textContent = t.customer_email; | |
| document.getElementById('m-id').textContent = '#' + t.id; | |
| document.getElementById('m-status').innerHTML = `<span class="badge ${t.status}">${t.status.replace('_',' ')}</span>`; | |
| document.getElementById('m-priority').innerHTML = `<span class="badge ${t.priority}">${t.priority}</span>`; | |
| document.getElementById('m-category').textContent = t.category; | |
| document.getElementById('m-desc').textContent = t.description; | |
| document.getElementById('m-ai-res').textContent = t.ai_resolution || 'N/A'; | |
| const conf = Math.round((t.ai_confidence || 0) * 100); | |
| document.getElementById('m-conf').textContent = conf + '%'; | |
| document.getElementById('m-conf-bar').style.width = conf + '%'; | |
| document.getElementById('m-agent-notes').value = t.agent_notes || ''; | |
| document.getElementById('m-resolution').value = t.resolution || ''; | |
| document.getElementById('m-new-status').value = t.status; | |
| document.getElementById('modal').classList.add('active'); | |
| } | |
| function closeModal() { | |
| document.getElementById('modal').classList.remove('active'); | |
| currentTicketId = null; | |
| } | |
| async function updateTicket() { | |
| if (!currentTicketId) return; | |
| const payload = { | |
| status: document.getElementById('m-new-status').value, | |
| agent_notes: document.getElementById('m-agent-notes').value, | |
| resolution: document.getElementById('m-resolution').value, | |
| }; | |
| await fetch(`/tickets/${currentTicketId}`, { | |
| method: 'PATCH', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload), | |
| }); | |
| closeModal(); | |
| loadTickets(); | |
| loadStats(); | |
| } | |
| document.getElementById('ticket-form').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const btn = document.getElementById('submit-btn'); | |
| btn.disabled = true; | |
| btn.innerHTML = '<span class="spinner"></span>AI is analyzing...'; | |
| const payload = { | |
| customer_name: document.getElementById('f-name').value, | |
| customer_email: document.getElementById('f-email').value, | |
| subject: document.getElementById('f-subject').value, | |
| description: document.getElementById('f-desc').value, | |
| }; | |
| try { | |
| await fetch('/tickets', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload), | |
| }); | |
| e.target.reset(); | |
| loadTickets(); | |
| loadStats(); | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = 'Submit Ticket'; | |
| } | |
| }); | |
| document.getElementById('modal').addEventListener('click', (e) => { | |
| if (e.target === document.getElementById('modal')) closeModal(); | |
| }); | |
| loadTickets(); | |
| loadStats(); | |
| </script> | |
| </body> | |
| </html> | |