AI-support-ticket / index.html
AdityaSharma79's picture
Add files via upload
369ed63 unverified
Raw
History Blame Contribute Delete
15.1 kB
<!DOCTYPE html>
<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>