/** * TraceScene — Frontend Application * * Single-page application for accident case management, * photo upload, AI analysis, and report viewing. */ const API_BASE = '/api'; // ── Mock Backend Interceptor ─────────────────────────────────────────── // This simulates the AI backend purely in the browser for the public UI demo const originalFetch = window.fetch; let mockNextId = 104; // Pre-populate some demo cases let mockCases = { 101: { case: { id: 101, case_number: "INV-2026-042", officer_name: "Sgt. Miller", location: "Hwy 101 N", incident_date: "2026-03-01", status: "complete", created_at: "2026-03-01T08:14:00Z", photo_count: 3 }, photos: [ { id: "img_demo_1", filename: "01a252ba0708_car_8.jpg", blob_url: "images/demo/01a252ba0708_car_8.jpg" }, { id: "img_demo_2", filename: "bb7313c8f601_car_10.jpg", blob_url: "images/demo/bb7313c8f601_car_10.jpg" }, { id: "img_demo_3", filename: "413495119937_accident_6.jpg", blob_url: "images/demo/413495119937_accident_6.jpg" } ], analyses: [ { filename: "01a252ba0708_car_8.jpg", raw_analysis: `[AI Observation] Vehicles Involved: 1 Make/Model: 2018 Toyota Highlander (Silver SUV) License Plate: CA 8XYZ123 Owner Details: Sarah Jenkins (DOB: 1985-11-20), Policy Status: ACTIVE [Condition Assessment] Tyre Condition: Front-Left (Good, 6mm tread), Front-Right (Good, 6mm tread), Rears (Worn, 3mm tread) [Damage Analysis & Insurance Assessment] - Front Bumper: Severe crush damage (Intensity: High). Insurance Clause 4A (Collision) -> FULLY COVERED - Radiator & Core Support: Destroyed (Intensity: Critical). Insurance Clause 4A (Collision) -> FULLY COVERED - Right Fender: Crumpled. Insurance Clause 4A (Collision) -> FULLY COVERED - Airbag Deployment: Driver and Passenger front active. Clause 5B (Safety Systems) -> COVERED [Summary] Severe front-end deformation observed consistent with high-speed frontal impact. Airbags deployed. Vehicle is likely a total loss.` } ], violations: [ { rule_id: "SPEED-001", rule_title: "Excessive Speed", severity: "CRITICAL", confidence: 0.92, party_label: "Silver SUV", evidence_summary: "Long skid marks and severe front-end crush profile indicate speeds greater than safe for conditions." }, { rule_id: "FOLLOW-001", rule_title: "Following Too Closely (Tailgating)", severity: "HIGH", confidence: 0.96, party_label: "Silver SUV", evidence_summary: "Impact pattern consistent with rear-ending a leading vehicle without sufficient braking distance." } ], fault_analysis: { determined: true, primary_fault_party: "Silver SUV", probable_cause: "Following too closely combined with excessive speed.", fault_distribution_json: { "Silver SUV": 100, "Other": 0 }, overall_confidence: 0.94, analysis_summary: "Based on severe front-end damage and skid marks, the Silver SUV is entirely at fault for failing to maintain a safe following distance and speed." } }, 102: { case: { id: 102, case_number: "INV-2026-043", officer_name: "Off. Davis", location: "Downtown Intersection", incident_date: "2026-03-02", status: "complete", created_at: "2026-03-02T10:30:00Z", photo_count: 2 }, photos: [ { id: "img_demo_4", filename: "a358e234c528_accident_scene_2.jpg", blob_url: "images/demo/a358e234c528_accident_scene_2.jpg" }, { id: "img_demo_5", filename: "e05bd82a2cfa_accident_9.jpg", blob_url: "images/demo/e05bd82a2cfa_accident_9.jpg" } ], analyses: [ { filename: "a358e234c528_accident_scene_2.jpg", raw_analysis: `[AI Observation] Vehicles Involved: 2 Vehicle A Make/Model: 2020 Honda Civic (Blue Sedan) Vehicle A License Plate: TX LMN-4567 Vehicle A Owner: David Chen (Policy: ABC-98765-Active) Vehicle B Make/Model: 2015 Ford F-150 (White Truck) Vehicle B License Plate: TX PQR-8910 Vehicle B Owner: M&M Deliveries Inc. (Commercial Policy: C-4455-Active) [Condition Assessment] Vehicle A Tyre Condition: All 4 Good (>5mm tread) Vehicle B Tyre Condition: Rear Left Bald (<2mm tread) - Potential hazard. [Damage Analysis & Insurance Assessment - Vehicle A] - Driver Side Doors (Front & Rear): Severe side impact damage (Intensity: High). Insurance Clause 2B (Not-at-fault Collision) -> SUBROGATION CLAIM - B-Pillar: Structural compromise. -> SUBROGATION CLAIM [Damage Analysis & Insurance Assessment - Vehicle B] - Front Bumper/Grille: Moderate damage (Intensity: Medium). Commercial Clause 1A (At-fault Liability) -> SUBJECT TO 000 DEDUCTIBLE [Summary] T-bone intersection collision. Blue Sedan sustained heavy side impact; White Truck shows front-end damage.` } ], violations: [ { rule_id: "SIGNAL-001", rule_title: "Red Light Violation", severity: "CRITICAL", confidence: 0.95, party_label: "White Truck", evidence_summary: "Point of impact and traffic cam footage suggests White Truck entered intersection after signal turned red." }, { rule_id: "MAINT-002", rule_title: "Defective Tires", severity: "HIGH", confidence: 0.88, party_label: "White Truck", evidence_summary: "Rear left tire on White Truck is severely worn (bald), reducing braking efficacy." } ], fault_analysis: { determined: true, primary_fault_party: "White Truck", probable_cause: "Failure to stop at red light.", fault_distribution_json: { "White Truck": 90, "Blue Sedan": 10 }, overall_confidence: 0.91, analysis_summary: "White Truck (Vehicle B) failed to stop for a red signal, directly causing the T-bone collision with the Blue Sedan. The bald tire on the truck exacerbated the inability to stop in time." } }, 103: { case: { id: 103, case_number: "INV-2026-044", officer_name: "Sgt. Miller", location: "Main St Bridge", incident_date: "2026-03-02", status: "ready", created_at: "2026-03-02T14:45:00Z", photo_count: 0 }, photos: [], analyses: [], violations: [], fault_analysis: null }, 104: { case: { id: 104, case_number: "INV-2026-045", officer_name: "Cpl. Ramirez", location: "I-80 Westbound", incident_date: "2026-03-03", status: "complete", created_at: "2026-03-03T09:15:00Z", photo_count: 1 }, photos: [ { id: "img_demo_104", filename: "car_10.jpg", blob_url: "images/demo/car_10.jpg" } ], analyses: [ { filename: "car_10.jpg", raw_analysis: `[AI Observation] Vehicles Involved: 1 Make/Model: 2022 Subaru Outback (Green) License Plate: NV 455-XYZ Owner Details: Elena Gomez, Policy #SUB-789-NV (Active) [Condition Assessment] Tyre Condition: Front right tire blown out. Other tires Good. Environmental factors: Wet road surface. [Damage Analysis & Insurance Assessment] - Front Right Quarter Panel: Scraped and dented (Intensity: Medium). Clause 4A -> FULLY COVERED - Front Right Wheel/Suspension: Broken control arm (Intensity: High). Clause 4A -> FULLY COVERED - Tire blowout: Not Covered (Mechanical wear exculsion Clause 7) [Summary] Single vehicle incident. Vehicle appears to have suffered a front right tire blowout leading to a collision with the highway barrier.` } ], violations: [ { rule_id: "SPEED-002", rule_title: "Too Fast for Conditions", severity: "HIGH", confidence: 0.78, party_label: "Green Subaru", evidence_summary: "Wet road surface indicates speed should have been reduced prior to loss of control." } ], fault_analysis: { determined: true, primary_fault_party: "Green Subaru", probable_cause: "Tire Blowout and exceeding safe speed for wet conditions.", fault_distribution_json: { "Green Subaru": 100 }, overall_confidence: 0.85, analysis_summary: "Driver lost control following a tire failure on wet pavement, striking the concrete barrier." } }, 105: { case: { id: 105, case_number: "INV-2026-046", officer_name: "Off. Jackson", location: "Elm Street Pedestrian Crossing", incident_date: "2026-03-03", status: "complete", created_at: "2026-03-03T11:20:00Z", photo_count: 1 }, photos: [ { id: "img_demo_105", filename: "accident_6.jpg", blob_url: "images/demo/accident_6.jpg" } ], analyses: [ { filename: "accident_6.jpg", raw_analysis: `[AI Observation] Vehicles Involved: 1 Make/Model: 2017 Nissan Altima (Black) License Plate: NY JKL-0123 Owner Details: Marcus Johnson, Policy NY-1122-ACTIVE Non-vehicle Parties: 1 Pedestrian [Condition Assessment] Tyre Condition: All tires adequate. [Damage Analysis & Insurance Assessment] - Front Hood & Windshield: Dented hood, spider-web shattered windshield (Intensity: High). Clause 4A -> COVERED - Pedestrian Injury Liability: Clause 1ABodily Injury -> COVERAGE TRIGGERED UP TO 50k LIMIT [Summary] Vehicle collided with a pedestrian on a marked crosswalk. Significant hood and windshield damage typical of a pedestrian strike at ~25-30mph.` } ], violations: [ { rule_id: "PEDES-003", rule_title: "Driver Failure to Stop for Pedestrian in Crosswalk", severity: "CRITICAL", confidence: 0.98, party_label: "Black Altima", evidence_summary: "Impact occurred directly on marked crosswalk paint." }, { rule_id: "SPEED-005", rule_title: "Failure to Reduce Speed in School Zone", severity: "CRITICAL", confidence: 0.90, party_label: "Black Altima", evidence_summary: "School zone markings active; impact damage indicates speed > 25mph." } ], fault_analysis: { determined: true, primary_fault_party: "Black Altima", probable_cause: "Failure to yield to pedestrian and speeding in school zone.", fault_distribution_json: { "Black Altima": 100, "Pedestrian": 0 }, overall_confidence: 0.99, analysis_summary: "Driver of the Black Altima is completely at fault for striking a pedestrian in a designated school zone crosswalk while failing to reduce speed." } } }; window.fetch = async (url, options = {}) => { // Only intercept our API calls if (typeof url === 'string' && url.startsWith('/api')) { console.log(`[Mock Demo API] ${options.method || 'GET'} ${url}`); // Helper to delay response const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); // 1. Health check if (url.match(/\/api\/health$/)) { return new Response(JSON.stringify({ model_loaded: true, device: "Browser Demo", rules_loaded: 43 }), { status: 200 }); } // 2. Get cases (list) — renderCases expects flat objects: [{id, case_number, ...}] if (url.match(/\/api\/cases$/) && (options.method === 'GET' || !options.method)) { const casesList = Object.values(mockCases).map(c => c.case); return new Response(JSON.stringify({ cases: casesList }), { status: 200 }); } // 3. Create case if (url.match(/\/api\/cases$/) && options.method === 'POST') { const formData = options.body; const newCase = { id: mockNextId++, case_number: formData.get('case_number'), officer_name: formData.get('officer_name'), location: formData.get('location'), incident_date: formData.get('incident_date'), notes: formData.get('notes'), status: 'new', created_at: new Date().toISOString() }; mockCases[newCase.id] = { case: newCase, photos: [], violations: [], fault_analysis: null, analyses: [] }; return new Response(JSON.stringify(newCase), { status: 200 }); } // 3.5 Delete case if (url.match(/\/api\/cases\/\d+$/) && options.method === 'DELETE') { const id = parseInt(url.split('/').pop()); delete mockCases[id]; return new Response(JSON.stringify({ detail: "Deleted" }), { status: 200 }); } // 4. Upload photos if (url.match(/\/api\/cases\/\d+\/photos$/) && options.method === 'POST') { const match = url.match(/\/api\/cases\/(\d+)\/photos$/); const id = parseInt(match[1]); const formData = options.body; const files = formData.getAll('files'); await delay(800); // Simulate upload files.forEach(f => { mockCases[id].photos.push({ id: Math.random().toString(36).substr(2, 9), filename: f.name, blob_url: URL.createObjectURL(f) }); }); mockCases[id].case.photo_count = mockCases[id].photos.length; mockCases[id].case.status = 'ready'; return new Response(JSON.stringify({ count: files.length }), { status: 200 }); } // 5. Update case (PUT) if (url.match(/\/api\/cases\/\d+$/) && options.method === 'PUT') { const id = parseInt(url.split('/').pop()); if (!mockCases[id]) return new Response(JSON.stringify({ detail: "Not found" }), { status: 404 }); const formData = options.body; if (formData.get('officer_name')) mockCases[id].case.officer_name = formData.get('officer_name'); if (formData.get('location')) mockCases[id].case.location = formData.get('location'); if (formData.get('incident_date')) mockCases[id].case.incident_date = formData.get('incident_date'); if (formData.get('notes')) mockCases[id].case.notes = formData.get('notes'); return new Response(JSON.stringify(mockCases[id].case), { status: 200 }); } // 5b. Get case details — renderCaseDetail expects {case: {...}, photos: [...], ...} if (url.match(/\/api\/cases\/\d+$/) && (options.method === 'GET' || !options.method)) { const id = parseInt(url.split('/').pop()); if (!mockCases[id]) return new Response(JSON.stringify({ detail: "Not found" }), { status: 404 }); return new Response(JSON.stringify(mockCases[id]), { status: 200 }); } // 5c. Rules if (url.match(/\/api\/rules$/) && (options.method === 'GET' || !options.method)) { return new Response(JSON.stringify({ "categories": [{ "id": "SIGNAL", "name": "Traffic Signal Violations", "rule_count": 6, "rules": [{ "id": "SIGNAL-001", "title": "Red Light Violation", "description": "Proceeding through a red traffic signal without stopping", "severity": "CRITICAL", "visual_indicators": ["vehicle past stop line at intersection", "traffic signal showing red", "vehicle in middle of intersection", "collision at signalized intersection"], "fault_weight": 0.95, "applicable_parties": ["moving_vehicle"] }, { "id": "SIGNAL-002", "title": "Failure to Stop at Yellow Signal", "description": "Entering intersection on yellow when safe stop was possible", "severity": "HIGH", "visual_indicators": ["vehicle in intersection", "traffic signal showing yellow", "skid marks before intersection", "late braking evidence"], "fault_weight": 0.7, "applicable_parties": ["moving_vehicle"] }, { "id": "SIGNAL-003", "title": "Failure to Obey Stop Sign", "description": "Failing to come to a complete stop at a stop sign", "severity": "CRITICAL", "visual_indicators": ["stop sign visible", "vehicle past stop line", "no evidence of stopping", "collision near stop sign"], "fault_weight": 0.9, "applicable_parties": ["moving_vehicle"] }, { "id": "SIGNAL-004", "title": "Failure to Obey Yield Sign", "description": "Failing to yield right of way at a yield sign", "severity": "HIGH", "visual_indicators": ["yield sign visible", "vehicle entering without yielding", "merging collision"], "fault_weight": 0.8, "applicable_parties": ["moving_vehicle"] }, { "id": "SIGNAL-005", "title": "Disobeying Traffic Control Device", "description": "Ignoring any official traffic control sign or signal", "severity": "HIGH", "visual_indicators": ["no entry sign visible", "one way sign", "vehicle going wrong direction", "do not enter sign"], "fault_weight": 0.85, "applicable_parties": ["moving_vehicle"] }, { "id": "SIGNAL-006", "title": "Ignoring Railroad Crossing Signal", "description": "Proceeding through an active railroad crossing signal", "severity": "CRITICAL", "visual_indicators": ["railroad crossing", "crossing gates", "flashing lights at crossing"], "fault_weight": 0.95, "applicable_parties": ["moving_vehicle"] }] }, { "id": "LANE", "name": "Lane Discipline Violations", "rule_count": 8, "rules": [{ "id": "LANE-001", "title": "Wrong-Way Driving", "description": "Driving against the direction of traffic on a one-way road or divided highway", "severity": "CRITICAL", "visual_indicators": ["vehicle facing opposite direction", "head-on collision", "vehicle on wrong side of divided highway", "one way sign with vehicle going wrong direction"], "fault_weight": 0.98, "applicable_parties": ["moving_vehicle"] }, { "id": "LANE-002", "title": "Unsafe Lane Change", "description": "Changing lanes without signaling or when unsafe to do so", "severity": "HIGH", "visual_indicators": ["sideswipe damage", "contact marks on sides of vehicles", "vehicles in adjacent lanes", "paint transfer on vehicle sides"], "fault_weight": 0.75, "applicable_parties": ["moving_vehicle"] }, { "id": "LANE-003", "title": "Crossing Double Yellow Line", "description": "Crossing a double yellow center line to pass or turn", "severity": "HIGH", "visual_indicators": ["double yellow line visible", "vehicle across center line", "head-on or angular collision", "vehicle in opposing lane"], "fault_weight": 0.85, "applicable_parties": ["moving_vehicle"] }, { "id": "LANE-004", "title": "Failure to Maintain Lane", "description": "Drifting out of designated travel lane", "severity": "MEDIUM", "visual_indicators": ["vehicle partly in multiple lanes", "tire marks crossing lane lines", "sideswipe on lane boundary", "erratic tire tracks"], "fault_weight": 0.65, "applicable_parties": ["moving_vehicle"] }, { "id": "LANE-005", "title": "Improper Passing", "description": "Passing another vehicle illegally or unsafely", "severity": "HIGH", "visual_indicators": ["passing zone violation", "solid line crossing", "head-on collision in passing zone", "vehicle in oncoming lane"], "fault_weight": 0.8, "applicable_parties": ["moving_vehicle"] }, { "id": "LANE-006", "title": "Driving on Shoulder", "description": "Operating a vehicle on the road shoulder", "severity": "MEDIUM", "visual_indicators": ["vehicle on shoulder", "tire marks on shoulder", "collision on road shoulder", "gravel spray from shoulder"], "fault_weight": 0.6, "applicable_parties": ["moving_vehicle"] }, { "id": "LANE-007", "title": "Improper U-Turn", "description": "Making a U-turn where prohibited or unsafe", "severity": "HIGH", "visual_indicators": ["vehicle turned perpendicular to traffic", "no u-turn sign", "vehicle blocking lanes during turn"], "fault_weight": 0.8, "applicable_parties": ["moving_vehicle"] }, { "id": "LANE-008", "title": "Failure to Use Turn Signal", "description": "Turning or changing lanes without activating turn signal", "severity": "MEDIUM", "visual_indicators": ["no turn signal visible during maneuver", "sudden lane change", "unexpected turn collision"], "fault_weight": 0.55, "applicable_parties": ["moving_vehicle"] }] }, { "id": "SPEED", "name": "Speed Related Violations", "rule_count": 5, "rules": [{ "id": "SPEED-001", "title": "Excessive Speed", "description": "Traveling at a speed greater than posted limit or unsafe for conditions", "severity": "CRITICAL", "visual_indicators": ["long skid marks", "severe impact damage", "extensive debris field", "vehicle displacement after impact", "deployed airbags", "significant vehicle deformation"], "fault_weight": 0.85, "applicable_parties": ["moving_vehicle"] }, { "id": "SPEED-002", "title": "Too Fast for Conditions", "description": "Driving too fast for weather, road, or traffic conditions", "severity": "HIGH", "visual_indicators": ["wet road surface", "skid marks on wet road", "reduced visibility conditions", "loss of control evidence", "single vehicle off-road"], "fault_weight": 0.75, "applicable_parties": ["moving_vehicle"] }, { "id": "SPEED-003", "title": "Speed Too Slow", "description": "Driving unreasonably slow and impeding traffic", "severity": "LOW", "visual_indicators": ["rear-end collision", "slow vehicle on highway", "hazard lights on moving vehicle"], "fault_weight": 0.4, "applicable_parties": ["moving_vehicle"] }, { "id": "SPEED-004", "title": "Racing or Speed Contest", "description": "Engaging in speed competition on public roads", "severity": "CRITICAL", "visual_indicators": ["multiple vehicles with severe damage", "very long skid marks", "extreme vehicle deformation", "vehicle rollover"], "fault_weight": 0.9, "applicable_parties": ["moving_vehicle"] }, { "id": "SPEED-005", "title": "Failure to Reduce Speed in School Zone", "description": "Not reducing speed in a designated school zone", "severity": "CRITICAL", "visual_indicators": ["school zone sign", "school crossing", "pedestrian collision near school", "school zone flashing lights"], "fault_weight": 0.9, "applicable_parties": ["moving_vehicle"] }] }, { "id": "YIELD", "name": "Right of Way Violations", "rule_count": 7, "rules": [{ "id": "YIELD-001", "title": "Failure to Yield at Intersection", "description": "Not yielding right of way to cross traffic at an intersection", "severity": "HIGH", "visual_indicators": ["T-bone collision at intersection", "side impact at intersection", "vehicle entering intersection from side street", "angular collision"], "fault_weight": 0.85, "applicable_parties": ["moving_vehicle"] }, { "id": "YIELD-002", "title": "Failure to Yield When Turning Left", "description": "Not yielding to oncoming traffic when making a left turn", "severity": "HIGH", "visual_indicators": ["left-turning vehicle struck", "front-corner impact on turning vehicle", "collision in intersection during left turn"], "fault_weight": 0.85, "applicable_parties": ["moving_vehicle"] }, { "id": "YIELD-003", "title": "Failure to Yield to Pedestrian", "description": "Not yielding right of way to pedestrian in crosswalk", "severity": "CRITICAL", "visual_indicators": ["pedestrian in crosswalk", "pedestrian struck", "crosswalk markings visible", "pedestrian crossing sign"], "fault_weight": 0.95, "applicable_parties": ["moving_vehicle"] }, { "id": "YIELD-004", "title": "Failure to Yield When Merging", "description": "Not yielding when entering a highway or merging into traffic", "severity": "HIGH", "visual_indicators": ["merge lane collision", "on-ramp collision", "merging vehicle sideswipe", "acceleration lane impact"], "fault_weight": 0.8, "applicable_parties": ["moving_vehicle"] }, { "id": "YIELD-005", "title": "Failure to Yield to Emergency Vehicle", "description": "Not pulling over for an emergency vehicle with lights/sirens", "severity": "CRITICAL", "visual_indicators": ["emergency vehicle involved", "police car, ambulance, or fire truck", "emergency lights visible"], "fault_weight": 0.9, "applicable_parties": ["moving_vehicle"] }, { "id": "YIELD-006", "title": "Failure to Yield at Roundabout", "description": "Not yielding to traffic already in a roundabout", "severity": "HIGH", "visual_indicators": ["roundabout collision", "circular intersection", "vehicle entering roundabout", "yield sign at roundabout"], "fault_weight": 0.8, "applicable_parties": ["moving_vehicle"] }, { "id": "YIELD-007", "title": "Right of Way at Uncontrolled Intersection", "description": "Failure to yield to vehicle on the right at an uncontrolled intersection", "severity": "HIGH", "visual_indicators": ["intersection without signals or signs", "side impact collision", "residential intersection collision"], "fault_weight": 0.7, "applicable_parties": ["moving_vehicle"] }] }, { "id": "FOLLOW", "name": "Following Distance Violations", "rule_count": 2, "rules": [{ "id": "FOLLOW-001", "title": "Following Too Closely (Tailgating)", "description": "Not maintaining a safe following distance behind another vehicle", "severity": "HIGH", "visual_indicators": ["rear-end collision", "front damage on following vehicle", "rear damage on lead vehicle", "chain reaction collision", "short skid marks before impact"], "fault_weight": 0.9, "applicable_parties": ["following_vehicle"] }, { "id": "FOLLOW-002", "title": "Sudden Stop Causing Rear Collision", "description": "Stopping abruptly without justification causing following vehicle to collide", "severity": "MEDIUM", "visual_indicators": ["rear-end collision", "no apparent hazard ahead", "brake lights"], "fault_weight": 0.4, "applicable_parties": ["lead_vehicle"] }] }, { "id": "MAINT", "name": "Vehicle Maintenance Violations", "rule_count": 6, "rules": [{ "id": "MAINT-001", "title": "Defective Brakes", "description": "Operating vehicle with defective or inadequate braking system", "severity": "CRITICAL", "visual_indicators": ["very long skid marks", "no skid marks despite high-speed impact", "brake fluid on road", "brake component debris"], "fault_weight": 0.7, "applicable_parties": ["vehicle_owner"] }, { "id": "MAINT-002", "title": "Defective Tires", "description": "Operating vehicle with bald, damaged, or improper tires", "severity": "HIGH", "visual_indicators": ["tire blowout debris", "bald tires visible", "irregular tire marks", "tire tread separation"], "fault_weight": 0.65, "applicable_parties": ["vehicle_owner"] }, { "id": "MAINT-003", "title": "Defective Lights", "description": "Operating vehicle with non-functional headlights, taillights, or signals", "severity": "MEDIUM", "visual_indicators": ["broken headlight pre-collision", "no tail lights", "nighttime collision with unlit vehicle"], "fault_weight": 0.55, "applicable_parties": ["vehicle_owner"] }, { "id": "MAINT-004", "title": "Obstructed View", "description": "Driving with obstructed windshield or mirrors", "severity": "MEDIUM", "visual_indicators": ["cracked windshield", "obstructed view", "missing mirrors", "tinted windows too dark"], "fault_weight": 0.5, "applicable_parties": ["vehicle_owner"] }, { "id": "MAINT-005", "title": "Unsecured Load", "description": "Carrying unsecured cargo that falls onto roadway", "severity": "HIGH", "visual_indicators": ["cargo on road", "items fallen from vehicle", "truck load spill", "debris from cargo"], "fault_weight": 0.75, "applicable_parties": ["vehicle_owner"] }, { "id": "MAINT-006", "title": "Vehicle Overloaded", "description": "Vehicle loaded beyond safe operating capacity", "severity": "MEDIUM", "visual_indicators": ["visibly overloaded vehicle", "sagging suspension", "items stacked above roof"], "fault_weight": 0.55, "applicable_parties": ["vehicle_owner"] }] }, { "id": "PEDES", "name": "Pedestrian Related Violations", "rule_count": 5, "rules": [{ "id": "PEDES-001", "title": "Pedestrian Jaywalking", "description": "Pedestrian crossing road outside of designated crosswalk", "severity": "MEDIUM", "visual_indicators": ["pedestrian outside crosswalk", "pedestrian crossing mid-block", "no crosswalk markings at impact location"], "fault_weight": 0.5, "applicable_parties": ["pedestrian"] }, { "id": "PEDES-002", "title": "Pedestrian Against Signal", "description": "Pedestrian crossing against a don't walk signal", "severity": "HIGH", "visual_indicators": ["pedestrian signal showing do not walk", "pedestrian in crosswalk against signal"], "fault_weight": 0.7, "applicable_parties": ["pedestrian"] }, { "id": "PEDES-003", "title": "Driver Failure to Stop for Pedestrian in Crosswalk", "description": "Driver not stopping for pedestrian in a marked or unmarked crosswalk", "severity": "CRITICAL", "visual_indicators": ["pedestrian struck in crosswalk", "crosswalk markings visible", "pedestrian crossing sign"], "fault_weight": 0.95, "applicable_parties": ["moving_vehicle"] }, { "id": "PEDES-004", "title": "Pedestrian on Highway", "description": "Pedestrian walking on a limited-access highway", "severity": "HIGH", "visual_indicators": ["pedestrian on highway", "limited access road", "no pedestrian facilities"], "fault_weight": 0.6, "applicable_parties": ["pedestrian"] }, { "id": "PEDES-005", "title": "Failure to Stop for School Bus", "description": "Passing a stopped school bus with flashing lights", "severity": "CRITICAL", "visual_indicators": ["school bus with stop sign extended", "school bus with flashing lights", "children near school bus"], "fault_weight": 0.95, "applicable_parties": ["moving_vehicle"] }] }, { "id": "ROAD", "name": "Road Condition Violations", "rule_count": 4, "rules": [{ "id": "ROAD-001", "title": "Hazardous Road Condition", "description": "Road surface or design defect contributing to accident", "severity": "MEDIUM", "visual_indicators": ["pothole visible", "road surface damage", "missing road markings", "inadequate signage", "construction zone without proper markings"], "fault_weight": 0.3, "applicable_parties": ["road_authority"] }, { "id": "ROAD-002", "title": "Inadequate Lighting", "description": "Road segment without adequate lighting contributing to collision", "severity": "MEDIUM", "visual_indicators": ["dark road", "no street lights", "nighttime collision in unlit area"], "fault_weight": 0.25, "applicable_parties": ["road_authority"] }, { "id": "ROAD-003", "title": "Missing or Damaged Guard Rail", "description": "Absent or damaged barrier that could have prevented injury", "severity": "MEDIUM", "visual_indicators": ["vehicle off road", "no guard rail on curve", "damaged guard rail", "vehicle in ditch or embankment"], "fault_weight": 0.2, "applicable_parties": ["road_authority"] }, { "id": "ROAD-004", "title": "Work Zone / Construction Hazard", "description": "Improperly marked or protected construction zone", "severity": "HIGH", "visual_indicators": ["construction zone", "work zone signs", "cones, barrels, barriers", "lane closure", "construction equipment on road"], "fault_weight": 0.35, "applicable_parties": ["road_authority", "moving_vehicle"] }] }] }), { status: 200 }); } // 6. Run AI Analysis if (url.match(/\/api\/cases\/\d+\/analyze$/) && options.method === 'POST') { const match = url.match(/\/api\/cases\/(\d+)\/analyze$/); const id = parseInt(match[1]); await delay(3000); // Simulate heavy AI reasoning mockCases[id].case.status = 'complete'; // Generate mock analysis data based on the uploaded photos mockCases[id].analyses = mockCases[id].photos.map(p => ({ filename: p.filename, raw_analysis: `[AI Observation]\nSubject: Vehicle A and Vehicle B.\nAction: Vehicle A exhibits front-end damage consistent with forward impact. Vehicle B exhibits rear damage. Road conditions appear clear.` })); mockCases[id].violations = [ { rule_id: "Article 18", rule_title: "Failure to Leave Safe Distance", severity: "HIGH", confidence: 0.94, party_label: "Vehicle A", evidence_summary: "Visual estimation of skid marks and impact deformation indicates Vehicle A failed to maintain adequate stopping distance." } ]; mockCases[id].fault_analysis = { determined: true, primary_fault_party: "Vehicle A", probable_cause: "Rear-end collision due to insufficient following distance.", fault_distribution_json: { "Vehicle A": 100, "Vehicle B": 0 }, overall_confidence: 0.92, analysis_summary: "Based on the visual evidence, Vehicle A is fully at fault for failing to stop in time." }; return new Response(JSON.stringify({ violations_found: 1 }), { status: 200 }); } // 7. Get Report if (url.match(/\/api\/cases\/\d+\/report$/) && (options.method === 'GET' || !options.method)) { const match = url.match(/\/api\/cases\/(\d+)\/report$/); const id = parseInt(match[1]); const c = mockCases[id]; const report = { case: c.case, report_type: 'AI Accident Analysis Report', disclaimer: 'This is a mock report for demo purposes.', statistics: { total_photos: c.photos.length, total_violations: c.violations.length, critical_violations: c.violations.filter(v => v.severity === 'HIGH').length, parties_identified: Object.keys(c.fault_analysis?.fault_distribution_json || {}).length }, scene_summary: "AI review of the provided imagery indicates a multi-vehicle traffic incident resulting in property damage.", parties: c.fault_analysis ? Object.keys(c.fault_analysis.fault_distribution_json).map(k => ({ label: k, vehicle_type: 'Automobile', vehicle_color: 'Unknown' })) : [], violations: { list: c.violations.map(v => ({ title: v.rule_title, severity: v.severity, party: v.party_label, confidence: v.confidence, evidence: v.evidence_summary })) }, fault_analysis: c.fault_analysis }; return new Response(JSON.stringify(report), { status: 200 }); } } // Intercept photo image requests to return the stored blob URL instead if (typeof url === 'string' && url.match(/\/api\/photos\/(.+)\/image$/)) { const match = url.match(/\/api\/photos\/(.+)\/image$/); const photoId = match[1]; let foundPhotoURL = null; Object.values(mockCases).forEach(c => { const p = c.photos.find(p => p.id === photoId); if (p) foundPhotoURL = p.blob_url; }); if (foundPhotoURL) { // Fetch the blob URL to return a proper image response return originalFetch(foundPhotoURL); } } // Fallback for non-API requests return originalFetch(url, options); }; // ── State ───────────────────────────────────────────────────────────── let currentView = 'landing'; let currentCaseId = null; let currentCaseData = null; let currentVertical = 'le'; // Store full case data for editing let selectedFiles = []; let additionalFiles = []; // For add photos modal // ── Init ────────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { initNavigation(); initDropZone(); initButtons(); initAccordions(); loadHealth(); loadCases(); }); // ── Accordions ──────────────────────────────────────────────────────── function initAccordions() { document.querySelectorAll('.accordion-header').forEach(btn => { btn.addEventListener('click', () => { const item = btn.closest('.accordion-item'); const isActive = item.classList.contains('active'); // Close all other accordions document.querySelectorAll('.accordion-item').forEach(i => { i.classList.remove('active'); i.querySelector('.accordion-header').setAttribute('aria-expanded', 'false'); }); // Toggle current if (!isActive) { item.classList.add('active'); btn.setAttribute('aria-expanded', 'true'); } }); }); } // ── Navigation ──────────────────────────────────────────────────────── function initNavigation() { document.querySelectorAll('.nav-item[data-view]').forEach(btn => { btn.addEventListener('click', () => { const view = btn.dataset.view; switchView(view); document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active')); btn.classList.add('active'); }); }); } function switchView(viewName) { document.querySelectorAll('.view').forEach(v => { v.classList.remove('active'); v.classList.add('hidden'); }); const target = document.getElementById(`view-${viewName}`); if (target) { target.classList.remove('hidden'); target.classList.add('active'); } currentView = viewName; if (viewName === 'le-dashboard') loadCases('le'); if (viewName === 'ins-dashboard') loadCases('ins'); if (viewName === 'rules') loadRules(); } // ── Buttons ─────────────────────────────────────────────────────────── function initButtons() { // LE Dashboard document.getElementById('btn-refresh-cases')?.addEventListener('click', () => loadCases('le')); document.getElementById('btn-le-empty-new-case')?.addEventListener('click', () => { switchView('le-new-case'); document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active')); document.querySelector('[data-view="le-new-case"]')?.classList.add('active'); }); // INS Dashboard document.getElementById('btn-refresh-cases-ins')?.addEventListener('click', () => loadCases('ins')); document.getElementById('btn-ins-empty-new-case')?.addEventListener('click', () => { switchView('ins-new-case'); document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active')); document.querySelector('[data-view="ins-new-case"]')?.classList.add('active'); }); // New Case Forms document.getElementById('btn-create-case')?.addEventListener('click', () => createCase('le')); document.getElementById('btn-ins-create-case')?.addEventListener('click', () => createCase('ins')); // Case Detail document.getElementById('btn-back-dashboard')?.addEventListener('click', () => switchView(currentVertical === 'ins' ? 'ins-dashboard' : 'le-dashboard')); document.getElementById('btn-run-analysis')?.addEventListener('click', runAnalysis); document.getElementById('btn-view-report')?.addEventListener('click', viewReport); document.getElementById('btn-edit-case')?.addEventListener('click', openEditModal); document.getElementById('btn-add-photos-inline')?.addEventListener('click', openAddPhotosModal); // Edit Modal document.getElementById('btn-close-edit-modal')?.addEventListener('click', closeEditModal); document.getElementById('btn-cancel-edit')?.addEventListener('click', closeEditModal); document.getElementById('btn-save-case')?.addEventListener('click', saveCaseChanges); // Photos Modal document.getElementById('btn-close-photos-modal')?.addEventListener('click', closePhotosModal); document.getElementById('btn-cancel-photos')?.addEventListener('click', closePhotosModal); document.getElementById('btn-upload-more')?.addEventListener('click', uploadMorePhotos); // Form logic document.getElementById('case-number')?.addEventListener('input', () => validateForm('le')); document.getElementById('ins-case-number')?.addEventListener('input', () => validateForm('ins')); initEditDropZone(); } function initEditDropZone() { const dropZone = document.getElementById('edit-drop-zone'); const fileInput = document.getElementById('edit-file-input'); if (!dropZone || !fileInput) return; dropZone.addEventListener('click', () => fileInput.click()); dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over')); dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); handleAdditionalFiles(Array.from(e.dataTransfer.files)); }); fileInput.addEventListener('change', () => { handleAdditionalFiles(Array.from(fileInput.files)); }); } function validateForm(vertical = 'le') { const cid = vertical === 'le' ? 'case-number' : 'ins-case-number'; const bid = vertical === 'le' ? 'btn-create-case' : 'btn-ins-create-case'; const caseNum = document.getElementById(cid)?.value.trim(); const btn = document.getElementById(bid); if (btn) btn.disabled = !caseNum; } // ── Health ───────────────────────────────────────────────────────────── async function loadHealth() { try { const resp = await fetch(`${API_BASE}/health`); const data = await resp.json(); document.getElementById('status-model').textContent = data.model_loaded ? '✓ Ready' : '✗ Not loaded'; document.getElementById('status-model').style.color = data.model_loaded ? '#22c55e' : '#ef4444'; document.getElementById('status-device').textContent = data.device || '—'; document.getElementById('status-rules').textContent = `${data.rules_loaded} rules`; } catch { document.getElementById('status-model').textContent = '✗ Offline'; document.getElementById('status-model').style.color = '#ef4444'; } } // ── Cases ───────────────────────────────────────────────────────────── async function loadCases(vertical = 'le') { currentVertical = vertical; try { const resp = await fetch(`${API_BASE}/cases`); const data = await resp.json(); renderCases(data.cases || [], vertical); } catch (e) { showToast('Failed to load cases', 'error'); } } function renderCases(cases, vertical) { const grid = document.getElementById(vertical === 'le' ? 'le-cases-grid' : 'ins-cases-grid'); const empty = document.getElementById(vertical === 'le' ? 'le-empty-state-dashboard' : 'ins-empty-state-dashboard'); if (!cases.length) { grid.innerHTML = ''; grid.style.display = 'none'; empty.style.display = 'flex'; return; } grid.style.display = 'grid'; empty.style.display = 'none'; grid.innerHTML = cases.map(c => `
No photos uploaded yet.
'; } // Analyses const analysisContent = document.getElementById('analysis-content'); if (data.analyses?.length) { analysisContent.innerHTML = data.analyses.map(a => `${escHtml(a.raw_analysis || '')}
Run analysis to see AI observations.
'; } // Violations const violationsList = document.getElementById('violations-list'); const violationBadge = document.getElementById('violation-badge'); violationBadge.textContent = data.violations?.length || 0; if (data.violations?.length) { violationsList.innerHTML = data.violations.map(v => `No violations detected yet.
'; } // Fault Analysis renderFaultAnalysis(data.fault_analysis, data.parties); } function renderFaultAnalysis(fault, parties) { const content = document.getElementById('fault-content'); if (!fault) { content.innerHTML = 'Run analysis to determine fault.
'; return; } let html = ''; // Fault distribution bars if (fault.fault_distribution_json) { let dist = {}; try { dist = typeof fault.fault_distribution_json === 'string' ? JSON.parse(fault.fault_distribution_json) : fault.fault_distribution_json; } catch { dist = {}; } const entries = Object.entries(dist); if (entries.length) { html += ''; } } // Probable cause if (fault.probable_cause) { html += `${escHtml(fault.probable_cause)}
${escHtml(fault.analysis_summary)}
`; } content.innerHTML = html || 'No fault analysis available.
'; } // ── Run Analysis ────────────────────────────────────────────────────── async function runAnalysis() { if (!currentCaseId) return; const overlay = document.getElementById('analysis-overlay'); const stepEl = document.getElementById('analysis-step'); const detailEl = document.getElementById('analysis-detail'); overlay.classList.remove('hidden'); stepEl.textContent = 'Analyzing accident scene photos...'; detailEl.textContent = 'Running AI vision analysis on each photo. This may take several minutes.'; try { const resp = await fetch(`${API_BASE}/cases/${currentCaseId}/analyze`, { method: 'POST', }); if (!resp.ok) { const err = await resp.json(); throw new Error(err.detail || 'Analysis failed'); } const data = await resp.json(); overlay.classList.add('hidden'); showToast(`Analysis complete: ${data.violations_found} violations found`, 'success'); // Reload case detail openCase(currentCaseId); } catch (e) { overlay.classList.add('hidden'); showToast(`Analysis failed: ${e.message}`, 'error'); } } // ── Report ──────────────────────────────────────────────────────────── async function viewReport() { if (!currentCaseId) return; switchView('report'); try { const resp = await fetch(`${API_BASE}/cases/${currentCaseId}/report`); const report = await resp.json(); renderReport(report); } catch (e) { showToast('Failed to load report', 'error'); } } function renderReport(report) { const content = document.getElementById('report-content'); let html = `Case ${escHtml(report.case?.case_number || '')} • Generated by AI
Case Number: ${escHtml(report.case?.case_number || 'N/A')}
`; html += `Officer: ${escHtml(report.case?.officer_name || 'N/A')}
`; html += `Location: ${escHtml(report.case?.location || 'N/A')}
`; html += `Date: ${report.case?.incident_date || 'N/A'}
`; if (report.case?.notes) html += `Notes: ${escHtml(report.case.notes)}
`; // Scene Summary if (report.scene_summary) { html += `${escHtml(report.scene_summary)}
`; } // Parties if (report.parties?.length) { html += `Type: ${p.vehicle_type || 'Unknown'} • Color: ${p.vehicle_color || 'Unknown'}
`; }); } // Violations if (report.violations?.list?.length) { html += `Party: ${escHtml(v.party)} • Confidence: ${Math.round(v.confidence * 100)}%
${v.evidence ? `${escHtml(v.evidence)}
` : ''}Primary Fault: ${escHtml(report.fault_analysis.primary_fault_party || 'Undetermined')}
`; if (report.fault_analysis.fault_distribution) { html += `Distribution: `; html += Object.entries(report.fault_analysis.fault_distribution) .map(([k, v]) => `${k}: ${v}%`).join(' • '); html += `
`; } html += `Confidence: ${Math.round((report.fault_analysis.overall_confidence || 0) * 100)}%
`; if (report.fault_analysis.probable_cause) { html += `${escHtml(report.fault_analysis.probable_cause)}
`; } } content.innerHTML = html; } // ── Rules ───────────────────────────────────────────────────────────── async function loadRules() { try { const resp = await fetch(`${API_BASE}/rules`); const data = await resp.json(); renderRules(data); } catch (e) { showToast('Failed to load rules', 'error'); } } function renderRules(data) { const content = document.getElementById('rules-content'); if (!data.categories?.length) { content.innerHTML = 'No rules loaded.
'; return; } content.innerHTML = data.categories.map(cat => `