Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>TrafficAI Review System Prototype</title> | |
| <style> | |
| :root{ | |
| --bg:#06111f; | |
| --panel:#0d1a2b; | |
| --panel2:#111d31; | |
| --line:#22314a; | |
| --text:#f6f7fb; | |
| --sub:#9fb0c8; | |
| --orange:#ff7a14; | |
| --green:#22c55e; | |
| --blue:#4688ff; | |
| --purple:#8b5cf6; | |
| --yellow:#f7c948; | |
| --red:#ef4444; | |
| } | |
| *{box-sizing:border-box} | |
| body{ | |
| margin:0; | |
| font-family:Inter, Segoe UI, system-ui, -apple-system, sans-serif; | |
| background: radial-gradient(circle at top right, #112342 0%, var(--bg) 42%); | |
| color:var(--text); | |
| } | |
| .hidden{display:none } | |
| .app{min-height:100vh;padding:20px} | |
| .card, .panel{ | |
| background:linear-gradient(180deg, rgba(17,29,49,.95), rgba(10,18,31,.98)); | |
| border:1px solid var(--line); | |
| border-radius:18px; | |
| box-shadow:0 12px 32px rgba(0,0,0,.28); | |
| } | |
| .topbar{ | |
| display:flex;align-items:center;justify-content:space-between; | |
| padding:16px 22px;margin-bottom:18px | |
| } | |
| .brand{display:flex;align-items:center;gap:14px} | |
| .logo{width:42px;height:42px;border-radius:12px;background:var(--orange);display:grid;place-items:center;font-size:18px;font-weight:700} | |
| .brand h1{font-size:18px;margin:0} | |
| .brand p{margin:2px 0 0;color:var(--sub);font-size:13px} | |
| .user-badge{display:flex;align-items:center;gap:14px} | |
| .avatar{width:42px;height:42px;border-radius:50%;display:grid;place-items:center;background:linear-gradient(135deg,#3654ff,#8b5cf6);font-weight:700} | |
| .metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:18px} | |
| .metric{padding:20px 22px;position:relative;overflow:hidden} | |
| .metric .accent{position:absolute;inset:0 auto 0 0;width:4px;background:var(--orange);opacity:.95} | |
| .metric.green .accent{background:var(--green)} .metric.blue .accent{background:var(--blue)} .metric.purple .accent{background:var(--purple)} | |
| .metric h3{margin:0 0 10px;color:#b7c4d9;font-size:14px;font-weight:600} | |
| .metric .value{font-size:36px;font-weight:800;line-height:1} | |
| .metric .note{font-size:13px;color:var(--sub);margin-top:8px} | |
| .searchbar, .triage{padding:16px 18px;margin-bottom:18px} | |
| .searchbar input{ | |
| width:100%;padding:14px 16px;border-radius:14px;border:1px solid var(--line); | |
| background:#07131f;color:var(--text);font-size:15px;outline:none | |
| } | |
| .section-title{font-size:18px;font-weight:700;margin:4px 0 14px} | |
| .triage-grid, .violations{display:grid;gap:16px} | |
| .triage-grid{grid-template-columns:repeat(3,1fr)} | |
| .lane{padding:18px;border-radius:16px;border:1px solid var(--line);background:rgba(8,17,31,.7)} | |
| .lane h4{margin:0 0 8px;font-size:16px} | |
| .lane p{margin:0;color:var(--sub);font-size:13px;line-height:1.4} | |
| .lane.fast{border-color:rgba(34,197,94,.5)} .lane.standard{border-color:rgba(70,136,255,.5)} .lane.exception{border-color:rgba(255,122,20,.5)} | |
| .violations{grid-template-columns:repeat(4,1fr)} | |
| .violation-card{padding:20px;cursor:pointer;transition:.2s transform,.2s border-color} | |
| .violation-card:hover{transform:translateY(-2px);border-color:rgba(255,122,20,.45)} | |
| .violation-card h4{margin:0 0 8px;font-size:18px} | |
| .violation-meta{display:flex;justify-content:space-between;align-items:center;font-size:13px;color:var(--sub);margin:10px 0} | |
| .badge{padding:6px 10px;border-radius:10px;border:1px solid var(--line);font-size:12px;background:rgba(255,255,255,.03);display:inline-flex;gap:6px;align-items:center} | |
| .bar{height:8px;border-radius:999px;background:#1b2d46;overflow:hidden} | |
| .bar > span{display:block;height:100%;background:var(--orange);border-radius:inherit} | |
| .login-wrap{min-height:100vh;display:grid;place-items:center;padding:24px} | |
| .login{width:min(420px,92vw);padding:24px} | |
| .login .brand{margin-bottom:18px} | |
| label{display:block;color:#b7c4d9;font-size:13px;margin:12px 0 6px} | |
| input, textarea{ | |
| width:100%;padding:13px 14px;border-radius:12px;border:1px solid var(--line); | |
| background:#08131f;color:var(--text);outline:none;font-size:14px | |
| } | |
| .btn{padding:14px 18px;border:none;border-radius:14px;background:var(--orange);color:white;font-weight:700;cursor:pointer;font-size:14px} | |
| .btn.secondary{background:transparent;border:1px solid var(--line);color:var(--text)} | |
| .dashboard-shell,.review-shell{max-width:1440px;margin:0 auto} | |
| .review-toolbar{display:flex;justify-content:space-between;align-items:center;padding:14px 20px;margin-bottom:18px} | |
| .review-toolbar .left{display:flex;align-items:center;gap:18px} | |
| .back{cursor:pointer;color:var(--sub)} | |
| .review-toolbar h2{font-size:22px;margin:0} | |
| .review-toolbar p{margin:2px 0 0;color:var(--sub);font-size:13px} | |
| .stats{display:flex;gap:24px;align-items:center;font-size:13px;color:var(--sub)} | |
| .stats strong{display:block;color:var(--text);font-size:16px} | |
| .progress{width:140px;height:10px;background:#1a2a42;border-radius:999px;overflow:hidden} | |
| .progress span{display:block;height:100%;width:2%;background:linear-gradient(90deg,var(--orange),#ffc56e)} | |
| .review-grid{display:grid;grid-template-columns:1.55fr .72fr .72fr;gap:18px} | |
| .review-col{padding:18px} | |
| .chips{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px} | |
| .chip{padding:6px 10px;border-radius:10px;font-size:12px;font-weight:600} | |
| .chip.blue{background:rgba(70,136,255,.18);color:#87b0ff;border:1px solid rgba(70,136,255,.35)} | |
| .chip.purple{background:rgba(139,92,246,.18);color:#c1a9ff;border:1px solid rgba(139,92,246,.35)} | |
| .video-box{height:360px;border-radius:18px;background:linear-gradient(180deg,#3567e8 0%,#2d58c4 60%,#1e3f95 100%);display:grid;place-items:center;font-size:110px;font-weight:800;letter-spacing:-2px;position:relative;overflow:hidden} | |
| .video-box:before{content:"";position:absolute;inset:auto 0 0 0;height:92px;background:linear-gradient(180deg, rgba(0,0,0,0), rgba(0,0,0,.45));} | |
| .playbar{display:flex;align-items:center;gap:14px;padding:14px 10px 2px;color:white;font-size:13px} | |
| .playline{flex:1;height:5px;background:rgba(255,255,255,.28);border-radius:999px;overflow:hidden} | |
| .playline span{display:block;width:33%;height:100%;background:var(--orange)} | |
| .meta-list{display:grid;gap:10px;margin-top:14px;color:#d8e0ee} | |
| .meta-item{display:flex;gap:10px;align-items:center;font-size:15px} | |
| .evidence-card{padding:14px;border-radius:16px;background:#0a1323;border:1px solid var(--line);margin-bottom:12px} | |
| .event-thumb{height:160px;border-radius:14px;background:linear-gradient(180deg,#20293b,#181f2e);display:grid;place-items:center;font-size:42px;font-weight:700} | |
| .event-thumb.red{background:#e31f25} | |
| .event-thumb.blue{background:linear-gradient(180deg,#4d73d6,#4065c6)} | |
| .evidence-title{font-size:15px;font-weight:700;margin:12px 0 6px} | |
| .muted{color:var(--sub);font-size:13px} | |
| textarea{height:236px;resize:none} | |
| .tags{display:flex;flex-wrap:wrap;gap:10px;margin-top:14px} | |
| .tag{padding:8px 12px;border-radius:12px;border:1px solid var(--line);background:#0a1323;color:var(--text);cursor:pointer;font-size:13px} | |
| .tag.active{border-color:rgba(255,122,20,.55);background:rgba(255,122,20,.14)} | |
| .actions{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:18px} | |
| .action{padding:18px;border-radius:16px;font-size:18px;font-weight:700;border:1px solid transparent;cursor:pointer;display:flex;justify-content:space-between;align-items:center} | |
| .action small{font-size:13px;opacity:.85} | |
| .dismiss{background:rgba(239,68,68,.08);border-color:rgba(239,68,68,.35);color:#ff9d9d} | |
| .escalate{background:rgba(247,201,72,.09);border-color:rgba(247,201,72,.35);color:#ffe08d} | |
| .approve{background:rgba(34,197,94,.08);border-color:rgba(34,197,94,.35);color:#86f2ae} | |
| .tip{font-size:13px;color:var(--sub);margin-top:12px} | |
| .notes-header{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px} | |
| .voice-btn{display:inline-flex;align-items:center;gap:8px;padding:10px 12px;border-radius:12px;border:1px solid rgba(255,122,20,.35);background:rgba(255,122,20,.12);color:#ffd0ad;font-size:13px;font-weight:700;cursor:pointer} | |
| .voice-btn.recording{background:rgba(239,68,68,.16);border-color:rgba(239,68,68,.42);color:#ffb7b7} | |
| .helper-copy{font-size:12px;line-height:1.45;color:var(--sub);margin-bottom:10px} | |
| .voice-status{margin-top:10px;padding:10px 12px;border-radius:12px;border:1px dashed rgba(255,122,20,.35);background:rgba(255,122,20,.06);font-size:12px;color:#ffd0ad} | |
| .transcript-preview{margin-top:10px;padding:12px;border-radius:14px;border:1px solid var(--line);background:#0a1323;font-size:13px;line-height:1.45;color:#d8e0ee} | |
| .transcript-preview strong{display:block;font-size:11px;color:var(--sub);margin-bottom:6px;text-transform:uppercase;letter-spacing:.04em} | |
| .footer-meta{margin-top:18px;padding-top:14px;border-top:1px solid var(--line);font-size:13px;color:var(--sub);display:grid;grid-template-columns:1fr auto;gap:10px} | |
| .toast{position:fixed;right:22px;bottom:22px;padding:14px 16px;border-radius:14px;background:#101d31;border:1px solid var(--line);box-shadow:0 12px 30px rgba(0,0,0,.3);display:none} | |
| .toast.show{display:block} | |
| @media (max-width: 1180px){ | |
| .metrics,.violations,.review-grid,.triage-grid,.actions{grid-template-columns:1fr 1fr} | |
| .review-grid .review-col:first-child{grid-column:1/-1} | |
| } | |
| @media (max-width: 820px){ | |
| .metrics,.violations,.triage-grid,.review-grid,.actions{grid-template-columns:1fr} | |
| .video-box{font-size:72px;height:260px} | |
| .topbar,.review-toolbar{flex-direction:column;align-items:flex-start;gap:14px} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="loginScreen" class="login-wrap"> | |
| <div class="login card"> | |
| <div class="brand"> | |
| <div class="logo">๐ฅ</div> | |
| <div> | |
| <h1>TrafficAI Review System</h1> | |
| <p>Traffic Enforcement Portal</p> | |
| </div> | |
| </div> | |
| <label>Officer ID</label> | |
| <input id="officerId" value="TPO-2847" /> | |
| <label>Password</label> | |
| <input type="password" value="password" /> | |
| <div style="display:flex;gap:10px;margin-top:18px"> | |
| <button class="btn" onclick="enterDashboard()">Sign In</button> | |
| <button class="btn secondary" onclick="enterDashboard()">Use Demo</button> | |
| </div> | |
| <p style="margin:14px 0 0;color:var(--sub);font-size:12px">Demo shortcut: press Enter to sign in.</p> | |
| </div> | |
| </div> | |
| <div id="app" class="app hidden"> | |
| <div id="dashboardScreen" class="dashboard-shell"> | |
| <div class="topbar card"> | |
| <div class="brand"> | |
| <div class="logo">๐ฅ</div> | |
| <div> | |
| <h1>TrafficAI Review System</h1> | |
| <p>Traffic Enforcement Dashboard</p> | |
| </div> | |
| </div> | |
| <div class="user-badge"> | |
| <div style="text-align:right"> | |
| <div style="font-size:12px;color:var(--sub)">Officer</div> | |
| <div id="officerLabel" style="font-size:22px;font-weight:800">TPO-2847</div> | |
| </div> | |
| <div class="avatar">TP</div> | |
| </div> | |
| </div> | |
| <div class="metrics"> | |
| <div class="metric card"> | |
| <div class="accent"></div> | |
| <h3>Pending Reviews</h3> | |
| <div class="value" id="pendingValue">1626</div> | |
| <div class="note">Across all violation types</div> | |
| </div> | |
| <div class="metric green card"> | |
| <div class="accent"></div> | |
| <h3>Today Reviewed</h3> | |
| <div class="value" id="reviewedValue">127</div> | |
| <div class="note">+23% from yesterday</div> | |
| </div> | |
| <div class="metric blue card"> | |
| <div class="accent"></div> | |
| <h3>Avg Review Time</h3> | |
| <div class="value" id="avgValue">7.8s</div> | |
| <div class="note">Target: < 10s</div> | |
| </div> | |
| <div class="metric purple card"> | |
| <div class="accent"></div> | |
| <h3>System Accuracy</h3> | |
| <div class="value">93.2%</div> | |
| <div class="note">AI-human agreement</div> | |
| </div> | |
| </div> | |
| <div class="searchbar card"> | |
| <input placeholder="Search violation types, case IDs, or locations..." /> | |
| </div> | |
| <div class="triage card"> | |
| <div class="section-title">Suggested triage lanes</div> | |
| <div class="triage-grid"> | |
| <div class="lane fast"> | |
| <h4>Fast lane</h4> | |
| <p>High-confidence cases with complete evidence. Best path for sub-10 second review.</p> | |
| </div> | |
| <div class="lane standard"> | |
| <h4>Standard lane</h4> | |
| <p>Normal review flow for cases that need officer validation but have no major risk flags.</p> | |
| </div> | |
| <div class="lane exception"> | |
| <h4>Exception lane</h4> | |
| <p>Poor visibility, emergency vehicle, unclear signage, or low OCR confidence.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="section-title">Violation Types - Quick Access</div> | |
| <div class="violations" id="violationGrid"></div> | |
| </div> | |
| </div> | |
| <div id="reviewScreen" class="review-shell hidden"> | |
| <div class="review-toolbar card"> | |
| <div class="left"> | |
| <div class="back" onclick="goDashboard()">โ Dashboard</div> | |
| <div> | |
| <h2 id="reviewTitle">Red Light Running</h2> | |
| <p id="caseCounter">Case 1 of 50</p> | |
| </div> | |
| </div> | |
| <div class="stats"> | |
| <div><span>Avg Review Time</span><strong id="sessionAvg">0.0s</strong></div> | |
| <div><span>Reviewed</span><strong id="sessionReviewed">0</strong></div> | |
| <div> | |
| <span>Progress</span> | |
| <div class="progress"><span id="reviewProgress"></span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="review-grid"> | |
| <div class="review-col panel"> | |
| <div class="chips"> | |
| <div class="chip blue" id="aiConfidence">AI: 83.5%</div> | |
| <div class="chip purple" id="plateConfidence">Plate: 93%</div> | |
| </div> | |
| <div class="video-box" id="videoStage">Vehicle</div> | |
| <div class="playbar"> | |
| <div>โถ</div> | |
| <div class="playline"><span></span></div> | |
| <div id="clipTime">0:03 / 0:08</div> | |
| </div> | |
| <div class="meta-list"> | |
| <div class="meta-item">๐ <span id="caseLocation">Banjara Hills Signal</span></div> | |
| <div class="meta-item">๐ท <span id="casePlate">TS 69 BP 4715</span></div> | |
| <div class="meta-item">๐ <span id="caseTimestamp">4/20/2026, 8:54:54 PM</span></div> | |
| <div class="meta-item">๐ฆ Signal state: <strong>Red</strong> ยท Stop line crossed: <strong>Yes</strong></div> | |
| </div> | |
| <div class="actions"> | |
| <button class="action dismiss" onclick="takeAction('dismiss')">Dismiss <small>โ A</small></button> | |
| <button class="action escalate" onclick="takeAction('escalate')">Escalate <small>โ S</small></button> | |
| <button class="action approve" onclick="takeAction('approve')">Issue Citation <small>D โ</small></button> | |
| </div> | |
| <div class="tip">๐ก Keyboard shortcuts: A = Dismiss ยท S = Escalate ยท D = Issue Citation</div> | |
| </div> | |
| <div class="review-col panel"> | |
| <div class="section-title" style="margin-top:0">Evidence Trail</div> | |
| <div class="evidence-card"> | |
| <div class="event-thumb">Approach</div> | |
| <div class="evidence-title">Vehicle approaching intersection</div> | |
| <div class="muted">Context frame ยท 0.0s</div> | |
| </div> | |
| <div class="evidence-card"> | |
| <div class="event-thumb red">Violation</div> | |
| <div class="evidence-title">Stop line crossed on red</div> | |
| <div class="muted">Violation frame ยท 2.5s</div> | |
| </div> | |
| <div class="evidence-card"> | |
| <div class="event-thumb blue" style="font-size:32px">TS 69 BP</div> | |
| <div class="evidence-title">Plate crop</div> | |
| <div class="muted">OCR: TS69BP4715 ยท Confidence 93%</div> | |
| </div> | |
| </div> | |
| <div class="review-col panel"> | |
| <div class="notes-header"> | |
| <div class="section-title" style="margin:0">Review Notes</div> | |
| <button id="voiceBtn" class="voice-btn" onclick="toggleVoiceNote()">๐ Start voice note</button> | |
| </div> | |
| <div class="helper-copy">Use quick tags for common reasons and a 3โ5 second voice note for dismiss or escalate cases so rationale capture stays within the <10 second target.</div> | |
| <textarea id="notes" placeholder="Add notes about this case... or tap Start voice note"></textarea> | |
| <div id="voiceStatus" class="voice-status">Ready for short dictation. Best used for exception cases that need audit-ready reasoning.</div> | |
| <div id="transcriptPreview" class="transcript-preview"><strong>Latest transcript</strong><span id="transcriptText">No voice note captured yet.</span></div> | |
| <div class="section-title" style="margin:16px 0 10px">Quick Tags</div> | |
| <div class="tags"> | |
| <button class="tag" onclick="toggleTag(this)">Clear violation</button> | |
| <button class="tag" onclick="toggleTag(this)">Unclear</button> | |
| <button class="tag" onclick="toggleTag(this)">Emergency vehicle</button> | |
| <button class="tag" onclick="toggleTag(this)">Poor visibility</button> | |
| <button class="tag" onclick="toggleTag(this)">Plate mismatch</button> | |
| </div> | |
| <div class="footer-meta"> | |
| <div>Case ID: <span id="caseId">CASE-1776709784805-0</span></div> | |
| <div>Current Time: <span id="timerValue">46.5s</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="toast" class="toast"></div> | |
| <script> | |
| const violations = [ | |
| { name:'Red Light Running', count:234, time:'7.2s', accuracy:'94.5%', progress:94 }, | |
| { name:'Speeding', count:456, time:'6.8s', accuracy:'96.2%', progress:96 }, | |
| { name:'Illegal Parking', count:189, time:'8.1s', accuracy:'91.3%', progress:91 }, | |
| { name:'Wrong Way', count:98, time:'9.2s', accuracy:'89.7%', progress:89 }, | |
| { name:'No Helmet', count:167, time:'5.9s', accuracy:'97.1%', progress:97 }, | |
| { name:'Triple Riding', count:145, time:'7.5s', accuracy:'92.8%', progress:92 }, | |
| { name:'Mobile While Driving', count:203, time:'8.7s', accuracy:'88.4%', progress:88 }, | |
| { name:'No Seatbelt', count:134, time:'6.3s', accuracy:'95.6%', progress:95 } | |
| ]; | |
| const cases = [ | |
| { | |
| title:'Red Light Running', ai:'83.5%', plate:'93%', location:'Banjara Hills Signal', plateText:'TS 69 BP 4715', | |
| timestamp:'4/20/2026, 8:54:54 PM', caseId:'CASE-1776709784805-0', stage:'Vehicle', clip:'0:03 / 0:08', | |
| voiceNote:'Driver entered the intersection after the light turned red. Plate readable and stop line clearly crossed.' | |
| }, | |
| { | |
| title:'Speeding', ai:'91.2%', plate:'96%', location:'Outer Ring Road - Exit 4', plateText:'TS 08 CR 2201', | |
| timestamp:'4/20/2026, 9:02:11 PM', caseId:'CASE-1776709784805-1', stage:'Speed', clip:'0:02 / 0:06', | |
| voiceNote:'Radar overlay matches vehicle lane. Speed above posted limit and plate OCR is high confidence.' | |
| }, | |
| { | |
| title:'No Helmet', ai:'88.9%', plate:'90%', location:'Madhapur Main Road', plateText:'TS 11 DU 8877', | |
| timestamp:'4/20/2026, 9:07:40 PM', caseId:'CASE-1776709784805-2', stage:'Rider', clip:'0:01 / 0:05', | |
| voiceNote:'Rider head is fully visible without helmet. No obstruction and number plate remains readable.' | |
| } | |
| ]; | |
| let state = { | |
| officer:'TPO-2847', | |
| pending:1626, | |
| reviewed:127, | |
| avg:7.8, | |
| currentCase:0, | |
| sessionReviewed:0, | |
| sessionSeconds:0, | |
| timer:46.5, | |
| recording:false | |
| }; | |
| function renderViolations(){ | |
| const grid = document.getElementById('violationGrid'); | |
| grid.innerHTML = violations.map((v, i) => ` | |
| <div class="violation-card card" data-index="${i}"> | |
| <div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start"> | |
| <div> | |
| <h4>${v.name}</h4> | |
| <div style="color:var(--sub);font-size:13px">Click to review cases</div> | |
| </div> | |
| <div class="badge">${v.count}</div> | |
| </div> | |
| <div class="violation-meta"><span>Avg Time</span><strong style="color:var(--text)">${v.time}</strong></div> | |
| <div class="violation-meta"><span>Accuracy</span><strong style="color:var(--text)">${v.accuracy}</strong></div> | |
| <div class="bar"><span style="width:${v.progress}%"></span></div> | |
| </div> | |
| `).join(''); | |
| grid.querySelectorAll('.violation-card').forEach(card => { | |
| card.addEventListener('click', () => openReviewByIndex(Number(card.dataset.index))); | |
| }); | |
| } | |
| function showToast(message){ | |
| const toast = document.getElementById('toast'); | |
| toast.textContent = message; | |
| toast.classList.add('show'); | |
| clearTimeout(showToast._t); | |
| showToast._t = setTimeout(() => toast.classList.remove('show'), 1800); | |
| } | |
| function enterDashboard(){ | |
| state.officer = document.getElementById('officerId').value || 'TPO-2847'; | |
| document.getElementById('officerLabel').textContent = state.officer; | |
| document.getElementById('loginScreen').classList.add('hidden'); | |
| document.getElementById('app').classList.remove('hidden'); | |
| renderViolations(); | |
| } | |
| function goDashboard(){ | |
| document.getElementById('reviewScreen').classList.add('hidden'); | |
| document.getElementById('dashboardScreen').classList.remove('hidden'); | |
| } | |
| function openReviewByIndex(index){ | |
| state.currentCase = index % cases.length; | |
| hydrateCase(); | |
| document.getElementById('dashboardScreen').classList.add('hidden'); | |
| document.getElementById('reviewScreen').classList.remove('hidden'); | |
| } | |
| function hydrateCase(){ | |
| const c = cases[state.currentCase]; | |
| document.getElementById('reviewTitle').textContent = c.title; | |
| document.getElementById('caseCounter').textContent = `Case ${state.currentCase + 1} of 50`; | |
| document.getElementById('aiConfidence').textContent = `AI: ${c.ai}`; | |
| document.getElementById('plateConfidence').textContent = `Plate: ${c.plate}`; | |
| document.getElementById('videoStage').textContent = c.stage; | |
| document.getElementById('caseLocation').textContent = c.location; | |
| document.getElementById('casePlate').textContent = c.plateText; | |
| document.getElementById('caseTimestamp').textContent = c.timestamp; | |
| document.getElementById('caseId').textContent = c.caseId; | |
| document.getElementById('clipTime').textContent = c.clip; | |
| document.getElementById('sessionReviewed').textContent = state.sessionReviewed; | |
| document.getElementById('sessionAvg').textContent = state.sessionReviewed ? `${(state.sessionSeconds/state.sessionReviewed).toFixed(1)}s` : '0.0s'; | |
| document.getElementById('reviewProgress').style.width = `${Math.max(2, (state.sessionReviewed / 50) * 100)}%`; | |
| document.getElementById('timerValue').textContent = `${state.timer.toFixed(1)}s`; | |
| document.getElementById('notes').value = ''; | |
| document.getElementById('transcriptText').textContent = 'No voice note captured yet.'; | |
| document.getElementById('voiceStatus').textContent = 'Ready for short dictation. Best used for exception cases that need audit-ready reasoning.'; | |
| state.recording = false; | |
| const voiceBtn = document.getElementById('voiceBtn'); | |
| voiceBtn.textContent = '๐ Start voice note'; | |
| voiceBtn.classList.remove('recording'); | |
| document.querySelectorAll('.tag').forEach(t => t.classList.remove('active')); | |
| } | |
| function toggleVoiceNote(){ | |
| const btn = document.getElementById('voiceBtn'); | |
| const status = document.getElementById('voiceStatus'); | |
| const transcriptText = document.getElementById('transcriptText'); | |
| const notes = document.getElementById('notes'); | |
| const c = cases[state.currentCase]; | |
| if (!state.recording) { | |
| state.recording = true; | |
| btn.textContent = 'โบ Recording...'; | |
| btn.classList.add('recording'); | |
| status.textContent = 'Listening... speak a short reason such as โclear violation, plate readable, issue citation.โ'; | |
| clearTimeout(toggleVoiceNote._timer); | |
| toggleVoiceNote._timer = setTimeout(() => { | |
| state.recording = false; | |
| btn.textContent = '๐ Add another voice note'; | |
| btn.classList.remove('recording'); | |
| status.textContent = 'Voice note captured and transcribed. This note will be saved with the audit trail.'; | |
| transcriptText.textContent = c.voiceNote; | |
| notes.value = c.voiceNote; | |
| showToast('Voice note transcribed'); | |
| }, 1200); | |
| } else { | |
| clearTimeout(toggleVoiceNote._timer); | |
| state.recording = false; | |
| btn.textContent = '๐ Add voice note'; | |
| btn.classList.remove('recording'); | |
| status.textContent = 'Voice note stopped. Tap again to capture a new 3โ5 second note.'; | |
| } | |
| } | |
| function takeAction(type){ | |
| const labels = { | |
| approve:'Citation issued', | |
| dismiss:'Case dismissed', | |
| escalate:'Case escalated for manual review' | |
| }; | |
| state.pending -= 1; | |
| state.reviewed += 1; | |
| state.sessionReviewed += 1; | |
| state.sessionSeconds += type === 'escalate' ? 11.2 : (type === 'dismiss' ? 6.4 : 7.1); | |
| state.avg = Math.max(6.8, ((state.avg * (state.reviewed - 1)) + (state.sessionSeconds / state.sessionReviewed)) / state.reviewed).toFixed(1); | |
| document.getElementById('pendingValue').textContent = state.pending; | |
| document.getElementById('reviewedValue').textContent = state.reviewed; | |
| document.getElementById('avgValue').textContent = `${state.avg}s`; | |
| showToast(labels[type]); | |
| state.currentCase = (state.currentCase + 1) % cases.length; | |
| state.timer = 42 + Math.random() * 8; | |
| hydrateCase(); | |
| } | |
| function toggleTag(el){ el.classList.toggle('active'); } | |
| document.addEventListener('keydown', (e) => { | |
| if (!document.getElementById('app').classList.contains('hidden') && document.getElementById('dashboardScreen').classList.contains('hidden')) { | |
| if (e.key.toLowerCase() === 'a') takeAction('dismiss'); | |
| if (e.key.toLowerCase() === 's') takeAction('escalate'); | |
| if (e.key.toLowerCase() === 'd') takeAction('approve'); | |
| } | |
| if (document.getElementById('loginScreen').classList.contains('hidden') === false && e.key === 'Enter') enterDashboard(); | |
| }); | |
| renderViolations(); | |
| </script> | |
| </body> | |
| </html> |