Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Multi-Agent Pipeline - Policy Issuance</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg:#040c18; --panel:#070f1e; --panel2:#0b1628; --border:#0d2240; --border2:#122d4f; | |
| --neon:#00d4ff; --neon2:#00ff9d; --neon3:#ff6b35; --neon4:#a855f7; --neon5:#f59e0b; | |
| --text:#c8e0f4; --dim:#3a6080; --dim2:#4a7090; | |
| --success:#00ff9d; --warn:#f59e0b; --danger:#ff4d4d; --info:#00d4ff; | |
| --agent-kyc:#00d4ff; --agent-prop:#ff6b35; --agent-uw:#f59e0b; --agent-price:#00ff9d; --agent-issue:#818cf8; | |
| } | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| body{background:var(--bg);font-family:'Syne',sans-serif;color:var(--text);min-height:100vh;overflow-x:hidden} | |
| body::before{content:'';position:fixed;inset:0;background-image:radial-gradient(ellipse 80% 60% at 50% -10%,rgba(0,212,255,0.07),transparent),linear-gradient(rgba(0,212,255,0.025) 1px,transparent 1px),linear-gradient(90deg,rgba(0,212,255,0.025) 1px,transparent 1px);background-size:100% 100%,48px 48px,48px 48px;pointer-events:none;z-index:0} | |
| .container{max-width:1280px;margin:0 auto;padding:20px 24px;position:relative;z-index:1} | |
| /* ββ HEADER ββ */ | |
| header{display:flex;align-items:flex-start;justify-content:space-between;padding:18px 0 22px;gap:20px;flex-wrap:wrap} | |
| .hd-left .eyebrow{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:4px;color:var(--neon);text-transform:uppercase;margin-bottom:8px;opacity:0;animation:fadeUp .5s ease .1s forwards} | |
| .hd-left h1{font-size:clamp(22px,3.5vw,40px);font-weight:800;letter-spacing:-1px;line-height:1.1;opacity:0;animation:fadeUp .6s ease .25s forwards} | |
| .hd-left h1 em{color:var(--neon);font-style:normal} | |
| .hd-left .sub{margin-top:7px;font-size:12px;color:var(--dim2);opacity:0;animation:fadeUp .6s ease .4s forwards;font-family:'Share Tech Mono',monospace} | |
| .hd-right{display:flex;flex-direction:column;align-items:flex-end;gap:8px;opacity:0;animation:fadeUp .6s ease .5s forwards} | |
| .db-status{display:flex;align-items:center;gap:8px;font-family:'Share Tech Mono',monospace;font-size:10px;padding:6px 14px;border-radius:20px;border:1px solid var(--border2);background:var(--panel)} | |
| .db-dot{width:7px;height:7px;border-radius:50%;background:var(--dim);flex-shrink:0;transition:all .3s} | |
| .db-dot.live{background:var(--success);box-shadow:0 0 8px var(--success);animation:pulse 2s ease infinite} | |
| .db-dot.err{background:var(--danger);box-shadow:0 0 8px var(--danger)} | |
| .hd-pills{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end} | |
| .hpill{font-family:'Share Tech Mono',monospace;font-size:9px;padding:3px 9px;border-radius:12px;border:1px solid var(--border2);color:var(--dim2)} | |
| /* ββ TABS ββ */ | |
| .tabs{display:flex;gap:4px;margin-bottom:18px;opacity:0;animation:fadeUp .6s ease .55s forwards} | |
| .tab{padding:7px 18px;border-radius:6px;font-family:'Share Tech Mono',monospace;font-size:11px;cursor:pointer;border:1px solid var(--border2);background:transparent;color:var(--dim2);transition:all .2s;text-transform:uppercase;letter-spacing:1px} | |
| .tab.active{background:rgba(0,212,255,.1);border-color:var(--neon);color:var(--neon)} | |
| .tab:hover:not(.active){border-color:var(--dim);color:var(--text)} | |
| /* ββ QUEUE SECTION ββ */ | |
| .queue-section{opacity:0;animation:fadeUp .6s ease .65s forwards;margin-bottom:18px} | |
| .queue-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px} | |
| .qh-left{display:flex;align-items:baseline;gap:12px} | |
| .queue-title{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:3px;color:var(--neon);text-transform:uppercase} | |
| .queue-sub{font-size:11px;color:var(--dim2)} | |
| .qh-right{display:flex;gap:8px;align-items:center} | |
| .reload-btn{padding:5px 12px;border-radius:5px;font-family:'Share Tech Mono',monospace;font-size:10px;cursor:pointer;border:1px solid var(--neon);background:transparent;color:var(--neon);transition:all .2s;text-transform:uppercase} | |
| .reload-btn:hover{background:rgba(0,212,255,.1)} | |
| .reload-btn:disabled{opacity:.35;cursor:not-allowed} | |
| .batch-info{font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--dim2)} | |
| .batch-info span{color:var(--neon5)} | |
| .policy-queue{display:grid;grid-template-columns:repeat(5,1fr);gap:10px} | |
| .pcb{background:var(--panel);border:2px solid var(--border);border-radius:12px;padding:12px 11px;cursor:pointer;transition:all .22s;text-align:left;color:var(--text);font-family:'Syne',sans-serif;position:relative;overflow:hidden;display:flex;flex-direction:column;gap:2px} | |
| .pcb:hover:not(:disabled){border-color:var(--dim);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.5)} | |
| .pcb.selected{border-color:var(--neon);background:rgba(0,212,255,.05);box-shadow:0 0 20px rgba(0,212,255,.15)} | |
| .pcb.rpass{border-color:rgba(0,255,157,.35);background:rgba(0,255,157,.03)} | |
| .pcb.rfail{border-color:rgba(255,77,77,.35);background:rgba(255,77,77,.03)} | |
| .pcb.live-sub{border-color:rgba(245,158,11,.4);background:rgba(245,158,11,.03)} | |
| .pcb.live-sub.selected{border-color:var(--neon5);box-shadow:0 0 20px rgba(245,158,11,.2)} | |
| .pcb-top{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:3px} | |
| .pcb-id{font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--dim2)} | |
| .pcb-result{font-size:13px} | |
| .pcb-source{font-family:'Share Tech Mono',monospace;font-size:8px;padding:1px 5px;border-radius:3px;background:rgba(245,158,11,.15);color:var(--neon5);border:1px solid rgba(245,158,11,.3)} | |
| .pcb-name{font-size:12px;font-weight:700;margin-bottom:1px} | |
| .pcb-loc{font-size:10px;color:var(--dim2);margin-bottom:6px} | |
| .pcb-tags{display:flex;gap:4px;flex-wrap:wrap} | |
| .pct{font-family:'Share Tech Mono',monospace;font-size:8px;padding:2px 5px;border-radius:3px;border:1px solid} | |
| .pct-cov{color:var(--neon);border-color:rgba(0,212,255,.3);background:rgba(0,212,255,.08)} | |
| .pct-lim{color:var(--warn);border-color:rgba(245,158,11,.3);background:rgba(245,158,11,.08)} | |
| .pcb-sel-dot{display:none;position:absolute;bottom:8px;right:9px;width:6px;height:6px;border-radius:50%;background:var(--neon);box-shadow:0 0 8px var(--neon);animation:pdot 1.5s ease infinite} | |
| .pcb.selected .pcb-sel-dot{display:block} | |
| .queue-empty{grid-column:1/-1;text-align:center;padding:40px;color:var(--dim2);font-family:'Share Tech Mono',monospace;font-size:12px} | |
| /* ββ CONTROLS ββ */ | |
| .controls{display:flex;gap:10px;align-items:center;margin-bottom:16px;flex-wrap:wrap;opacity:0;animation:fadeUp .6s ease .75s forwards} | |
| .btn{padding:9px 20px;border-radius:6px;font-family:'Share Tech Mono',monospace;font-size:11px;cursor:pointer;border:1px solid;transition:all .2s;text-transform:uppercase;letter-spacing:1px;display:flex;align-items:center;gap:6px} | |
| .btn-primary{background:var(--neon);color:#000;border-color:var(--neon);box-shadow:0 0 18px rgba(0,212,255,.3);font-weight:700} | |
| .btn-primary:hover:not(:disabled){box-shadow:0 0 28px rgba(0,212,255,.6);transform:translateY(-1px)} | |
| .btn-primary:disabled{opacity:.35;cursor:not-allowed} | |
| .btn-live{background:var(--neon5);color:#000;border-color:var(--neon5);box-shadow:0 0 18px rgba(245,158,11,.3);font-weight:700} | |
| .btn-live:hover:not(:disabled){box-shadow:0 0 28px rgba(245,158,11,.5);transform:translateY(-1px)} | |
| .btn-live:disabled{opacity:.35;cursor:not-allowed} | |
| .btn-sec{background:transparent;color:var(--neon);border-color:var(--neon)} | |
| .btn-sec:hover{background:rgba(0,212,255,.1)} | |
| .sel-info{font-family:'Share Tech Mono',monospace;font-size:11px;color:var(--dim2);margin-left:auto} | |
| .sel-info span{color:var(--neon)} | |
| .live-badge{display:inline-flex;align-items:center;gap:5px;font-family:'Share Tech Mono',monospace;font-size:9px;padding:3px 8px;border-radius:12px;background:rgba(245,158,11,.12);border:1px solid rgba(245,158,11,.3);color:var(--neon5)} | |
| .live-badge::before{content:'';width:5px;height:5px;border-radius:50%;background:var(--neon5);box-shadow:0 0 6px var(--neon5);animation:pulse 1.5s ease infinite} | |
| /* ββ PROGRESS ββ */ | |
| .prog-wrap{background:var(--panel);border:1px solid var(--border);border-radius:4px;height:4px;margin-bottom:20px;overflow:hidden} | |
| .prog{height:100%;background:linear-gradient(90deg,var(--neon),var(--neon2));width:0%;transition:width .4s;box-shadow:0 0 10px var(--neon)} | |
| .prog.fail{background:linear-gradient(90deg,var(--danger),#ff8c00)!important;box-shadow:0 0 10px var(--danger)!important} | |
| /* ββ TIMELINE ββ */ | |
| .tl{display:flex;justify-content:space-between;margin-bottom:20px;position:relative;opacity:0;animation:fadeUp .6s ease .9s forwards} | |
| .tl::before{content:'';position:absolute;top:19px;left:38px;right:38px;height:2px;background:var(--border)} | |
| .tl-prog{position:absolute;top:19px;left:38px;height:2px;background:linear-gradient(90deg,var(--neon),var(--neon2));box-shadow:0 0 8px var(--neon);z-index:1;width:0%;transition:width .5s} | |
| .tls{display:flex;flex-direction:column;align-items:center;gap:6px;z-index:2;flex:1} | |
| .tld{width:38px;height:38px;border-radius:50%;border:2px solid var(--border);background:var(--panel);display:flex;align-items:center;justify-content:center;font-size:14px;transition:all .4s} | |
| .tls.active .tld{border-color:var(--neon);box-shadow:0 0 15px rgba(0,212,255,.5);animation:pglow 1.5s ease infinite} | |
| .tls.done .tld{border-color:var(--success);background:rgba(0,255,157,.1);box-shadow:0 0 10px rgba(0,255,157,.3)} | |
| .tls.failed .tld{border-color:var(--danger)!important;background:rgba(255,77,77,.1)!important;box-shadow:0 0 14px rgba(255,77,77,.5)!important;animation:none!important} | |
| .tll{font-size:9px;font-family:'Share Tech Mono',monospace;color:var(--dim2);text-align:center;max-width:70px;line-height:1.3;transition:color .3s} | |
| .tls.active .tll{color:var(--neon)} | |
| .tls.done .tll{color:var(--success)} | |
| .tls.failed .tll{color:var(--danger)!important} | |
| /* ββ MAIN GRID ββ */ | |
| .main-grid{display:grid;grid-template-columns:215px 1fr;gap:16px;align-items:start} | |
| .agents-sidebar{display:flex;flex-direction:column;gap:7px;position:sticky;top:20px} | |
| .ac{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:10px 12px;transition:all .3s;position:relative;overflow:hidden} | |
| .ac::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;border-radius:2px 0 0 2px;background:var(--agent-color,var(--border));transition:all .3s} | |
| .ac.active{border-color:var(--agent-color,var(--neon));background:color-mix(in srgb,var(--agent-color) 8%,var(--panel));box-shadow:0 0 20px color-mix(in srgb,var(--agent-color) 20%,transparent)} | |
| .ac.active::before{width:4px;box-shadow:0 0 12px var(--agent-color,var(--neon))} | |
| .ac.done{opacity:.65}.ac.done::before{background:var(--success)} | |
| .ac.rejected{opacity:.65}.ac.rejected::before{background:var(--danger)!important} | |
| .ai{font-size:17px;margin-bottom:3px} | |
| .an{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px} | |
| .ar{font-size:9px;color:var(--dim2);margin-top:1px;font-family:'Share Tech Mono',monospace} | |
| .astat{font-size:9px;font-family:'Share Tech Mono',monospace;margin-top:4px;height:13px} | |
| .astat.processing{color:var(--warn)}.astat.complete{color:var(--success)}.astat.idle{color:var(--dim2)} | |
| /* ββ LIVE AGENT BADGE ββ */ | |
| .agent-live-tag{display:inline-flex;align-items:center;gap:4px;font-family:'Share Tech Mono',monospace;font-size:8px;padding:1px 6px;border-radius:8px;background:rgba(245,158,11,.15);border:1px solid rgba(245,158,11,.35);color:var(--neon5);margin-left:6px;vertical-align:middle} | |
| /* ββ CHAT ββ */ | |
| .chat-area{display:flex;flex-direction:column;gap:12px;min-height:400px} | |
| .ph{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:10px 15px;display:flex;align-items:center;gap:12px;opacity:0;transition:opacity .4s} | |
| .ph.v{opacity:1} | |
| .pbadge{font-family:'Share Tech Mono',monospace;font-size:9px;padding:3px 9px;border-radius:16px;background:rgba(0,212,255,.1);border:1px solid var(--neon);color:var(--neon);white-space:nowrap} | |
| .pbadge.live{background:rgba(245,158,11,.12);border-color:var(--neon5);color:var(--neon5)} | |
| .ptitle{font-size:12px;font-weight:700} | |
| .pdesc{font-size:10px;color:var(--dim2)} | |
| .msg{display:flex;gap:9px;opacity:0;transform:translateY(8px);transition:all .4s;align-items:flex-start} | |
| .msg.v{opacity:1;transform:translateY(0)} | |
| .msg.r{flex-direction:row-reverse} | |
| .mav{width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0;border:1px solid} | |
| .mbody{flex:1;max-width:88%} | |
| .mfrom{font-size:10px;font-family:'Share Tech Mono',monospace;color:var(--dim2);margin-bottom:3px;display:flex;align-items:center;gap:7px} | |
| .msg.r .mfrom{justify-content:flex-end} | |
| .mtime{font-size:9px;color:rgba(74,112,144,.5)} | |
| .mbub{border-radius:10px;padding:9px 12px;font-size:12px;line-height:1.6;border:1px solid} | |
| .mtag{font-family:'Share Tech Mono',monospace;font-size:9px;padding:2px 6px;border-radius:3px;margin-right:5px;background:rgba(0,0,0,.3)} | |
| .cb{font-family:'Share Tech Mono',monospace;font-size:10px;background:rgba(0,0,0,.45);border-radius:5px;padding:8px 11px;margin-top:7px;border:1px solid rgba(255,255,255,.05);line-height:1.75;white-space:pre;overflow-x:auto} | |
| .cb .k{color:var(--neon)}.cb .vs{color:var(--neon2)}.cb .vn{color:var(--warn)}.cb .vp{color:var(--success)}.cb .vf{color:var(--danger)}.cb .vw{color:var(--warn)}.cb .p{color:var(--dim2)} | |
| .s3b{font-family:'Share Tech Mono',monospace;font-size:9px;padding:2px 7px;border-radius:3px;background:rgba(255,107,53,.1);color:var(--neon3);border:1px solid rgba(255,107,53,.25);display:inline-block;margin-top:4px} | |
| .model-badge{font-family:'Share Tech Mono',monospace;font-size:8px;padding:1px 6px;border-radius:3px;background:rgba(0,255,157,.1);color:var(--success);border:1px solid rgba(0,255,157,.25);display:inline-block;margin-top:4px;margin-left:4px} | |
| .ti{display:none;gap:9px;align-items:flex-start} | |
| .ti.v{display:flex} | |
| .tdots{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:9px 14px;display:flex;gap:5px;align-items:center} | |
| .tdots span{width:5px;height:5px;background:var(--dim);border-radius:50%;animation:bnc 1.2s ease infinite} | |
| .tdots span:nth-child(2){animation-delay:.2s}.tdots span:nth-child(3){animation-delay:.4s} | |
| .ob{background:var(--panel);border:1px solid rgba(168,85,247,.3);border-radius:9px;padding:8px 12px;display:flex;align-items:center;gap:9px;opacity:0;transition:opacity .4s} | |
| .ob.v{opacity:1} | |
| .obicon{font-size:15px} | |
| .obt{color:var(--neon4);font-family:'Share Tech Mono',monospace;font-size:9px} | |
| .oba{color:var(--text);font-size:11px} | |
| /* ββ LIVE CALL STATUS ββ */ | |
| .live-call-box{background:rgba(245,158,11,.04);border:1px solid rgba(245,158,11,.25);border-radius:9px;padding:10px 14px;display:flex;align-items:center;gap:10px;opacity:0;transition:opacity .4s} | |
| .live-call-box.v{opacity:1} | |
| .lcb-icon{font-size:18px} | |
| .lcb-title{font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--neon5);text-transform:uppercase;letter-spacing:1px;margin-bottom:2px} | |
| .lcb-text{font-size:11px;color:var(--text)} | |
| .lcb-spinner{width:14px;height:14px;border:2px solid rgba(245,158,11,.3);border-top-color:var(--neon5);border-radius:50%;animation:spin 1s linear infinite;flex-shrink:0;margin-left:auto} | |
| /* ββ CHIPS ββ */ | |
| .chip{display:inline-flex;align-items:center;padding:3px 8px;border-radius:16px;font-family:'Share Tech Mono',monospace;font-size:9px;font-weight:600} | |
| .chip.pass{background:rgba(0,255,157,.1);color:var(--success);border:1px solid rgba(0,255,157,.25)} | |
| .chip.fail{background:rgba(255,77,77,.1);color:var(--danger);border:1px solid rgba(255,77,77,.25)} | |
| .chip.warn{background:rgba(245,158,11,.1);color:var(--warn);border:1px solid rgba(245,158,11,.25)} | |
| .chip.info{background:rgba(0,212,255,.1);color:var(--neon);border:1px solid rgba(0,212,255,.25)} | |
| .chip.purple{background:rgba(168,85,247,.1);color:var(--neon4);border:1px solid rgba(168,85,247,.25)} | |
| .chip.amber{background:rgba(245,158,11,.12);color:var(--neon5);border:1px solid rgba(245,158,11,.3)} | |
| /* ββ RESULT CARDS ββ */ | |
| .rc{border-radius:14px;padding:18px;opacity:0;transform:translateY(16px);transition:all .6s} | |
| .rc.v{opacity:1;transform:translateY(0)} | |
| .rc.app{background:linear-gradient(135deg,#071020,#041508);border:1px solid var(--neon2);box-shadow:0 0 40px rgba(0,255,157,.1)} | |
| .rc.rej{background:linear-gradient(135deg,#180a0a,#0c0404);border:1px solid var(--danger);box-shadow:0 0 40px rgba(255,77,77,.1)} | |
| .rchead{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:14px;padding-bottom:12px} | |
| .rc.app .rchead{border-bottom:1px solid rgba(0,255,157,.12)} | |
| .rc.rej .rchead{border-bottom:1px solid rgba(255,77,77,.18)} | |
| .rcidl{font-size:9px;font-family:'Share Tech Mono',monospace;color:var(--dim2);margin-bottom:3px} | |
| .rcid{font-family:'Share Tech Mono',monospace;font-size:17px;font-weight:700} | |
| .rc.app .rcid{color:var(--neon2)}.rc.rej .rcid{color:var(--danger)} | |
| .rcbadge{font-family:'Share Tech Mono',monospace;font-size:10px;padding:3px 10px;border-radius:16px} | |
| .rc.app .rcbadge{color:var(--success);border:1px solid var(--success)} | |
| .rc.rej .rcbadge{color:var(--danger);border:1px solid var(--danger)} | |
| .rcgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:9px} | |
| .rcs{text-align:center;padding:10px;background:rgba(0,0,0,.3);border-radius:7px;border:1px solid var(--border)} | |
| .rc.rej .rcs{border-color:rgba(255,77,77,.12)} | |
| .rcs .rl{font-size:8px;color:var(--dim2);text-transform:uppercase;letter-spacing:1px;margin-bottom:3px;font-family:'Share Tech Mono',monospace} | |
| .rcs .rv{font-size:14px;font-weight:700;color:var(--text)} | |
| .rv.g{color:var(--success)}.rv.r{color:var(--danger)} | |
| .rreasons{background:rgba(255,77,77,.05);border:1px solid rgba(255,77,77,.18);border-radius:7px;padding:12px 14px;margin-top:12px} | |
| .rrt{font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--danger);letter-spacing:1px;text-transform:uppercase;margin-bottom:8px} | |
| .rri{display:flex;align-items:flex-start;gap:7px;margin-bottom:6px;font-size:11.5px;color:var(--text);line-height:1.5} | |
| .rri::before{content:'β';color:var(--danger);flex-shrink:0;font-weight:700;margin-top:1px} | |
| /* ββ LIVE RESULT INDICATOR ββ */ | |
| .live-result-tag{display:inline-flex;align-items:center;gap:5px;font-family:'Share Tech Mono',monospace;font-size:8px;padding:2px 8px;border-radius:10px;background:rgba(245,158,11,.15);border:1px solid rgba(245,158,11,.35);color:var(--neon5);margin-top:4px} | |
| /* ββ API CONFIG PANEL ββ */ | |
| .cfg-panel{background:var(--panel);border:1px solid var(--border2);border-radius:10px;padding:16px;margin-bottom:16px;display:none} | |
| .cfg-panel.open{display:block} | |
| .cfg-title{font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--neon);text-transform:uppercase;letter-spacing:2px;margin-bottom:14px} | |
| .cfg-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px} | |
| .cfg-field{display:flex;flex-direction:column;gap:5px} | |
| .cfg-label{font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--dim2);text-transform:uppercase;letter-spacing:1px} | |
| .cfg-input{background:rgba(0,0,0,.4);border:1px solid var(--border2);border-radius:5px;padding:7px 10px;font-family:'Share Tech Mono',monospace;font-size:11px;color:var(--text);outline:none;transition:border-color .2s} | |
| .cfg-input:focus{border-color:var(--neon)} | |
| .cfg-actions{display:flex;gap:8px;margin-top:14px} | |
| /* ββ ANIMATIONS ββ */ | |
| @keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}} | |
| @keyframes bnc{0%,80%,100%{transform:translateY(0)}40%{transform:translateY(-5px)}} | |
| @keyframes pglow{0%,100%{box-shadow:0 0 15px rgba(0,212,255,.5)}50%{box-shadow:0 0 25px rgba(0,212,255,.9)}} | |
| @keyframes pdot{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.6;transform:scale(1.3)}} | |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| @media(max-width:900px){.policy-queue{grid-template-columns:repeat(3,1fr)}.cfg-grid{grid-template-columns:1fr 1fr}} | |
| @media(max-width:600px){.policy-queue{grid-template-columns:repeat(2,1fr)}.main-grid{grid-template-columns:1fr}.tl{display:none}} | |
| /* ββ AUDIT MODAL βββββββββββββββββββββββββββββββββββββββββββ */ | |
| .audit-modal-overlay { | |
| position:fixed;inset:0;z-index:2000; | |
| background:rgba(0,0,0,0.82);backdrop-filter:blur(8px); | |
| display:flex;align-items:center;justify-content:center; | |
| opacity:0;pointer-events:none;transition:opacity 0.25s; | |
| } | |
| .audit-modal-overlay.open{opacity:1;pointer-events:all;} | |
| .audit-modal { | |
| background:#0d1626;border:1px solid rgba(244,114,182,0.3); | |
| width:min(720px,95vw);max-height:88vh;overflow-y:auto; | |
| padding:0;position:relative; | |
| transform:translateY(20px) scale(0.97);transition:transform 0.25s; | |
| scrollbar-width:thin;scrollbar-color:rgba(244,114,182,.3) transparent; | |
| } | |
| .audit-modal-overlay.open .audit-modal{transform:translateY(0) scale(1);} | |
| .audit-modal-header { | |
| background:linear-gradient(135deg,rgba(244,114,182,.15),rgba(168,85,247,.1)); | |
| border-bottom:1px solid rgba(244,114,182,.2); | |
| padding:1.2rem 1.5rem;display:flex;align-items:center;justify-content:space-between; | |
| position:sticky;top:0;z-index:1; | |
| } | |
| .audit-modal-title{font-family:'Share Tech Mono',monospace;font-size:14px;color:#f472b6;font-weight:700;letter-spacing:.1em;} | |
| .audit-modal-sub{font-size:10px;color:rgba(255,255,255,.5);margin-top:2px;letter-spacing:.05em;} | |
| .audit-modal-close { | |
| background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12); | |
| color:rgba(255,255,255,.7);cursor:pointer;width:28px;height:28px; | |
| display:flex;align-items:center;justify-content:center;font-size:14px; | |
| transition:background .2s; | |
| } | |
| .audit-modal-close:hover{background:rgba(255,77,77,.2);color:#ff4d4d;} | |
| .audit-modal-body{padding:1.5rem;} | |
| /* Summary box */ | |
| .audit-summary-box { | |
| background:rgba(244,114,182,.07);border-left:3px solid #f472b6; | |
| padding:.9rem 1.1rem;margin-bottom:1.2rem; | |
| font-size:12px;color:rgba(255,255,255,.85);line-height:1.7; | |
| font-family:'Share Tech Mono',monospace; | |
| } | |
| .audit-summary-label{font-size:9px;letter-spacing:.15em;color:#f472b6;text-transform:uppercase;margin-bottom:.4rem;} | |
| /* Score bar */ | |
| .audit-score-row{display:flex;align-items:center;gap:.8rem;margin-bottom:1.2rem;} | |
| .audit-score-label{font-size:9px;letter-spacing:.12em;color:rgba(255,255,255,.5);text-transform:uppercase;white-space:nowrap;} | |
| .audit-score-track{flex:1;height:6px;background:rgba(255,255,255,.08);border-radius:3px;overflow:hidden;} | |
| .audit-score-fill{height:100%;border-radius:3px;transition:width 1s ease;} | |
| .audit-score-val{font-family:'Share Tech Mono',monospace;font-size:13px;font-weight:700;width:40px;text-align:right;} | |
| /* Factor cards */ | |
| .audit-factors-title{font-size:9px;letter-spacing:.15em;color:rgba(255,255,255,.4);text-transform:uppercase;margin-bottom:.6rem;} | |
| .audit-factor { | |
| background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07); | |
| padding:.75rem 1rem;margin-bottom:.5rem; | |
| } | |
| .audit-factor-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.3rem;} | |
| .audit-factor-agent{font-size:11px;color:rgba(255,255,255,.9);font-family:'Share Tech Mono',monospace;font-weight:700;} | |
| .audit-factor-outcome{font-size:9px;font-weight:700;letter-spacing:.1em;padding:.2rem .6rem;border:1px solid;} | |
| .audit-factor-factor{font-size:9px;color:rgba(255,255,255,.4);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.3rem;} | |
| .audit-factor-detail{font-size:11px;color:rgba(255,255,255,.65);line-height:1.5;font-family:'Share Tech Mono',monospace;} | |
| /* Decline reasons */ | |
| .audit-decline-box{background:rgba(239,68,68,.06);border:1px solid rgba(239,68,68,.2);padding:.9rem 1.1rem;margin-bottom:1.2rem;} | |
| .audit-decline-title{font-size:9px;letter-spacing:.15em;color:#ef4444;text-transform:uppercase;margin-bottom:.5rem;} | |
| .audit-decline-item{font-size:11px;color:#fca5a5;line-height:1.6;margin-bottom:.3rem;font-family:'Share Tech Mono',monospace;} | |
| /* Export button */ | |
| .audit-export-btn { | |
| display:flex;align-items:center;gap:.5rem; | |
| background:linear-gradient(135deg,rgba(0,212,255,.15),rgba(0,212,255,.08)); | |
| border:1px solid rgba(0,212,255,.35);color:#00d4ff; | |
| padding:.6rem 1.2rem;font-family:'Share Tech Mono',monospace; | |
| font-size:11px;letter-spacing:.08em;cursor:pointer; | |
| transition:background .2s,box-shadow .2s;width:100%;justify-content:center;margin-top:1rem; | |
| } | |
| .audit-export-btn:hover{background:rgba(0,212,255,.2);box-shadow:0 0 20px rgba(0,212,255,.2);} | |
| /* View Audit button on result card */ | |
| .btn-view-audit { | |
| display:inline-flex;align-items:center;gap:.4rem;margin-top:10px; | |
| background:rgba(244,114,182,.12);border:1px solid rgba(244,114,182,.35); | |
| color:#f472b6;padding:.45rem 1rem;font-family:'Share Tech Mono',monospace; | |
| font-size:10px;letter-spacing:.08em;cursor:pointer;transition:background .2s; | |
| } | |
| .btn-view-audit:hover{background:rgba(244,114,182,.22);} | |
| /* ββ CLAIMSENSE ββ */ | |
| .claims-section{display:none;animation:fadeUp .4s ease forwards} | |
| .claims-section.active{display:block} | |
| .claims-header{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:3px;color:#fb923c;text-transform:uppercase;margin-bottom:14px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px} | |
| .claim-queue{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px;margin-bottom:18px} | |
| .claim-card{background:rgba(255,255,255,.03);border:1px solid rgba(251,146,60,.2);padding:12px;cursor:pointer;transition:border-color .2s} | |
| .claim-card:hover,.claim-card.selected{border-color:#fb923c;background:rgba(251,146,60,.06)} | |
| .claim-card-id{font-family:'Share Tech Mono',monospace;font-size:9px;color:#fb923c;margin-bottom:4px} | |
| .claim-card-name{font-size:13px;font-weight:600;color:var(--text);margin-bottom:4px} | |
| .claim-card-meta{font-size:10px;color:var(--dim2);margin-bottom:6px} | |
| .claim-card-chips{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:6px} | |
| .claim-chip{font-size:9px;padding:2px 6px;border:1px solid rgba(255,255,255,.1);color:var(--dim2)} | |
| .claim-chip.fire{border-color:rgba(239,68,68,.4);color:#fca5a5} | |
| .claim-chip.theft{border-color:rgba(251,146,60,.4);color:#fdba74} | |
| .claim-chip.water{border-color:rgba(59,130,246,.4);color:#93c5fd} | |
| .claim-chip.wind{border-color:rgba(99,102,241,.4);color:#c7d2fe} | |
| .claim-chip.flood{border-color:rgba(59,130,246,.4);color:#93c5fd} | |
| .claim-chip.liability{border-color:rgba(168,85,247,.4);color:#d8b4fe} | |
| .claim-chip.auto_settle{border-color:rgba(34,197,94,.4);color:#86efac} | |
| .claim-chip.adjuster_review{border-color:rgba(251,191,36,.4);color:#fde68a} | |
| .claim-chip.siu_referral{border-color:rgba(239,68,68,.4);color:#fca5a5} | |
| .claim-chip.deny{border-color:rgba(107,114,128,.4);color:#9ca3af} | |
| .fnol-form{background:rgba(255,255,255,.03);border:1px solid rgba(251,146,60,.2);padding:20px;margin-bottom:16px} | |
| .fnol-title{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;color:#fb923c;text-transform:uppercase;margin-bottom:14px} | |
| .fnol-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px} | |
| .fnol-field{display:flex;flex-direction:column;gap:4px} | |
| .fnol-field.full{grid-column:1/-1} | |
| .fnol-label{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:1px;color:var(--dim2);text-transform:uppercase} | |
| .fnol-input{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);color:var(--text);padding:7px 10px;font-family:'Share Tech Mono',monospace;font-size:11px;outline:none;transition:border-color .2s} | |
| .fnol-input:focus{border-color:#fb923c} | |
| .fnol-select{background:rgba(20,25,40,1);border:1px solid rgba(255,255,255,.1);color:var(--text);padding:7px 10px;font-family:'Share Tech Mono',monospace;font-size:11px;outline:none} | |
| .fnol-select:focus{border-color:#fb923c} | |
| .fnol-actions{display:flex;gap:8px;margin-top:14px;flex-wrap:wrap;align-items:center} | |
| .triage-card{border:1px solid;padding:16px;margin-top:12px} | |
| .triage-card.auto_settle{border-color:rgba(34,197,94,.4);background:rgba(34,197,94,.05)} | |
| .triage-card.adjuster_review{border-color:rgba(251,191,36,.3);background:rgba(251,191,36,.04)} | |
| .triage-card.siu_referral{border-color:rgba(239,68,68,.35);background:rgba(239,68,68,.05)} | |
| .triage-card.deny{border-color:rgba(107,114,128,.3);background:rgba(107,114,128,.04)} | |
| .triage-metrics{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px;margin-bottom:10px} | |
| .triage-metric{background:rgba(255,255,255,.03);padding:8px 10px;border:1px solid rgba(255,255,255,.06)} | |
| .triage-metric-label{font-family:'Share Tech Mono',monospace;font-size:8px;letter-spacing:1px;color:var(--dim2);text-transform:uppercase;margin-bottom:3px} | |
| .triage-metric-val{font-size:14px;font-weight:700;color:var(--text)} | |
| .claim-steps{display:flex;gap:0;margin-bottom:16px;overflow:hidden;border:1px solid rgba(255,255,255,.07)} | |
| .claim-step{flex:1;padding:8px 6px;text-align:center;border-right:1px solid rgba(255,255,255,.07);transition:background .3s} | |
| .claim-step:last-child{border-right:none} | |
| .claim-step.idle{background:transparent} | |
| .claim-step.running{background:rgba(251,146,60,.12)} | |
| .claim-step.done{background:rgba(34,197,94,.07)} | |
| .claim-step.failed{background:rgba(239,68,68,.07)} | |
| .claim-step-icon{font-size:14px;margin-bottom:2px} | |
| .claim-step-label{font-family:'Share Tech Mono',monospace;font-size:8px;letter-spacing:.5px;color:var(--dim2)} | |
| .claim-step.done .claim-step-label{color:#86efac} | |
| .claim-step.failed .claim-step-label{color:#fca5a5} | |
| .claim-step.running .claim-step-label{color:#fb923c} | |
| /* ββ RISKRADAR ββ */ | |
| .radar-section{display:none;animation:fadeUp .4s ease forwards} | |
| .radar-section.active{display:block} | |
| .radar-header{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:3px;color:#38bdf8;text-transform:uppercase;margin-bottom:14px} | |
| .radar-layout{display:grid;grid-template-columns:340px 1fr;gap:16px;min-height:520px} | |
| @media(max-width:900px){.radar-layout{grid-template-columns:1fr}} | |
| .radar-panel{display:flex;flex-direction:column;gap:12px} | |
| .radar-form{background:rgba(255,255,255,.03);border:1px solid rgba(56,189,248,.2);padding:16px} | |
| .radar-form-title{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;color:#38bdf8;text-transform:uppercase;margin-bottom:12px} | |
| .radar-field{display:flex;flex-direction:column;gap:4px;margin-bottom:8px} | |
| .radar-label{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:1px;color:var(--dim2);text-transform:uppercase} | |
| .radar-input{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);color:var(--text);padding:7px 10px;font-family:'Share Tech Mono',monospace;font-size:11px;outline:none;width:100%;transition:border-color .2s} | |
| .radar-input:focus{border-color:#38bdf8} | |
| .radar-select{background:rgba(20,25,40,1);border:1px solid rgba(255,255,255,.1);color:var(--text);padding:7px 10px;font-family:'Share Tech Mono',monospace;font-size:11px;outline:none;width:100%} | |
| .radar-grid2{display:grid;grid-template-columns:1fr 1fr;gap:8px} | |
| .radar-btn{background:rgba(56,189,248,.12);border:1px solid rgba(56,189,248,.5);color:#38bdf8;padding:9px 16px;font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:.1em;cursor:pointer;width:100%;transition:background .2s} | |
| .radar-btn:hover{background:rgba(56,189,248,.2)} | |
| .radar-btn:disabled{opacity:.4;cursor:default} | |
| /* Peril score cards */ | |
| .peril-cards{display:grid;grid-template-columns:1fr 1fr;gap:8px} | |
| .peril-card{border:1px solid rgba(255,255,255,.07);padding:12px;position:relative;overflow:hidden} | |
| .peril-card-icon{font-size:20px;margin-bottom:4px} | |
| .peril-card-label{font-family:'Share Tech Mono',monospace;font-size:8px;letter-spacing:2px;color:var(--dim2);text-transform:uppercase;margin-bottom:4px} | |
| .peril-card-score{font-size:28px;font-weight:700;font-family:'Share Tech Mono',monospace;margin-bottom:2px} | |
| .peril-card-band{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:1px;padding:2px 7px;border:1px solid;display:inline-block} | |
| .peril-bar{height:3px;background:rgba(255,255,255,.06);margin-top:8px} | |
| .peril-bar-fill{height:100%;transition:width 1.2s ease} | |
| .band-critical{color:#ef4444;border-color:rgba(239,68,68,.5)} | |
| .band-high{color:#f97316;border-color:rgba(249,115,22,.5)} | |
| .band-medium{color:#fbbf24;border-color:rgba(251,191,36,.5)} | |
| .band-low{color:#34d399;border-color:rgba(52,211,153,.5)} | |
| .overall-card{border:1px solid rgba(56,189,248,.25);padding:14px;background:rgba(56,189,248,.04)} | |
| .overall-score-big{font-size:42px;font-weight:700;font-family:'Share Tech Mono',monospace;color:#38bdf8} | |
| .radar-narrative{font-size:11px;color:var(--dim);line-height:1.7;padding:10px;background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.06);margin-top:8px} | |
| /* Map */ | |
| .radar-map-wrap{border:1px solid rgba(56,189,248,.2);position:relative;min-height:480px} | |
| #radarMap{width:100%;height:480px;background:#0d1117} | |
| .map-legend{position:absolute;bottom:12px;right:12px;background:rgba(10,14,26,.92);border:1px solid rgba(255,255,255,.12);padding:10px 12px;z-index:1000;font-family:'Share Tech Mono',monospace;font-size:9px} | |
| .map-legend-title{color:rgba(255,255,255,.5);letter-spacing:2px;text-transform:uppercase;margin-bottom:6px} | |
| .legend-row{display:flex;align-items:center;gap:6px;margin-bottom:3px} | |
| .legend-dot{width:10px;height:10px;border-radius:50%} | |
| .legend-label{color:rgba(255,255,255,.6)} | |
| .peril-tabs{display:flex;gap:4px;margin-bottom:8px} | |
| .peril-tab{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:.08em;padding:4px 10px;border:1px solid rgba(255,255,255,.1);color:var(--dim2);cursor:pointer;transition:all .2s} | |
| .peril-tab.active{border-color:#38bdf8;color:#38bdf8;background:rgba(56,189,248,.08)} | |
| .radar-history{margin-top:16px} | |
| .radar-history-title{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;color:#38bdf8;text-transform:uppercase;margin-bottom:8px} | |
| .history-row{display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.05);cursor:pointer;transition:background .15s} | |
| .history-row:hover{background:rgba(56,189,248,.04)} | |
| .history-addr{font-size:11px;color:var(--text);flex:1} | |
| .history-chip{font-family:'Share Tech Mono',monospace;font-size:9px;padding:1px 6px;border:1px solid} | |
| /* ββ POLICYBRIDGE ββ */ | |
| .pb-section{display:none;animation:fadeUp .4s ease forwards} | |
| .pb-section.active{display:block} | |
| .pb-layout{display:grid;grid-template-columns:220px 1fr 240px;min-height:calc(100vh - 54px)} | |
| @media(max-width:900px){.pb-layout{grid-template-columns:1fr}} | |
| .pb-nav{background:rgba(255,255,255,.02);border-right:1px solid var(--border);padding:16px 12px;position:sticky;top:0;height:calc(100vh - 54px);overflow-y:auto} | |
| .pb-nav-title{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;color:#818cf8;text-transform:uppercase;margin-bottom:10px} | |
| .pb-nav-item{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:500;color:var(--dim);margin-bottom:2px;transition:all .2s;border:1px solid transparent} | |
| .pb-nav-item:hover{background:rgba(129,140,248,.06);color:var(--text)} | |
| .pb-nav-item.active{background:rgba(129,140,248,.1);border-color:rgba(129,140,248,.3);color:#818cf8} | |
| .pb-nav-item.done{color:#10b981} | |
| .pb-nav-num{width:20px;height:20px;min-width:20px;border-radius:50%;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;flex-shrink:0} | |
| .pb-nav-item.active .pb-nav-num{background:#818cf8;border-color:#818cf8;color:#fff} | |
| .pb-nav-item.done .pb-nav-num{background:#10b981;border-color:#10b981;color:#fff} | |
| .pb-main{padding:22px 26px;overflow-y:auto} | |
| .pb-panel{background:rgba(255,255,255,.02);border-left:1px solid var(--border);padding:16px 14px;position:sticky;top:0;height:calc(100vh - 54px);overflow-y:auto} | |
| .pb-step{display:none;animation:fadeUp .25s ease} | |
| .pb-step.active{display:block} | |
| .pb-step-hdr{display:flex;align-items:center;gap:12px;margin-bottom:20px;padding-bottom:14px;border-bottom:1px solid var(--border)} | |
| .pb-step-icon{width:38px;height:38px;border-radius:9px;background:rgba(129,140,248,.1);border:1px solid rgba(129,140,248,.3);display:flex;align-items:center;justify-content:center;font-size:18px} | |
| .pb-step-title{font-size:18px;font-weight:700;color:var(--text)} | |
| .pb-step-sub{font-size:11px;color:var(--dim)} | |
| .pb-card{background:rgba(255,255,255,.025);border:1px solid var(--border);border-radius:10px;padding:16px 18px;margin-bottom:14px} | |
| .pb-card-title{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;color:#818cf8;text-transform:uppercase;margin-bottom:12px;display:flex;align-items:center;gap:6px} | |
| .pb-card-title::before{content:'';width:3px;height:12px;background:#818cf8;border-radius:2px;display:inline-block} | |
| .pb-fg{display:grid;gap:12px} | |
| .pb-fg2{grid-template-columns:1fr 1fr} | |
| .pb-fg3{grid-template-columns:1fr 1fr 1fr} | |
| .pb-cs2{grid-column:span 2} | |
| .pb-field{display:flex;flex-direction:column;gap:4px} | |
| .pb-label{font-size:11px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:4px} | |
| .pb-label .req{color:var(--danger)} | |
| .pb-input{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.12);color:var(--text);padding:7px 10px;font-family:'Share Tech Mono',monospace;font-size:11px;border-radius:6px;outline:none;width:100%;transition:border-color .2s} | |
| .pb-input:focus{border-color:#818cf8} | |
| .pb-select{background:#0a0e1a;border:1px solid rgba(255,255,255,.12);color:var(--text);padding:7px 10px;font-family:'Share Tech Mono',monospace;font-size:11px;border-radius:6px;outline:none;width:100%} | |
| .pb-btn-row{display:flex;gap:8px;align-items:center;margin-top:20px;padding-top:14px;border-top:1px solid var(--border)} | |
| .pb-cov-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:14px} | |
| .pb-cov-card{border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:10px;cursor:pointer;transition:all .2s;background:rgba(255,255,255,.02);position:relative} | |
| .pb-cov-card:hover,.pb-cov-card.sel{border-color:#818cf8;background:rgba(129,140,248,.08)} | |
| .pb-cov-card input{position:absolute;opacity:0} | |
| .pb-cov-code{font-family:'Share Tech Mono',monospace;font-size:13px;font-weight:500;color:#818cf8;margin-bottom:2px} | |
| .pb-cov-name{font-size:10px;color:var(--text);font-weight:600} | |
| .pb-cov-desc{font-size:9px;color:var(--dim);margin-top:2px} | |
| .pb-chk-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px} | |
| .pb-chk{display:flex;align-items:center;gap:7px;padding:6px 9px;border:1px solid rgba(255,255,255,.08);border-radius:6px;cursor:pointer;font-size:11px;color:var(--dim);transition:all .2s;background:rgba(255,255,255,.02)} | |
| .pb-chk:hover{border-color:#818cf8;color:#818cf8} | |
| .pb-chk.on{border-color:#818cf8;background:rgba(129,140,248,.08);color:#818cf8;font-weight:600} | |
| .pb-step-bar{display:flex;gap:0;align-items:center;margin-bottom:20px;padding:12px 16px;background:rgba(255,255,255,.02);border:1px solid var(--border);border-radius:8px} | |
| .pb-sb-wrap{display:flex;flex-direction:column;align-items:center;gap:3px;flex:1} | |
| .pb-sb-bub{width:24px;height:24px;border-radius:50%;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;color:var(--dim);transition:all .25s;flex-shrink:0} | |
| .pb-sb-bub.active{background:#818cf8;border-color:#818cf8;color:#fff} | |
| .pb-sb-bub.done{background:#10b981;border-color:#10b981;color:#fff} | |
| .pb-sb-name{font-size:8px;color:var(--dim);white-space:nowrap;font-family:'Share Tech Mono',monospace} | |
| .pb-sb-wrap.active .pb-sb-name{color:#818cf8} | |
| .pb-sb-wrap.done .pb-sb-name{color:#10b981} | |
| .pb-sb-line{flex:1;height:1px;background:rgba(255,255,255,.08);margin:0 4px} | |
| .pb-sb-line.done{background:#10b981} | |
| .pb-panel-card{background:rgba(255,255,255,.03);border:1px solid var(--border);border-radius:8px;padding:10px 12px;margin-bottom:10px} | |
| .pb-panel-title{font-family:'Share Tech Mono',monospace;font-size:8px;letter-spacing:1.5px;color:#818cf8;text-transform:uppercase;margin-bottom:7px} | |
| .pb-prow{display:flex;justify-content:space-between;gap:6px;font-size:10px;margin-bottom:3px} | |
| .pb-pk{color:var(--dim)} | |
| .pb-pv{color:var(--text);font-weight:500;text-align:right;font-family:'Share Tech Mono',monospace;font-size:10px} | |
| .mga-overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);backdrop-filter:blur(6px);z-index:3000;display:flex;align-items:center;justify-content:center} | |
| .mga-modal{background:#0d1626;border:1px solid rgba(129,140,248,.3);border-radius:12px;width:min(640px,95vw);max-height:90vh;overflow-y:auto;padding:0} | |
| .mga-header{background:linear-gradient(135deg,rgba(129,140,248,.15),rgba(99,102,241,.08));border-bottom:1px solid rgba(129,140,248,.2);padding:1.2rem 1.5rem;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:1} | |
| .mga-title{font-family:'Share Tech Mono',monospace;font-size:13px;color:#818cf8;font-weight:700;letter-spacing:.1em} | |
| .mga-close{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);color:rgba(255,255,255,.6);cursor:pointer;width:26px;height:26px;display:flex;align-items:center;justify-content:center;font-size:13px;border-radius:4px} | |
| .mga-step{display:none} | |
| .mga-step.active{display:block;animation:fadeUp .25s ease} | |
| .mga-search-result{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:7px;padding:10px 12px;cursor:pointer;margin-bottom:6px;transition:all .2s} | |
| .mga-search-result:hover,.mga-search-result.sel{border-color:#818cf8;background:rgba(129,140,248,.08)} | |
| .mga-result-name{font-size:13px;font-weight:600;color:var(--text);margin-bottom:2px} | |
| .mga-result-meta{font-size:10px;color:var(--dim);font-family:'Share Tech Mono',monospace} | |
| .mga-code-badge{font-family:'Share Tech Mono',monospace;font-size:14px;font-weight:700;color:#818cf8;background:rgba(129,140,248,.1);border:1px solid rgba(129,140,248,.3);padding:8px 16px;border-radius:6px;text-align:center;letter-spacing:.1em;margin:10px 0} | |
| .mga-spinner{display:inline-block;width:16px;height:16px;border:2px solid rgba(129,140,248,.3);border-top-color:#818cf8;border-radius:50%;animation:spin .8s linear infinite;vertical-align:middle;margin-right:6px} | |
| .mga-progress{display:flex;gap:0;margin-bottom:16px} | |
| .mga-prog-step{flex:1;text-align:center;padding:6px 4px;font-family:'Share Tech Mono',monospace;font-size:8px;letter-spacing:.05em;color:var(--dim);border-bottom:2px solid rgba(255,255,255,.08);transition:all .2s} | |
| .mga-prog-step.active{color:#818cf8;border-color:#818cf8} | |
| .mga-prog-step.done{color:#10b981;border-color:#10b981} | |
| .mga-confirm-row{display:flex;justify-content:space-between;font-size:11px;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.05)} | |
| .mga-ck{color:var(--dim)} | |
| .mga-cv{color:var(--text);font-weight:500;font-family:'Share Tech Mono',monospace;font-size:10px} | |
| /* ββ QUOTE COMPARISON ββ */ | |
| .quote-section{margin-top:20px} | |
| .quote-header-bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;flex-wrap:wrap;gap:8px} | |
| .quote-title{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:2px;color:#818cf8;text-transform:uppercase} | |
| .quote-market-avg{font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--dim2)} | |
| .quote-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;margin-bottom:16px} | |
| .quote-card{border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:14px;position:relative;transition:all .2s;cursor:pointer;background:rgba(255,255,255,.02)} | |
| .quote-card:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.4)} | |
| .quote-card.our-quote{border-color:rgba(129,140,248,.5);background:rgba(129,140,248,.06)} | |
| .quote-card.selected-quote{border-color:#10b981;background:rgba(16,185,129,.06)} | |
| .quote-badge{position:absolute;top:-8px;left:12px;font-family:'Share Tech Mono',monospace;font-size:8px;font-weight:700;padding:2px 8px;border-radius:10px;letter-spacing:.08em} | |
| .qb-recommended{background:#818cf8;color:#fff} | |
| .qb-cheapest{background:#10b981;color:#fff} | |
| .qb-established{background:#f59e0b;color:#000} | |
| .quote-carrier{font-size:13px;font-weight:700;color:var(--text);margin-bottom:2px;margin-top:4px} | |
| .quote-am-best{font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--dim2);margin-bottom:8px} | |
| .quote-premium-big{font-size:28px;font-weight:800;font-family:'Share Tech Mono',monospace;margin-bottom:2px} | |
| .quote-monthly{font-size:11px;color:var(--dim2);margin-bottom:8px} | |
| .quote-saving{font-size:10px;padding:2px 8px;border-radius:10px;display:inline-block;margin-bottom:8px;font-family:'Share Tech Mono',monospace;font-weight:600} | |
| .qs-positive{background:rgba(16,185,129,.15);color:#10b981;border:1px solid rgba(16,185,129,.3)} | |
| .qs-negative{background:rgba(239,68,68,.1);color:#ef4444;border:1px solid rgba(239,68,68,.25)} | |
| .quote-features{font-size:10px;color:var(--dim);line-height:1.6;margin-bottom:8px} | |
| .quote-score-bar{height:4px;background:rgba(255,255,255,.08);border-radius:2px;margin-bottom:10px;overflow:hidden} | |
| .quote-score-fill{height:100%;border-radius:2px;transition:width .8s ease} | |
| .quote-select-btn{width:100%;padding:6px;border-radius:6px;font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:.08em;cursor:pointer;transition:all .2s;border:1px solid rgba(255,255,255,.15);background:transparent;color:var(--text)} | |
| .quote-select-btn:hover{background:rgba(255,255,255,.06)} | |
| .quote-card.selected-quote .quote-select-btn{background:#10b981;border-color:#10b981;color:#fff} | |
| .quote-narrative{background:rgba(129,140,248,.06);border:1px solid rgba(129,140,248,.2);border-left:3px solid #818cf8;padding:12px 14px;font-size:11px;color:var(--text);line-height:1.7;margin-bottom:14px;font-family:'Share Tech Mono',monospace} | |
| .quote-proceed-bar{display:flex;gap:10px;align-items:center;padding:12px 14px;background:rgba(16,185,129,.06);border:1px solid rgba(16,185,129,.2);border-radius:8px} | |
| .qpb-info{flex:1;font-size:11px;color:var(--text)} | |
| .qpb-carrier{font-weight:700;color:#10b981} | |
| .qpb-premium{font-family:'Share Tech Mono',monospace;font-size:13px;color:#818cf8;font-weight:700} | |
| </style> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <!-- HEADER --> | |
| <header> | |
| <div class="hd-left"> | |
| <div class="eyebrow">βΆ Live Inference Β· Policy Issuance Multi-Agent System</div> | |
| <h1>P&C Insurance <em>Multi-Agent</em> Pipeline</h1> | |
| <p class="sub">Bronze β Silver β Gold Β· 5 Agents Β· Real-Time Inference Β· Flask API</p> | |
| </div> | |
| <div class="hd-right"> | |
| <div class="db-status"> | |
| <div class="db-dot" id="dbDot"></div> | |
| <span id="dbStatusText">Connecting...</span> | |
| </div> | |
| <!-- <div class="hd-pills"> | |
| <span class="hpill">agent1_kyc_classifier.pkl</span> | |
| <span class="hpill">agent2_property_risk.pkl</span> | |
| <span class="hpill">agent3_underwriting.pkl</span> | |
| <span class="hpill">agent4_pricing.pkl</span> | |
| </div> --> | |
| <div> <button class="btn btn-sec" style="font-size:9px;padding:5px 12px" onclick="toggleConfig()">β API Config</button> </div> | |
| </div> | |
| </header> | |
| <!-- API CONFIG --> | |
| <div class="cfg-panel" id="cfgPanel"> | |
| <div class="cfg-title">π§ Flask API Settings (HuggingFace β Clever Cloud)</div> | |
| <div class="cfg-grid" style="grid-template-columns:1fr"> | |
| <div class="cfg-field"><label class="cfg-label">API Base URL</label><input class="cfg-input" id="cfgApi" value="https://itnovaml-pcagentinai.hf.space" placeholder="https://itnovaml-pcagentinai.hf.space" style="width:100%"></div> | |
| </div> | |
| <div style="font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--dim2);margin-top:6px;padding:6px 8px;background:rgba(0,212,255,0.05);border:1px solid rgba(0,212,255,0.15);border-radius:4px"> | |
| DB: btvbbpqhvnttzvptguj3-mysql.services.clever-cloud.com Β· bronze_* tables Β· Clever Cloud MySQL | |
| </div> | |
| <div class="cfg-actions"> | |
| <button class="btn btn-primary" onclick="testConnection()">βΆ Test Connection</button> | |
| <button class="btn btn-sec" onclick="toggleConfig()">β Close</button> | |
| <span id="cfgTestResult" style="font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--dim2);margin-left:8px;align-self:center"></span> | |
| </div> | |
| </div> | |
| <!-- TABS --> | |
| <div class="tabs"> | |
| <!-- <button class="tab active" onclick="switchTab('queue')">π Submission Queue</button> --> | |
| <button class="tab" onclick="switchTab('live')">β‘ Live Submissions</button> | |
| <button class="tab" onclick="switchTab('demo')">π Demo Scenarios</button> | |
| <button class="tab" id="claimsTab" onclick="switchTab('claims')" style="border-color:rgba(251,146,60,.5);color:#fb923c">π΄ ClaimSense</button> | |
| <button class="tab" id="radarTab" onclick="switchTab('radar')" style="border-color:rgba(56,189,248,.5);color:#38bdf8">π RiskRadar</button> | |
| <button class="tab" id="pbTab" onclick="switchTab('policybridge')" style="border-color:rgba(99,102,241,.5);color:#818cf8">π PolicyBridge</button> | |
| </div> | |
| <!-- QUEUE SECTION --> | |
| <div class="queue-section"> | |
| <div class="queue-header"> | |
| <div class="qh-left"> | |
| <span class="queue-title" id="queueTitle">π Recent Policy Submissions</span> | |
| <span class="queue-sub" id="queueSub">β Loaded from Bronze layer Β· Click any submission to process</span> | |
| </div> | |
| <div class="qh-right"> | |
| <span class="batch-info" id="batchInfo">Showing <span id="batchCount">0</span> submissions</span> | |
| <button class="reload-btn" id="reloadBtn" onclick="loadSubmissions()">βΊ Refresh</button> | |
| </div> | |
| </div> | |
| <div class="policy-queue" id="policyQueue"> | |
| <div class="queue-empty">Loading submissions from Bronze layer...</div> | |
| </div> | |
| </div> | |
| <!-- CONTROLS --> | |
| <div class="controls"> | |
| <button class="btn btn-primary" id="startBtn" onclick="startSimulation()" disabled>βΆ Run Simulation</button> | |
| <button class="btn btn-live" id="liveBtn" onclick="runLiveAgents()" disabled style="display:none">β‘ Run Live Agents</button> | |
| <button class="btn btn-sec" onclick="resetDemo()">βΊ Reset</button> | |
| <button class="btn btn-sec" id="speedBtn" onclick="toggleSpeed()">β‘ Speed: Normal</button> | |
| <div class="sel-info" id="selInfo">No submission selected</div> | |
| </div> | |
| <!-- PROGRESS --> | |
| <div class="prog-wrap"><div class="prog" id="prog"></div></div> | |
| <!-- TIMELINE --> | |
| <div class="tl"> | |
| <div class="tl-prog" id="tlProg"></div> | |
| <div class="tls" id="tl-0"><div class="tld">π₯</div><div class="tll">Submission</div></div> | |
| <div class="tls" id="tl-1"><div class="tld">πͺͺ</div><div class="tll">Document & Identity</div></div> | |
| <div class="tls" id="tl-2"><div class="tld">π </div><div class="tll">Property Risk</div></div> | |
| <div class="tls" id="tl-3"><div class="tld">π</div><div class="tll">Underwriting</div></div> | |
| <div class="tls" id="tl-4"><div class="tld">π²</div><div class="tll">ML Pricing</div></div> | |
| <div class="tls" id="tl-5"><div class="tld">π</div><div class="tll">Issuance</div></div> | |
| <div class="tls" id="tl-6"><div class="tld">π</div><div class="tll">Audit</div></div> | |
| </div> | |
| <!-- MAIN GRID --> | |
| <div class="main-grid"> | |
| <div class="agents-sidebar"> | |
| <div class="ac" id="ac-orch" style="--agent-color:#a855f7"><div class="ai">π§ </div><div class="an">Orchestrator</div><div class="ar">AWS Step Functions</div><div class="astat idle" id="as-orch">β STANDBY</div></div> | |
| <div class="ac" id="ac-kyc" style="--agent-color:#00d4ff"><div class="ai">πͺͺ</div><div class="an">Doc Agent<span class="agent-live-tag" id="tag-kyc" style="display:none">LIVE</span></div><div class="ar">XGBoost Β· Identity</div><div class="astat idle" id="as-kyc">β IDLE</div></div> | |
| <div class="ac" id="ac-prop" style="--agent-color:#ff6b35"><div class="ai">π </div><div class="an">Property Agent<span class="agent-live-tag" id="tag-prop" style="display:none">LIVE</span></div><div class="ar">XGBoost Β· Geo-Risk</div><div class="astat idle" id="as-prop">β IDLE</div></div> | |
| <div class="ac" id="ac-uw" style="--agent-color:#f59e0b"><div class="ai">π</div><div class="an">UW Agent<span class="agent-live-tag" id="tag-uw" style="display:none">LIVE</span></div><div class="ar">XGBoost Β· Rules Engine</div><div class="astat idle" id="as-uw">β IDLE</div></div> | |
| <div class="ac" id="ac-price" style="--agent-color:#00ff9d"><div class="ai">π€</div><div class="an">ML Pricing<span class="agent-live-tag" id="tag-price" style="display:none">LIVE</span></div><div class="ar">XGBoost + GLM Ensemble</div><div class="astat idle" id="as-price">β IDLE</div></div> | |
| <div class="ac" id="ac-issue" style="--agent-color:#818cf8"><div class="ai">π</div><div class="an">Issuance Agent</div><div class="ar">Policy Binding</div><div class="astat idle" id="as-issue">β IDLE</div></div> | |
| <div class="ac" id="ac-audit" style="--agent-color:#f472b6"><div class="ai">π</div><div class="an">Audit Agent<span class="agent-live-tag" id="tag-audit" style="display:none">LIVE</span></div><div class="ar">AXIOM Β· Decision Trail</div><div class="astat idle" id="as-audit">β IDLE</div></div> | |
| </div> | |
| <div class="chat-area" id="chatArea"> | |
| <div style="text-align:center;padding:70px 20px;color:var(--dim2)"> | |
| <div style="font-size:42px;margin-bottom:12px;opacity:.2">π</div> | |
| <div style="font-family:'Share Tech Mono',monospace;font-size:13px">Select a submission from the queue above</div> | |
| <div style="font-size:11px;margin-top:6px;opacity:.6">Live submissions pull from Bronze MySQL Β· Demo scenarios use static data</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- CLAIMSENSE SECTION --> | |
| <div class="claims-section" id="claimsSection"> | |
| <div class="fnol-form" id="fnolForm"> | |
| <div class="fnol-title">π΄ Submit New FNOL β First Notice of Loss</div> | |
| <div class="fnol-grid"> | |
| <div class="fnol-field"><label class="fnol-label">Policy Number *</label><input class="fnol-input" id="fnol_policy" placeholder="PC-2025-00612TX" /></div> | |
| <div class="fnol-field"><label class="fnol-label">Claimant Name *</label><input class="fnol-input" id="fnol_name" placeholder="John Smith" /></div> | |
| <div class="fnol-field"><label class="fnol-label">Incident Date *</label><input class="fnol-input" id="fnol_date" type="date" /></div> | |
| <div class="fnol-field"><label class="fnol-label">Incident Type *</label> | |
| <select class="fnol-select" id="fnol_type"> | |
| <option value="FIRE">Fire</option><option value="WATER">Water / Burst Pipe</option> | |
| <option value="WIND">Wind / Storm</option><option value="HAIL">Hail</option> | |
| <option value="THEFT">Theft / Burglary</option><option value="VANDALISM">Vandalism</option> | |
| <option value="LIABILITY">Liability / Injury</option><option value="FLOOD">Flood</option> | |
| <option value="STRUCTURAL">Structural Damage</option><option value="VEHICLE_IMPACT">Vehicle Impact</option> | |
| <option value="OTHER">Other</option> | |
| </select> | |
| </div> | |
| <div class="fnol-field"><label class="fnol-label">Estimated Damage ($)</label><input class="fnol-input" id="fnol_damage" type="number" placeholder="25000" /></div> | |
| <div class="fnol-field"><label class="fnol-label">Coverage Type</label> | |
| <select class="fnol-select" id="fnol_coverage"> | |
| <option value="HO-3">HO-3 (Standard Homeowners)</option><option value="HO-5">HO-5 (Comprehensive)</option> | |
| <option value="HO-4">HO-4 (Renters)</option><option value="HO-6">HO-6 (Condo)</option><option value="DP-3">DP-3 (Dwelling)</option> | |
| </select> | |
| </div> | |
| <div class="fnol-field full"><label class="fnol-label">Incident Description</label><input class="fnol-input" id="fnol_desc" placeholder="Brief description of what happened..." /></div> | |
| <div class="fnol-field"><label class="fnol-label">Has Police Report?</label><select class="fnol-select" id="fnol_police"><option value="0">No</option><option value="1">Yes</option></select></div> | |
| <div class="fnol-field"><label class="fnol-label">Photos Submitted?</label><select class="fnol-select" id="fnol_photos"><option value="0">No</option><option value="1">Yes</option></select></div> | |
| </div> | |
| <div class="fnol-actions"> | |
| <button class="btn btn-live" id="submitFnolBtn" onclick="submitAndRunClaim()" style="border-color:#fb923c;background:#fb923c;color:#0a0e1a;font-weight:600">π΄ Submit FNOL & Run Triage</button> | |
| <button class="btn" onclick="loadRecentClaims()" style="font-size:11px">βΊ Load Recent Claims</button> | |
| <span id="fnolStatus" style="font-size:11px;color:var(--dim2)"></span> | |
| </div> | |
| </div> | |
| <div class="claim-steps" id="claimSteps"> | |
| <div class="claim-step idle" id="cstep1"><div class="claim-step-icon">π</div><div class="claim-step-label">FNOL Intake</div></div> | |
| <div class="claim-step idle" id="cstep2"><div class="claim-step-icon">π‘</div><div class="claim-step-label">Coverage</div></div> | |
| <div class="claim-step idle" id="cstep3"><div class="claim-step-icon">π</div><div class="claim-step-label">Fraud Signal</div></div> | |
| <div class="claim-step idle" id="cstep4"><div class="claim-step-icon">β</div><div class="claim-step-label">Severity</div></div> | |
| <div class="claim-step idle" id="cstep5"><div class="claim-step-icon">π¦</div><div class="claim-step-label">Triage</div></div> | |
| </div> | |
| <div id="triageResult" style="display:none"></div> | |
| <div class="claims-header"> | |
| <span>π Recent Claims</span> | |
| <div style="display:flex;gap:8px;align-items:center"> | |
| <span id="claimCount" style="font-size:10px;color:var(--dim2)">0 claims</span> | |
| <button class="reload-btn" onclick="loadRecentClaims()">βΊ Refresh</button> | |
| </div> | |
| </div> | |
| <div class="claim-queue" id="claimQueue"> | |
| <div style="color:var(--dim2);font-size:11px;font-family:'Share Tech Mono',monospace">Submit a claim or click Refresh to load recent claims.</div> | |
| </div> | |
| </div> | |
| <!-- RISKRADAR SECTION --> | |
| <div class="radar-section" id="radarSection"> | |
| <div class="radar-header">π RiskRadar β Geo Risk Intelligence Β· Address-Level Peril Scoring</div> | |
| <div class="radar-layout"> | |
| <!-- Left panel: form + scores --> | |
| <div class="radar-panel"> | |
| <div class="radar-form"> | |
| <div class="radar-form-title">Property Lookup</div> | |
| <div class="radar-field"><label class="radar-label">Street Address</label><input class="radar-input" id="rr_address" placeholder="4821 Elm Ridge Drive" /></div> | |
| <div class="radar-grid2"> | |
| <div class="radar-field"><label class="radar-label">State *</label> | |
| <select class="radar-select" id="rr_state"> | |
| <option value="TX">TX β Texas</option><option value="FL">FL β Florida</option> | |
| <option value="CA">CA β California</option><option value="NY">NY β New York</option> | |
| <option value="LA">LA β Louisiana</option><option value="NC">NC β N. Carolina</option> | |
| <option value="SC">SC β S. Carolina</option><option value="GA">GA β Georgia</option> | |
| <option value="AL">AL β Alabama</option><option value="MS">MS β Mississippi</option> | |
| <option value="OK">OK β Oklahoma</option><option value="KS">KS β Kansas</option> | |
| <option value="CO">CO β Colorado</option><option value="AZ">AZ β Arizona</option> | |
| <option value="NM">NM β New Mexico</option><option value="NV">NV β Nevada</option> | |
| <option value="OR">OR β Oregon</option><option value="WA">WA β Washington</option> | |
| <option value="IL">IL β Illinois</option><option value="OH">OH β Ohio</option> | |
| <option value="VA">VA β Virginia</option><option value="MD">MD β Maryland</option> | |
| <option value="NJ">NJ β New Jersey</option><option value="PA">PA β Pennsylvania</option> | |
| <option value="TN">TN β Tennessee</option><option value="MO">MO β Missouri</option> | |
| <option value="AR">AR β Arkansas</option><option value="UT">UT β Utah</option> | |
| <option value="ID">ID β Idaho</option><option value="MT">MT β Montana</option> | |
| <option value="WY">WY β Wyoming</option><option value="AK">AK β Alaska</option> | |
| <option value="HI">HI β Hawaii</option><option value="MA">MA β Massachusetts</option> | |
| <option value="CT">CT β Connecticut</option><option value="MI">MI β Michigan</option> | |
| <option value="WI">WI β Wisconsin</option><option value="MN">MN β Minnesota</option> | |
| <option value="IA">IA β Iowa</option><option value="NE">NE β Nebraska</option> | |
| <option value="KY">KY β Kentucky</option><option value="IN">IN β Indiana</option> | |
| <option value="DC">DC β Washington DC</option> | |
| </select> | |
| </div> | |
| <div class="radar-field"><label class="radar-label">ZIP Code *</label><input class="radar-input" id="rr_zip" placeholder="77001" maxlength="5"/></div> | |
| </div> | |
| <div class="radar-grid2"> | |
| <div class="radar-field"><label class="radar-label">Year Built</label><input class="radar-input" id="rr_year" type="number" placeholder="2005" /></div> | |
| <div class="radar-field"><label class="radar-label">Roof Year</label><input class="radar-input" id="rr_roof" type="number" placeholder="2018" /></div> | |
| </div> | |
| <div class="radar-grid2"> | |
| <div class="radar-field"><label class="radar-label">Construction</label> | |
| <select class="radar-select" id="rr_constr"> | |
| <option value="Frame">Frame</option><option value="Masonry">Masonry</option> | |
| <option value="Concrete">Concrete</option><option value="Steel">Steel Frame</option> | |
| </select> | |
| </div> | |
| <div class="radar-field"><label class="radar-label">Stories</label> | |
| <select class="radar-select" id="rr_stories"><option value="1">1</option><option value="2" selected>2</option><option value="3">3</option><option value="4">4+</option></select> | |
| </div> | |
| </div> | |
| <button class="radar-btn" id="rrScoreBtn" onclick="runRiskRadar()">β‘ SCORE PROPERTY RISK</button> | |
| <div id="rrStatus" style="font-size:10px;color:var(--dim2);font-family:Share Tech Mono,monospace;margin-top:6px;text-align:center"></div> | |
| </div> | |
| <!-- Overall score card --> | |
| <div class="overall-card" id="rrOverall" style="display:none"> | |
| <div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:6px"> | |
| <div> | |
| <div style="font-family:'Share Tech Mono',monospace;font-size:8px;letter-spacing:2px;color:rgba(56,189,248,.7);text-transform:uppercase;margin-bottom:4px">Overall Risk Score</div> | |
| <div class="overall-score-big" id="rrOverallScore">β</div> | |
| <div class="peril-card-band" id="rrOverallBand" style="margin-top:4px">β</div> | |
| </div> | |
| <div style="text-align:right"> | |
| <button onclick="exportRiskReport()" style="background:transparent;border:1px solid rgba(56,189,248,.35);color:#38bdf8;padding:5px 10px;font-family:Share Tech Mono,monospace;font-size:8px;letter-spacing:.1em;cursor:pointer;margin-bottom:4px">β¬ EXPORT</button> | |
| <div id="rrMethod" style="font-size:9px;color:var(--dim2);font-family:Share Tech Mono,monospace"></div> | |
| </div> | |
| </div> | |
| <div class="radar-narrative" id="rrNarrative"></div> | |
| </div> | |
| <!-- 4 peril score cards --> | |
| <div class="peril-cards" id="rrPerilCards" style="display:none"> | |
| <div class="peril-card" id="pc_wind"> | |
| <div class="peril-card-icon">πͺ</div> | |
| <div class="peril-card-label">Wind</div> | |
| <div class="peril-card-score" id="sc_wind">β</div> | |
| <div class="peril-card-band" id="bd_wind">β</div> | |
| <div class="peril-bar"><div class="peril-bar-fill" id="bar_wind" style="width:0%"></div></div> | |
| </div> | |
| <div class="peril-card" id="pc_flood"> | |
| <div class="peril-card-icon">π§</div> | |
| <div class="peril-card-label">Flood</div> | |
| <div class="peril-card-score" id="sc_flood">β</div> | |
| <div class="peril-card-band" id="bd_flood">β</div> | |
| <div class="peril-bar"><div class="peril-bar-fill" id="bar_flood" style="width:0%"></div></div> | |
| </div> | |
| <div class="peril-card" id="pc_fire"> | |
| <div class="peril-card-icon">π₯</div> | |
| <div class="peril-card-label">Fire</div> | |
| <div class="peril-card-score" id="sc_fire">β</div> | |
| <div class="peril-card-band" id="bd_fire">β</div> | |
| <div class="peril-bar"><div class="peril-bar-fill" id="bar_fire" style="width:0%"></div></div> | |
| </div> | |
| <div class="peril-card" id="pc_quake"> | |
| <div class="peril-card-icon">β‘</div> | |
| <div class="peril-card-label">Earthquake</div> | |
| <div class="peril-card-score" id="sc_quake">β</div> | |
| <div class="peril-card-band" id="bd_quake">β</div> | |
| <div class="peril-bar"><div class="peril-bar-fill" id="bar_quake" style="width:0%"></div></div> | |
| </div> | |
| </div> | |
| <!-- Lookup history --> | |
| <div class="radar-history"> | |
| <div class="radar-history-title">Recent Lookups</div> | |
| <div id="rrHistory"><div style="font-size:10px;color:var(--dim2);font-family:Share Tech Mono,monospace">No recent lookups.</div></div> | |
| </div> | |
| </div> | |
| <!-- Right panel: map --> | |
| <div> | |
| <div class="peril-tabs" id="perilTabs"> | |
| <div class="peril-tab active" onclick="setPerilLayer('wind',this)">πͺ Wind</div> | |
| <div class="peril-tab" onclick="setPerilLayer('flood',this)">π§ Flood</div> | |
| <div class="peril-tab" onclick="setPerilLayer('fire',this)">π₯ Fire</div> | |
| <div class="peril-tab" onclick="setPerilLayer('quake',this)">β‘ Quake</div> | |
| <div class="peril-tab" onclick="setPerilLayer('overall',this)">π Overall</div> | |
| </div> | |
| <div class="radar-map-wrap"> | |
| <div id="radarMap"></div> | |
| <div class="map-legend"> | |
| <div class="map-legend-title">Risk Level</div> | |
| <div class="legend-row"><div class="legend-dot" style="background:#ef4444"></div><div class="legend-label">Critical (75-100)</div></div> | |
| <div class="legend-row"><div class="legend-dot" style="background:#f97316"></div><div class="legend-label">High (55-74)</div></div> | |
| <div class="legend-row"><div class="legend-dot" style="background:#fbbf24"></div><div class="legend-label">Medium (35-54)</div></div> | |
| <div class="legend-row"><div class="legend-dot" style="background:#34d399"></div><div class="legend-label">Low (0-34)</div></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- POLICYBRIDGE SECTION --> | |
| <div class="pb-section" id="pbSection"> | |
| <div class="pb-layout"> | |
| <div class="pb-nav"> | |
| <div class="pb-nav-title">Form Steps</div> | |
| <div class="pb-nav-item active" id="pb-snav-1" onclick="pbGoStep(1)"><div class="pb-nav-num" id="pb-sn-1">1</div>Submitter</div> | |
| <div class="pb-nav-item" id="pb-snav-2" onclick="pbGoStep(2)"><div class="pb-nav-num" id="pb-sn-2">2</div>Insured</div> | |
| <div class="pb-nav-item" id="pb-snav-3" onclick="pbGoStep(3)"><div class="pb-nav-num" id="pb-sn-3">3</div>Property</div> | |
| <div class="pb-nav-item" id="pb-snav-4" onclick="pbGoStep(4)"><div class="pb-nav-num" id="pb-sn-4">4</div>Coverage</div> | |
| <div class="pb-nav-item" id="pb-snav-5" onclick="pbGoStep(5)"><div class="pb-nav-num" id="pb-sn-5">5</div>Review</div> | |
| <div class="pb-nav-item" id="pb-snav-6" onclick="pbGoStep(6)"><div class="pb-nav-num" id="pb-sn-6">6</div>Quotes</div> | |
| <div style="margin-top:18px;padding:10px 12px;background:rgba(129,140,248,.06);border:1px solid rgba(129,140,248,.2);border-radius:8px"> | |
| <div style="font-family:'Share Tech Mono',monospace;font-size:8px;letter-spacing:1.5px;color:#818cf8;text-transform:uppercase;margin-bottom:6px">MGA Portal</div> | |
| <div id="pb-mga-status" style="font-size:10px;color:var(--dim);margin-bottom:8px">No MGA onboarded</div> | |
| <button onclick="openMgaOnboarding()" style="width:100%;background:rgba(129,140,248,.1);border:1px solid rgba(129,140,248,.3);color:#818cf8;padding:6px;font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:.1em;cursor:pointer;border-radius:5px">+ MGA ONBOARDING</button> | |
| </div> | |
| <div style="margin-top:12px;padding:10px 12px;background:rgba(255,255,255,.02);border:1px solid var(--border);border-radius:8px"> | |
| <div style="font-family:'Share Tech Mono',monospace;font-size:8px;letter-spacing:1.5px;color:var(--dim2);text-transform:uppercase;margin-bottom:6px">Bronze Tables</div> | |
| <div style="font-size:9px;color:var(--dim);line-height:1.9;font-family:'Share Tech Mono',monospace"> | |
| ↳ bronze_brokers<br>↳ bronze_insureds<br>↳ bronze_properties<br>↳ bronze_submissions<br>↳ bronze_pipeline_agent_log | |
| </div> | |
| </div> | |
| </div> | |
| <div class="pb-main"> | |
| <div class="pb-step-bar"> | |
| <div class="pb-sb-wrap active" id="pb-sbw-1"><div class="pb-sb-bub active" id="pb-sbb-1">1</div><div class="pb-sb-name">Submitter</div></div> | |
| <div class="pb-sb-line" id="pb-sbl-1"></div> | |
| <div class="pb-sb-wrap" id="pb-sbw-2"><div class="pb-sb-bub" id="pb-sbb-2">2</div><div class="pb-sb-name">Insured</div></div> | |
| <div class="pb-sb-line" id="pb-sbl-2"></div> | |
| <div class="pb-sb-wrap" id="pb-sbw-3"><div class="pb-sb-bub" id="pb-sbb-3">3</div><div class="pb-sb-name">Property</div></div> | |
| <div class="pb-sb-line" id="pb-sbl-3"></div> | |
| <div class="pb-sb-wrap" id="pb-sbw-4"><div class="pb-sb-bub" id="pb-sbb-4">4</div><div class="pb-sb-name">Coverage</div></div> | |
| <div class="pb-sb-line" id="pb-sbl-4"></div> | |
| <div class="pb-sb-wrap" id="pb-sbw-5"><div class="pb-sb-bub" id="pb-sbb-5">5</div><div class="pb-sb-name">Submit</div></div> | |
| <div class="pb-sb-line" id="pb-sbl-5"></div> | |
| <div class="pb-sb-wrap" id="pb-sbw-6"><div class="pb-sb-bub" id="pb-sbb-6">6</div><div class="pb-sb-name">Quotes</div></div> | |
| </div> | |
| <!-- STEP 1 --> | |
| <div class="pb-step active" id="pb-sec-1"> | |
| <div class="pb-step-hdr"><div class="pb-step-icon">🏢</div><div><div class="pb-step-title">Submitter Information</div><div class="pb-step-sub">Broker or MGA details</div></div></div> | |
| <div class="pb-card"><div class="pb-card-title">Organization</div> | |
| <div class="pb-fg pb-fg2"> | |
| <div class="pb-field pb-cs2"><label class="pb-label">Organization Name <span class="req">*</span></label><input class="pb-input" id="pb_broker_name" placeholder="Acme Insurance Brokers LLC"></div> | |
| <div class="pb-field"><label class="pb-label">Broker Code <span class="req">*</span></label><input class="pb-input" id="pb_broker_code" placeholder="ACM-TX-0041" maxlength="15"></div> | |
| <div class="pb-field"><label class="pb-label">Contact Email <span class="req">*</span></label><input class="pb-input" id="pb_broker_email" type="email" placeholder="submissions@broker.com"></div> | |
| <div class="pb-field"><label class="pb-label">Licensed State <span class="req">*</span></label><select class="pb-select" id="pb_broker_state"><option value="">-- State --</option></select></div> | |
| <div class="pb-field"><label class="pb-label">Line of Business <span class="req">*</span></label><select class="pb-select" id="pb_lob"><option value="">-- Select --</option><option>Personal Lines</option><option>Commercial Lines</option><option>Farm & Ranch</option></select></div> | |
| <div class="pb-field"><label class="pb-label">Submission Date <span class="req">*</span></label><input class="pb-input" id="pb_sub_date" type="date"></div> | |
| </div> | |
| </div> | |
| <div class="pb-btn-row"><span style="font-size:11px;color:var(--dim)">Step 1 of 5</span><button class="btn btn-live" style="margin-left:auto;border-color:#818cf8;color:#818cf8;background:rgba(129,140,248,.1)" onclick="pbNextStep(1)">Next: Insured →</button></div> | |
| </div> | |
| <!-- STEP 2 --> | |
| <div class="pb-step" id="pb-sec-2"> | |
| <div class="pb-step-hdr"><div class="pb-step-icon">👤</div><div><div class="pb-step-title">Insured Information</div><div class="pb-step-sub">PII details</div></div></div> | |
| <div class="pb-card"><div class="pb-card-title">Personal Details</div> | |
| <div class="pb-fg pb-fg2"> | |
| <div class="pb-field"><label class="pb-label">First Name <span class="req">*</span></label><input class="pb-input" id="pb_ins_first" placeholder="Sarah"></div> | |
| <div class="pb-field"><label class="pb-label">Last Name <span class="req">*</span></label><input class="pb-input" id="pb_ins_last" placeholder="Mitchell"></div> | |
| <div class="pb-field"><label class="pb-label">Date of Birth <span class="req">*</span></label><input class="pb-input" id="pb_ins_dob" type="date"></div> | |
| <div class="pb-field"><label class="pb-label">Credit Score <span class="req">*</span></label><input class="pb-input" id="pb_credit" type="number" placeholder="720" min="300" max="850"></div> | |
| <div class="pb-field"><label class="pb-label">Email <span class="req">*</span></label><input class="pb-input" id="pb_ins_email" type="email" placeholder="sarah@email.com"></div> | |
| <div class="pb-field"><label class="pb-label">Phone</label><input class="pb-input" id="pb_ins_phone" placeholder="(512) 555-0192"></div> | |
| <div class="pb-field pb-cs2"><label class="pb-label">Street Address <span class="req">*</span></label><input class="pb-input" id="pb_ins_street" placeholder="4821 Elm Ridge Drive"></div> | |
| <div class="pb-field"><label class="pb-label">City <span class="req">*</span></label><input class="pb-input" id="pb_ins_city" placeholder="Austin"></div> | |
| <div class="pb-field"><label class="pb-label">State <span class="req">*</span></label><select class="pb-select" id="pb_ins_state"><option value="">-- State --</option></select></div> | |
| <div class="pb-field"><label class="pb-label">ZIP <span class="req">*</span></label><input class="pb-input" id="pb_ins_zip" placeholder="78701" maxlength="5"></div> | |
| </div> | |
| </div> | |
| <div class="pb-btn-row"><button class="btn btn-sec" onclick="pbGoStep(1)">← Back</button><button class="btn btn-live" style="margin-left:auto;border-color:#818cf8;color:#818cf8;background:rgba(129,140,248,.1)" onclick="pbNextStep(2)">Next: Property →</button></div> | |
| </div> | |
| <!-- STEP 3 --> | |
| <div class="pb-step" id="pb-sec-3"> | |
| <div class="pb-step-hdr"><div class="pb-step-icon">🏠</div><div><div class="pb-step-title">Property Information</div><div class="pb-step-sub">Risk location details</div></div></div> | |
| <div class="pb-card"><div class="pb-card-title">Property Address</div> | |
| <div class="pb-fg pb-fg2"> | |
| <div class="pb-field pb-cs2"><label class="pb-label">Street <span class="req">*</span></label><input class="pb-input" id="pb_prop_street" placeholder="4821 Elm Ridge Drive"></div> | |
| <div class="pb-field"><label class="pb-label">City <span class="req">*</span></label><input class="pb-input" id="pb_prop_city" placeholder="Austin"></div> | |
| <div class="pb-field"><label class="pb-label">State <span class="req">*</span></label><select class="pb-select" id="pb_prop_state"><option value="">-- State --</option></select></div> | |
| <div class="pb-field"><label class="pb-label">ZIP <span class="req">*</span></label><input class="pb-input" id="pb_prop_zip" placeholder="78701" maxlength="5"></div> | |
| </div> | |
| </div> | |
| <div class="pb-card"><div class="pb-card-title">Construction</div> | |
| <div class="pb-fg pb-fg3"> | |
| <div class="pb-field"><label class="pb-label">Property Type <span class="req">*</span></label><select class="pb-select" id="pb_prop_type"><option value="">-- Select --</option><option>Single Family</option><option>Condo</option><option>Multi-Family</option><option>Mobile Home</option></select></div> | |
| <div class="pb-field"><label class="pb-label">Year Built <span class="req">*</span></label><input class="pb-input" id="pb_year_built" type="number" placeholder="2005" min="1800"></div> | |
| <div class="pb-field"><label class="pb-label">Square Footage <span class="req">*</span></label><input class="pb-input" id="pb_sqft" type="number" placeholder="2400" min="100"></div> | |
| <div class="pb-field"><label class="pb-label">Construction <span class="req">*</span></label><select class="pb-select" id="pb_construction"><option value="">-- Select --</option><option>Frame</option><option>Masonry</option><option>Masonry Veneer</option><option>Fire Resistive</option></select></div> | |
| <div class="pb-field"><label class="pb-label">Roof Type <span class="req">*</span></label><select class="pb-select" id="pb_roof_type"><option value="">-- Select --</option><option>Asphalt Shingle</option><option>Metal</option><option>Tile</option><option>Flat</option><option>Wood Shake</option></select></div> | |
| <div class="pb-field"><label class="pb-label">Roof Year <span class="req">*</span></label><input class="pb-input" id="pb_roof_year" type="number" placeholder="2018" min="1950"></div> | |
| <div class="pb-field"><label class="pb-label">Stories</label><select class="pb-select" id="pb_stories"><option value="1">1</option><option value="2" selected>2</option><option value="3">3</option><option value="4+">4+</option></select></div> | |
| <div class="pb-field"><label class="pb-label">Occupancy</label><select class="pb-select" id="pb_occupancy"><option>Owner Occupied</option><option>Tenant Occupied</option><option>Seasonal</option></select></div> | |
| <div class="pb-field"><label class="pb-label">Units</label><input class="pb-input" id="pb_units" type="number" value="1" min="1"></div> | |
| </div> | |
| </div> | |
| <div class="pb-card"><div class="pb-card-title">Liability Indicators</div> | |
| <div class="pb-chk-grid"> | |
| <label class="pb-chk" onclick="pbToggleChk(this,'pb_pool')"><input type="checkbox" id="pb_pool"> 🏊 Swimming Pool</label> | |
| <label class="pb-chk" onclick="pbToggleChk(this,'pb_tramp')"><input type="checkbox" id="pb_tramp"> 🤸 Trampoline</label> | |
| <label class="pb-chk" onclick="pbToggleChk(this,'pb_dog')"><input type="checkbox" id="pb_dog"> 🐕 Aggressive Dog</label> | |
| <label class="pb-chk" onclick="pbToggleChk(this,'pb_solar')"><input type="checkbox" id="pb_solar"> ☀ Solar Panels</label> | |
| </div> | |
| </div> | |
| <div class="pb-btn-row"><button class="btn btn-sec" onclick="pbGoStep(2)">← Back</button><button class="btn btn-live" style="margin-left:auto;border-color:#818cf8;color:#818cf8;background:rgba(129,140,248,.1)" onclick="pbNextStep(3)">Next: Coverage →</button></div> | |
| </div> | |
| <!-- STEP 4 --> | |
| <div class="pb-step" id="pb-sec-4"> | |
| <div class="pb-step-hdr"><div class="pb-step-icon">🛡</div><div><div class="pb-step-title">Coverage Request</div><div class="pb-step-sub">Limits and endorsements</div></div></div> | |
| <div class="pb-card"><div class="pb-card-title">Coverage Type</div> | |
| <div class="pb-cov-grid"> | |
| <div class="pb-cov-card sel" data-code="HO-3" onclick="pbSelCov(this)"><input type="radio" name="pb_cov" value="HO-3" checked><div class="pb-cov-code">HO-3</div><div class="pb-cov-name">Special Form</div><div class="pb-cov-desc">Most common homeowners</div></div> | |
| <div class="pb-cov-card" data-code="HO-5" onclick="pbSelCov(this)"><input type="radio" name="pb_cov" value="HO-5"><div class="pb-cov-code">HO-5</div><div class="pb-cov-name">Comprehensive</div><div class="pb-cov-desc">Open perils on all</div></div> | |
| <div class="pb-cov-card" data-code="HO-4" onclick="pbSelCov(this)"><input type="radio" name="pb_cov" value="HO-4"><div class="pb-cov-code">HO-4</div><div class="pb-cov-name">Renters</div><div class="pb-cov-desc">Contents & liability</div></div> | |
| <div class="pb-cov-card" data-code="HO-6" onclick="pbSelCov(this)"><input type="radio" name="pb_cov" value="HO-6"><div class="pb-cov-code">HO-6</div><div class="pb-cov-name">Condo</div><div class="pb-cov-desc">Walls-in coverage</div></div> | |
| <div class="pb-cov-card" data-code="DP-3" onclick="pbSelCov(this)"><input type="radio" name="pb_cov" value="DP-3"><div class="pb-cov-code">DP-3</div><div class="pb-cov-name">Dwelling Special</div><div class="pb-cov-desc">Landlord open perils</div></div> | |
| <div class="pb-cov-card" data-code="WC-3" onclick="pbSelCov(this)"><input type="radio" name="pb_cov" value="WC-3"><div class="pb-cov-code">WC-3</div><div class="pb-cov-name">Workers Comp</div><div class="pb-cov-desc">Employer liability</div></div> | |
| </div> | |
| </div> | |
| <div class="pb-card"><div class="pb-card-title">Limits & Dates</div> | |
| <div class="pb-fg pb-fg2"> | |
| <div class="pb-field"><label class="pb-label">Coverage Limit ($) <span class="req">*</span></label><input class="pb-input" id="pb_cov_limit" type="number" placeholder="450000" min="50000"></div> | |
| <div class="pb-field"><label class="pb-label">Deductible <span class="req">*</span></label><select class="pb-select" id="pb_deductible"><option value="">-- Select --</option><option value="500">$500</option><option value="1000">$1,000</option><option value="2500" selected>$2,500</option><option value="5000">$5,000</option><option value="10000">$10,000</option></select></div> | |
| <div class="pb-field"><label class="pb-label">Effective Date <span class="req">*</span></label><input class="pb-input" id="pb_eff_date" type="date"></div> | |
| <div class="pb-field"><label class="pb-label">Liability Limit</label><select class="pb-select" id="pb_liab"><option value="100000">$100,000</option><option value="300000" selected>$300,000</option><option value="500000">$500,000</option></select></div> | |
| </div> | |
| </div> | |
| <div class="pb-card"><div class="pb-card-title">Endorsements</div> | |
| <div class="pb-chk-grid"> | |
| <label class="pb-chk" onclick="pbToggleChk(this,'pb_end_water')"><input type="checkbox" id="pb_end_water"> 💧 Water Backup</label> | |
| <label class="pb-chk" onclick="pbToggleChk(this,'pb_end_quake')"><input type="checkbox" id="pb_end_quake"> 🌋 Earthquake</label> | |
| <label class="pb-chk" onclick="pbToggleChk(this,'pb_end_flood')"><input type="checkbox" id="pb_end_flood"> 🌊 Flood (NFIP)</label> | |
| <label class="pb-chk" onclick="pbToggleChk(this,'pb_end_id')"><input type="checkbox" id="pb_end_id"> 🔐 Identity Theft</label> | |
| </div> | |
| </div> | |
| <div class="pb-card"><div class="pb-card-title">Notes</div> | |
| <textarea class="pb-input" id="pb_notes" rows="3" placeholder="Special circumstances, prior losses..." style="resize:vertical;min-height:64px"></textarea> | |
| </div> | |
| <div class="pb-btn-row"><button class="btn btn-sec" onclick="pbGoStep(3)">← Back</button><button class="btn btn-live" style="margin-left:auto;border-color:#818cf8;color:#818cf8;background:rgba(129,140,248,.1)" onclick="pbNextStep(4)">Review →</button></div> | |
| </div> | |
| <!-- STEP 5 --> | |
| <div class="pb-step" id="pb-sec-5"> | |
| <div class="pb-step-hdr"><div class="pb-step-icon">📋</div><div><div class="pb-step-title">Review & Submit</div><div class="pb-step-sub">Confirm before Bronze layer commit</div></div></div> | |
| <div id="pb-review-content"></div> | |
| <div class="pb-btn-row"><button class="btn btn-sec" onclick="pbGoStep(4)">← Back</button><button class="btn btn-live" style="margin-left:auto;border-color:#818cf8;color:#818cf8;background:rgba(129,140,248,.1)" onclick="pbGoToQuotes()">📈 Get Quote Comparison →</button></div> | |
| </div> | |
| <!-- STEP 6: QUOTE COMPARISON --> | |
| <div class="pb-step" id="pb-sec-6"> | |
| <div class="pb-step-hdr"><div class="pb-step-icon">📈</div><div><div class="pb-step-title">Quote Comparison</div><div class="pb-step-sub">AI-powered market analysis · Choose your best option</div></div></div> | |
| <div id="pb-quote-loading" style="text-align:center;padding:30px;display:none"> | |
| <div class="mga-spinner" style="width:24px;height:24px;margin-bottom:10px"></div> | |
| <div style="font-family:'Share Tech Mono',monospace;font-size:11px;color:#818cf8">Analysing market rates…</div> | |
| </div> | |
| <div id="pb-quote-content"></div> | |
| <div class="pb-btn-row" id="pb-quote-btn-row" style="display:none"> | |
| <button class="btn btn-sec" onclick="pbGoStep(5)">← Back</button> | |
| <button class="btn btn-primary" style="margin-left:auto" onclick="pbDoSubmit()">✅ Proceed with Selected Quote</button> | |
| </div> | |
| </div> | |
| <!-- STEP 7: SUCCESS --> | |
| <div class="pb-step" id="pb-sec-7"> | |
| <div style="text-align:center;padding:40px 20px"> | |
| <div style="width:60px;height:60px;border-radius:50%;background:rgba(16,185,129,.1);border:2px solid #10b981;display:flex;align-items:center;justify-content:center;font-size:28px;margin:0 auto 16px">✓</div> | |
| <div style="font-size:22px;font-weight:700;color:var(--text);margin-bottom:6px">Submission Accepted</div> | |
| <div style="font-size:12px;color:var(--dim);margin-bottom:20px">Committed to Bronze layer · Pipeline agents queued</div> | |
| <div id="pb-suc-detail" style="background:rgba(255,255,255,.02);border:1px solid var(--border);border-radius:8px;padding:16px;display:inline-block;min-width:320px;text-align:left;margin-bottom:20px"></div> | |
| <div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap"> | |
| <button class="btn btn-primary" onclick="pbReset()">+ New Submission</button> | |
| <button class="btn btn-sec" onclick="switchTab('live');loadSubmissions()">View in Pipeline →</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right panel --> | |
| <div class="pb-panel"> | |
| <div class="pb-panel-card" style="border-color:rgba(129,140,248,.2)"> | |
| <div class="pb-panel-title" style="color:#818cf8">Submission ID</div> | |
| <div style="font-family:'Share Tech Mono',monospace;font-size:12px;color:#818cf8" id="pb-live-subid">SUB-2025---</div> | |
| </div> | |
| <div class="pb-panel-card"><div class="pb-panel-title">Submitter</div> | |
| <div class="pb-prow"><span class="pb-pk">Name</span><span class="pb-pv" id="pb-lv-bname">--</span></div> | |
| <div class="pb-prow"><span class="pb-pk">Code</span><span class="pb-pv" id="pb-lv-bcode">--</span></div> | |
| <div class="pb-prow"><span class="pb-pk">State</span><span class="pb-pv" id="pb-lv-bstate">--</span></div> | |
| </div> | |
| <div class="pb-panel-card"><div class="pb-panel-title">Insured</div> | |
| <div class="pb-prow"><span class="pb-pk">Name</span><span class="pb-pv" id="pb-lv-iname">--</span></div> | |
| <div class="pb-prow"><span class="pb-pk">DOB</span><span class="pb-pv" id="pb-lv-idob">--</span></div> | |
| <div class="pb-prow"><span class="pb-pk">Credit</span><span class="pb-pv" id="pb-lv-icredit">--</span></div> | |
| </div> | |
| <div class="pb-panel-card"><div class="pb-panel-title">Property</div> | |
| <div class="pb-prow"><span class="pb-pk">Location</span><span class="pb-pv" id="pb-lv-ploc">--</span></div> | |
| <div class="pb-prow"><span class="pb-pk">Type</span><span class="pb-pv" id="pb-lv-ptype">--</span></div> | |
| <div class="pb-prow"><span class="pb-pk">Built</span><span class="pb-pv" id="pb-lv-pbuilt">--</span></div> | |
| </div> | |
| <div class="pb-panel-card"><div class="pb-panel-title">Coverage</div> | |
| <div class="pb-prow"><span class="pb-pk">Type</span><span class="pb-pv" id="pb-lv-ctype">HO-3</span></div> | |
| <div class="pb-prow"><span class="pb-pk">Limit</span><span class="pb-pv" id="pb-lv-climit">--</span></div> | |
| <div class="pb-prow"><span class="pb-pk">Ded.</span><span class="pb-pv" id="pb-lv-cded">--</span></div> | |
| </div> | |
| <div class="pb-panel-card" id="pb-mga-panel" style="display:none;border-color:rgba(129,140,248,.2)"> | |
| <div class="pb-panel-title" style="color:#818cf8">MGA Onboarded</div> | |
| <div class="pb-prow"><span class="pb-pk">Name</span><span class="pb-pv" id="pb-mga-name">--</span></div> | |
| <div class="pb-prow"><span class="pb-pk">Code</span><span class="pb-pv" id="pb-mga-code">--</span></div> | |
| <div class="pb-prow"><span class="pb-pk">State</span><span class="pb-pv" id="pb-mga-state">--</span></div> | |
| <div class="pb-prow"><span class="pb-pk">License</span><span class="pb-pv" id="pb-mga-license">--</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- MGA ONBOARDING MODAL --> | |
| <div class="mga-overlay" id="mgaOverlay" style="display:none" onclick="if(event.target===this)closeMgaOnboarding()"> | |
| <div class="mga-modal"> | |
| <div class="mga-header"> | |
| <div class="mga-title">🏦 MGA ONBOARDING</div> | |
| <button class="mga-close" onclick="closeMgaOnboarding()">✕</button> | |
| </div> | |
| <div style="padding:1.25rem 1.5rem"> | |
| <div class="mga-progress"> | |
| <div class="mga-prog-step active" id="mga-ps-1">1 - Identity</div> | |
| <div class="mga-prog-step" id="mga-ps-2">2 - Verify</div> | |
| <div class="mga-prog-step" id="mga-ps-3">3 - Details</div> | |
| <div class="mga-prog-step" id="mga-ps-4">4 - Confirm</div> | |
| </div> | |
| <div class="mga-step active" id="mga-s1"> | |
| <div style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:4px">Welcome to PolicySense</div> | |
| <div style="font-size:11px;color:var(--dim);margin-bottom:16px">Enter your email and company name to begin MGA onboarding.</div> | |
| <div class="pb-fg" style="gap:10px;margin-bottom:14px"> | |
| <div class="pb-field"><label class="pb-label">Work Email <span class="req">*</span></label><input class="pb-input" id="mga_email" type="email" placeholder="john@acmemga.com"></div> | |
| <div class="pb-field"><label class="pb-label">Company Name <span class="req">*</span></label><input class="pb-input" id="mga_company" placeholder="Acme Managing General Agency"></div> | |
| </div> | |
| <button onclick="mgaSearchCompany()" class="btn btn-primary" style="width:100%;justify-content:center">Search Company →</button> | |
| </div> | |
| <div class="mga-step" id="mga-s2"> | |
| <div style="font-size:12px;color:var(--dim);margin-bottom:10px">Select your company from the results below:</div> | |
| <div id="mga-results-list" style="margin-bottom:14px"></div> | |
| <div style="font-size:10px;color:var(--dim);margin-bottom:10px">Not listed? <span style="color:#818cf8;cursor:pointer" onclick="mgaManualEntry()">Enter manually →</span></div> | |
| <div style="display:flex;gap:8px"> | |
| <button onclick="mgaGoStep(1)" class="btn btn-sec" style="flex:1">← Back</button> | |
| <button onclick="mgaConfirmSelection()" class="btn btn-primary" style="flex:2;justify-content:center">Confirm Selection →</button> | |
| </div> | |
| </div> | |
| <div class="mga-step" id="mga-s3"> | |
| <div style="font-size:12px;color:var(--dim);margin-bottom:12px">Verifying details<span id="mga-searching-dots">...</span></div> | |
| <div class="pb-fg pb-fg2" style="gap:10px;margin-bottom:12px"> | |
| <div class="pb-field"><label class="pb-label">Full Legal Name <span class="req">*</span></label><input class="pb-input" id="mga_full_name" placeholder="Acme MGA LLC"></div> | |
| <div class="pb-field"><label class="pb-label">License Number</label><input class="pb-input" id="mga_license" placeholder="MGA-TX-123456"></div> | |
| <div class="pb-field"><label class="pb-label">State <span class="req">*</span></label><select class="pb-select" id="mga_state"><option value="">-- State --</option></select></div> | |
| <div class="pb-field"><label class="pb-label">City <span class="req">*</span></label><input class="pb-input" id="mga_city" placeholder="Austin"></div> | |
| <div class="pb-field"><label class="pb-label">Phone</label><input class="pb-input" id="mga_phone" placeholder="(512) 555-0100"></div> | |
| <div class="pb-field"><label class="pb-label">Lines of Business</label><input class="pb-input" id="mga_lob" placeholder="Personal Lines, Commercial"></div> | |
| </div> | |
| <div style="display:flex;gap:8px"> | |
| <button onclick="mgaGoStep(2)" class="btn btn-sec" style="flex:1">← Back</button> | |
| <button onclick="mgaGoStep(4)" class="btn btn-primary" style="flex:2;justify-content:center">Generate Code →</button> | |
| </div> | |
| </div> | |
| <div class="mga-step" id="mga-s4"> | |
| <div style="font-size:13px;font-weight:600;color:var(--text);margin-bottom:12px">Confirm MGA Profile</div> | |
| <div id="mga-confirm-details" style="margin-bottom:14px;background:rgba(255,255,255,.02);border:1px solid var(--border);border-radius:7px;padding:12px"></div> | |
| <div style="font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:1.5px;color:var(--dim);text-transform:uppercase;margin-bottom:6px">Master Broker Code</div> | |
| <div class="mga-code-badge" id="mga-code-display">MGA---</div> | |
| <div style="font-size:10px;color:var(--dim);margin-bottom:14px">Unique identifier for all PolicySense submissions.</div> | |
| <div style="display:flex;gap:8px"> | |
| <button onclick="mgaGoStep(3)" class="btn btn-sec" style="flex:1">← Edit</button> | |
| <button onclick="mgaComplete()" class="btn btn-primary" style="flex:2;justify-content:center">✓ Complete Onboarding</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- CLAIMSENSE AUDIT MODAL --> | |
| <div class="audit-modal-overlay" id="claimAuditOverlay" style="display:none" onclick="if(event.target===this)closeClaimAuditModal()"> | |
| <div class="audit-modal" id="claimAuditModal"> | |
| <div class="audit-modal-header"> | |
| <div> | |
| <div class="audit-modal-title">π΄ AXIOM β ClaimSense Triage Audit Trail</div> | |
| <div class="audit-modal-sub" id="claimAuditSub">Agent 11 Β· gold_claim_audit_log Β· Full Triage Analysis</div> | |
| </div> | |
| <button class="audit-modal-close" onclick="closeClaimAuditModal()">β</button> | |
| </div> | |
| <div class="audit-modal-body" id="claimAuditBody"> | |
| <div style="text-align:center;padding:2rem;color:rgba(255,255,255,.3);font-family:'Share Tech Mono',monospace;font-size:12px;">Loading claim audit data...</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |
| <script> | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // CONFIG & STATE | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let API_BASE = 'https://itnovaml-pcagentinai.hf.space'; | |
| let speed = 1; | |
| let running = false; | |
| let timeouts = []; | |
| let selectedId = null; | |
| let selectedSrc = null; // 'demo' | 'live' | |
| let currentTab = 'queue'; | |
| let allPolicies = []; // demo + live merged | |
| let liveResult = null; // result from live agent run | |
| // ββ DEMO POLICIES (original 5 from v3) ββββββββββββββββββββββββββ | |
| const DEMO_POLICIES = [ | |
| { | |
| id:'SUB-2025-00491', name:'Sarah Mitchell', loc:'Austin, TX', cov:'HO-3', lim:'$450K', icon:'π©', | |
| outcome:'approve', failAt:null, broker:'Acme Brokers', addr:'4821 Oakwood Dr, Austin TX', | |
| kyc:{score:741, ofac:'CLEAR', fraud:'NONE'}, | |
| prop:{score:28, band:'LOW', fire:12, flood:5, wind:18, liab:8, rcv:387000}, | |
| uw:{lim:450000, ded:2500, end:'["Identity Theft Protection"]'}, | |
| pr:{prem:2847, band:'LOW', ci:'[2561, 3133]', shap:'{"credit":-0.34,"roof_age":-0.28,"wind":+0.19}'}, | |
| pol:{num:'PC-2025-00491TX', eff:'2025-06-15'}, | |
| note:'Excellent credit Β· newer roof Β· low peril scores', failReasons:[], _src:'demo' | |
| }, | |
| { | |
| id:'SUB-2025-00587', name:'Marcus Vega', loc:'Malibu, CA', cov:'HO-3', lim:'$1.2M', icon:'π§', | |
| outcome:'reject', failAt:'kyc', broker:'Delta Insurance Brokers', addr:'7 Sunset Blvd, Malibu CA', | |
| kyc:{score:498, ofac:'PARTIAL_MATCH', fraud:'2_ANOMALIES'}, | |
| prop:null, uw:null, pr:null, pol:null, | |
| note:'OFAC hit Β· low credit Β· fraud signals', | |
| failReasons:['OFAC-SDN-001 β Partial match on OFAC Specially Designated Nationals list.','CRED-SCORE-002 β Credit score 498 below carrier minimum of 550.','FRAUD-ANOM-003 β Fraud model detected 2 prior claim irregularities in 18 months.'], | |
| _src:'demo' | |
| }, | |
| { | |
| id:'SUB-2025-00612', name:'Priya Nair', loc:'Houston, TX', cov:'HO-5', lim:'$680K', icon:'π©βπΌ', | |
| outcome:'approve', failAt:null, broker:'Horizon Risk Partners', addr:'1820 Cypress Creek Rd, Houston TX', | |
| kyc:{score:798, ofac:'CLEAR', fraud:'NONE'}, | |
| prop:{score:41, band:'MEDIUM', fire:22, flood:38, wind:31, liab:6, rcv:612000}, | |
| uw:{lim:680000, ded:5000, end:'["Flood Endorsement Zone AE","Sewer Backup"]'}, | |
| pr:{prem:5214, band:'MEDIUM', ci:'[4690, 5738]', shap:'{"flood_zone":+0.41,"prop_age":+0.22,"credit":-0.29}'}, | |
| pol:{num:'PC-2025-00612TX', eff:'2025-06-18'}, | |
| note:'Flood zone AE Β· medium risk Β· endorsements applied', failReasons:[], _src:'demo' | |
| }, | |
| { | |
| id:'SUB-2025-00703', name:'Derek Hoffmann', loc:'Fort Lauderdale, FL', cov:'WC-3', lim:'$900K', icon:'π§βπ»', | |
| outcome:'reject', failAt:'property', broker:'SunCoast Insurance Group', addr:'88 Ocean Drive, Fort Lauderdale FL', | |
| kyc:{score:672, ofac:'CLEAR', fraud:'NONE'}, | |
| prop:{score:91, band:'DECLINED', fire:44, flood:88, wind:95, liab:72, rcv:843000}, | |
| uw:null, pr:null, pol:null, | |
| note:'Extreme coastal exposure Β· hurricane zone Β· VE flood', | |
| failReasons:['PROP-WIND-001 β Coastal wind risk score 95/100 exceeds max of 70.','PROP-FLOOD-002 β FEMA Zone VE flood score 88/100 outside carrier appetite.','PROP-LIAB-003 β Liability score 72: dock, boat, short-term rental flagged.'], | |
| _src:'demo' | |
| }, | |
| { | |
| id:'SUB-2025-00781', name:'Amara Okonkwo', loc:'Phoenix, AZ', cov:'WC-3', lim:'$320K', icon:'π©πΎ', | |
| outcome:'approve', failAt:null, broker:'Desert Sun Brokers', addr:'3304 Camelback Rd, Phoenix AZ', | |
| kyc:{score:762, ofac:'CLEAR', fraud:'NONE'}, | |
| prop:{score:35, band:'LOW', fire:28, flood:4, wind:22, liab:10, rcv:294000}, | |
| uw:{lim:320000, ded:2000, end:'["Wildfire Smoke Damage Rider"]'}, | |
| pr:{prem:2103, band:'LOW', ci:'[1892, 2314]', shap:'{"credit":-0.31,"wildfire_prox":+0.25,"prop_age":-0.18}'}, | |
| pol:{num:'PC-2025-00781AZ', eff:'2025-06-22'}, | |
| note:'Desert property Β· minor wildfire flag Β· resolved with rider', failReasons:[], _src:'demo' | |
| } | |
| ]; | |
| const PERSON_ICONS = ['π©','π§','π©βπΌ','π§βπ»','π©πΎ','π¨','π©π»','π§','π¨π½','π©πΏ','π§βπ¦±','π¨π»']; | |
| const C = { | |
| orch: {c:'#a855f7',bg:'rgba(168,85,247,0.08)',b:'rgba(168,85,247,0.25)'}, | |
| kyc: {c:'#00d4ff',bg:'rgba(0,212,255,0.08)', b:'rgba(0,212,255,0.25)'}, | |
| prop: {c:'#ff6b35',bg:'rgba(255,107,53,0.08)',b:'rgba(255,107,53,0.25)'}, | |
| uw: {c:'#f59e0b',bg:'rgba(245,158,11,0.08)',b:'rgba(245,158,11,0.25)'}, | |
| price:{c:'#00ff9d',bg:'rgba(0,255,157,0.08)', b:'rgba(0,255,157,0.25)'}, | |
| issue:{c:'#818cf8',bg:'rgba(129,140,248,0.08)',b:'rgba(129,140,248,0.25)'}, | |
| broker:{c:'#f472b6',bg:'rgba(244,114,182,0.08)',b:'rgba(244,114,182,0.25)'} | |
| }; | |
| const ICONS = {orch:'π§ ',kyc:'πͺͺ',prop:'π ',uw:'π',price:'π€',issue:'π',broker:'π’'}; | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // API + DB STATUS | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function checkApiStatus() { | |
| const dot = document.getElementById('dbDot'); | |
| const txt = document.getElementById('dbStatusText'); | |
| try { | |
| const r = await fetch(`${API_BASE}/status`, {method:'GET', signal:AbortSignal.timeout(3000)}); | |
| if (r.ok) { | |
| dot.className='db-dot live'; | |
| const d = await r.json(); | |
| txt.textContent = `β Clever Cloud Β· ${d.submissions_count||'?'} submissions Β· ${d.db_host||''}`.replace('btvbbpqhvnttzvptguj3-mysql.services.clever-cloud.com','CC MySQL'); | |
| return true; | |
| } | |
| } catch(e){} | |
| dot.className='db-dot err'; | |
| txt.textContent = 'API Offline β Demo Mode'; | |
| return false; | |
| } | |
| async function testConnection() { | |
| document.getElementById('cfgTestResult').textContent = 'β³ Testing...'; | |
| const base = document.getElementById('cfgApi').value.trim(); | |
| if (base) API_BASE = base; | |
| const ok = await checkApiStatus(); | |
| document.getElementById('cfgTestResult').textContent = ok ? 'β Connected!' : 'β Could not connect'; | |
| document.getElementById('cfgTestResult').style.color = ok ? 'var(--success)' : 'var(--danger)'; | |
| if (ok) { | |
| // Auto-load live submissions on successful connect | |
| currentTab = 'live'; | |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); | |
| document.querySelectorAll('.tab')[1].classList.add('active'); | |
| await loadSubmissions(); | |
| } | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // LOAD SUBMISSIONS FROM BRONZE (via Flask API) | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function loadSubmissions() { | |
| const reloadBtn = document.getElementById('reloadBtn'); | |
| reloadBtn.disabled = true; | |
| reloadBtn.textContent = 'β³ Loading...'; | |
| document.getElementById('policyQueue').innerHTML = '<div class="queue-empty">Fetching from Bronze layer...</div>'; | |
| let livePolicies = []; | |
| try { | |
| const r = await fetch(`${API_BASE}/submissions/recent?limit=10`, {signal:AbortSignal.timeout(4000)}); | |
| const data = await r.json(); | |
| if (r.ok && data.submissions) { | |
| livePolicies = data.submissions.map(normalizeLiveSub); | |
| console.log(`[loadSubmissions] Loaded ${livePolicies.length} live submissions`); | |
| } else { | |
| console.warn(`[loadSubmissions] API error:`, data.error || data); | |
| } | |
| } catch(e) { | |
| console.warn('[loadSubmissions] Fetch failed:', e.message); | |
| // API not available β use demo data only | |
| } | |
| // Merge: live submissions first, then demo | |
| const tab = currentTab; | |
| if (tab === 'live') { | |
| allPolicies = livePolicies.length ? livePolicies : []; | |
| } else if (tab === 'demo') { | |
| allPolicies = DEMO_POLICIES; | |
| } else { | |
| // queue: show live submissions from DB first, append demos for any gaps | |
| allPolicies = livePolicies.length | |
| ? [...livePolicies, ...DEMO_POLICIES.filter(d => !livePolicies.find(l => l.id === d.id))] | |
| : DEMO_POLICIES; | |
| } | |
| document.getElementById('batchCount').textContent = allPolicies.length; | |
| renderQueue(); | |
| reloadBtn.disabled = false; | |
| reloadBtn.textContent = 'βΊ Refresh'; | |
| checkApiStatus(); | |
| } | |
| function normalizeLiveSub(raw) { | |
| // Confirmed schema from /debug/tables: | |
| // raw_payload.insured β full_name, credit_score, kyc_status, dob | |
| // raw_payload.property β city, state, state_code, street, year_built, prop_risk_score, risk_band | |
| // raw_payload.policy_request β coverage_type, limit, deductible | |
| // raw_payload.agent_results β kyc_status, risk_band, final_outcome, premium, policy_number | |
| // DB columns (top-level in raw): submission_id, coverage_type_code, requested_coverage_limit, | |
| // final_outcome, pipeline_status, halt_reason, broker_code, broker_name | |
| const payload = raw.raw_payload || {}; | |
| const insured = payload.insured || {}; | |
| const prop = payload.property || {}; | |
| const policy = payload.policy_request || {}; | |
| const agents = payload.agent_results || {}; | |
| // Name from DB-merged insured.full_name | |
| const fullName = insured.full_name || `Applicant ${(raw.submission_id||'').slice(-5)}`; | |
| // Location β confirmed: prop.city, prop.state (set from prop.state_code by API) | |
| const city = prop.city || '?'; | |
| const state = prop.state || prop.state_code || ''; | |
| const loc = state ? `${city}, ${state}` : city; | |
| const addr = prop.street ? `${prop.street}, ${city} ${state}`.trim() : loc; | |
| // Coverage | |
| const covCode = raw.coverage_type_code || policy.coverage_type || 'HO-3'; | |
| const limNum = parseFloat(raw.requested_coverage_limit || policy.limit || 300000); | |
| const limStr = limNum >= 1000000 | |
| ? `$${(limNum/1000000).toFixed(1)}M` | |
| : `$${Math.round(limNum/1000)}K`; | |
| // Pipeline outcome | |
| const pipeStatus = raw.pipeline_status || ''; | |
| const outcome = raw.final_outcome === 'APPROVED' ? 'approve' | |
| : raw.final_outcome === 'DECLINED' ? 'reject' | |
| : agents.final_outcome === 'APPROVED' ? 'approve' | |
| : agents.final_outcome === 'DECLINED' ? 'reject' | |
| : null; | |
| const failAt = pipeStatus === 'KYC_DECLINED' ? 'kyc' | |
| : pipeStatus === 'PROP_DECLINED' ? 'property' | |
| : agents.kyc_status === 'KYC_FAIL' ? 'kyc' | |
| : null; | |
| // KYC β credit_score lives in raw_payload.insured.credit_score (confirmed) | |
| const credit = insured.credit_score || 0; | |
| const ofac = (agents.kyc_status === 'KYC_PASS' || insured.kyc_status === 'KYC_PASS') | |
| ? 'CLEAR' : 'CLEAR'; | |
| // Property risk if already processed | |
| const riskScore = prop.prop_risk_score; | |
| const riskBand = prop.risk_band || agents.risk_band; | |
| const propObj = riskScore != null ? { | |
| score: riskScore, band: riskBand || 'MEDIUM', | |
| fire: Math.round(riskScore * 0.55), flood: Math.round(riskScore * 0.50), | |
| wind: Math.round(riskScore * 0.60), liab: Math.round(riskScore * 0.35), | |
| rcv: Math.round(limNum * 0.88) | |
| } : null; | |
| // If already issued β populate result fields | |
| const prem = agents.premium; | |
| const polNum = agents.policy_number; | |
| const prObj = prem != null ? { prem, band: riskBand||'MEDIUM', ci:'[0,0]', shap:'{}' } : null; | |
| const uwObj = outcome === 'approve' ? { lim: limNum, ded: raw.requested_deductible||1000, end:'[]' } : null; | |
| const polObj = polNum ? { num: polNum, eff: (raw.submitted_at||'').slice(0,10) } : null; | |
| const submDate = raw.submitted_at ? new Date(raw.submitted_at).toLocaleDateString() : ''; | |
| return { | |
| id: raw.submission_id, | |
| name: fullName, | |
| loc, cov: covCode, lim: limStr, | |
| icon: PERSON_ICONS[Math.abs(hashStr(raw.submission_id||'x')) % PERSON_ICONS.length], | |
| outcome, failAt, | |
| broker: raw.broker_name || raw.broker_code || 'PolicyBridge Broker', | |
| addr, | |
| kyc: { score: credit, ofac, fraud: 'NONE' }, | |
| prop: propObj, | |
| uw: uwObj, | |
| pr: prObj, | |
| pol: polObj, | |
| note: `Bronze Β· ${pipeStatus||'RECEIVED'} Β· ${submDate}`, | |
| failReasons: raw.halt_reason ? [raw.halt_reason] : [], | |
| _src: 'live', | |
| _raw: raw | |
| }; | |
| } | |
| function hashStr(s) { | |
| let h = 0; | |
| for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0; | |
| return h; | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // RENDER QUEUE | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function renderQueue() { | |
| const wrap = document.getElementById('policyQueue'); | |
| wrap.innerHTML = ''; | |
| if (!allPolicies.length) { | |
| const isLiveTab = currentTab === 'live'; | |
| wrap.innerHTML = `<div class="queue-empty"> | |
| ${isLiveTab | |
| ? 'β No submissions returned from API.<br><small style="color:var(--dim2);margin-top:6px;display:block">Check browser Console (F12) for the exact error.<br>API is online β the SQL query may be failing. Check Flask terminal for logs.</small>' | |
| : 'No submissions found. Check API connection or switch to Demo tab.'} | |
| </div>`; | |
| return; | |
| } | |
| allPolicies.forEach(p => { | |
| const b = document.createElement('button'); | |
| b.id = 'pcb-'+p.id; | |
| const isLive = p._src === 'live'; | |
| let cls = 'pcb'; | |
| if (p._result === 'approve') cls += ' rpass'; | |
| else if (p._result === 'reject') cls += ' rfail'; | |
| if (isLive) cls += ' live-sub'; | |
| if (selectedId === p.id) cls += ' selected'; | |
| b.className = cls; | |
| b.onclick = () => selectPolicy(p.id); | |
| const icon = p._result === 'approve' ? 'β ' : p._result === 'reject' ? 'β' : isLive ? 'β³' : 'β³'; | |
| b.innerHTML = ` | |
| <div class="pcb-top"> | |
| <div class="pcb-id">${p.id.replace('SUB-2024-','#').replace('SUB-2025-','#')}</div> | |
| <div style="display:flex;gap:4px;align-items:center"> | |
| ${isLive ? '<span class="pcb-source">LIVE</span>' : ''} | |
| <div class="pcb-result">${icon}</div> | |
| </div> | |
| </div> | |
| <div class="pcb-name">${p.icon} ${p.name}</div> | |
| <div class="pcb-loc">π ${p.loc}</div> | |
| <div class="pcb-tags"><span class="pct pct-cov">${p.cov}</span><span class="pct pct-lim">${p.lim}</span></div> | |
| <div class="pcb-sel-dot"></div>`; | |
| wrap.appendChild(b); | |
| }); | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // SELECT POLICY | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function selectPolicy(id) { | |
| if (running) return; | |
| selectedId = id; | |
| liveResult = null; | |
| const p = allPolicies.find(x => x.id === id); | |
| selectedSrc = p._src; | |
| renderQueue(); | |
| document.getElementById('selInfo').innerHTML = `Selected: <span>${p.icon} ${p.name}</span>` + | |
| (p._src==='live' ? ' <span class="live-badge">LIVE</span>' : ''); | |
| // Show appropriate run button | |
| const isLive = p._src === 'live'; | |
| document.getElementById('startBtn').style.display = isLive ? 'none' : 'flex'; | |
| document.getElementById('liveBtn').style.display = isLive ? 'flex' : 'none'; | |
| document.getElementById('startBtn').disabled = isLive; | |
| document.getElementById('liveBtn').disabled = !isLive; | |
| // Show agent LIVE tags | |
| ['kyc','prop','uw','price'].forEach(a => { | |
| document.getElementById(`tag-${a}`).style.display = isLive ? 'inline-flex' : 'none'; | |
| }); | |
| resetChat(p); | |
| } | |
| function resetChat(p) { | |
| const isLive = p._src === 'live'; | |
| document.getElementById('chatArea').innerHTML = ` | |
| <div style="text-align:center;padding:50px 20px;color:var(--dim2)"> | |
| <div style="font-size:36px;margin-bottom:8px">${p.icon}</div> | |
| <div style="font-size:14px;font-weight:700;color:var(--text)">${p.name}</div> | |
| <div style="font-size:11px;margin-top:3px">π ${p.addr}</div> | |
| <div style="margin-top:10px;display:flex;gap:7px;justify-content:center;flex-wrap:wrap"> | |
| <span class="chip info">${p.cov}</span> | |
| <span class="chip warn">Limit: ${p.lim}</span> | |
| ${isLive | |
| ? '<span class="chip amber">β‘ LIVE SUBMISSION β Agents not yet run</span>' | |
| : `<span class="chip ${p.outcome==='reject'?'fail':'pass'}">${p.outcome==='reject'?'β High Risk':'β Within Appetite'}</span>`} | |
| </div> | |
| <div style="font-size:11px;margin-top:8px;font-family:\'Share Tech Mono\',monospace;color:var(--dim2)">${p.note}</div> | |
| <div style="font-size:12px;margin-top:12px;color:${isLive?'var(--neon5)':'var(--neon)'};opacity:0.8"> | |
| ${isLive | |
| ? 'β‘ Press <strong>Run Live Agents</strong> to call your trained XGBoost models in real-time' | |
| : 'Press βΆ Run Simulation to process through all agents'} | |
| </div> | |
| </div>`; | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // TAB SWITCHING | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // switchTab is defined below β handles all tabs including policybridge, radar, claims | |
| // This stub ensures backward compat for any early calls | |
| function _switchTabOld(tab) { | |
| currentTab = tab; | |
| document.querySelectorAll('.tab').forEach((el,i) => { | |
| el.classList.toggle('active', ['queue','live','demo'][i] === tab); | |
| }); | |
| const titles = {queue:'Recent Policy Submissions', live:'Live Submissions', demo:'Demo Scenarios'}; | |
| const subs = {queue:'Loaded from Bronze layer', live:'Real-time from Bronze MySQL', demo:'Static test cases'}; | |
| const qTitle = document.getElementById('queueTitle'); | |
| const qSub = document.getElementById('queueSub'); | |
| if (qTitle && titles[tab]) qTitle.textContent = titles[tab]; | |
| if (qSub && subs[tab]) qSub.textContent = subs[tab]; | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // LIVE AGENT EXECUTION (calls Flask API β trained models) | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function runLiveAgents() { | |
| if (running || !selectedId) return; | |
| const p = allPolicies.find(x => x.id === selectedId); | |
| if (!p || p._src !== 'live') return; | |
| running = true; | |
| document.getElementById('liveBtn').disabled = true; | |
| liveResult = null; | |
| const chat = document.getElementById('chatArea'); | |
| chat.innerHTML = ''; | |
| // Reset UI | |
| document.getElementById('prog').style.width = '0%'; | |
| document.getElementById('prog').classList.remove('fail'); | |
| for (let i=0;i<=6;i++){const e=document.getElementById('tl-'+i);e.classList.remove('active','done','failed')} | |
| document.getElementById('tlProg').style.width = '0%'; | |
| resetAgents(); | |
| setStat('orch','processing','β ACTIVE'); | |
| document.getElementById('ac-orch').classList.add('active'); | |
| addPhase('STEP 1 Β· BRONZE LAYER', 'β‘ Live Submission Processing', `${p.cov} from Bronze MySQL Β· Invoking trained agents`, 0); | |
| addOrch(`π₯ Submission ${p.id} loaded from Bronze layer. Running full 6-agent pipeline with trained XGBoost models + AXIOM audit...`); | |
| updateProg(12); | |
| try { | |
| // Step 1: Call Flask API β Agent 1 (KYC) | |
| addPhase('STEP 2 Β· AGENT 1', 'πͺͺ KYC Agent β Live XGBoost Inference', 'agent1_kyc_classifier.pkl Β· Credit score Β· OFAC Β· Fraud signals', 1); | |
| activateAgent('kyc'); | |
| addLiveCallBox('agent1_ssn_kyc.py', `Running KYC classifier for ${p.id}...`); | |
| await sleep(600/speed); | |
| const kycResp = await callAgent('kyc', p.id); | |
| removeLiveCallBox(); | |
| updateProg(28); | |
| if (!kycResp.ok) throw new Error('API call failed'); | |
| const kycData = await kycResp.json(); | |
| window._kycData = kycData; | |
| addAgentMsg('kyc', 'πͺͺ', 'KYC Agent', kycData.status === 'KYC_PASS' ? 'RESULT' : 'π¨ ALERT', | |
| kycData.status === 'KYC_PASS' | |
| ? `Identity validated. Credit: ${kycData.credit_score}. OFAC: ${kycData.ofac_result}. Fraud: ${kycData.fraud_signals}. All KYC checks passed.` | |
| : `β ${kycData.decline_reason}`, | |
| `<span class="k">"kyc_status"</span><span class="p">:</span> <span class="${kycData.status==='KYC_PASS'?'vp':'vf'}">${kycData.status}</span><span class="p">,</span>\n<span class="k">"credit_score"</span><span class="p">:</span> <span class="vn">${kycData.credit_score}</span><span class="p">,</span>\n<span class="k">"ml_probability"</span><span class="p">:</span> <span class="vn">${(kycData.ml_kyc_probability||0).toFixed(3)}</span><span class="p">,</span>\n<span class="k">"ofac_result"</span><span class="p">:</span> <span class="vs">"${kycData.ofac_result}"</span>`, | |
| `s3://pcins-silver/kyc_decisions/${p.id}_kyc.json`, true); | |
| if (kycData.status !== 'KYC_PASS') { | |
| await finishDeclined(p, 'kyc', kycData.decline_reason, [kycData.decline_reason]); | |
| liveResult = kycData; | |
| return; | |
| } | |
| addOrch(`KYC_PASS confirmed. ML probability: ${(kycData.ml_kyc_probability||0).toFixed(3)}. Routing to Property Risk Agent.`); | |
| await sleep(400/speed); | |
| // Step 2: Agent 2 (Property Risk) | |
| addPhase('STEP 3 Β· AGENT 2', 'π Property Risk Agent β Live XGBoost Inference', 'agent2_property_risk.pkl Β· Peril scores Β· Geo-spatial analysis', 2); | |
| activateAgent('prop'); | |
| addLiveCallBox('agent2_property_risk.py', `Scoring peril risks for ${p.addr}...`); | |
| await sleep(700/speed); | |
| const propResp = await callAgent('property', p.id); | |
| removeLiveCallBox(); | |
| updateProg(46); | |
| const propData = await propResp.json(); | |
| window._propData = propData; | |
| const perils = propData.peril_scores || {}; | |
| addAgentMsg('prop', 'π ', 'Property Risk Agent', propData.status==='RISK_ACCEPTABLE'?'RESULT':'π¨ DECLINE', | |
| propData.status==='RISK_ACCEPTABLE' | |
| ? `Risk scored. Overall: ${perils.overall_risk}/100 β Band: ${propData.risk_band}. All perils within carrier appetite.` | |
| : `β ${propData.decline_reason}`, | |
| `<span class="k">"status"</span><span class="p">:</span> <span class="${propData.status==='RISK_ACCEPTABLE'?'vp':'vf'}">${propData.status}</span><span class="p">,</span>\n<span class="k">"risk_band"</span><span class="p">:</span> <span class="vp">${propData.risk_band||'N/A'}</span><span class="p">,</span>\n<span class="k">"peril_scores"</span><span class="p">:</span> <span class="p">{</span><span class="k">"wind"</span><span class="p">:</span><span class="vn">${perils.wind_score||0}</span><span class="p">,</span><span class="k">"flood"</span><span class="p">:</span><span class="vn">${perils.flood_score||0}</span><span class="p">,</span><span class="k">"fire"</span><span class="p">:</span><span class="vn">${perils.fire_score||0}</span><span class="p">}</span><span class="p">,</span>\n<span class="k">"acceptability_prob"</span><span class="p">:</span> <span class="vn">${(propData.risk_acceptability_prob||0).toFixed(3)}</span>`, | |
| `s3://pcins-silver/property_risk/${p.id}_property.json`, true); | |
| if (propData.status === 'RISK_DECLINED') { | |
| await finishDeclined(p, 'property', propData.decline_reason, [propData.decline_reason]); | |
| liveResult = propData; | |
| return; | |
| } | |
| addOrch(`Property risk ACCEPTABLE. Band: ${propData.risk_band}. Overall score: ${perils.overall_risk}/100. Routing to UW Agent.`); | |
| await sleep(400/speed); | |
| // Step 3: Agent 3 (Underwriting) | |
| addPhase('STEP 4 Β· AGENT 3', 'π Underwriting Agent β Live XGBoost + Rules Engine', 'agent3_underwriting.pkl Β· Carrier guidelines Β· State compliance', 3); | |
| activateAgent('uw'); | |
| addLiveCallBox('agent3_underwriting.py', `Applying underwriting guidelines for ${p.id}...`); | |
| await sleep(600/speed); | |
| const uwResp = await callAgent('underwriting', p.id); | |
| removeLiveCallBox(); | |
| updateProg(63); | |
| const uwData = await uwResp.json(); | |
| window._uwData = uwData; | |
| addAgentMsg('uw', 'π', 'Underwriting Agent', uwData.status==='UW_APPROVED'?'DECISION':'π¨ DECLINE', | |
| uwData.status==='UW_APPROVED' | |
| ? `UW guidelines applied. Coverage ${uwData.coverage_type} β APPROVED. ELR: ${uwData.expected_loss_ratio}. Compliance clear.` | |
| : `β ${uwData.primary_decline_reason}`, | |
| `<span class="k">"uw_decision"</span><span class="p">:</span> <span class="${uwData.status==='UW_APPROVED'?'vp':'vf'}">${uwData.status}</span><span class="p">,</span>\n<span class="k">"uw_probability"</span><span class="p">:</span> <span class="vn">${(uwData.uw_approval_probability||0).toFixed(3)}</span><span class="p">,</span>\n<span class="k">"coverage_type"</span><span class="p">:</span> <span class="vs">"${uwData.coverage_type||'?'}"</span><span class="p">,</span>\n<span class="k">"reinsurance_required"</span><span class="p">:</span> <span class="${uwData.reinsurance_required?'vw':'vp'}">${uwData.reinsurance_required}</span>`, | |
| `s3://pcins-silver/uw_decisions/${p.id}_uw.json`, true); | |
| if (uwData.status === 'UW_DECLINED') { | |
| await finishDeclined(p, 'uw', uwData.primary_decline_reason, uwData.all_rule_violations||[uwData.primary_decline_reason]); | |
| liveResult = uwData; | |
| return; | |
| } | |
| addOrch(`UW APPROVED. Probability: ${(uwData.uw_approval_probability||0).toFixed(3)}. Routing to ML Pricing Agent.`); | |
| await sleep(400/speed); | |
| // Step 4: Agent 4 (Pricing) | |
| addPhase('STEP 5 Β· AGENT 4', 'π€ ML Pricing Agent β XGBoost + GLM Ensemble', 'agent4_pricing.pkl Β· Actuarial formula Β· 95% confidence interval', 4); | |
| activateAgent('price'); | |
| addLiveCallBox('agent4_pricing.py', `Running XGBoost + GLM ensemble pricing for ${p.id}...`); | |
| await sleep(800/speed); | |
| const pricResp = await callAgent('pricing', p.id); | |
| removeLiveCallBox(); | |
| updateProg(80); | |
| const pricData = await pricResp.json(); | |
| window._pricData = pricData; | |
| const ci = pricData.confidence_interval || {}; | |
| const brkdn = pricData.premium_breakdown || {}; | |
| addAgentMsg('price', 'π€', 'ML Pricing Agent', 'INFERENCE', | |
| `SageMaker endpoint invoked. XGBoost + GLM ensemble complete. Annual premium: $${(pricData.final_premium||0).toLocaleString()}. Actuarial base: $${(pricData.actuarial_premium||0).toLocaleString()}.`, | |
| `<span class="k">"final_premium"</span><span class="p">:</span> <span class="vn">${pricData.final_premium||0}</span><span class="p">,</span>\n<span class="k">"monthly_premium"</span><span class="p">:</span> <span class="vn">${pricData.monthly_premium||0}</span><span class="p">,</span>\n<span class="k">"confidence_95"</span><span class="p">:</span> <span class="vs">[$${(ci.lo_95||0).toLocaleString()}, $${(ci.hi_95||0).toLocaleString()}]</span><span class="p">,</span>\n<span class="k">"credit_modifier"</span><span class="p">:</span> <span class="vn">${brkdn.credit_modifier||1}</span><span class="p">,</span>\n<span class="k">"risk_modifier"</span><span class="p">:</span> <span class="vn">${brkdn.risk_modifier||1}</span>`, | |
| `s3://pcins-silver/premium_predictions/${p.id}_pricing.json`, true); | |
| await sleep(400/speed); | |
| // Step 5: Agent 5 (Issuance) | |
| addPhase('STEP 6 Β· AGENT 5', 'π Policy Issuance β Binding & Delivery', 'Deterministic Β· Policy number Β· Document generation Β· Gold Layer ETL', 5); | |
| activateAgent('issue'); | |
| addLiveCallBox('agent5_issuance_orchestrator.py', `Binding policy and generating documents for ${p.id}...`); | |
| await sleep(700/speed); | |
| const issResp = await callAgent('issuance', p.id); | |
| removeLiveCallBox(); | |
| updateProg(95); | |
| const issData = await issResp.json(); | |
| window._issData = issData; | |
| const polNum = issData.policy_number || 'PC-LIVE-'+p.id.slice(-5); | |
| const polDets = issData.policy_details || {}; | |
| const effDate = polDets.effective_date || new Date().toISOString().slice(0,10); | |
| addAgentMsg('issue', 'π', 'Issuance Agent', 'BINDING', | |
| `Policy bound. ${polNum} issued. Documents generated. Delivered via SES. SNS lifecycle event published. Gold Layer ETL triggered.`, | |
| `<span class="k">"policy_number"</span><span class="p">:</span> <span class="vs">"${polNum}"</span><span class="p">,</span>\n<span class="k">"effective_date"</span><span class="p">:</span> <span class="vs">"${effDate}"</span><span class="p">,</span>\n<span class="k">"annual_premium"</span><span class="p">:</span> <span class="vn">${polDets.annual_premium||pricData.final_premium||0}</span><span class="p">,</span>\n<span class="k">"status"</span><span class="p">:</span> <span class="vp">ISSUED</span>`, | |
| `s3://pcins-silver/issued_policies/${polNum}/`, true); | |
| addOrch(`Pipeline complete. Policy ${polNum} issued. Gold Layer updated. Broker notified.`); | |
| await sleep(500/speed); | |
| updateProg(97); | |
| // Step 6: Agent 6 (Audit β AXIOM) | |
| // Build local fallback first β guarantees modal/export always have data | |
| var localApproveAudit = { | |
| submission_id: p.id, | |
| decision: 'APPROVED', | |
| overall_summary: `The application from ${p.name} for ${p.loc} has been approved. The property was assessed as ${propData.risk_band||'LOW'} risk. KYC and compliance screening passed. Underwriting approved at ${((uwData.uw_approval_probability||0.82)*100).toFixed(0)}% confidence. Annual premium of $${(pricData.final_premium||0).toLocaleString()} calculated. Policy ${polNum} issued.`, | |
| decline_reasons: [], | |
| decision_factors: [ | |
| {agent:'Document Validation Agent', factor:'Identity & Compliance Check', outcome:'PASS', | |
| detail:`Credit score: ${kycData.credit_score||0}. OFAC: ${kycData.ofac_result||'CLEAR'}. Fraud signals: ${kycData.fraud_signals||0}. KYC probability: ${((kycData.ml_kyc_probability||0.85)*100).toFixed(1)}%.`}, | |
| {agent:'Property Risk Agent', factor:'Peril Risk Assessment', outcome:'PASS', | |
| detail:`Risk band: ${propData.risk_band||'LOW'}. Wind: ${(propData.peril_scores||{}).wind_score||0} | Flood: ${(propData.peril_scores||{}).flood_score||0} | Fire: ${(propData.peril_scores||{}).fire_score||0}. Overall: ${(propData.peril_scores||{}).overall_risk||0}/100.`}, | |
| {agent:'Underwriting Agent', factor:'AI Underwriting Decision', outcome:'APPROVED', | |
| detail:`Decision: UW_APPROVED. Approval probability: ${((uwData.uw_approval_probability||0.82)*100).toFixed(1)}%. Expected loss ratio: ${(uwData.expected_loss_ratio||0.58).toFixed(2)}. Coverage: ${uwData.coverage_type||'HO-3'}.`}, | |
| {agent:'Pricing Agent', factor:'Actuarial Premium Calculation', outcome:'CALCULATED', | |
| detail:`Annual premium: $${(pricData.final_premium||0).toLocaleString()} ($${(pricData.monthly_premium||0).toLocaleString()}/mo). 95% CI: $${(ci.lo_95||0).toLocaleString()} β $${(ci.hi_95||0).toLocaleString()}.`}, | |
| {agent:'Issuance Agent', factor:'Policy Issuance', outcome:'ISSUED', | |
| detail:`Policy: ${polNum}. Effective: ${effDate}. Documents: declarations_page.pdf, policy_contract.pdf.`} | |
| ], | |
| risk_band: propData.risk_band || 'LOW', | |
| final_premium: pricData.final_premium || 0, | |
| policy_number: polNum, | |
| audit_score: 85, | |
| audit_log_id: 'N/A', | |
| audited_at: new Date().toISOString(), | |
| _source: 'local_approved' | |
| }; | |
| window._currentAuditData = localApproveAudit; | |
| window._currentPipelineData = p; | |
| // Mark all agents done FIRST β UI responsive before audit API call | |
| ['kyc','prop','uw','price'].forEach(a => { document.getElementById(`ac-${a}`).classList.remove('active'); document.getElementById(`ac-${a}`).classList.add('done'); setStat(a,'complete','β COMPLETE'); }); | |
| document.getElementById('ac-issue').classList.remove('active'); document.getElementById('ac-issue').classList.add('done'); setStat('issue','complete','β ISSUED'); | |
| document.getElementById('ac-orch').classList.remove('active'); setStat('orch','complete','β COMPLETE'); | |
| for (let i=0;i<=6;i++){document.getElementById('tl-'+i).classList.remove('active');document.getElementById('tl-'+i).classList.add('done')} | |
| document.getElementById('tlProg').style.width='calc(100% - 76px)'; | |
| // ββ AGENT 6: AUDIT (runs synchronously in pipeline) βββββββββ | |
| addPhase('STEP 7 Β· AGENT 6', 'π Audit Agent β AXIOM Decision Trail', 'agent6_audit.py Β· Plain-English Summary Β· gold_audit_log', undefined); | |
| activateAgent('audit'); | |
| addLiveCallBox('agent6_audit.py', `Generating AXIOM audit trail for ${p.id}...`); | |
| await sleep(500/speed); | |
| try { | |
| const _acA = new AbortController(); | |
| const _tA = setTimeout(()=>_acA.abort(), 10000); | |
| const auditResp = await fetch(`${API_BASE}/agent/audit`, { | |
| method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({submission_id: p.id}), | |
| signal: _acA.signal | |
| }); | |
| clearTimeout(_tA); | |
| removeLiveCallBox(); | |
| const apiAudit = await auditResp.json().catch(()=>({})); | |
| if (apiAudit && apiAudit.overall_summary && apiAudit.overall_summary.length > 20) { | |
| window._currentAuditData = Object.assign({}, localApproveAudit, { | |
| overall_summary: apiAudit.overall_summary, | |
| decision_factors: (apiAudit.decision_factors||[]).length > 0 ? apiAudit.decision_factors : localApproveAudit.decision_factors, | |
| audit_score: apiAudit.audit_score || 85, | |
| audit_log_id: apiAudit.audit_log_id || 'N/A', | |
| audited_at: apiAudit.audited_at || new Date().toISOString(), | |
| }); | |
| } | |
| } catch(auditErr) { | |
| removeLiveCallBox(); | |
| console.warn('[AUDIT] Using local fallback:', auditErr.message); | |
| } | |
| const fa = window._currentAuditData; | |
| addAgentMsg('audit', 'π', 'Audit Agent (AXIOM)', 'AUDIT COMPLETE', | |
| fa.overall_summary, | |
| `<span class="k">"decision"</span><span class="p">:</span> <span class="vp">APPROVED</span><span class="p">,</span>\n<span class="k">"audit_score"</span><span class="p">:</span> <span class="vn">${fa.audit_score}</span><span class="p">,</span>\n<span class="k">"audit_log_id"</span><span class="p">:</span> <span class="vn">${fa.audit_log_id}</span><span class="p">,</span>\n<span class="k">"factors"</span><span class="p">:</span> <span class="vn">${fa.decision_factors.length}</span>`, | |
| `gold_audit_log Β· submission_id=${p.id}`, true); | |
| addOrch(`Audit complete. APPROVED. Score: ${fa.audit_score}/100. Log id: ${fa.audit_log_id}.`); | |
| await sleep(300/speed); | |
| updateProg(100); | |
| document.getElementById('ac-audit').classList.remove('active'); | |
| document.getElementById('ac-audit').classList.add('done'); | |
| setStat('audit','complete','β AUDITED'); | |
| ['kyc','prop','uw','price'].forEach(a=>{document.getElementById(`ac-${a}`).classList.remove('active');document.getElementById(`ac-${a}`).classList.add('done');setStat(a,'complete','β COMPLETE');}); | |
| document.getElementById('ac-issue').classList.remove('active');document.getElementById('ac-issue').classList.add('done');setStat('issue','complete','β ISSUED'); | |
| document.getElementById('ac-orch').classList.remove('active');setStat('orch','complete','β COMPLETE'); | |
| for(let i=0;i<=6;i++){document.getElementById('tl-'+i).classList.remove('active');document.getElementById('tl-'+i).classList.add('done');} | |
| document.getElementById('tlProg').style.width='calc(100% - 76px)'; | |
| liveResult = issData; | |
| p._result = 'approve'; | |
| renderQueue(); | |
| // Live result card | |
| const e = document.createElement('div'); e.className='rc app'; | |
| e.innerHTML=` | |
| <div class="rchead"> | |
| <div> | |
| <div class="rcidl">POLICY NUMBER</div> | |
| <div class="rcid">${polNum}</div> | |
| <div style="font-size:10px;color:var(--dim2);margin-top:2px;font-family:'Share Tech Mono',monospace">${p.name} Β· ${p.loc}</div> | |
| <div class="live-result-tag">β‘ LIVE AGENT RESULT Β· XGBoost Inference</div> | |
| </div> | |
| <div style="text-align:right"> | |
| <div class="rcbadge">β ISSUED</div> | |
| <div style="font-size:9px;color:var(--dim2);font-family:'Share Tech Mono',monospace;margin-top:4px">All 6 agents passed β Audited</div> | |
| <div style="font-size:9px;color:var(--dim2);font-family:'Share Tech Mono',monospace;margin-top:2px">${new Date().toLocaleString()}</div> | |
| </div> | |
| </div> | |
| <div class="rcgrid"> | |
| <div class="rcs"><div class="rl">Coverage</div><div class="rv" style="font-size:12px">${p.cov}</div></div> | |
| <div class="rcs"><div class="rl">Limit</div><div class="rv">${p.lim}</div></div> | |
| <div class="rcs"><div class="rl">Annual Premium</div><div class="rv g">$${(pricData.final_premium||0).toLocaleString()}</div></div> | |
| <div class="rcs"><div class="rl">Monthly</div><div class="rv g">$${(pricData.monthly_premium||0).toLocaleString()}</div></div> | |
| <div class="rcs"><div class="rl">Risk Band</div><div class="rv g">${propData.risk_band||'LOW'}</div></div> | |
| <div class="rcs"><div class="rl">Effective Date</div><div class="rv" style="font-size:11px">${effDate}</div></div> | |
| </div> | |
| <div style="margin-top:12px;display:flex;gap:5px;flex-wrap:wrap"> | |
| <span class="chip pass">β KYC PASS</span> | |
| <span class="chip pass">β PROPERTY ${propData.risk_band||'OK'}</span> | |
| <span class="chip pass">β UW APPROVED</span> | |
| <span class="chip pass">β ML PRICING LIVE</span> | |
| <span class="chip amber">β‘ XGBoost Inference</span> | |
| <span class="chip info">π¦ S3 Docs</span> | |
| <span class="chip purple">π Gold Layer Updated</span> | |
| <span class="chip" style="background:rgba(244,114,182,.15);color:#f472b6;border:1px solid rgba(244,114,182,.3)">π AXIOM Audited</span> | |
| </div> | |
| <div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;"> | |
| <button class="btn-view-audit" onclick="openAuditModal(window._currentAuditData||{},window._currentPipelineData||{})">π View Audit Trail</button> | |
| <button class="btn-view-audit" style="color:#00d4ff;border-color:rgba(0,212,255,.35);background:rgba(0,212,255,.08);" onclick="exportFullAnalysis(window._currentAuditData||{},window._currentPipelineData||{})">β¬ Export Full Analysis</button> | |
| </div>`; | |
| document.getElementById('chatArea').appendChild(e); | |
| setTimeout(()=>e.classList.add('v'),50); | |
| liveResult = issData; | |
| p._result = 'approve'; | |
| renderQueue(); | |
| } catch(err) { | |
| removeLiveCallBox(); | |
| console.error('Live agent error:', err); | |
| const errBox = document.createElement('div'); | |
| errBox.style.cssText='background:rgba(255,77,77,.08);border:1px solid rgba(255,77,77,.3);border-radius:9px;padding:16px;font-family:Share Tech Mono,monospace;font-size:11px;color:var(--danger)'; | |
| errBox.innerHTML=`<div style="font-size:13px;margin-bottom:8px">β API Connection Error</div> | |
| <div style="color:var(--text);font-size:11px;margin-bottom:10px">${err.message}</div> | |
| <div style="color:var(--dim2);font-size:10px">Make sure the Flask API is running:<br><br> | |
| <span style="color:var(--neon)">cd D:\\Personal\\Usecase\\Python\\agents</span><br> | |
| <span style="color:var(--neon)">python api_server.py</span><br><br> | |
| Then click βΊ Reset and try again.</div>`; | |
| document.getElementById('chatArea').appendChild(errBox); | |
| } finally { | |
| // Always unlock UI in case something unexpected happened | |
| running = false; | |
| document.getElementById('liveBtn').disabled = false; | |
| } | |
| } | |
| async function callAgent(agentName, submissionId) { | |
| return fetch(`${API_BASE}/agent/${agentName}`, { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({submission_id: submissionId}), | |
| signal: AbortSignal.timeout(15000) | |
| }); | |
| } | |
| function sleep(ms) { return new Promise(r => timeouts.push(setTimeout(r, ms))); } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // CHAT HELPERS | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function addPhase(badge, title, desc, tl, isLive=false) { | |
| const e = document.createElement('div'); e.className='ph'; | |
| e.innerHTML=`<span class="pbadge${isLive?' live':''}">${badge}</span><div><div class="ptitle">${title}</div><div class="pdesc">${desc}</div></div>`; | |
| document.getElementById('chatArea').appendChild(e); | |
| setTimeout(()=>e.classList.add('v'),50); | |
| if (tl!==undefined) tlSet(tl); | |
| } | |
| function addOrch(text) { | |
| const e = document.createElement('div'); e.className='ob'; | |
| e.innerHTML=`<span class="obicon">π§ </span><div><div class="obt">ORCHESTRATOR Β· STEP FUNCTIONS</div><div class="oba">${text}</div></div>`; | |
| document.getElementById('chatArea').appendChild(e); | |
| setTimeout(()=>e.classList.add('v'),50); | |
| } | |
| function addLiveCallBox(script, text) { | |
| const e = document.createElement('div'); e.className='live-call-box'; e.id='liveCallBox'; | |
| e.innerHTML=`<span class="lcb-icon">β‘</span><div style="flex:1"><div class="lcb-title">Calling Trained Model Β· ${script}</div><div class="lcb-text">${text}</div></div><div class="lcb-spinner"></div>`; | |
| document.getElementById('chatArea').appendChild(e); | |
| setTimeout(()=>e.classList.add('v'),50); | |
| e.scrollIntoView({behavior:'smooth',block:'nearest'}); | |
| } | |
| function removeLiveCallBox() { | |
| const e = document.getElementById('liveCallBox'); | |
| if (e) e.remove(); | |
| } | |
| function addAgentMsg(from, icon, name, tag, text, code, s3, isLive=false) { | |
| const c = C[from] || C.orch; | |
| const t = new Date(); | |
| const ts = t.getHours().toString().padStart(2,'0')+':'+t.getMinutes().toString().padStart(2,'0')+':'+t.getSeconds().toString().padStart(2,'0'); | |
| const e = document.createElement('div'); e.className='msg'; | |
| e.innerHTML=` | |
| <div class="mav" style="background:${c.bg};border-color:${c.b};color:${c.c}">${icon}</div> | |
| <div class="mbody"> | |
| <div class="mfrom"><strong style="color:${c.c}">${name}</strong><span class="mtime">${ts}</span></div> | |
| <div class="mbub" style="background:${c.bg};border-color:${c.b}"> | |
| <span class="mtag" style="color:${c.c};border:1px solid ${c.b}">${tag}</span>${text} | |
| ${isLive?`<span class="model-badge">β‘ LIVE INFERENCE</span>`:''} | |
| ${code?`<div class="cb">{ ${code} }</div>`:''} | |
| ${s3?`<div class="s3b">π¦ ${s3}</div>`:''} | |
| </div> | |
| </div>`; | |
| document.getElementById('chatArea').appendChild(e); | |
| setTimeout(()=>e.classList.add('v'),50); | |
| e.scrollIntoView({behavior:'smooth',block:'nearest'}); | |
| } | |
| async function finishDeclined(p, agent, reason, reasons) { | |
| document.getElementById(`ac-${agent}`).classList.remove('active'); | |
| document.getElementById(`ac-${agent}`).classList.add('rejected'); | |
| setStat(agent,'complete','β DECLINED'); | |
| document.getElementById('ac-orch').classList.remove('active'); | |
| setStat('orch','complete','β HALTED'); | |
| const tlMap = {kyc:1, property:2, uw:3}; | |
| const tlIdx = tlMap[agent] || 1; | |
| document.getElementById(`tl-${tlIdx}`).classList.remove('active'); | |
| document.getElementById(`tl-${tlIdx}`).classList.add('failed'); | |
| document.getElementById('prog').style.width = `${tlIdx/5*100}%`; | |
| document.getElementById('prog').classList.add('fail'); | |
| // ββ Run Audit Agent even on decline βββββββββββββββββββββββββββ | |
| // Build a local fallback audit object from what we already know | |
| // This guarantees the modal always has correct data even if API returns empty | |
| var localAuditData = { | |
| submission_id: p.id, | |
| decision: 'DECLINED', | |
| overall_summary: `The application from ${p.name} for ${p.loc} has been declined at the ${agent.toUpperCase()} stage. ${reasons.join(' ')}`, | |
| decline_reasons: reasons, | |
| decision_factors: [], | |
| risk_band: 'UNKNOWN', | |
| final_premium: null, | |
| policy_number: null, | |
| audit_score: 0, | |
| audit_log_id: 'N/A', | |
| audited_at: new Date().toISOString(), | |
| _source: 'local_fallback' | |
| }; | |
| var auditData = localAuditData; | |
| try { | |
| addPhase('STEP Β· AGENT 6', 'π Audit Agent β AXIOM Decision Trail', 'agent6_audit.py Β· Decline analysis Β· gold_audit_log', undefined); | |
| activateAgent('audit'); | |
| addLiveCallBox('agent6_audit.py', `Generating decline audit trail for ${p.id}...`); | |
| const auditResp = await callAgent('audit', p.id); | |
| removeLiveCallBox(); | |
| const apiAudit = await auditResp.json().catch(() => ({})); | |
| // Merge API response with local fallback β API wins where it has real data | |
| auditData = { | |
| submission_id: p.id, | |
| decision: apiAudit.decision || 'DECLINED', | |
| overall_summary: (apiAudit.overall_summary && apiAudit.overall_summary !== 'Audit complete.') | |
| ? apiAudit.overall_summary | |
| : localAuditData.overall_summary, | |
| decline_reasons: (apiAudit.decline_reasons && apiAudit.decline_reasons.length > 0) | |
| ? apiAudit.decline_reasons | |
| : reasons, | |
| decision_factors: (apiAudit.decision_factors && apiAudit.decision_factors.length > 0) | |
| ? apiAudit.decision_factors | |
| : localAuditData.decision_factors, | |
| risk_band: apiAudit.risk_band || 'UNKNOWN', | |
| final_premium: apiAudit.final_premium || null, | |
| policy_number: apiAudit.policy_number || null, | |
| audit_score: apiAudit.audit_score || 0, | |
| audit_log_id: apiAudit.audit_log_id || 'N/A', | |
| audited_at: apiAudit.audited_at || new Date().toISOString(), | |
| halted_at: agent.toUpperCase(), | |
| }; | |
| // Force decision to DECLINED β it was declined, regardless of what API says | |
| auditData.decision = 'DECLINED'; | |
| // If decline_reasons still empty, use what we know | |
| if (!auditData.decline_reasons || auditData.decline_reasons.length === 0) { | |
| auditData.decline_reasons = reasons; | |
| } | |
| } catch(auditErr) { | |
| removeLiveCallBox(); | |
| console.warn('[AUDIT] API call failed, using local data:', auditErr); | |
| // auditData already set to localAuditData above | |
| } | |
| window._currentAuditData = auditData; | |
| window._currentPipelineData = p; | |
| const auditScore = auditData.audit_score || 0; | |
| const auditLogId = auditData.audit_log_id || 'N/A'; | |
| const auditSummary = auditData.overall_summary; | |
| addAgentMsg('audit', 'π', 'Audit Agent (AXIOM)', 'DECLINE AUDIT', | |
| auditSummary, | |
| `<span class="k">"decision"</span><span class="p">:</span> <span class="vf">DECLINED</span><span class="p">,</span>\n<span class="k">"audit_score"</span><span class="p">:</span> <span class="vn">${auditScore}</span><span class="p">,</span>\n<span class="k">"audit_log_id"</span><span class="p">:</span> <span class="vn">${auditLogId}</span><span class="p">,</span>\n<span class="k">"halted_at"</span><span class="p">:</span> <span class="vs">"${agent.toUpperCase()}"</span><span class="p">,</span>\n<span class="k">"decline_reasons"</span><span class="p">:</span> <span class="vn">${auditData.decline_reasons.length}</span>`, | |
| `gold_audit_log Β· submission_id=${p.id}`, true); | |
| addOrch(`Decline audit complete. Halted at ${agent.toUpperCase()}. ${auditData.decline_reasons.length} decline reason(s) recorded. Audit id=${auditLogId}.`); | |
| document.getElementById('ac-audit').classList.remove('active'); | |
| document.getElementById('ac-audit').classList.add('done'); | |
| setStat('audit','complete','β AUDITED'); | |
| // ββ Declined result card βββββββββββββββββββββββββββββββββββββββ | |
| const e = document.createElement('div'); e.className='rc rej'; | |
| e.innerHTML=` | |
| <div class="rchead"> | |
| <div> | |
| <div class="rcidl">SUBMISSION ID</div> | |
| <div class="rcid">${p.id}</div> | |
| <div style="font-size:10px;color:var(--dim2);margin-top:2px;font-family:'Share Tech Mono',monospace">${p.name} Β· ${p.loc}</div> | |
| <div class="live-result-tag">β‘ LIVE AGENT RESULT</div> | |
| </div> | |
| <div style="text-align:right"> | |
| <div class="rcbadge">β DECLINED</div> | |
| <div style="font-size:9px;color:var(--dim2);font-family:'Share Tech Mono',monospace;margin-top:4px">Halted at ${agent.toUpperCase()}</div> | |
| <div style="font-size:9px;color:#f472b6;font-family:'Share Tech Mono',monospace;margin-top:2px">π AXIOM Audited</div> | |
| </div> | |
| </div> | |
| <div class="rreasons"><div class="rrt">β Decline Reasons</div>${reasons.map(r=>`<div class="rri">${r}</div>`).join('')}</div> | |
| <div style="margin-top:11px;display:flex;gap:5px;flex-wrap:wrap"> | |
| <span class="chip fail">β DECLINED</span> | |
| <span class="chip amber">β‘ XGBoost Decision</span> | |
| <span class="chip warn">π Human Review Queued</span> | |
| <span class="chip info">π§ Adverse Action β SES</span> | |
| <span class="chip purple">π Decline β Gold Layer</span> | |
| <span class="chip" style="background:rgba(244,114,182,.15);color:#f472b6;border:1px solid rgba(244,114,182,.3)">π AXIOM Audited</span> | |
| </div> | |
| <div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;"> | |
| <button class="btn-view-audit" onclick="openAuditModal(window._currentAuditData||{},window._currentPipelineData||{})">π View Decline Audit Trail</button> | |
| <button class="btn-view-audit" style="color:#00d4ff;border-color:rgba(0,212,255,.35);background:rgba(0,212,255,.08);" onclick="exportFullAnalysis(window._currentAuditData||{},window._currentPipelineData||{})">β¬ Export Full Analysis</button> | |
| </div>`; | |
| document.getElementById('chatArea').appendChild(e); | |
| setTimeout(()=>e.classList.add('v'),50); | |
| p._result = 'reject'; | |
| renderQueue(); | |
| running = false; | |
| document.getElementById('liveBtn').disabled = false; | |
| } | |
| function updateProg(pct) { | |
| document.getElementById('prog').style.width = pct+'%'; | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // SIMULATION (demo mode β same script engine as v3) | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function startSimulation() { | |
| if (running || !selectedId) return; | |
| running = true; | |
| document.getElementById('startBtn').disabled = true; | |
| const p = allPolicies.find(x => x.id === selectedId); | |
| document.getElementById('chatArea').innerHTML = ''; | |
| const prog = document.getElementById('prog'); | |
| prog.style.width='0%'; | |
| p.outcome==='reject' ? prog.classList.add('fail') : prog.classList.remove('fail'); | |
| for(let i=0;i<=5;i++){const e=document.getElementById('tl-'+i);e.classList.remove('active','done','failed')} | |
| document.getElementById('tlProg').style.width='0%'; | |
| resetAgents(); | |
| setStat('orch','processing','β ACTIVE'); | |
| document.getElementById('ac-orch').classList.add('active'); | |
| const sc = buildScript(p); | |
| runScript(sc, p, 0); | |
| } | |
| function runScript(sc, p, i) { | |
| if (i>=sc.length){ running=false; document.getElementById('startBtn').disabled=false; p._result=p.outcome; renderQueue(); return; } | |
| const s=sc[i]; | |
| const D={phase:500,msg:950,typ:700,orch:450,act:80,result:600}; | |
| const delay=(D[s.t]||600)/speed; | |
| timeouts.push(setTimeout(()=>{ execStep(s,p); document.getElementById('prog').style.width=Math.round(i/(sc.length-1)*100)+'%'; runScript(sc,p,i+1); }, delay)); | |
| } | |
| function execStep(s, p) { | |
| const chat = document.getElementById('chatArea'); | |
| if (s.t==='phase') { | |
| const e=document.createElement('div');e.className='ph'; | |
| e.innerHTML=`<span class="pbadge">${s.badge}</span><div><div class="ptitle">${s.title}</div><div class="pdesc">${s.desc}</div></div>`; | |
| chat.appendChild(e);setTimeout(()=>e.classList.add('v'),50); | |
| if(s.tl!==undefined)tlSet(s.tl); | |
| } else if (s.t==='act'){activateAgent(s.agent);} | |
| else if (s.t==='orch'){ | |
| const e=document.createElement('div');e.className='ob'; | |
| e.innerHTML=`<span class="obicon">π§ </span><div><div class="obt">ORCHESTRATOR Β· STEP FUNCTIONS</div><div class="oba">${s.text}</div></div>`; | |
| chat.appendChild(e);setTimeout(()=>e.classList.add('v'),50); | |
| } else if (s.t==='typ'){ | |
| const c=agentColor(s.agent); | |
| const e=document.createElement('div');e.className='ti v'; | |
| e.innerHTML=`<div class="mav" style="background:${c.bg};border-color:${c.b};color:${c.c}">${ICONS[s.agent]||'π€'}</div><div class="tdots"><span></span><span></span><span></span></div>`; | |
| chat.appendChild(e);setTimeout(()=>{if(e.parentNode)e.remove();},900/speed); | |
| } else if (s.t==='msg'){ | |
| const c=agentColor(s.from); | |
| const t=new Date();const ts=t.getHours().toString().padStart(2,'0')+':'+t.getMinutes().toString().padStart(2,'0')+':'+t.getSeconds().toString().padStart(2,'0'); | |
| const e=document.createElement('div');e.className='msg'+(s.right?' r':''); | |
| e.innerHTML=` | |
| <div class="mav" style="background:${c.bg};border-color:${c.b};color:${c.c}">${s.icon}</div> | |
| <div class="mbody"> | |
| <div class="mfrom"><strong style="color:${c.c}">${s.name}</strong><span class="mtime">${ts}</span></div> | |
| <div class="mbub" style="background:${c.bg};border-color:${c.b}"> | |
| <span class="mtag" style="color:${c.c};border:1px solid ${c.b}">${s.tag}</span>${s.text} | |
| ${s.code?`<div class="cb">{ ${s.code} }</div>`:''} | |
| ${s.s3?`<div class="s3b">π¦ ${s.s3}</div>`:''} | |
| </div> | |
| </div>`; | |
| chat.appendChild(e);setTimeout(()=>e.classList.add('v'),50); | |
| } else if (s.t==='result'){ | |
| if(s.outcome==='approve'){ | |
| document.getElementById('ac-issue').classList.remove('active');document.getElementById('ac-issue').classList.add('done');setStat('issue','complete','β ISSUED'); | |
| document.getElementById('ac-orch').classList.remove('active');setStat('orch','complete','β COMPLETE'); | |
| for(let i=0;i<=5;i++){document.getElementById('tl-'+i).classList.remove('active');document.getElementById('tl-'+i).classList.add('done')} | |
| document.getElementById('prog').style.width='100%';document.getElementById('tlProg').style.width='calc(100% - 76px)'; | |
| // Build sim audit data from pre-recorded submission data | |
| var simAuditApprove = { | |
| submission_id: p.id, | |
| decision: 'APPROVED', | |
| overall_summary: 'The application from ' + p.name + ' for ' + p.loc + ' has been approved. The property was assessed as ' + p.prop.band + ' risk. Identity and compliance screening passed. The underwriting model approved the risk and a final annual premium of $' + p.pr.prem.toLocaleString() + ' has been calculated. Policy ' + p.pol.num + ' has been issued.', | |
| decline_reasons: [], | |
| decision_factors: [ | |
| {agent:'Document Validation Agent', factor:'Identity & Compliance Check', outcome:'PASS', detail:'Credit score: ' + p.kyc.score + '. OFAC: ' + p.kyc.ofac + '. Fraud signals: ' + p.kyc.fraud + '.'}, | |
| {agent:'Property Risk Agent', factor:'Peril Risk Assessment', outcome:'PASS', detail:'Risk band: ' + p.prop.band + '. Wind: ' + p.prop.wind + ' | Flood: ' + p.prop.flood + ' | Overall: ' + p.prop.score + '/100.'}, | |
| {agent:'Underwriting Agent', factor:'AI Underwriting Decision', outcome:'APPROVED', detail:'UW approved. Coverage: ' + p.cov + '. Limit: $' + (p.uw.lim/1000).toFixed(0) + 'K.'}, | |
| {agent:'Pricing Agent', factor:'Actuarial Premium Calculation', outcome:'CALCULATED', detail:'Annual premium: $' + p.pr.prem.toLocaleString() + '. Monthly: $' + Math.round(p.pr.prem/12).toLocaleString() + '. 95% CI: [$' + p.pr.lo.toLocaleString() + ' β $' + p.pr.hi.toLocaleString() + '].'}, | |
| {agent:'Issuance Agent', factor:'Policy Issuance', outcome:'ISSUED', detail:'Policy: ' + p.pol.num + '. Effective: ' + p.pol.eff + '. Documents generated.'} | |
| ], | |
| risk_band: p.prop.band, | |
| final_premium: p.pr.prem, | |
| policy_number: p.pol.num, | |
| audit_score: 85, | |
| audit_log_id: 'SIM-' + p.id, | |
| audited_at: new Date().toISOString(), | |
| _source: 'simulation' | |
| }; | |
| // Store sim data for export | |
| window._currentAuditData = simAuditApprove; | |
| window._currentPipelineData = p; | |
| window._kycData = {status:'KYC_PASS', credit_score:p.kyc.score, ofac_result:p.kyc.ofac, fraud_signals:p.kyc.fraud, ml_kyc_probability:0.92}; | |
| window._propData = {status:'RISK_ACCEPTABLE', risk_band:p.prop.band, peril_scores:{wind_score:p.prop.wind, flood_score:p.prop.flood, fire_score:p.prop.fire||0, overall_risk:p.prop.score}, risk_acceptability_prob:0.85}; | |
| window._uwData = {status:'UW_APPROVED', coverage_type:p.cov, uw_approval_probability:0.88, expected_loss_ratio:0.56, reinsurance_required:false, all_rule_violations:[]}; | |
| window._pricData = {final_premium:p.pr.prem, monthly_premium:Math.round(p.pr.prem/12), actuarial_premium:p.pr.prem, confidence_interval:{lo_95:p.pr.lo, hi_95:p.pr.hi}, premium_breakdown:{credit_modifier:1.0,risk_modifier:1.0,age_modifier:1.0}}; | |
| window._issData = {policy_number:p.pol.num, documents_generated:['declarations_page.pdf','policy_contract.pdf'], notifications_sent:{email:true}, policy_details:{effective_date:p.pol.eff, annual_premium:p.pr.prem, coverage_type:p.cov, status:'ISSUED'}}; | |
| const e=document.createElement('div');e.className='rc app'; | |
| e.innerHTML=` | |
| <div class="rchead"> | |
| <div><div class="rcidl">POLICY NUMBER</div><div class="rcid">${p.pol.num}</div><div style="font-size:10px;color:var(--dim2);margin-top:2px;font-family:'Share Tech Mono',monospace">${p.name} Β· ${p.loc}</div></div> | |
| <div style="text-align:right"><div class="rcbadge">β ISSUED</div><div style="font-size:9px;color:var(--dim2);font-family:'Share Tech Mono',monospace;margin-top:4px">All 5 agents passed Β· Simulated</div></div> | |
| </div> | |
| <div class="rcgrid"> | |
| <div class="rcs"><div class="rl">Coverage</div><div class="rv" style="font-size:12px">${p.cov} Homeowners</div></div> | |
| <div class="rcs"><div class="rl">Approved Limit</div><div class="rv">$${(p.uw.lim/1000).toFixed(0)}K</div></div> | |
| <div class="rcs"><div class="rl">Annual Premium</div><div class="rv g">$${p.pr.prem.toLocaleString()}</div></div> | |
| <div class="rcs"><div class="rl">Risk Band</div><div class="rv g">${p.prop.band}</div></div> | |
| <div class="rcs"><div class="rl">Credit Score</div><div class="rv">${p.kyc.score}</div></div> | |
| <div class="rcs"><div class="rl">Effective Date</div><div class="rv" style="font-size:11px">${p.pol.eff}</div></div> | |
| </div> | |
| <div style="margin-top:11px;display:flex;gap:5px;flex-wrap:wrap"> | |
| <span class="chip pass">β KYC PASS</span><span class="chip pass">β PROPERTY ${p.prop.band}</span><span class="chip pass">β UW APPROVED</span> | |
| <span class="chip pass">β ML PRICING</span><span class="chip info">π¦ S3 Docs</span><span class="chip info">π§ SES</span><span class="chip purple">π Gold Layer</span> | |
| <span class="chip" style="background:rgba(244,114,182,.15);color:#f472b6;border:1px solid rgba(244,114,182,.3)">π AXIOM Audited</span> | |
| </div> | |
| <div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;"> | |
| <button class="btn-view-audit" onclick="openAuditModal(window._currentAuditData||{},window._currentPipelineData||{})">π View Audit Trail</button> | |
| <button class="btn-view-audit" style="color:#00d4ff;border-color:rgba(0,212,255,.35);background:rgba(0,212,255,.08);" onclick="exportFullAnalysis(window._currentAuditData||{},window._currentPipelineData||{})">β¬ Export Full Analysis</button> | |
| </div>`; | |
| chat.appendChild(e);setTimeout(()=>e.classList.add('v'),50); | |
| } else { | |
| document.getElementById('ac-orch').classList.remove('active');setStat('orch','complete','β HALTED'); | |
| if(p.failAt==='kyc'){ | |
| document.getElementById('ac-kyc').classList.remove('active');document.getElementById('ac-kyc').classList.add('rejected');setStat('kyc','complete','β KYC_FAIL'); | |
| ['prop','uw','price','issue'].forEach(a=>setStat(a,'idle','β SKIPPED')); | |
| document.getElementById('tl-0').classList.add('done'); | |
| document.getElementById('tl-1').classList.remove('active');document.getElementById('tl-1').classList.add('failed'); | |
| document.getElementById('prog').style.width='22%'; | |
| } else { | |
| document.getElementById('ac-kyc').classList.remove('active');document.getElementById('ac-kyc').classList.add('done');setStat('kyc','complete','β COMPLETE'); | |
| document.getElementById('ac-prop').classList.remove('active');document.getElementById('ac-prop').classList.add('rejected');setStat('prop','complete','β DECLINED'); | |
| ['uw','price','issue'].forEach(a=>setStat(a,'idle','β SKIPPED')); | |
| ['tl-0','tl-1'].forEach(id=>{document.getElementById(id).classList.remove('active');document.getElementById(id).classList.add('done')}); | |
| document.getElementById('tl-2').classList.remove('active');document.getElementById('tl-2').classList.add('failed'); | |
| document.getElementById('prog').style.width='42%'; | |
| } | |
| // Build sim audit data for declined submission | |
| var haltedAt = p.failAt === 'kyc' ? 'KYC' : 'PROPERTY'; | |
| var simAuditDecline = { | |
| submission_id: p.id, | |
| decision: 'DECLINED', | |
| overall_summary: 'The application from ' + p.name + ' for ' + p.loc + ' has been declined at the ' + haltedAt + ' stage. ' + (p.failReasons||[]).join(' '), | |
| decline_reasons: p.failReasons || [], | |
| decision_factors: p.failAt === 'kyc' ? [ | |
| {agent:'Document Validation Agent', factor:'Identity & Compliance Check', outcome:'FAIL', detail:'Credit score: ' + p.kyc.score + '. OFAC: ' + p.kyc.ofac + '. KYC failed β submission halted.'} | |
| ] : [ | |
| {agent:'Document Validation Agent', factor:'Identity & Compliance Check', outcome:'PASS', detail:'Credit score: ' + p.kyc.score + '. OFAC: ' + p.kyc.ofac + '. KYC passed.'}, | |
| {agent:'Property Risk Agent', factor:'Peril Risk Assessment', outcome:'DECLINE', detail:'Risk band: DECLINED. Wind: ' + p.prop.wind + ' | Flood: ' + p.prop.flood + ' β exceeded maximum thresholds.'} | |
| ], | |
| risk_band: p.failAt === 'kyc' ? 'UNKNOWN' : 'DECLINED', | |
| final_premium: null, | |
| policy_number: null, | |
| audit_score: 0, | |
| audit_log_id: 'SIM-' + p.id, | |
| audited_at: new Date().toISOString(), | |
| halted_at: haltedAt, | |
| _source: 'simulation' | |
| }; | |
| window._currentAuditData = simAuditDecline; | |
| window._currentPipelineData = p; | |
| window._kycData = {status: p.failAt==='kyc' ? 'KYC_FAIL' : 'KYC_PASS', credit_score:p.kyc.score, ofac_result:p.kyc.ofac, fraud_signals:p.kyc.fraud||0}; | |
| window._propData = p.failAt!=='kyc' ? {status:'RISK_DECLINED', risk_band:'DECLINED', peril_scores:{wind_score:p.prop.wind, flood_score:p.prop.flood, overall_risk:p.prop.score||90}, decline_reason:(p.failReasons||[])[0]||''} : {}; | |
| const e=document.createElement('div');e.className='rc rej'; | |
| e.innerHTML=` | |
| <div class="rchead"> | |
| <div><div class="rcidl">SUBMISSION ID</div><div class="rcid">${p.id}</div><div style="font-size:10px;color:var(--dim2);margin-top:2px;font-family:'Share Tech Mono',monospace">${p.name} Β· ${p.loc}</div></div> | |
| <div style="text-align:right"><div class="rcbadge">β DECLINED</div><div style="font-size:9px;color:var(--dim2);font-family:'Share Tech Mono',monospace;margin-top:4px">Halted at ${p.failAt==='kyc'?'Step 2':'Step 3'}</div><div style="font-size:9px;color:#f472b6;font-family:'Share Tech Mono',monospace;margin-top:2px">π AXIOM Audited</div></div> | |
| </div> | |
| <div class="rreasons"><div class="rrt">β Decline Reasons</div>${(p.failReasons||[]).map(r=>`<div class="rri">${r}</div>`).join('')}</div> | |
| <div style="margin-top:11px;display:flex;gap:5px;flex-wrap:wrap"> | |
| <span class="chip fail">β DECLINED</span> | |
| <span class="chip warn">π Human Review Queued</span><span class="chip info">π§ SES Adverse Action</span><span class="chip purple">π Gold Layer</span> | |
| <span class="chip" style="background:rgba(244,114,182,.15);color:#f472b6;border:1px solid rgba(244,114,182,.3)">π AXIOM Audited</span> | |
| </div> | |
| <div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;"> | |
| <button class="btn-view-audit" onclick="openAuditModal(window._currentAuditData||{},window._currentPipelineData||{})">π View Decline Audit Trail</button> | |
| <button class="btn-view-audit" style="color:#00d4ff;border-color:rgba(0,212,255,.35);background:rgba(0,212,255,.08);" onclick="exportFullAnalysis(window._currentAuditData||{},window._currentPipelineData||{})">β¬ Export Decline Report</button> | |
| </div>`; | |
| chat.appendChild(e);setTimeout(()=>e.classList.add('v'),50); | |
| } | |
| } | |
| setTimeout(()=>{chat.lastElementChild?.scrollIntoView({behavior:'smooth',block:'nearest'})},80); | |
| } | |
| // ββ buildScript (same as v3) βββββββββββββββββββββββββββββββββββ | |
| function buildScript(p) { | |
| const sc=[]; | |
| const limNum=parseInt(p.lim.replace(/[^0-9.]/g,''))*(p.lim.includes('M')?1000000:1000); | |
| sc.push({t:'phase',badge:'STEP 1 Β· BRONZE LAYER',title:'Policy Submission Received',desc:`${p.cov} submission via API Gateway β S3 Bronze`,tl:0}); | |
| sc.push({t:'msg',from:'broker',icon:'π’',name:p.broker,right:true,tag:'API POST',text:`Submitting new ${p.cov} policy for ${p.name} β ${p.addr}.`,code:`<span class="k">"submission_id"</span><span class="p">:</span> <span class="vs">"${p.id}"</span><span class="p">,</span>\n<span class="k">"insured"</span><span class="p">:</span> <span class="vs">"${p.name}"</span><span class="p">,</span>\n<span class="k">"coverage"</span><span class="p">:</span> <span class="vs">"${p.cov}"</span><span class="p">,</span>\n<span class="k">"requested_limit"</span><span class="p">:</span> <span class="vn">${limNum}</span>`}); | |
| sc.push({t:'orch',text:`π₯ Stored β s3://pcins-bronze/submissions/2025/${p.id}.json | Step Functions triggered...`}); | |
| sc.push({t:'phase',badge:'STEP 2 Β· SILVER LAYER',title:'Agent 1 β KYC & Identity Validation',desc:'OFAC screening Β· Credit bureau pull Β· Fraud anomaly detection',tl:1}); | |
| sc.push({t:'act',agent:'kyc'}); | |
| sc.push({t:'msg',from:'orch',icon:'π§ ',name:'Orchestrator',tag:'INVOKE',text:`KYC Agent β validate identity for ${p.id}. Run OFAC, credit pull, fraud detection.`,s3:`s3://pcins-bronze/submissions/${p.id}.json`}); | |
| sc.push({t:'typ',agent:'kyc'}); | |
| if(p.failAt==='kyc'){ | |
| sc.push({t:'msg',from:'kyc',icon:'πͺͺ',name:'KYC Agent',tag:'π¨ ALERT',text:`β OFAC PARTIAL_MATCH. Credit ${p.kyc.score} below minimum 550. Fraud signals detected.`,code:`<span class="k">"kyc_status"</span><span class="p">:</span> <span class="vf">KYC_FAIL</span><span class="p">,</span>\n<span class="k">"credit_score"</span><span class="p">:</span> <span class="vn">${p.kyc.score}</span><span class="p">,</span>\n<span class="k">"ofac"</span><span class="p">:</span> <span class="vf">${p.kyc.ofac}</span>`,s3:`s3://pcins-silver/kyc_decisions/${p.id}_decline.json`}); | |
| sc.push({t:'msg',from:'orch',icon:'π§ ',name:'Orchestrator',tag:'β HALT',text:`KYC_FAIL. Pipeline terminated at Step 2. Agents 2β5 will NOT be invoked.`}); | |
| sc.push({t:'result',outcome:'reject'});return sc; | |
| } | |
| sc.push({t:'msg',from:'kyc',icon:'πͺͺ',name:'KYC Agent',tag:'RESULT',text:`Identity validated. OFAC: ${p.kyc.ofac}. Credit ${p.kyc.score} β above threshold. Fraud: ${p.kyc.fraud}. All checks passed.`,code:`<span class="k">"kyc_status"</span><span class="p">:</span> <span class="vp">KYC_PASS</span><span class="p">,</span>\n<span class="k">"credit_score"</span><span class="p">:</span> <span class="vn">${p.kyc.score}</span><span class="p">,</span>\n<span class="k">"ofac"</span><span class="p">:</span> <span class="vp">${p.kyc.ofac}</span>`,s3:`s3://pcins-silver/kyc_decisions/${p.id}_kyc.json`}); | |
| sc.push({t:'msg',from:'orch',icon:'π§ ',name:'Orchestrator',tag:'ROUTE',text:'KYC_PASS. Routing to Property Risk Agent.'}); | |
| sc.push({t:'phase',badge:'STEP 3 Β· SILVER LAYER',title:'Agent 2 β Property Risk Assessment',desc:'Geo-spatial Β· FEMA zones Β· Satellite imagery Β· Peril scoring',tl:2}); | |
| sc.push({t:'act',agent:'prop'}); | |
| sc.push({t:'msg',from:'orch',icon:'π§ ',name:'Orchestrator',tag:'INVOKE',text:`Property Agent β assess risk for ${p.addr}.`,s3:`s3://pcins-silver/kyc_decisions/${p.id}_kyc.json`}); | |
| sc.push({t:'typ',agent:'prop'}); | |
| if(p.failAt==='property'){ | |
| sc.push({t:'msg',from:'prop',icon:'π ',name:'Property Risk Agent',tag:'π¨ DECLINE',text:`β Wind ${p.prop.wind}/100 > max 70. Flood ${p.prop.flood}/100 > max 75. Overall risk DECLINED.`,code:`<span class="k">"risk_band"</span><span class="p">:</span> <span class="vf">DECLINED</span><span class="p">,</span>\n<span class="k">"wind"</span><span class="p">:</span> <span class="vf">${p.prop.wind}</span><span class="p">,</span>\n<span class="k">"flood"</span><span class="p">:</span> <span class="vf">${p.prop.flood}</span>`,s3:`s3://pcins-silver/property_risk/${p.id}_property.json`}); | |
| sc.push({t:'msg',from:'orch',icon:'π§ ',name:'Orchestrator',tag:'β HALT',text:'Property DECLINE. Pipeline terminated at Step 3.'}); | |
| sc.push({t:'result',outcome:'reject'});return sc; | |
| } | |
| sc.push({t:'msg',from:'prop',icon:'π ',name:'Property Risk Agent',tag:'RESULT',text:`Score ${p.prop.score}/100 β Band ${p.prop.band}. RCV $${(p.prop.rcv/1000).toFixed(0)}K. All perils within appetite.`,code:`<span class="k">"risk_band"</span><span class="p">:</span> <span class="vp">${p.prop.band}</span><span class="p">,</span>\n<span class="k">"wind"</span><span class="p">:</span> <span class="vn">${p.prop.wind}</span><span class="p">,</span>\n<span class="k">"flood"</span><span class="p">:</span> <span class="vn">${p.prop.flood}</span><span class="p">,</span>\n<span class="k">"overall"</span><span class="p">:</span> <span class="vn">${p.prop.score}</span>`,s3:`s3://pcins-silver/property_risk/${p.id}_property.json`}); | |
| sc.push({t:'phase',badge:'STEP 4 Β· SILVER LAYER',title:'Agent 3 β Underwriting Validation',desc:'Carrier guidelines Β· Coverage eligibility Β· State compliance Β· Reinsurance',tl:3}); | |
| sc.push({t:'act',agent:'uw'}); | |
| sc.push({t:'msg',from:'orch',icon:'π§ ',name:'Orchestrator',tag:'INVOKE',text:`UW Agent β consolidate KYC and Property for ${p.id}.`,s3:`s3://pcins-silver/property_risk/${p.id}_property.json`}); | |
| sc.push({t:'typ',agent:'uw'}); | |
| sc.push({t:'msg',from:'uw',icon:'π',name:'Underwriting Agent',tag:'DECISION',text:`${p.cov} eligibility confirmed. Endorsements applied. Compliance clear.`,code:`<span class="k">"uw_decision"</span><span class="p">:</span> <span class="vp">ISSUE_POLICY</span><span class="p">,</span>\n<span class="k">"limit"</span><span class="p">:</span> <span class="vn">${p.uw.lim}</span><span class="p">,</span>\n<span class="k">"deductible"</span><span class="p">:</span> <span class="vn">${p.uw.ded}</span>`,s3:`s3://pcins-silver/uw_decisions/${p.id}_uw.json`}); | |
| sc.push({t:'phase',badge:'STEP 5 Β· SILVER LAYER',title:'Agent 4 β ML Premium Pricing',desc:'XGBoost + GLM ensemble Β· SHAP explainability Β· 95% CI',tl:4}); | |
| sc.push({t:'act',agent:'price'}); | |
| sc.push({t:'msg',from:'orch',icon:'π§ ',name:'Orchestrator',tag:'INVOKE',text:`ML Pricing Agent β generate premium for ${p.id}. XGBoost/GLM ensemble + SHAP.`}); | |
| sc.push({t:'typ',agent:'price'}); | |
| sc.push({t:'msg',from:'price',icon:'π€',name:'ML Pricing Agent',tag:'INFERENCE',text:`Ensemble inference complete. Premium $${p.pr.prem.toLocaleString()}. CI: ${p.pr.ci}.`,code:`<span class="k">"premium"</span><span class="p">:</span> <span class="vn">${p.pr.prem}</span><span class="p">,</span>\n<span class="k">"band"</span><span class="p">:</span> <span class="vp">${p.pr.band}</span><span class="p">,</span>\n<span class="k">"ci_95"</span><span class="p">:</span> <span class="vs">${p.pr.ci}</span><span class="p">,</span>\n<span class="k">"shap"</span><span class="p">:</span> <span class="vs">${p.pr.shap}</span>`,s3:`s3://pcins-silver/premium_predictions/${p.id}_pricing.json`}); | |
| sc.push({t:'phase',badge:'STEP 6 Β· SILVER β GOLD',title:'Agent 5 β Policy Issuance & Delivery',desc:'Bind coverage Β· Generate documents Β· SES delivery Β· SNS events',tl:5}); | |
| sc.push({t:'act',agent:'issue'}); | |
| sc.push({t:'msg',from:'orch',icon:'π§ ',name:'Orchestrator',tag:'INVOKE',text:'All 5 agents PASSED. Invoking Issuance Agent.'}); | |
| sc.push({t:'typ',agent:'issue'}); | |
| sc.push({t:'msg',from:'issue',icon:'π',name:'Issuance Agent',tag:'BINDING',text:`Policy bound. Documents generated. SES delivery confirmed. Gold Layer ETL triggered.`,code:`<span class="k">"policy_number"</span><span class="p">:</span> <span class="vs">"${p.pol.num}"</span><span class="p">,</span>\n<span class="k">"effective_date"</span><span class="p">:</span> <span class="vs">"${p.pol.eff}"</span><span class="p">,</span>\n<span class="k">"status"</span><span class="p">:</span> <span class="vp">ISSUED</span>`,s3:`s3://pcins-silver/issued_policies/${p.pol.num}/`}); | |
| sc.push({t:'msg',from:'orch',icon:'π§ ',name:'Orchestrator',tag:'COMPLETE',text:`Pipeline complete. ${p.pol.num} issued. Gold Layer updated.`}); | |
| sc.push({t:'msg',from:'broker',icon:'π’',name:p.broker,right:true,tag:'RECEIVED',text:`β Policy documents received! ${p.name} is now covered under ${p.pol.num}.`}); | |
| sc.push({t:'result',outcome:'approve'}); | |
| return sc; | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // HELPERS | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function tlSet(step) { | |
| for(let i=0;i<step;i++){const e=document.getElementById('tl-'+i);e.classList.remove('active');e.classList.add('done')} | |
| const cur=document.getElementById('tl-'+step);cur.classList.remove('done','failed');cur.classList.add('active'); | |
| const pct=step===0?0:(step/6)*100; | |
| document.getElementById('tlProg').style.width=`calc(${pct}% - ${pct>0?38:0}px)`; | |
| } | |
| function activateAgent(agent) { | |
| const map={kyc:'ac-kyc',prop:'ac-prop',uw:'ac-uw',price:'ac-price',issue:'ac-issue',audit:'ac-audit'}; | |
| Object.values(map).forEach(id=>{ | |
| const el=document.getElementById(id); | |
| if(el&&el!==document.getElementById(map[agent])&&el.classList.contains('active')){el.classList.remove('active');el.classList.add('done');setStat(id.replace('ac-',''),'complete','β COMPLETE')} | |
| }); | |
| document.getElementById(map[agent])?.classList.add('active'); | |
| setStat(agent,'processing','β PROCESSING'); | |
| } | |
| function agentColor(key){const m={orch:'orch',orchestrator:'orch',kyc:'kyc',prop:'prop',property:'prop',uw:'uw',price:'price',pricing:'price',issue:'issue',issuance:'issue',broker:'broker'};return C[m[key]]||C.orch} | |
| function setStat(agent,cls,text){const map={orch:'as-orch',kyc:'as-kyc',prop:'as-prop',uw:'as-uw',price:'as-price',issue:'as-issue',audit:'as-audit'};const el=document.getElementById(map[agent]);if(el){el.className='astat '+cls;el.textContent=text}} | |
| function resetAgents(){[['ac-orch','as-orch','β STANDBY'],['ac-kyc','as-kyc','β IDLE'],['ac-prop','as-prop','β IDLE'],['ac-uw','as-uw','β IDLE'],['ac-price','as-price','β IDLE'],['ac-issue','as-issue','β IDLE'],['ac-audit','as-audit','β IDLE']].forEach(([cid,sid,txt])=>{const c=document.getElementById(cid);if(c)c.classList.remove('active','done','rejected');const s=document.getElementById(sid);if(s){s.className='astat idle';s.textContent=txt}})} | |
| function resetDemo(){timeouts.forEach(t=>clearTimeout(t));timeouts=[];running=false;liveResult=null;document.getElementById('startBtn').disabled=!selectedId||selectedSrc==='live';document.getElementById('liveBtn').disabled=!selectedId||selectedSrc!=='live';document.getElementById('prog').style.width='0%';document.getElementById('prog').classList.remove('fail');document.getElementById('tlProg').style.width='0%';for(let i=0;i<=6;i++){const e=document.getElementById('tl-'+i);e.classList.remove('active','done','failed')}resetAgents();if(selectedId){const p=allPolicies.find(x=>x.id===selectedId);if(p)resetChat(p)}else document.getElementById('chatArea').innerHTML='<div style="text-align:center;padding:70px 20px;color:var(--dim2)"><div style="font-size:42px;margin-bottom:12px;opacity:.2">π</div><div style="font-family:\'Share Tech Mono\',monospace;font-size:13px">Select a submission from the queue above</div></div>'} | |
| function toggleSpeed(){speed=speed===1?3:1;document.getElementById('speedBtn').textContent=speed===1?'β‘ Speed: Normal':'β‘ Speed: Fast'} | |
| function toggleConfig(){document.getElementById('cfgPanel').classList.toggle('open')} | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // INIT | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| checkApiStatus(); | |
| loadSubmissions(); | |
| // ββ AXIOM AUDIT MODAL βββββββββββββββββββββββββββββββββββββββββ | |
| var _lastAuditData = null; | |
| var _lastPipelineData = null; | |
| function openAuditModal(auditData, pipelineData) { | |
| _lastAuditData = auditData; | |
| _lastPipelineData = pipelineData; | |
| var overlay = document.getElementById('auditModalOverlay'); | |
| var body = document.getElementById('auditModalBody'); | |
| var sub = document.getElementById('auditModalSub'); | |
| var decision = auditData.decision || 'APPROVED'; | |
| var score = auditData.audit_score || 0; | |
| var summary = auditData.overall_summary || 'No summary available.'; | |
| var factors = auditData.decision_factors || []; | |
| var declines = auditData.decline_reasons || []; | |
| var logId = auditData.audit_log_id || 'N/A'; | |
| var subId = auditData.submission_id || (pipelineData && pipelineData.id) || ''; | |
| sub.textContent = 'Submission: ' + subId + ' Β· Audit Log ID: ' + logId + ' Β· ' + new Date().toLocaleString(); | |
| var scoreColor = score >= 70 ? '#00e5a0' : score >= 40 ? '#f59e0b' : '#ef4444'; | |
| var decColor = decision === 'APPROVED' ? '#00e5a0' : '#ef4444'; | |
| var html = ''; | |
| // Decision badge + halted at | |
| var haltedAt = auditData.halted_at ? ' Β· Halted at ' + auditData.halted_at : ''; | |
| html += '<div style="display:flex;align-items:center;gap:.8rem;margin-bottom:1rem;flex-wrap:wrap;">'; | |
| html += '<div style="font-size:9px;letter-spacing:.15em;color:rgba(255,255,255,.4);text-transform:uppercase;">Final Decision</div>'; | |
| html += '<div style="font-size:11px;font-weight:700;letter-spacing:.12em;padding:.25rem .8rem;border:1px solid ' + decColor + ';color:' + decColor + ';font-family:Share Tech Mono,monospace;">' + decision + '</div>'; | |
| if (haltedAt) html += '<div style="font-size:10px;color:rgba(255,255,255,.4);font-family:Share Tech Mono,monospace;">' + haltedAt + '</div>'; | |
| html += '</div>'; | |
| // Summary | |
| html += '<div class="audit-summary-box"><div class="audit-summary-label">Plain-English Decision Summary</div>' + summary + '</div>'; | |
| // Score bar | |
| html += '<div class="audit-score-row">'; | |
| html += '<div class="audit-score-label">Audit Confidence Score</div>'; | |
| html += '<div class="audit-score-track"><div class="audit-score-fill" id="auditScoreFill" style="width:0%;background:' + scoreColor + '"></div></div>'; | |
| html += '<div class="audit-score-val" style="color:' + scoreColor + '">' + score + '%</div>'; | |
| html += '</div>'; | |
| // Decline reasons | |
| if (declines.length > 0) { | |
| html += '<div class="audit-decline-box"><div class="audit-decline-title">β Decline Reasons</div>'; | |
| declines.forEach(function(r, i) { | |
| html += '<div class="audit-decline-item">' + (i+1) + '. ' + r + '</div>'; | |
| }); | |
| html += '</div>'; | |
| } | |
| // Agent factors | |
| html += '<div class="audit-factors-title">Agent Decision Factors (' + factors.length + ' checks)</div>'; | |
| var outcomeColors = { | |
| 'PASS':'#00e5a0','APPROVED':'#00e5a0','ISSUED':'#00e5a0','CALCULATED':'#00d4ff', | |
| 'FAIL':'#ef4444','DECLINED':'#ef4444','DECLINE':'#ef4444', | |
| 'REFER':'#f59e0b','NOT ISSUED':'#f59e0b' | |
| }; | |
| factors.forEach(function(f) { | |
| var oc = outcomeColors[f.outcome] || '#7a9ab8'; | |
| html += '<div class="audit-factor">'; | |
| html += '<div class="audit-factor-header">'; | |
| html += '<div class="audit-factor-agent">' + f.agent + '</div>'; | |
| html += '<div class="audit-factor-outcome" style="border-color:' + oc + ';color:' + oc + '">' + f.outcome + '</div>'; | |
| html += '</div>'; | |
| html += '<div class="audit-factor-factor">' + f.factor + '</div>'; | |
| html += '<div class="audit-factor-detail">' + f.detail + '</div>'; | |
| html += '</div>'; | |
| }); | |
| // Export button | |
| html += '<button class="audit-export-btn" onclick="exportFullAnalysis(_lastAuditData||{},_lastPipelineData||{})">β¬ Export Full Analysis to Excel</button>'; | |
| body.innerHTML = html; | |
| overlay.classList.add('open'); | |
| // Animate score bar | |
| setTimeout(function() { | |
| var fill = document.getElementById('auditScoreFill'); | |
| if (fill) fill.style.width = score + '%'; | |
| }, 100); | |
| } | |
| function closeAuditModal() { | |
| document.getElementById('auditModalOverlay').classList.remove('open'); | |
| } | |
| document.addEventListener('keydown', function(e) { | |
| if (e.key === 'Escape') closeAuditModal(); | |
| }); | |
| // ββ FULL ANALYSIS EXCEL EXPORT ββββββββββββββββββββββββββββββββ | |
| function exportAuditToExcel() { | |
| exportFullAnalysis(_lastAuditData || {}, _lastPipelineData || {}); | |
| } | |
| function exportFullAnalysis(audit, pipeline) { | |
| audit = audit || _lastAuditData || {}; | |
| pipeline = pipeline || _lastPipelineData || {}; | |
| var factors = audit.decision_factors || []; | |
| var declines = audit.decline_reasons || []; | |
| var kyc = window._kycData || {}; | |
| var prop = window._propData || {}; | |
| var uw = window._uwData || {}; | |
| var pric = window._pricData || {}; | |
| var iss = window._issData || {}; | |
| var perils = prop.peril_scores || {}; | |
| var ci = pric.confidence_interval || {}; | |
| var brkdn = pric.premium_breakdown || {}; | |
| var rows = []; | |
| var sep = ['βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ']; | |
| // ββ COVER ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| rows.push(['POLICYBRIDGE β COMPLETE PIPELINE ANALYSIS REPORT']); | |
| rows.push(['Generated', new Date().toLocaleString()]); | |
| rows.push(['Submission ID', audit.submission_id || pipeline.id || '']); | |
| rows.push(['Insured Name', pipeline.name || '']); | |
| rows.push(['Location', pipeline.loc || '']); | |
| rows.push(['Coverage Type', pipeline.cov || '']); | |
| rows.push(['Coverage Limit', pipeline.lim || '']); | |
| rows.push(['Final Decision', audit.decision || '']); | |
| rows.push(['Policy Number', audit.decision === 'DECLINED' ? 'N/A β Not Issued' : (audit.policy_number || iss.policy_number || '')]); | |
| rows.push(['Audit Log ID', audit.audit_log_id || '']); | |
| rows.push(['Audit Confidence Score', (audit.audit_score || 0) + '%']); | |
| rows.push(['Report Timestamp', new Date().toISOString()]); | |
| rows.push([]); | |
| // ββ DECISION SUMMARY ββββββββββββββββββββββββββββββββββββββββ | |
| rows.push(sep); | |
| rows.push(['SECTION 1: DECISION SUMMARY']); | |
| rows.push(sep); | |
| rows.push([audit.overall_summary || '']); | |
| rows.push([]); | |
| if (declines.length > 0) { | |
| rows.push(['DECLINE REASONS']); | |
| declines.forEach(function(r, i) { rows.push([(i+1) + '. ' + r]); }); | |
| rows.push([]); | |
| } | |
| // ββ AGENT 1: DOCUMENT VALIDATION ββββββββββββββββββββββββββββ | |
| rows.push(sep); | |
| rows.push(['SECTION 2: AGENT 1 β DOCUMENT VALIDATION (AURA/KYC)']); | |
| rows.push(sep); | |
| rows.push(['Field', 'Value']); | |
| rows.push(['KYC Status', kyc.status || '']); | |
| rows.push(['Credit Score', kyc.credit_score || '']); | |
| rows.push(['OFAC Result', kyc.ofac_result || '']); | |
| rows.push(['Fraud Signals', kyc.fraud_signals || 0]); | |
| rows.push(['ML KYC Probability', kyc.ml_kyc_probability ? (kyc.ml_kyc_probability * 100).toFixed(1) + '%' : '']); | |
| rows.push(['Decline Reason', kyc.decline_reason || 'None']); | |
| rows.push([]); | |
| // ββ AGENT 2: PROPERTY RISK βββββββββββββββββββββββββββββββββββ | |
| rows.push(sep); | |
| rows.push(['SECTION 3: AGENT 2 β PROPERTY RISK ASSESSMENT']); | |
| rows.push(sep); | |
| rows.push(['Field', 'Value']); | |
| rows.push(['Status', prop.status || '']); | |
| rows.push(['Risk Band', prop.risk_band || '']); | |
| rows.push(['Wind Score', perils.wind_score || 0]); | |
| rows.push(['Flood Score', perils.flood_score || 0]); | |
| rows.push(['Fire Score', perils.fire_score || 0]); | |
| rows.push(['Overall Risk Score', perils.overall_risk || 0]); | |
| rows.push(['Risk Acceptability Probability', prop.risk_acceptability_prob ? (prop.risk_acceptability_prob * 100).toFixed(1) + '%' : '']); | |
| rows.push(['Decline Reason', prop.decline_reason || 'None']); | |
| rows.push([]); | |
| // ββ AGENT 3: UNDERWRITING ββββββββββββββββββββββββββββββββββββ | |
| rows.push(sep); | |
| rows.push(['SECTION 4: AGENT 3 β UNDERWRITING DECISION']); | |
| rows.push(sep); | |
| rows.push(['Field', 'Value']); | |
| rows.push(['UW Decision', uw.status || '']); | |
| rows.push(['Coverage Type', uw.coverage_type || '']); | |
| rows.push(['Approval Probability', uw.uw_approval_probability ? (uw.uw_approval_probability * 100).toFixed(1) + '%' : '']); | |
| rows.push(['Expected Loss Ratio', uw.expected_loss_ratio || '']); | |
| rows.push(['Reinsurance Required', uw.reinsurance_required ? 'Yes' : 'No']); | |
| rows.push(['Primary Decline Reason', uw.primary_decline_reason || 'None']); | |
| rows.push(['Rule Violations', (uw.all_rule_violations || []).join('; ') || 'None']); | |
| rows.push([]); | |
| // ββ AGENT 4: PRICING βββββββββββββββββββββββββββββββββββββββββ | |
| rows.push(sep); | |
| rows.push(['SECTION 5: AGENT 4 β PREMIUM PRICING']); | |
| rows.push(sep); | |
| rows.push(['Field', 'Value']); | |
| rows.push(['Final Annual Premium', pric.final_premium ? '$' + Number(pric.final_premium).toLocaleString() : '']); | |
| rows.push(['Monthly Premium', pric.monthly_premium ? '$' + Number(pric.monthly_premium).toLocaleString() : '']); | |
| rows.push(['Actuarial Premium', pric.actuarial_premium ? '$' + Number(pric.actuarial_premium).toLocaleString() : '']); | |
| rows.push(['95% CI Low', ci.lo_95 ? '$' + Number(ci.lo_95).toLocaleString() : '']); | |
| rows.push(['95% CI High', ci.hi_95 ? '$' + Number(ci.hi_95).toLocaleString() : '']); | |
| rows.push(['Credit Modifier', brkdn.credit_modifier || 1.0]); | |
| rows.push(['Risk Modifier', brkdn.risk_modifier || 1.0]); | |
| rows.push(['Age Modifier', brkdn.age_modifier || 1.0]); | |
| rows.push([]); | |
| // ββ AGENT 5: ISSUANCE ββββββββββββββββββββββββββββββββββββββββ | |
| rows.push(sep); | |
| rows.push(['SECTION 6: AGENT 5 β POLICY ISSUANCE']); | |
| rows.push(sep); | |
| var isDeclined = (audit.decision === 'DECLINED'); | |
| if (isDeclined) { | |
| rows.push(['Status', 'NOT ISSUED β Pipeline halted before issuance']); | |
| rows.push(['Policy Number', 'N/A β Policy was not issued']); | |
| rows.push(['Reason', (audit.decline_reasons || []).join('; ') || 'Declined by prior agent']); | |
| rows.push(['Documents Generated', 'None β policy was not issued']); | |
| } else { | |
| var polDets = iss.policy_details || {}; | |
| rows.push(['Field', 'Value']); | |
| rows.push(['Policy Number', iss.policy_number || audit.policy_number || 'N/A']); | |
| rows.push(['Status', 'ISSUED']); | |
| rows.push(['Coverage Type', polDets.coverage_type || '']); | |
| rows.push(['Effective Date', polDets.effective_date || '']); | |
| rows.push(['Expiration Date', polDets.expiration_date || '']); | |
| rows.push(['Annual Premium', polDets.annual_premium ? '$' + Number(polDets.annual_premium).toLocaleString() : (audit.final_premium ? '$' + Number(audit.final_premium).toLocaleString() : '')]); | |
| rows.push(['Documents Generated', (iss.documents_generated || []).join(', ') || '']); | |
| rows.push(['Email Notification', (iss.notifications_sent || {}).email ? 'Sent' : 'Pending']); | |
| } | |
| rows.push([]); | |
| // ββ AGENT 6: AUDIT TRAIL βββββββββββββββββββββββββββββββββββββ | |
| rows.push(sep); | |
| rows.push(['SECTION 7: AGENT 6 β AXIOM AUDIT TRAIL']); | |
| rows.push(sep); | |
| rows.push(['Field', 'Value']); | |
| rows.push(['Final Decision', audit.decision || '']); | |
| rows.push(['Policy Issued', audit.decision === 'DECLINED' ? 'NO β Not Issued' : 'YES']); | |
| rows.push(['Halted At Agent', audit.halted_at || (audit.decision === 'DECLINED' ? 'See decline reasons' : 'N/A β all agents passed')]); | |
| rows.push(['Audit Confidence Score', (audit.audit_score || 0) + '%']); | |
| rows.push(['Risk Band', audit.risk_band || '']); | |
| rows.push(['Audit Log ID', audit.audit_log_id || '']); | |
| rows.push(['Audited At', audit.audited_at || '']); | |
| rows.push([]); | |
| rows.push(['Plain-English Summary']); | |
| rows.push([audit.overall_summary || '']); | |
| rows.push([]); | |
| // Agent decision factors table | |
| if (factors.length > 0) { | |
| rows.push(['AGENT DECISION FACTORS BREAKDOWN']); | |
| rows.push(['Agent', 'Factor Checked', 'Outcome', 'Detail']); | |
| factors.forEach(function(f) { | |
| rows.push([f.agent || '', f.factor || '', f.outcome || '', f.detail || '']); | |
| }); | |
| rows.push([]); | |
| } | |
| // ββ Convert to CSV βββββββββββββββββββββββββββββββββββββββββββ | |
| function escCell(v) { | |
| var s = String(v == null ? '' : v).replace(/"/g, '""'); | |
| return (s.indexOf(',') >= 0 || s.indexOf('"') >= 0 || s.indexOf('\n') >= 0) ? '"' + s + '"' : s; | |
| } | |
| var csv = rows.map(function(r) { | |
| return Array.isArray(r) ? r.map(escCell).join(',') : escCell(r); | |
| }).join('\n'); | |
| var blob = new Blob(['\uFEFF' + csv], {type:'text/csv;charset=utf-8;'}); | |
| var url = URL.createObjectURL(blob); | |
| var a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'PolicyBridge_Analysis_' + (audit.submission_id || pipeline.id || 'report') + '_' + new Date().toISOString().slice(0,10) + '.csv'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // CLAIMSENSE JS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| var claimRunning = false; | |
| var _lastTriageData = null; | |
| var _lastClaimAuditData = null; | |
| function switchTab(tab) { | |
| var queueSection = document.querySelector('.queue-section'); | |
| var mainGrid = document.querySelector('.main-grid'); | |
| var controls = document.querySelector('.controls'); | |
| var timeline = document.querySelector('.tl'); | |
| var claimsSec = document.getElementById('claimsSection'); | |
| var radarSec = document.getElementById('radarSection'); | |
| var pbSec = document.getElementById('pbSection'); | |
| if (radarSec) { radarSec.style.display='none'; radarSec.classList.remove('active'); } | |
| if (claimsSec) { claimsSec.style.display='none'; claimsSec.classList.remove('active'); } | |
| if (pbSec) { pbSec.style.display='none'; pbSec.classList.remove('active'); } | |
| if (tab === 'policybridge') { | |
| if (queueSection) queueSection.style.display = 'none'; | |
| if (mainGrid) mainGrid.style.display = 'none'; | |
| if (controls) controls.style.display = 'none'; | |
| if (timeline) timeline.style.display = 'none'; | |
| pbSec.classList.add('active'); pbSec.style.display = 'block'; | |
| pbInit(); | |
| document.querySelectorAll('.tab').forEach(function(t){ t.classList.remove('active'); }); | |
| if (event && event.target) event.target.classList.add('active'); | |
| return; | |
| } | |
| if (tab === 'radar') { | |
| if (queueSection) queueSection.style.display = 'none'; | |
| if (mainGrid) mainGrid.style.display = 'none'; | |
| if (controls) controls.style.display = 'none'; | |
| if (timeline) timeline.style.display = 'none'; | |
| radarSec.classList.add('active'); radarSec.style.display = 'block'; | |
| setTimeout(function(){ | |
| if (!_rrMap) _initMap(); | |
| if (_rrMap) _rrMap.invalidateSize(); | |
| loadRRHistory(); | |
| }, 100); | |
| document.querySelectorAll('.tab').forEach(function(t){ t.classList.remove('active'); }); | |
| if (event && event.target) event.target.classList.add('active'); | |
| return; | |
| } | |
| if (tab === 'claims') { | |
| if (queueSection) queueSection.style.display = 'none'; | |
| if (mainGrid) mainGrid.style.display = 'none'; | |
| if (controls) controls.style.display = 'none'; | |
| if (timeline) timeline.style.display = 'none'; | |
| claimsSec.classList.add('active'); | |
| claimsSec.style.display = 'block'; | |
| loadRecentClaims(); | |
| } else { | |
| if (queueSection) queueSection.style.display = ''; | |
| if (mainGrid) mainGrid.style.display = ''; | |
| if (controls) controls.style.display = ''; | |
| if (timeline) timeline.style.display = ''; | |
| if (typeof selectedSrc !== 'undefined') selectedSrc = tab === 'live' ? 'live' : 'demo'; | |
| loadSubmissions(); | |
| var startBtn = document.getElementById('startBtn'); | |
| var liveBtn = document.getElementById('liveBtn'); | |
| if (startBtn) startBtn.style.display = tab === 'demo' ? '' : 'none'; | |
| if (liveBtn) liveBtn.style.display = tab === 'live' ? '' : 'none'; | |
| } | |
| document.querySelectorAll('.tab').forEach(function(t){ t.classList.remove('active'); }); | |
| if (event && event.target) event.target.classList.add('active'); | |
| } | |
| function _setFnolStatus(msg, color) { | |
| var el = document.getElementById('fnolStatus'); | |
| if (el) { el.textContent = msg; el.style.color = color || 'var(--dim2)'; } | |
| } | |
| async function submitAndRunClaim() { | |
| if (claimRunning) return; | |
| var policy = document.getElementById('fnol_policy').value.trim(); | |
| var name = document.getElementById('fnol_name').value.trim(); | |
| var iDate = document.getElementById('fnol_date').value; | |
| var iType = document.getElementById('fnol_type').value; | |
| if (!policy || !name || !iDate) { | |
| _setFnolStatus('β Policy number, claimant name and incident date are required.', '#fca5a5'); | |
| return; | |
| } | |
| // Duplicate check | |
| try { | |
| var _acd = new AbortController(); var _td = setTimeout(function(){_acd.abort()},6000); | |
| var dupResp = await fetch(API_BASE + '/claims/recent?limit=50', {signal:_acd.signal}); | |
| clearTimeout(_td); | |
| var dupData = await dupResp.json(); | |
| var existing = (dupData.claims || []).find(function(c) { | |
| return c.policy_number === policy && c.incident_type === iType && c.incident_date === iDate; | |
| }); | |
| if (existing) { | |
| _setFnolStatus('β Duplicate: ' + existing.claim_id + ' already filed for this policy / date / type.', '#fca5a5'); | |
| return; | |
| } | |
| } catch(e) { /* allow if check fails */ } | |
| claimRunning = true; | |
| document.getElementById('submitFnolBtn').disabled = true; | |
| _claimStepsReset(); | |
| document.getElementById('triageResult').style.display = 'none'; | |
| var claimId = 'CLM-' + new Date().getFullYear() + '-' + String(Math.floor(Math.random()*90000)+10000); | |
| var payload = { | |
| claim_id: claimId, policy_number: policy, claimant_name: name, | |
| incident_date: iDate, incident_type: iType, | |
| coverage_type_code: document.getElementById('fnol_coverage').value, | |
| estimated_damage: parseFloat(document.getElementById('fnol_damage').value) || 0, | |
| incident_description: document.getElementById('fnol_desc').value.trim(), | |
| has_police_report: document.getElementById('fnol_police').value === '1', | |
| has_photos: document.getElementById('fnol_photos').value === '1', | |
| }; | |
| _setFnolStatus('Submitting FNOL...', '#fb923c'); | |
| try { | |
| var _ac0 = new AbortController(); var _t0 = setTimeout(function(){_ac0.abort()},10000); | |
| var subResp = await fetch(API_BASE + '/claim/submit', { | |
| method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify(payload), signal: _ac0.signal | |
| }); | |
| clearTimeout(_t0); | |
| if (!subResp.ok) throw new Error('Submit failed: ' + subResp.status); | |
| var steps = ['fnol','coverage','fraud','severity','triage']; | |
| var labels = ['FNOL Intake','Coverage','Fraud Signal','Severity','Triage']; | |
| var results = {}; | |
| for (var i=0; i<steps.length; i++) { | |
| _claimStepSet(i+1, 'running'); | |
| _setFnolStatus('Agent ' + (i+1) + '/5 β ' + labels[i] + '...', '#fb923c'); | |
| var _ac = new AbortController(); var _t = setTimeout(function(){_ac.abort()},12000); | |
| var r = await fetch(API_BASE + '/claim/agent/' + steps[i], { | |
| method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({claim_id: claimId}), signal: _ac.signal | |
| }); | |
| clearTimeout(_t); | |
| results[steps[i]] = await r.json().catch(function(){return {};}); | |
| _claimStepSet(i+1, 'done'); | |
| if (steps[i]==='fnol' && results['fnol'].status==='FNOL_INVALID') { | |
| for (var j=i+2;j<=5;j++) _claimStepSet(j,'failed'); | |
| _renderTriageResult(claimId,'DENY',results,payload); break; | |
| } | |
| if (steps[i]==='coverage' && results['coverage'].coverage_status==='EXCLUDED') { | |
| for (var j=i+2;j<=5;j++) _claimStepSet(j,'failed'); | |
| _renderTriageResult(claimId,'DENY',results,payload); break; | |
| } | |
| await new Promise(function(res){setTimeout(res,300)}); | |
| } | |
| if (results['triage']) _renderTriageResult(claimId, results['triage'].triage_outcome, results, payload); | |
| _setFnolStatus('Triage complete β ' + claimId, '#86efac'); | |
| await loadRecentClaims(); | |
| } catch(err) { | |
| _setFnolStatus('Error: ' + err.message, '#fca5a5'); | |
| console.error('[CLAIMSENSE]', err); | |
| } finally { | |
| claimRunning = false; | |
| document.getElementById('submitFnolBtn').disabled = false; | |
| } | |
| } | |
| function _renderTriageResult(claimId, outcome, results, payload) { | |
| var triage = results['triage'] || {}; | |
| var fraud = results['fraud'] || {}; | |
| var severity = results['severity'] || {}; | |
| var coverage = results['coverage'] || {}; | |
| var fnol = results['fnol'] || {}; | |
| _lastTriageData = {claimId, outcome, triage, fraud, severity, coverage, fnol, payload}; | |
| var outcomeColor = {AUTO_SETTLE:'#86efac',ADJUSTER_REVIEW:'#fde68a',SIU_REFERRAL:'#fca5a5',DENY:'#9ca3af'}[outcome]||'#fff'; | |
| var html = '<div class="triage-card ' + (outcome||'').toLowerCase() + '">'; | |
| html += '<div style="font-size:11px;font-weight:700;letter-spacing:.1em;padding:4px 12px;border:1px solid ' + outcomeColor + ';color:' + outcomeColor + ';display:inline-block;margin-bottom:8px;font-family:Share Tech Mono,monospace;">' + (outcome||'UNKNOWN') + '</div>'; | |
| html += '<div style="font-size:12px;color:var(--dim);line-height:1.7;margin-bottom:10px;">' + (triage.overall_summary||'Triage complete.') + '</div>'; | |
| html += '<div class="triage-metrics">'; | |
| var fraudScore = fraud.fraud_score||0; | |
| html += '<div class="triage-metric"><div class="triage-metric-label">Fraud Score</div><div class="triage-metric-val" style="color:' + (fraudScore>=70?'#fca5a5':fraudScore>=40?'#fde68a':'#86efac') + '">' + fraudScore + '/100</div></div>'; | |
| html += '<div class="triage-metric"><div class="triage-metric-label">Severity</div><div class="triage-metric-val">' + (severity.severity_band||'β') + '</div></div>'; | |
| html += '<div class="triage-metric"><div class="triage-metric-label">Reserve Est.</div><div class="triage-metric-val" style="color:#fb923c">$' + ((severity.reserve_estimate||0).toLocaleString()) + '</div></div>'; | |
| html += '<div class="triage-metric"><div class="triage-metric-label">Coverage</div><div class="triage-metric-val" style="color:' + (coverage.coverage_status==='COVERED'?'#86efac':'#fca5a5') + '">' + (coverage.coverage_status||'β') + '</div></div>'; | |
| html += '<div class="triage-metric"><div class="triage-metric-label">Priority</div><div class="triage-metric-val">' + (triage.triage_priority||'β') + '</div></div>'; | |
| html += '<div class="triage-metric"><div class="triage-metric-label">Queue</div><div class="triage-metric-val" style="font-size:10px">' + (triage.adjuster_queue||'β').replace(/_/g,' ') + '</div></div>'; | |
| html += '</div>'; | |
| var flags = fraud.fraud_flags||[]; | |
| if (flags.length>0) { | |
| html += '<div style="margin-bottom:10px"><div style="font-family:Share Tech Mono,monospace;font-size:8px;letter-spacing:2px;color:#fca5a5;margin-bottom:5px">β FRAUD SIGNALS</div>'; | |
| flags.forEach(function(f){ html += '<div style="font-size:11px;color:#fca5a5;margin-bottom:2px">β’ ' + f + '</div>'; }); | |
| html += '</div>'; | |
| } | |
| html += '<div style="display:flex;gap:8px;margin-top:8px;flex-wrap:wrap">'; | |
| html += '<button class="btn" style="font-size:10px;border-color:rgba(0,229,160,.4);color:#00e5a0" onclick="openClaimAuditFromResult()">π View Audit Trail</button>'; | |
| html += '<button class="btn" style="font-size:10px;border-color:rgba(251,146,60,.4);color:#fb923c" onclick="exportClaimAuditReport()">β¬ Export Report</button>'; | |
| html += '</div>'; | |
| html += '</div>'; | |
| var el = document.getElementById('triageResult'); | |
| el.innerHTML = html; el.style.display = 'block'; | |
| el.scrollIntoView({behavior:'smooth',block:'nearest'}); | |
| } | |
| function openClaimAuditFromResult() { | |
| if (!_lastTriageData) return; | |
| var d = _lastTriageData; | |
| openClaimAuditModal({ | |
| claim_id: d.claimId, | |
| triage_outcome: d.outcome, | |
| audit_score: d.triage.audit_score || 0, | |
| overall_summary: d.triage.overall_summary || '', | |
| decision_factors: d.triage.decision_factors || [], | |
| fraud_score: (d.fraud||{}).fraud_score || 0, | |
| fraud_band: (d.fraud||{}).fraud_band || 'LOW', | |
| fraud_flags: (d.fraud||{}).fraud_flags || [], | |
| severity_band: (d.severity||{}).severity_band || '', | |
| reserve_estimate: (d.severity||{}).reserve_estimate || 0, | |
| reserve_note: (d.severity||{}).reserve_note || '', | |
| coverage_status: (d.coverage||{}).coverage_status || '', | |
| coverage_type: (d.coverage||{}).coverage_type || '', | |
| applicable_limit: (d.coverage||{}).applicable_limit || 0, | |
| deductible: (d.coverage||{}).deductible || 0, | |
| net_payable_est: (d.coverage||{}).net_payable_est || 0, | |
| days_to_report: (d.fnol||{}).days_to_report || 0, | |
| completeness_score:(d.fnol||{}).completeness_score || 0, | |
| fnol_status: (d.fnol||{}).status || '', | |
| incident_type: d.payload.incident_type || '', | |
| incident_date: d.payload.incident_date || '', | |
| claimant_name: d.payload.claimant_name || '', | |
| policy_number: d.payload.policy_number || '', | |
| audit_log_id: d.triage.audit_log_id || 'N/A', | |
| }); | |
| } | |
| async function loadAndOpenClaimAudit(claimId) { | |
| try { | |
| var _ac = new AbortController(); var _t = setTimeout(function(){_ac.abort()},8000); | |
| var r = await fetch(API_BASE + '/claim/audit/' + claimId, {signal:_ac.signal}); | |
| clearTimeout(_t); | |
| if (!r.ok) { alert('No audit record for ' + claimId + '. Run the pipeline first.'); return; } | |
| var a = await r.json(); | |
| openClaimAuditModal({ | |
| claim_id: claimId, | |
| triage_outcome: a.triage_outcome, | |
| audit_score: a.audit_score || 0, | |
| overall_summary: a.overall_summary || '', | |
| decision_factors: a.decision_factors || (a.decision_factors_json ? JSON.parse(a.decision_factors_json) : []), | |
| fraud_flags: a.fraud_flags || (a.fraud_flags_json ? JSON.parse(a.fraud_flags_json) : []), | |
| fraud_score: a.fraud_score || 0, | |
| fraud_band: a.fraud_score>=70?'HIGH':a.fraud_score>=40?'MEDIUM':'LOW', | |
| severity_band: a.severity_band || '', | |
| reserve_estimate: a.reserve_estimate || 0, | |
| coverage_status: a.coverage_status || '', | |
| triage_priority: a.triage_priority || '', | |
| adjuster_queue: a.assigned_adjuster_queue || '', | |
| policy_number: a.policy_number || '', | |
| audit_log_id: a.id || 'N/A', | |
| }); | |
| } catch(err) { alert('Could not load audit: ' + err.message); } | |
| } | |
| function openClaimAuditModal(d) { | |
| _lastClaimAuditData = d; | |
| var overlay = document.getElementById('claimAuditOverlay'); | |
| var body = document.getElementById('claimAuditBody'); | |
| var sub = document.getElementById('claimAuditSub'); | |
| var outcome = d.triage_outcome || 'UNKNOWN'; | |
| var score = d.audit_score || 0; | |
| var fraud = d.fraud_score || 0; | |
| var reserve = d.reserve_estimate || 0; | |
| var factors = d.decision_factors || []; | |
| var flags = d.fraud_flags || []; | |
| sub.textContent = 'Claim: ' + (d.claim_id||'') + ' Β· Audit Log: ' + (d.audit_log_id||'N/A') + ' Β· ' + new Date().toLocaleString(); | |
| var oc = {AUTO_SETTLE:'#00e5a0',ADJUSTER_REVIEW:'#f59e0b',SIU_REFERRAL:'#ef4444',DENY:'#888888'}[outcome]||'#fff'; | |
| var sc = score>=70?'#00e5a0':score>=40?'#f59e0b':'#ef4444'; | |
| var fc = fraud>=70?'#ef4444':fraud>=40?'#f59e0b':'#00e5a0'; | |
| var html = ''; | |
| html += '<div style="display:flex;align-items:center;gap:.8rem;margin-bottom:1rem;flex-wrap:wrap;">'; | |
| html += '<div style="font-size:9px;letter-spacing:.15em;color:rgba(255,255,255,.4);text-transform:uppercase;">Triage Decision</div>'; | |
| html += '<div style="font-size:11px;font-weight:700;letter-spacing:.12em;padding:.25rem .8rem;border:1px solid ' + oc + ';color:' + oc + ';font-family:Share Tech Mono,monospace;">' + outcome + '</div>'; | |
| html += '<div style="font-size:10px;color:rgba(255,255,255,.4);font-family:Share Tech Mono,monospace;">Priority: ' + (d.triage_priority||'β') + '</div>'; | |
| html += '</div>'; | |
| html += '<div class="audit-summary-box"><div class="audit-summary-label">Plain-English Triage Summary</div>' + (d.overall_summary||'No summary available.') + '</div>'; | |
| html += '<div class="audit-score-row"><div class="audit-score-label">Triage Confidence Score</div>'; | |
| html += '<div class="audit-score-track"><div class="audit-score-fill" id="claimScoreFill" style="width:0%;background:' + sc + '"></div></div>'; | |
| html += '<div class="audit-score-val" style="color:' + sc + '">' + score + '%</div></div>'; | |
| // Metrics grid | |
| html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px;margin:1rem 0;">'; | |
| [{l:'Fraud Score',v:fraud+'/100',c:fc},{l:'Severity',v:d.severity_band||'β',c:'#fff'}, | |
| {l:'Reserve Est.',v:'$'+Number(reserve).toLocaleString(),c:'#fb923c'}, | |
| {l:'Coverage',v:d.coverage_status||'β',c:d.coverage_status==='COVERED'?'#00e5a0':'#ef4444'}, | |
| {l:'Priority',v:d.triage_priority||'β',c:'#fff'}, | |
| {l:'Queue',v:(d.adjuster_queue||'β').replace(/_/g,' '),c:'#00d4ff'} | |
| ].forEach(function(m){ | |
| html += '<div style="background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);padding:8px 10px;">'; | |
| html += '<div style="font-family:Share Tech Mono,monospace;font-size:8px;letter-spacing:1px;color:rgba(255,255,255,.4);text-transform:uppercase;margin-bottom:3px;">' + m.l + '</div>'; | |
| html += '<div style="font-size:13px;font-weight:700;color:' + m.c + ';font-family:Share Tech Mono,monospace;">' + m.v + '</div></div>'; | |
| }); | |
| html += '</div>'; | |
| if (flags.length>0) { | |
| html += '<div class="audit-decline-box"><div class="audit-decline-title">β Fraud Signal Flags</div>'; | |
| flags.forEach(function(f,i){ html += '<div class="audit-decline-item">'+(i+1)+'. '+f+'</div>'; }); | |
| html += '</div>'; | |
| } | |
| var agentColors = { | |
| 'PASS':'#00e5a0','COVERED':'#00e5a0','FNOL_ACCEPTED':'#00e5a0','LOW':'#00e5a0','AUTO_SETTLE':'#00e5a0','MINOR':'#00e5a0', | |
| 'MEDIUM':'#f59e0b','PARTIAL':'#f59e0b','ADJUSTER_REVIEW':'#f59e0b','MODERATE':'#f59e0b','MAJOR':'#f59e0b', | |
| 'HIGH':'#ef4444','SIU_REFERRAL':'#ef4444','EXCLUDED':'#ef4444','FNOL_INVALID':'#ef4444','DENY':'#ef4444','CATASTROPHIC':'#ef4444', | |
| 'SCORED':'#00d4ff','CALCULATED':'#00d4ff', | |
| }; | |
| html += '<div class="audit-factors-title">Agent Decision Factors (' + factors.length + ' checks)</div>'; | |
| factors.forEach(function(f){ | |
| var fc2 = agentColors[f.outcome]||'#888'; | |
| html += '<div class="audit-factor-card"><div class="audit-factor-header">'; | |
| html += '<span class="audit-factor-agent">'+(f.agent||'')+'</span>'; | |
| html += '<span class="audit-factor-badge" style="border-color:'+fc2+';color:'+fc2+'">'+(f.outcome||'')+'</span></div>'; | |
| html += '<div class="audit-factor-label">'+(f.factor||'')+'</div>'; | |
| html += '<div class="audit-factor-detail">'+(f.detail||'')+'</div></div>'; | |
| }); | |
| html += '<div style="margin-top:1.2rem;padding-top:1rem;border-top:1px solid rgba(255,255,255,.08);">'; | |
| html += '<button onclick="exportClaimAuditReport()" style="background:transparent;border:1px solid rgba(251,146,60,.5);color:#fb923c;padding:7px 16px;font-family:Share Tech Mono,monospace;font-size:10px;letter-spacing:.1em;cursor:pointer;">β¬ EXPORT FULL TRIAGE REPORT (CSV)</button>'; | |
| html += '</div>'; | |
| body.innerHTML = html; | |
| overlay.style.display = 'flex'; | |
| document.addEventListener('keydown', _claimEscHandler); | |
| setTimeout(function(){ | |
| var fill = document.getElementById('claimScoreFill'); | |
| if (fill) fill.style.cssText += ';width:'+score+'%;transition:width 1s ease;'; | |
| }, 80); | |
| } | |
| function _claimEscHandler(e){ if(e.key==='Escape') closeClaimAuditModal(); } | |
| function closeClaimAuditModal(){ | |
| document.getElementById('claimAuditOverlay').style.display='none'; | |
| document.removeEventListener('keydown',_claimEscHandler); | |
| } | |
| function exportClaimAuditReport() { | |
| var d = _lastClaimAuditData; if (!d) return; | |
| var outcome = d.triage_outcome||''; | |
| var factors = d.decision_factors||[]; | |
| var flags = d.fraud_flags||[]; | |
| var sep = ['ββββββββββββββββββββββββββββββββββββββ']; | |
| var rows = []; | |
| rows.push(['CLAIMSENSE β AXIOM TRIAGE AUDIT REPORT']); | |
| rows.push(['Generated', new Date().toLocaleString()]); | |
| rows.push(['Claim ID', d.claim_id||'']); rows.push(['Policy Number', d.policy_number||'']); | |
| rows.push(['Triage Outcome', outcome]); rows.push(['Priority', d.triage_priority||'']); | |
| rows.push(['Audit Confidence', (d.audit_score||0)+'%']); rows.push([]); | |
| rows.push(sep); | |
| rows.push(['SECTION 1: FNOL INTAKE']); rows.push(sep); | |
| rows.push(['Claimant Name', d.claimant_name||'']); rows.push(['Incident Type', d.incident_type||'']); | |
| rows.push(['Incident Date', d.incident_date||'']); rows.push(['Days to Report', d.days_to_report||'']); | |
| rows.push(['FNOL Status', d.fnol_status||'']); rows.push(['Completeness', (d.completeness_score||'')+'%']); rows.push([]); | |
| rows.push(sep); | |
| rows.push(['SECTION 2: COVERAGE VERIFICATION']); rows.push(sep); | |
| rows.push(['Coverage Status', d.coverage_status||'']); rows.push(['Coverage Type', d.coverage_type||'']); | |
| rows.push(['Applicable Limit', d.applicable_limit?'$'+Number(d.applicable_limit).toLocaleString():'']); | |
| rows.push(['Deductible', d.deductible?'$'+Number(d.deductible).toLocaleString():'']); | |
| rows.push(['Net Payable Est.', d.net_payable_est?'$'+Number(d.net_payable_est).toLocaleString():'']); rows.push([]); | |
| rows.push(sep); | |
| rows.push(['SECTION 3: FRAUD SIGNAL DETECTION']); rows.push(sep); | |
| rows.push(['Fraud Score', (d.fraud_score||0)+'/100']); rows.push(['Fraud Band', d.fraud_band||'']); | |
| if (flags.length>0) rows.push(['Fraud Flags', flags.join(' | ')]); rows.push([]); | |
| rows.push(sep); | |
| rows.push(['SECTION 4: SEVERITY & RESERVE']); rows.push(sep); | |
| rows.push(['Severity Band', d.severity_band||'']); | |
| rows.push(['Reserve Estimate', d.reserve_estimate?'$'+Number(d.reserve_estimate).toLocaleString():'']); | |
| rows.push(['Reserve Note', d.reserve_note||'']); rows.push([]); | |
| rows.push(sep); | |
| rows.push(['SECTION 5: TRIAGE & ROUTING']); rows.push(sep); | |
| rows.push(['Triage Outcome', outcome]); rows.push(['Priority', d.triage_priority||'']); | |
| rows.push(['Adjuster Queue', (d.adjuster_queue||'').replace(/_/g,' ')]); | |
| rows.push(['Audit Log ID', d.audit_log_id||'N/A']); rows.push([]); | |
| rows.push(sep); | |
| rows.push(['SECTION 6: AGENT DECISION FACTORS']); rows.push(sep); | |
| rows.push(['Agent','Factor','Outcome','Detail']); | |
| factors.forEach(function(f){ rows.push([f.agent||'',f.factor||'',f.outcome||'',f.detail||'']); }); | |
| rows.push([]); rows.push(sep); | |
| rows.push(['SECTION 7: PLAIN-ENGLISH SUMMARY']); rows.push(sep); | |
| rows.push([d.overall_summary||'']); | |
| function esc(v){ var s=String(v==null?'':v).replace(/"/g,'""'); return(s.indexOf(',')>=0||s.indexOf('"')>=0||s.indexOf('\n')>=0)?'"'+s+'"':s; } | |
| var csv = rows.map(function(r){ return Array.isArray(r)?r.map(esc).join(','):esc(r); }).join('\n'); | |
| var blob = new Blob(['\uFEFF'+csv],{type:'text/csv;charset=utf-8;'}); | |
| var url = URL.createObjectURL(blob); | |
| var a = document.createElement('a'); | |
| a.href=url; a.download='ClaimSense_Audit_'+(d.claim_id||'')+'_'+new Date().toISOString().slice(0,10)+'.csv'; | |
| document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); | |
| } | |
| async function loadRecentClaims() { | |
| try { | |
| var _ac = new AbortController(); var _t = setTimeout(function(){_ac.abort()},8000); | |
| var r = await fetch(API_BASE + '/claims/recent?limit=15', {signal:_ac.signal}); | |
| clearTimeout(_t); | |
| var data = await r.json(); | |
| var claims = data.claims||[]; | |
| document.getElementById('claimCount').textContent = claims.length + ' claims'; | |
| var qEl = document.getElementById('claimQueue'); | |
| if (claims.length===0) { qEl.innerHTML='<div style="color:var(--dim2);font-size:11px;font-family:Share Tech Mono,monospace">No claims yet. Submit a FNOL above.</div>'; return; } | |
| qEl.innerHTML = claims.map(function(c){ | |
| var cc = (c.incident_type||'other').toLowerCase(); | |
| var oc = c.triage_outcome ? '<span class="claim-chip '+(c.triage_outcome.toLowerCase())+'">'+(c.triage_outcome.replace(/_/g,' '))+'</span>' : ''; | |
| var ab = c.triage_outcome | |
| ? '<button onclick="event.stopPropagation();loadAndOpenClaimAudit(\'' + c.claim_id + '\')" style="margin-top:6px;width:100%;background:transparent;border:1px solid rgba(0,229,160,.25);color:#00e5a0;padding:4px;font-family:Share Tech Mono,monospace;font-size:8px;letter-spacing:.08em;cursor:pointer;">π VIEW AUDIT TRAIL</button>' | |
| : '<div style="margin-top:6px;font-size:9px;color:var(--dim2);font-family:Share Tech Mono,monospace;text-align:center">Pipeline not yet run</div>'; | |
| return '<div class="claim-card" onclick="loadClaimDetail(\'' + c.claim_id + '\')">' | |
| + '<div class="claim-card-id">'+c.claim_id+'</div>' | |
| + '<div class="claim-card-name">'+(c.claimant_name||'β')+'</div>' | |
| + '<div class="claim-card-meta">'+(c.incident_type||'')+' Β· '+(c.incident_date||'')+'</div>' | |
| + '<div class="claim-card-chips"><span class="claim-chip '+cc+'">'+(c.incident_type||'OTHER')+'</span>' | |
| + (c.fraud_score!=null?'<span class="claim-chip">Fraud: '+c.fraud_score+'</span>':'') | |
| + (c.severity_band?'<span class="claim-chip">'+c.severity_band+'</span>':'') | |
| + oc + '</div>' + ab + '</div>'; | |
| }).join(''); | |
| } catch(err) { | |
| console.warn('[CLAIMS] loadRecentClaims:', err.message); | |
| document.getElementById('claimQueue').innerHTML='<div style="color:#fca5a5;font-size:11px;font-family:Share Tech Mono,monospace">API offline β submit a claim to test.</div>'; | |
| } | |
| } | |
| async function loadClaimDetail(claimId) { | |
| document.querySelectorAll('.claim-card').forEach(function(c){c.classList.remove('selected')}); | |
| if (event && event.currentTarget) event.currentTarget.classList.add('selected'); | |
| _setFnolStatus('Selected: ' + claimId, 'var(--dim2)'); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // RISKRADAR JS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| var _rrMap = null; | |
| var _rrHeatLayer = null; | |
| var _rrMarker = null; | |
| var _rrData = null; | |
| var _rrPeril = 'wind'; | |
| var _rrRunning = false; | |
| function _initMap() { | |
| if (_rrMap) return; | |
| _rrMap = L.map('radarMap', {zoomControl:true, attributionControl:true}) | |
| .setView([37.8, -96], 4); | |
| L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { | |
| attribution:'© OpenStreetMap © CARTO', | |
| maxZoom:18 | |
| }).addTo(_rrMap); | |
| } | |
| function _bandColor(score) { | |
| if (score >= 75) return '#ef4444'; | |
| if (score >= 55) return '#f97316'; | |
| if (score >= 35) return '#fbbf24'; | |
| return '#34d399'; | |
| } | |
| function _renderHeatmap(points, peril) { | |
| if (!_rrMap) return; | |
| if (_rrHeatLayer) { _rrMap.removeLayer(_rrHeatLayer); _rrHeatLayer = null; } | |
| if (_rrMarker) { _rrMap.removeLayer(_rrMarker); _rrMarker = null; } | |
| var subject = points.find(function(p){ return p.is_subject; }); | |
| var others = points.filter(function(p){ return !p.is_subject; }); | |
| // Draw surrounding points as circles | |
| var layerGroup = L.layerGroup(); | |
| others.forEach(function(pt) { | |
| var score = pt[peril] !== undefined ? pt[peril] : pt.overall; | |
| var color = _bandColor(score); | |
| var radius = 30 + score * 0.6; | |
| L.circle([pt.lat, pt.lng], { | |
| radius: radius * 18, | |
| color: color, | |
| fillColor: color, | |
| fillOpacity: 0.28, | |
| weight: 0.5, | |
| opacity: 0.6, | |
| }).addTo(layerGroup); | |
| }); | |
| layerGroup.addTo(_rrMap); | |
| _rrHeatLayer = layerGroup; | |
| // Subject property marker | |
| if (subject) { | |
| var score = subject[peril] !== undefined ? subject[peril] : subject.overall; | |
| var color = _bandColor(score); | |
| var icon = L.divIcon({ | |
| className: '', | |
| html: '<div style="width:20px;height:20px;border-radius:50%;background:' + color + ';border:2px solid #fff;box-shadow:0 0 8px ' + color + '"></div>', | |
| iconSize: [20,20], iconAnchor:[10,10] | |
| }); | |
| _rrMarker = L.marker([subject.lat, subject.lng], {icon:icon}).addTo(_rrMap); | |
| var band = score>=75?'CRITICAL':score>=55?'HIGH':score>=35?'MEDIUM':'LOW'; | |
| _rrMarker.bindPopup( | |
| '<div style="font-family:Share Tech Mono,monospace;font-size:11px;color:#fff;background:#0d1117;padding:8px 12px;border:1px solid ' + color + '">' + | |
| '<div style="color:' + color + ';font-weight:700;letter-spacing:.1em;margin-bottom:4px">' + peril.toUpperCase() + ' RISK: ' + score + '/100</div>' + | |
| '<div style="color:rgba(255,255,255,.6)">Band: ' + band + '</div>' + | |
| '<div style="color:rgba(255,255,255,.6)">Subject Property</div></div>', | |
| {className:'rr-popup'} | |
| ).openPopup(); | |
| _rrMap.setView([subject.lat, subject.lng], 12); | |
| } | |
| } | |
| function setPerilLayer(peril, tabEl) { | |
| _rrPeril = peril; | |
| document.querySelectorAll('.peril-tab').forEach(function(t){ t.classList.remove('active'); }); | |
| if (tabEl) tabEl.classList.add('active'); | |
| if (_rrData) _renderHeatmap(_rrData.heatmap_points, peril); | |
| // Update card highlights | |
| ['wind','flood','fire','quake'].forEach(function(p){ | |
| var card = document.getElementById('pc_'+p); | |
| if (card) card.style.borderColor = (p===peril||peril==='overall') ? 'rgba(56,189,248,.4)' : 'rgba(255,255,255,.07)'; | |
| }); | |
| } | |
| function _setRRStatus(msg, color) { | |
| var el = document.getElementById('rrStatus'); | |
| if (el) { el.textContent=msg; el.style.color=color||'var(--dim2)'; } | |
| } | |
| function _bandClass(band) { | |
| return 'band-' + (band||'low').toLowerCase(); | |
| } | |
| function _perilBarColor(score) { | |
| if (score>=75) return '#ef4444'; | |
| if (score>=55) return '#f97316'; | |
| if (score>=35) return '#fbbf24'; | |
| return '#34d399'; | |
| } | |
| function _updateScoreCards(result) { | |
| var scores = result.scores; | |
| var bands = result.bands; | |
| ['wind','flood','fire','quake'].forEach(function(p){ | |
| var sc = scores[p]; var bd = (bands[p]||'LOW').toUpperCase(); | |
| document.getElementById('sc_'+p).textContent = sc; | |
| document.getElementById('sc_'+p).style.color = _perilBarColor(sc); | |
| var bdEl = document.getElementById('bd_'+p); | |
| bdEl.textContent = bd; | |
| bdEl.className = 'peril-card-band ' + _bandClass(bd); | |
| setTimeout(function(){ | |
| document.getElementById('bar_'+p).style.cssText = | |
| 'width:'+sc+'%;background:'+_perilBarColor(sc)+';transition:width 1.2s ease'; | |
| }, 100); | |
| }); | |
| var os = result.overall_score; | |
| var ob = (result.overall_band||'LOW').toUpperCase(); | |
| document.getElementById('rrOverallScore').textContent = os; | |
| var obEl = document.getElementById('rrOverallBand'); | |
| obEl.textContent = ob; obEl.className = 'peril-card-band ' + _bandClass(ob); | |
| document.getElementById('rrOverallScore').style.color = _perilBarColor(os); | |
| document.getElementById('rrNarrative').textContent = result.narrative || ''; | |
| document.getElementById('rrMethod').textContent = 'Model: ' + (result._method||'rules'); | |
| document.getElementById('rrOverall').style.display = 'block'; | |
| document.getElementById('rrPerilCards').style.display = 'grid'; | |
| } | |
| async function runRiskRadar() { | |
| if (_rrRunning) return; | |
| var state = document.getElementById('rr_state').value; | |
| var zip = document.getElementById('rr_zip').value.trim(); | |
| if (!zip || zip.length < 5) { _setRRStatus('β Enter a 5-digit ZIP code.','#fca5a5'); return; } | |
| _rrRunning = true; | |
| document.getElementById('rrScoreBtn').disabled = true; | |
| _setRRStatus('Scoring peril risk...','#38bdf8'); | |
| if (!_rrMap) _initMap(); | |
| var payload = { | |
| address: document.getElementById('rr_address').value.trim(), | |
| state_code: state, | |
| zip: zip, | |
| year_built: parseInt(document.getElementById('rr_year').value)||2000, | |
| roof_year: parseInt(document.getElementById('rr_roof').value)||2015, | |
| construction_type: document.getElementById('rr_constr').value, | |
| num_stories: parseInt(document.getElementById('rr_stories').value)||2, | |
| }; | |
| try { | |
| var _ac = new AbortController(); var _t = setTimeout(function(){_ac.abort()},12000); | |
| var r = await fetch(API_BASE + '/riskradar/score', { | |
| method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify(payload), signal: _ac.signal | |
| }); | |
| clearTimeout(_t); | |
| if (!r.ok) throw new Error('Score failed: ' + r.status); | |
| var result = await r.json(); | |
| _rrData = result; | |
| _updateScoreCards(result); | |
| _renderHeatmap(result.heatmap_points, _rrPeril); | |
| _setRRStatus('β Risk scored β ' + (result.address || zip + ', ' + state), '#34d399'); | |
| loadRRHistory(); | |
| } catch(err) { | |
| _setRRStatus('Error: ' + err.message, '#fca5a5'); | |
| console.error('[RISKRADAR]', err); | |
| } finally { | |
| _rrRunning = false; | |
| document.getElementById('rrScoreBtn').disabled = false; | |
| } | |
| } | |
| async function loadRRHistory() { | |
| try { | |
| var _ac = new AbortController(); var _t = setTimeout(function(){_ac.abort()},6000); | |
| var r = await fetch(API_BASE + '/riskradar/history?limit=8', {signal:_ac.signal}); | |
| clearTimeout(_t); | |
| var data = await r.json(); | |
| var lookups = data.lookups||[]; | |
| var el = document.getElementById('rrHistory'); | |
| if (lookups.length===0) { el.innerHTML='<div style="font-size:10px;color:var(--dim2);font-family:Share Tech Mono,monospace">No lookups yet.</div>'; return; } | |
| el.innerHTML = lookups.map(function(l){ | |
| var band = l.overall_band||'LOW'; | |
| var color = {CRITICAL:'#ef4444',HIGH:'#f97316',MEDIUM:'#fbbf24',LOW:'#34d399'}[band]||'#888'; | |
| return '<div class="history-row" onclick="reuseHistoryLookup('+JSON.stringify(l)+')">' | |
| + '<div class="history-addr">' + (l.address||l.zip_code+', '+l.state_code) + '</div>' | |
| + '<span class="history-chip" style="color:'+color+';border-color:'+color+'">'+band+'</span>' | |
| + '<span style="font-family:Share Tech Mono,monospace;font-size:9px;color:var(--dim2)">'+(l.overall_score||0)+'</span>' | |
| + '</div>'; | |
| }).join(''); | |
| } catch(e) { /* silent */ } | |
| } | |
| function reuseHistoryLookup(l) { | |
| if (l.state_code) document.getElementById('rr_state').value = l.state_code; | |
| if (l.zip_code) document.getElementById('rr_zip').value = l.zip_code; | |
| if (l.address) document.getElementById('rr_address').value = l.address; | |
| runRiskRadar(); | |
| } | |
| function exportRiskReport() { | |
| var d = _rrData; if (!d) return; | |
| var scores = d.scores||{}; var bands = d.bands||{}; | |
| var rows = []; | |
| rows.push(['RISKRADAR β GEO RISK INTELLIGENCE REPORT']); | |
| rows.push(['Generated', new Date().toLocaleString()]); | |
| rows.push(['Address', d.address||'']); rows.push(['State', d.state_code]); | |
| rows.push(['ZIP', d.zip_code]); rows.push(['Lat/Lng', d.latitude+' / '+d.longitude]); | |
| rows.push([]); rows.push(['βββββββββββββββββββββββββββββββββ']); | |
| rows.push(['PERIL SCORES']); rows.push(['Peril','Score','Band']); | |
| rows.push(['Wind', scores.wind||0, (bands.wind||'').toUpperCase()]); | |
| rows.push(['Flood', scores.flood||0, (bands.flood||'').toUpperCase()]); | |
| rows.push(['Fire', scores.fire||0, (bands.fire||'').toUpperCase()]); | |
| rows.push(['Earthquake', scores.quake||0, (bands.quake||'').toUpperCase()]); | |
| rows.push([]); rows.push(['Overall Score', d.overall_score, d.overall_band]); | |
| rows.push([]); rows.push(['NARRATIVE']); rows.push([d.narrative||'']); | |
| function esc(v){var s=String(v==null?'':v).replace(/"/g,'""');return(s.indexOf(',')>=0||s.indexOf('"')>=0)?'"'+s+'"':s;} | |
| var csv = rows.map(function(r){return Array.isArray(r)?r.map(esc).join(','):esc(r);}).join('\n'); | |
| var blob = new Blob(['\uFEFF'+csv],{type:'text/csv;charset=utf-8;'}); | |
| var url = URL.createObjectURL(blob); | |
| var a = document.createElement('a'); | |
| a.href=url; a.download='RiskRadar_'+d.zip_code+'_'+d.state_code+'_'+new Date().toISOString().slice(0,10)+'.csv'; | |
| document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); | |
| } | |
| // ========================================================= | |
| // POLICYBRIDGE + MGA JS | |
| // ========================================================= | |
| var PB = { step:1, coverage:'HO-3', subId:'', json:null, mgaData:null }; | |
| var MGA = { step:1, email:'', company:'', selected:null, data:{}, _matches:[] }; | |
| var MGA_DB = [ | |
| {name:'Acme Managing General Agency',state:'TX',city:'Austin',license:'MGA-TX-48291',lob:'Personal Lines, Commercial'}, | |
| {name:'Acme MGA Holdings LLC',state:'CA',city:'Los Angeles',license:'MGA-CA-77032',lob:'Commercial Lines'}, | |
| {name:'Pacific Bridge MGA',state:'FL',city:'Miami',license:'MGA-FL-19283',lob:'Personal Lines'}, | |
| {name:'Meridian Underwriting Group',state:'NY',city:'New York',license:'MGA-NY-66104',lob:'Specialty'}, | |
| {name:'Summit Risk Partners',state:'CO',city:'Denver',license:'MGA-CO-33215',lob:'Personal Lines, Farm'}, | |
| ]; | |
| var _pbInitDone = false; | |
| function pbInit() { | |
| if (_pbInitDone) return; | |
| _pbInitDone = true; | |
| var states = ['AL','AK','AZ','AR','CA','CO','CT','DE','FL','GA','HI','ID','IL','IN','IA', | |
| 'KS','KY','LA','ME','MD','MA','MI','MN','MS','MO','MT','NE','NV','NH','NJ','NM','NY', | |
| 'NC','ND','OH','OK','OR','PA','RI','SC','SD','TN','TX','UT','VT','VA','WA','WV','WI','WY','DC']; | |
| ['pb_broker_state','pb_ins_state','pb_prop_state'].forEach(function(id){ | |
| var el = document.getElementById(id); | |
| if (!el) return; | |
| if (el.options.length > 1) return; | |
| states.forEach(function(s){ var o=document.createElement('option'); o.value=s; o.textContent=s; el.appendChild(o); }); | |
| }); | |
| PB.subId = 'SUB-'+new Date().getFullYear()+'-'+String(Math.floor(10000+Math.random()*89999)); | |
| var pbLiveSubId = document.getElementById('pb-live-subid'); | |
| if (pbLiveSubId) pbLiveSubId.textContent = PB.subId; | |
| var today = new Date().toISOString().split('T')[0]; | |
| var tmr = new Date(); tmr.setDate(tmr.getDate()+1); | |
| var pbDate = document.getElementById('pb_sub_date'); | |
| var pbEff = document.getElementById('pb_eff_date'); | |
| if (pbDate) pbDate.value = today; | |
| if (pbEff) pbEff.value = tmr.toISOString().split('T')[0]; | |
| setInterval(pbSyncPanel, 1000); | |
| } | |
| function pbGoStep(n) { | |
| document.querySelectorAll('.pb-step').forEach(function(s){ s.classList.remove('active'); }); | |
| var sec = document.getElementById('pb-sec-'+n); | |
| if (sec) sec.classList.add('active'); | |
| for (var i=1; i<=5; i++) { | |
| var nav=document.getElementById('pb-snav-'+i); | |
| var bub=document.getElementById('pb-sbb-'+i); | |
| var wrap=document.getElementById('pb-sbw-'+i); | |
| var line=document.getElementById('pb-sbl-'+i); | |
| if (!nav) continue; | |
| nav.classList.remove('active','done'); bub.classList.remove('active','done'); wrap.classList.remove('active','done'); | |
| if (i<n){ nav.classList.add('done'); bub.classList.add('done'); bub.textContent='v'; wrap.classList.add('done'); if(line)line.classList.add('done'); } | |
| else if (i===n){ nav.classList.add('active'); bub.classList.add('active'); bub.textContent=i; wrap.classList.add('active'); if(line)line.classList.remove('done'); } | |
| else { bub.textContent=i; if(line)line.classList.remove('done'); } | |
| } | |
| PB.step = n; | |
| if (n===5) pbBuildReview(); | |
| var main = document.querySelector('.pb-main'); | |
| if (main) main.scrollTop = 0; | |
| } | |
| function pbNextStep(n) { | |
| if (!pbValidateStep(n)) return; | |
| pbGoStep(n+1); | |
| } | |
| function pbValidateStep(n) { | |
| function req(id){ var el=document.getElementById(id); return !!(el && el.value && el.value.trim()); } | |
| var ok = true; | |
| if (n===1) ok = req('pb_broker_name') && req('pb_broker_code') && req('pb_broker_email') && req('pb_broker_state') && req('pb_lob'); | |
| if (n===2) ok = req('pb_ins_first') && req('pb_ins_last') && req('pb_ins_dob') && req('pb_ins_email') && req('pb_ins_street') && req('pb_ins_city') && req('pb_ins_state') && req('pb_ins_zip') && req('pb_credit'); | |
| if (n===3) ok = req('pb_prop_street') && req('pb_prop_city') && req('pb_prop_state') && req('pb_prop_zip') && req('pb_prop_type') && req('pb_year_built') && req('pb_sqft') && req('pb_construction') && req('pb_roof_type') && req('pb_roof_year'); | |
| if (n===4) ok = req('pb_cov_limit') && req('pb_deductible') && req('pb_eff_date'); | |
| if (!ok) alert('Please fill in all required fields before continuing.'); | |
| return ok; | |
| } | |
| function pbToggleChk(lbl, id) { | |
| var c = document.getElementById(id); c.checked=!c.checked; lbl.classList.toggle('on', c.checked); | |
| } | |
| function pbSelCov(card) { | |
| document.querySelectorAll('.pb-cov-card').forEach(function(c){ c.classList.remove('sel'); c.querySelector('input').checked=false; }); | |
| card.classList.add('sel'); card.querySelector('input').checked=true; PB.coverage=card.dataset.code; | |
| } | |
| function pbSyncPanel() { | |
| function g(id){ var el=document.getElementById(id); return el?el.value.trim():''; } | |
| var bn=g('pb_broker_name'); if(bn){ var e=document.getElementById('pb-lv-bname'); if(e) e.textContent=bn; } | |
| var bc=g('pb_broker_code'); if(bc){ var e=document.getElementById('pb-lv-bcode'); if(e) e.textContent=bc; } | |
| var bs=g('pb_broker_state'); if(bs){ var e=document.getElementById('pb-lv-bstate'); if(e) e.textContent=bs; } | |
| var fn=g('pb_ins_first'), ln=g('pb_ins_last'); | |
| if(fn||ln){ var e=document.getElementById('pb-lv-iname'); if(e) e.textContent=[fn,ln].filter(Boolean).join(' '); } | |
| var dob=g('pb_ins_dob'); if(dob){ var e=document.getElementById('pb-lv-idob'); if(e) e.textContent=dob; } | |
| var cr=g('pb_credit'); if(cr){ var e=document.getElementById('pb-lv-icredit'); if(e) e.textContent=cr; } | |
| var pc=g('pb_prop_city'), ps=g('pb_prop_state'); | |
| if(pc||ps){ var e=document.getElementById('pb-lv-ploc'); if(e) e.textContent=[pc,ps].filter(Boolean).join(', '); } | |
| var pt=g('pb_prop_type'); if(pt){ var e=document.getElementById('pb-lv-ptype'); if(e) e.textContent=pt; } | |
| var yb=g('pb_year_built'); if(yb){ var e=document.getElementById('pb-lv-pbuilt'); if(e) e.textContent=yb; } | |
| var ctEl=document.getElementById('pb-lv-ctype'); if(ctEl) ctEl.textContent=PB.coverage; | |
| var cl=g('pb_cov_limit'); if(cl){ var e=document.getElementById('pb-lv-climit'); if(e) e.textContent='$'+parseInt(cl).toLocaleString(); } | |
| var dd=g('pb_deductible'); if(dd){ var e=document.getElementById('pb-lv-cded'); if(e) e.textContent='$'+parseInt(dd).toLocaleString(); } | |
| } | |
| function pbBuildReview() { | |
| function g(id){ var el=document.getElementById(id); return el?el.value.trim():''; } | |
| var rows=[ | |
| ['Submission ID',PB.subId],['Broker',g('pb_broker_name')],['Broker Code',g('pb_broker_code')], | |
| ['State',g('pb_broker_state')],['LOB',g('pb_lob')], | |
| ['Insured',g('pb_ins_first')+' '+g('pb_ins_last')],['DOB',g('pb_ins_dob')],['Credit',g('pb_credit')], | |
| ['Property',g('pb_prop_street')+', '+g('pb_prop_city')+', '+g('pb_prop_state')+' '+g('pb_prop_zip')], | |
| ['Type',g('pb_prop_type')],['Built',g('pb_year_built')],['Construction',g('pb_construction')], | |
| ['Roof',g('pb_roof_type')+' ('+g('pb_roof_year')+')'], | |
| ['Coverage',PB.coverage],['Limit','$'+parseInt(g('pb_cov_limit')||0).toLocaleString()], | |
| ['Deductible','$'+parseInt(g('pb_deductible')||0).toLocaleString()],['Effective',g('pb_eff_date')], | |
| ]; | |
| var html='<div class="pb-card"><div class="pb-card-title">Submission Summary</div><div style="display:grid;grid-template-columns:1fr 1fr;gap:4px 16px;">'; | |
| rows.forEach(function(r){ | |
| html+='<div style="font-size:10px;color:var(--dim);padding:4px 0;border-bottom:1px solid rgba(255,255,255,.04)">'+r[0]+'</div>'; | |
| html+='<div style="font-size:11px;color:var(--text);font-weight:500;padding:4px 0;border-bottom:1px solid rgba(255,255,255,.04);font-family:Share Tech Mono,monospace">'+r[1]+'</div>'; | |
| }); | |
| html+='</div></div>'; | |
| var rc=document.getElementById('pb-review-content'); if(rc) rc.innerHTML=html; | |
| } | |
| async function pbDoSubmit() { | |
| function g(id){ var el=document.getElementById(id); return el?el.value.trim():''; } | |
| var payload={ | |
| submission_id:PB.subId, | |
| broker:{broker_code:g('pb_broker_code'),broker_name:g('pb_broker_name'),contact_email:g('pb_broker_email'),state_code:g('pb_broker_state')}, | |
| insured:{full_name:g('pb_ins_first')+' '+g('pb_ins_last'),dob:g('pb_ins_dob'),email:g('pb_ins_email'),phone:g('pb_ins_phone'), | |
| credit_score:parseInt(g('pb_credit'))||650,kyc_status:'CLEAR',ofac_result:'CLEAR',fraud_signals:0, | |
| street:g('pb_ins_street'),city:g('pb_ins_city'),state:g('pb_ins_state'),zip:g('pb_ins_zip')}, | |
| property:{street:g('pb_prop_street'),city:g('pb_prop_city'),state_code:g('pb_prop_state'),zip:g('pb_prop_zip'), | |
| year_built:parseInt(g('pb_year_built'))||2000,square_footage:parseInt(g('pb_sqft'))||2000, | |
| construction_type:g('pb_construction'),roof_type:g('pb_roof_type'),roof_year:parseInt(g('pb_roof_year'))||2015, | |
| num_stories:parseInt(g('pb_stories'))||2,property_type:g('pb_prop_type'),occupancy:g('pb_occupancy'),num_units:parseInt(g('pb_units'))||1}, | |
| policy_request:{coverage_type:PB.coverage,requested_coverage_limit:parseInt(g('pb_cov_limit'))||300000, | |
| requested_deductible:parseInt(g('pb_deductible'))||2500,effective_date:g('pb_eff_date'),notes:g('pb_notes')} | |
| }; | |
| if (PB.mgaData) payload.mga = PB.mgaData; | |
| try { | |
| var _ac=new AbortController(); var _t=setTimeout(function(){_ac.abort()},12000); | |
| var resp=await fetch(API_BASE+'/policybridge/submit',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload),signal:_ac.signal}); | |
| clearTimeout(_t); | |
| var result=await resp.json().catch(function(){return {};}); | |
| console.log('[PB] Submit result:', result); | |
| } catch(err) { console.warn('[PB] API submit failed:', err.message); } | |
| pbShowSuccess(payload); | |
| } | |
| function pbShowSuccess(payload) { | |
| pbGoStep(6); | |
| var rows=[['Submission ID',PB.subId],['Insured',payload.insured.full_name],['Coverage',payload.policy_request.coverage_type], | |
| ['Limit','$'+parseInt(payload.policy_request.requested_coverage_limit).toLocaleString()], | |
| ['Effective',payload.policy_request.effective_date],['Broker',payload.broker.broker_name],['Status','RECEIVED -> Pipeline queued']]; | |
| var html=''; | |
| rows.forEach(function(r){ | |
| html+='<div style="display:flex;justify-content:space-between;font-size:11px;padding:4px 0;border-bottom:1px solid rgba(255,255,255,.05)">'; | |
| html+='<span style="color:var(--dim)">'+r[0]+'</span><span style="color:var(--text);font-weight:500;font-family:Share Tech Mono,monospace;font-size:10px">'+r[1]+'</span></div>'; | |
| }); | |
| var sd=document.getElementById('pb-suc-detail'); if(sd) sd.innerHTML=html; | |
| } | |
| function pbReset() { | |
| PB.step=1; PB.coverage='HO-3'; _pbInitDone=false; | |
| PB.subId='SUB-'+new Date().getFullYear()+'-'+String(Math.floor(10000+Math.random()*89999)); | |
| var pbLiveSubId=document.getElementById('pb-live-subid'); if(pbLiveSubId) pbLiveSubId.textContent=PB.subId; | |
| document.querySelectorAll('.pb-input, .pb-select').forEach(function(el){ if(el.type!=='radio'&&el.type!=='checkbox') el.value=''; }); | |
| pbGoStep(1); | |
| } | |
| function openMgaOnboarding() { | |
| var overlay=document.getElementById('mgaOverlay'); if(overlay) overlay.style.display='flex'; | |
| mgaGoStep(1); | |
| var sel=document.getElementById('mga_state'); | |
| if (sel && sel.options.length<=1) { | |
| var states=['AL','AK','AZ','AR','CA','CO','CT','DE','FL','GA','HI','ID','IL','IN','IA','KS','KY','LA','ME','MD','MA','MI','MN','MS','MO','MT','NE','NV','NH','NJ','NM','NY','NC','ND','OH','OK','OR','PA','RI','SC','SD','TN','TX','UT','VT','VA','WA','WV','WI','WY','DC']; | |
| states.forEach(function(s){ var o=document.createElement('option'); o.value=s; o.textContent=s; sel.appendChild(o); }); | |
| } | |
| document.addEventListener('keydown', _mgaEscHandler); | |
| } | |
| function _mgaEscHandler(e){ if(e.key==='Escape') closeMgaOnboarding(); } | |
| function closeMgaOnboarding(){ var o=document.getElementById('mgaOverlay'); if(o) o.style.display='none'; document.removeEventListener('keydown',_mgaEscHandler); } | |
| function mgaGoStep(n) { | |
| MGA.step=n; | |
| document.querySelectorAll('.mga-step').forEach(function(s){ s.classList.remove('active'); }); | |
| var step=document.getElementById('mga-s'+n); if(step) step.classList.add('active'); | |
| for(var i=1;i<=4;i++){ var el=document.getElementById('mga-ps-'+i); if(!el) continue; el.classList.remove('active','done'); if(i<n) el.classList.add('done'); else if(i===n) el.classList.add('active'); } | |
| if (n===4) mgaBuildConfirm(); | |
| } | |
| async function mgaSearchCompany() { | |
| var email=document.getElementById('mga_email').value.trim(); | |
| var company=document.getElementById('mga_company').value.trim(); | |
| if (!email||!company){ alert('Please enter both email and company name.'); return; } | |
| MGA.email=email; MGA.company=company; | |
| mgaGoStep(2); | |
| var list=document.getElementById('mga-results-list'); | |
| list.innerHTML='<div style="color:var(--dim);font-size:11px;padding:8px"><span class="mga-spinner"></span>Searching...</div>'; | |
| await new Promise(function(res){ setTimeout(res,1200); }); | |
| var term=company.toLowerCase(); | |
| var matches=MGA_DB.filter(function(m){ return m.name.toLowerCase().includes(term.split(' ')[0]) || term.includes(m.name.toLowerCase().split(' ')[0]); }); | |
| if (matches.length===0) matches=MGA_DB.slice(0,2); | |
| if (matches.length<2) matches=matches.concat(MGA_DB.slice(0,2-matches.length)); | |
| MGA._matches=matches; | |
| list.innerHTML=matches.map(function(m,i){ | |
| return '<div class="mga-search-result" onclick="mgaSelectResult('+i+',this)"><div class="mga-result-name">'+m.name+'</div><div class="mga-result-meta">'+m.state+' - '+m.city+' - License: '+m.license+'</div></div>'; | |
| }).join(''); | |
| } | |
| function mgaSelectResult(idx,el){ document.querySelectorAll('.mga-search-result').forEach(function(r){r.classList.remove('sel');}); el.classList.add('sel'); MGA.selectedIdx=idx; MGA.selected=MGA._matches[idx]; } | |
| function mgaConfirmSelection() { | |
| if (MGA.selected===undefined){ alert('Please select a company.'); return; } | |
| var m=MGA.selected; | |
| document.getElementById('mga_full_name').value=m.name; | |
| document.getElementById('mga_license').value=m.license; | |
| document.getElementById('mga_state').value=m.state; | |
| document.getElementById('mga_city').value=m.city; | |
| document.getElementById('mga_lob').value=m.lob; | |
| mgaGoStep(3); | |
| var dots=document.getElementById('mga-searching-dots'); var i=0; | |
| var iv=setInterval(function(){ if(dots) dots.textContent=['...',' ..','.. ','...'][i++%4]; },400); | |
| setTimeout(function(){ clearInterval(iv); if(dots) dots.textContent=' - Verified'; },2000); | |
| } | |
| function mgaManualEntry(){ MGA.selected={name:MGA.company,state:'',city:'',license:'',lob:''}; mgaGoStep(3); } | |
| function mgaBuildConfirm() { | |
| function g(id){ var el=document.getElementById(id); return el?el.value.trim():''; } | |
| var name=g('mga_full_name'), license=g('mga_license')||'Not found', state=g('mga_state'), city=g('mga_city'), lob=g('mga_lob'); | |
| var code='MGA-'+(state||'XX')+'-'+Math.floor(10000+Math.random()*89999); | |
| var bd=document.getElementById('mga-code-display'); if(bd) bd.textContent=code; | |
| var rows=[['Company',name],['License',license],['State',state],['City',city],['LOB',lob],['Email',MGA.email]]; | |
| var html=rows.map(function(r){ return '<div class="mga-confirm-row"><span class="mga-ck">'+r[0]+'</span><span class="mga-cv">'+r[1]+'</span></div>'; }).join(''); | |
| var cd=document.getElementById('mga-confirm-details'); if(cd) cd.innerHTML=html; | |
| MGA.data={company_name:name,license:license,state:state,city:city,lob:lob,email:MGA.email,master_broker_code:code}; | |
| } | |
| function mgaComplete() { | |
| PB.mgaData=MGA.data; | |
| var s=document.getElementById('pb-mga-status'); if(s) s.textContent=MGA.data.company_name; | |
| var p=document.getElementById('pb-mga-panel'); if(p) p.style.display='block'; | |
| var nm=document.getElementById('pb-mga-name'); if(nm) nm.textContent=MGA.data.company_name; | |
| var cd=document.getElementById('pb-mga-code'); if(cd) cd.textContent=MGA.data.master_broker_code; | |
| var st=document.getElementById('pb-mga-state'); if(st) st.textContent=MGA.data.state; | |
| var li=document.getElementById('pb-mga-license'); if(li) li.textContent=MGA.data.license; | |
| var bnEl=document.getElementById('pb_broker_name'); if(bnEl&&!bnEl.value) bnEl.value=MGA.data.company_name; | |
| var bcEl=document.getElementById('pb_broker_code'); if(bcEl&&!bcEl.value) bcEl.value=MGA.data.master_broker_code; | |
| var bsEl=document.getElementById('pb_broker_state'); if(bsEl&&!bsEl.value) bsEl.value=MGA.data.state; | |
| var beEl=document.getElementById('pb_broker_email'); if(beEl&&!beEl.value) beEl.value=MGA.data.email; | |
| fetch(API_BASE+'/mga/onboard',{method:'POST',headers:{'Content-Type':'application/json'}, | |
| body:JSON.stringify({master_broker_code:MGA.data.master_broker_code,company_name:MGA.data.company_name, | |
| email:MGA.data.email,license_number:MGA.data.license,state_code:MGA.data.state,city:MGA.data.city, | |
| lines_of_business:MGA.data.lob,verified_by:'WEB_SEARCH'}) | |
| }).then(function(r){return r.json();}).then(function(d){console.log('[MGA] Saved:',d.master_broker_code);}).catch(function(e){console.warn('[MGA] Save failed:',e.message);}); | |
| closeMgaOnboarding(); | |
| var banner=document.createElement('div'); | |
| banner.style.cssText='position:fixed;top:64px;right:20px;background:#10b981;color:#fff;padding:10px 16px;border-radius:8px;font-size:12px;font-weight:500;z-index:9999;box-shadow:0 4px 20px rgba(16,185,129,.3)'; | |
| banner.textContent='MGA onboarded: '+MGA.data.master_broker_code; | |
| document.body.appendChild(banner); | |
| setTimeout(function(){if(banner.parentNode)document.body.removeChild(banner);},3000); | |
| } | |
| // ========================================================= | |
| // QUOTE COMPARISON JS | |
| // ========================================================= | |
| var _pbSelectedQuote = null; | |
| var _pbQuoteData = null; | |
| function pbGoToQuotes() { | |
| pbBuildReview(); | |
| pbGoStep(6); | |
| loadPBQuotes(); | |
| } | |
| async function loadPBQuotes() { | |
| var loading = document.getElementById('pb-quote-loading'); | |
| var content = document.getElementById('pb-quote-content'); | |
| var btnRow = document.getElementById('pb-quote-btn-row'); | |
| if (loading) loading.style.display = 'block'; | |
| if (content) content.innerHTML = ''; | |
| if (btnRow) btnRow.style.display = 'none'; | |
| function g(id){ var el=document.getElementById(id); return el?el.value.trim():''; } | |
| var payload = { | |
| submission_id: PB.subId, | |
| property: { | |
| state_code: g('pb_prop_state'), zip: g('pb_prop_zip'), | |
| year_built: parseInt(g('pb_year_built'))||2000, | |
| square_footage: parseInt(g('pb_sqft'))||2000, | |
| construction_type: g('pb_construction'), | |
| roof_type: g('pb_roof_type'), | |
| roof_year: parseInt(g('pb_roof_year'))||2015, | |
| }, | |
| insured: { credit_score: parseInt(g('pb_credit'))||680 }, | |
| policy_request: { | |
| coverage_type: PB.coverage, | |
| requested_coverage_limit: parseInt(g('pb_cov_limit'))||300000, | |
| requested_deductible: parseInt(g('pb_deductible'))||2500, | |
| } | |
| }; | |
| try { | |
| var _ac = new AbortController(); var _t = setTimeout(function(){_ac.abort()},15000); | |
| var r = await fetch(API_BASE + '/quote/compare', { | |
| method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify(payload), signal: _ac.signal | |
| }); | |
| clearTimeout(_t); | |
| var data = await r.json(); | |
| _pbQuoteData = data; | |
| if (loading) loading.style.display = 'none'; | |
| renderQuoteComparison(data); | |
| if (btnRow) btnRow.style.display = 'flex'; | |
| } catch(err) { | |
| if (loading) loading.style.display = 'none'; | |
| if (content) content.innerHTML = '<div style="color:#ef4444;font-size:12px;padding:16px">Could not load quotes: ' + err.message + '</div>'; | |
| console.error('[QUOTE]', err); | |
| } | |
| } | |
| function renderQuoteComparison(data) { | |
| var content = document.getElementById('pb-quote-content'); | |
| if (!content) return; | |
| var quotes = data.quotes || []; | |
| if (!quotes.length) { content.innerHTML='<div style="color:var(--dim2)">No quotes available.</div>'; return; } | |
| var marketAvg = data.market_avg || 0; | |
| var html = ''; | |
| // AI narrative | |
| html += '<div class="quote-narrative">'; | |
| html += '<div style="font-size:8px;letter-spacing:2px;color:#818cf8;text-transform:uppercase;margin-bottom:4px">AI Recommendation</div>'; | |
| html += data.recommendation || ''; | |
| html += '</div>'; | |
| // Market avg bar | |
| html += '<div class="quote-header-bar">'; | |
| html += '<div class="quote-title">📈 5 Competitive Quotes</div>'; | |
| html += '<div class="quote-market-avg">Market avg: <span style="color:var(--warn);font-weight:700">$' + marketAvg.toLocaleString() + '/yr</span></div>'; | |
| html += '</div>'; | |
| // Quote cards | |
| html += '<div class="quote-grid">'; | |
| quotes.forEach(function(q, i) { | |
| var isOurs = q.tier === 'our_quote'; | |
| var isBest = i === 0; | |
| var cardClass = 'quote-card' + (isOurs ? ' our-quote' : ''); | |
| var scoreColor = q.score >= 85 ? '#818cf8' : q.score >= 75 ? '#10b981' : q.score >= 65 ? '#f59e0b' : '#ef4444'; | |
| var savingClass = q.savings_vs_market >= 0 ? 'qs-positive' : 'qs-negative'; | |
| var savingText = q.savings_vs_market >= 0 | |
| ? 'Save $' + Math.abs(q.savings_vs_market).toLocaleString() + '/yr vs avg' | |
| : '$' + Math.abs(q.savings_vs_market).toLocaleString() + '/yr above avg'; | |
| html += '<div class="' + cardClass + '" id="qcard-' + i + '" onclick="selectQuote(' + i + ')">'; | |
| // Badge | |
| if (isOurs) html += '<div class="quote-badge qb-recommended">⭐ RECOMMENDED</div>'; | |
| else if (isBest) html += '<div class="quote-badge qb-cheapest">LOWEST PRICE</div>'; | |
| else if (i === 1) html += '<div class="quote-badge qb-established">ESTABLISHED</div>'; | |
| html += '<div class="quote-carrier" style="color:' + q.color + '">' + q.carrier + '</div>'; | |
| html += '<div class="quote-am-best">AM Best: ' + q.am_best + ' · ' + (q.market_info||'') + '</div>'; | |
| html += '<div class="quote-premium-big" style="color:' + q.color + '">$' + (q.annual||0).toLocaleString() + '</div>'; | |
| html += '<div class="quote-monthly">$' + (q.monthly||0).toLocaleString() + '/month</div>'; | |
| html += '<div class="quote-saving ' + savingClass + '">' + savingText + '</div>'; | |
| // Score bar | |
| html += '<div class="quote-score-bar"><div class="quote-score-fill" style="width:' + (q.score||0) + '%;background:' + scoreColor + '"></div></div>'; | |
| // Features | |
| html += '<div class="quote-features">'; | |
| (q.features||[]).slice(0,3).forEach(function(f){ html += '✓ ' + f + '<br>'; }); | |
| html += '</div>'; | |
| // Pros | |
| if ((q.pros||[]).length) { | |
| html += '<div style="font-size:9px;color:#10b981;margin-bottom:4px">'; | |
| q.pros.slice(0,2).forEach(function(p){ html += '+ ' + p + '<br>'; }); | |
| html += '</div>'; | |
| } | |
| // Cons | |
| if ((q.cons||[]).length) { | |
| html += '<div style="font-size:9px;color:#ef4444;margin-bottom:8px">'; | |
| q.cons.slice(0,1).forEach(function(c){ html += '- ' + c + '<br>'; }); | |
| html += '</div>'; | |
| } | |
| html += '<button class="quote-select-btn" id="qbtn-' + i + '">' + (isOurs ? '✅ Select This Quote' : 'Select Quote') + '</button>'; | |
| html += '</div>'; | |
| }); | |
| html += '</div>'; | |
| // Proceed bar (hidden until selection) | |
| html += '<div class="quote-proceed-bar" id="pb-proceed-bar" style="display:none">'; | |
| html += '<div class="qpb-info">Selected: <span class="qpb-carrier" id="qpb-carrier">--</span> · <span class="qpb-premium" id="qpb-premium">--</span></div>'; | |
| html += '<div style="font-size:10px;color:var(--dim2);font-family:Share Tech Mono,monospace" id="qpb-saving"></div>'; | |
| html += '</div>'; | |
| content.innerHTML = html; | |
| // Auto-select our quote | |
| var ourIdx = quotes.findIndex(function(q){ return q.tier === 'our_quote'; }); | |
| if (ourIdx >= 0) selectQuote(ourIdx); | |
| } | |
| function selectQuote(idx) { | |
| if (!_pbQuoteData) return; | |
| var quotes = _pbQuoteData.quotes || []; | |
| var q = quotes[idx]; | |
| if (!q) return; | |
| _pbSelectedQuote = q; | |
| // Update card styles | |
| document.querySelectorAll('.quote-card').forEach(function(c){ c.classList.remove('selected-quote'); }); | |
| document.querySelectorAll('.quote-select-btn').forEach(function(b){ b.textContent = 'Select Quote'; }); | |
| var card = document.getElementById('qcard-' + idx); | |
| var btn = document.getElementById('qbtn-' + idx); | |
| if (card) card.classList.add('selected-quote'); | |
| if (btn) btn.textContent = '✓ Selected'; | |
| // Update proceed bar | |
| var bar = document.getElementById('pb-proceed-bar'); | |
| if (bar) bar.style.display = 'flex'; | |
| var carrier = document.getElementById('qpb-carrier'); | |
| var premium = document.getElementById('qpb-premium'); | |
| var saving = document.getElementById('qpb-saving'); | |
| if (carrier) carrier.textContent = q.carrier; | |
| if (premium) premium.textContent = '$' + q.annual.toLocaleString() + '/yr ($' + q.monthly.toLocaleString() + '/mo)'; | |
| if (saving && q.savings_vs_market > 0) | |
| saving.textContent = 'Saving $' + q.savings_vs_market.toLocaleString() + '/yr vs market average'; | |
| else if (saving) | |
| saving.textContent = ''; | |
| } | |
| // Override pbDoSubmit to attach selected quote | |
| var _pbDoSubmitOrig = pbDoSubmit; | |
| pbDoSubmit = async function() { | |
| function g(id){ var el=document.getElementById(id); return el?el.value.trim():''; } | |
| var payload = { | |
| submission_id: PB.subId, | |
| broker:{ broker_code:g('pb_broker_code'), broker_name:g('pb_broker_name'), | |
| contact_email:g('pb_broker_email'), state_code:g('pb_broker_state') }, | |
| insured:{ full_name:g('pb_ins_first')+' '+g('pb_ins_last'), dob:g('pb_ins_dob'), | |
| email:g('pb_ins_email'), phone:g('pb_ins_phone'), credit_score:parseInt(g('pb_credit'))||650, | |
| kyc_status:'CLEAR', ofac_result:'CLEAR', fraud_signals:0, | |
| street:g('pb_ins_street'), city:g('pb_ins_city'), state:g('pb_ins_state'), zip:g('pb_ins_zip') }, | |
| property:{ street:g('pb_prop_street'), city:g('pb_prop_city'), state_code:g('pb_prop_state'), | |
| zip:g('pb_prop_zip'), year_built:parseInt(g('pb_year_built'))||2000, | |
| square_footage:parseInt(g('pb_sqft'))||2000, construction_type:g('pb_construction'), | |
| roof_type:g('pb_roof_type'), roof_year:parseInt(g('pb_roof_year'))||2015, | |
| num_stories:parseInt(g('pb_stories'))||2, property_type:g('pb_prop_type'), | |
| occupancy:g('pb_occupancy'), num_units:parseInt(g('pb_units'))||1 }, | |
| policy_request:{ coverage_type:PB.coverage, | |
| requested_coverage_limit:parseInt(g('pb_cov_limit'))||300000, | |
| requested_deductible:parseInt(g('pb_deductible'))||2500, | |
| effective_date:g('pb_eff_date'), notes:g('pb_notes') }, | |
| selected_quote: _pbSelectedQuote || null, | |
| quote_comparison: _pbQuoteData ? { market_avg: _pbQuoteData.market_avg, our_premium: _pbQuoteData.our_premium } : null, | |
| }; | |
| if (PB.mgaData) payload.mga = PB.mgaData; | |
| try { | |
| var _ac=new AbortController(); var _t=setTimeout(function(){_ac.abort()},12000); | |
| var resp=await fetch(API_BASE+'/policybridge/submit',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload),signal:_ac.signal}); | |
| clearTimeout(_t); | |
| var result=await resp.json().catch(function(){return{};}); | |
| console.log('[PB] Submit result:', result.message || result.error); | |
| } catch(err){ console.warn('[PB] API submit failed:',err.message); } | |
| pbShowSuccess(payload); | |
| }; | |
| // Update pbGoStep to handle step 6 in nav | |
| var _pbGoStepOrig = pbGoStep; | |
| pbGoStep = function(n) { | |
| document.querySelectorAll('.pb-step').forEach(function(s){ s.classList.remove('active'); }); | |
| var sec=document.getElementById('pb-sec-'+n); if(sec) sec.classList.add('active'); | |
| for(var i=1;i<=6;i++){ | |
| var nav=document.getElementById('pb-snav-'+i); var bub=document.getElementById('pb-sbb-'+i); | |
| var wrap=document.getElementById('pb-sbw-'+i); var line=document.getElementById('pb-sbl-'+i); | |
| if(!nav) continue; | |
| nav.classList.remove('active','done'); bub.classList.remove('active','done'); wrap.classList.remove('active','done'); | |
| if(i<n){ nav.classList.add('done'); bub.classList.add('done'); bub.textContent='v'; wrap.classList.add('done'); if(line) line.classList.add('done'); } | |
| else if(i===n){ nav.classList.add('active'); bub.classList.add('active'); bub.textContent=i; wrap.classList.add('active'); if(line) line.classList.remove('done'); } | |
| else{ bub.textContent=i; if(line) line.classList.remove('done'); } | |
| } | |
| PB.step=n; | |
| if(n===5) pbBuildReview(); | |
| var main=document.querySelector('.pb-main'); if(main) main.scrollTop=0; | |
| }; | |
| // Also update pbShowSuccess to go to step 7 | |
| function pbShowSuccess(payload) { | |
| pbGoStep(7); | |
| var q = _pbSelectedQuote; | |
| var rows=[ | |
| ['Submission ID',PB.subId], | |
| ['Insured',payload.insured.full_name], | |
| ['Coverage',payload.policy_request.coverage_type], | |
| ['Limit','$'+parseInt(payload.policy_request.requested_coverage_limit).toLocaleString()], | |
| ['Effective',payload.policy_request.effective_date], | |
| ['Broker',payload.broker.broker_name], | |
| ['Selected Quote', q ? q.carrier + ' @ $' + q.annual.toLocaleString() + '/yr' : 'PolicySense Direct'], | |
| ['Status','RECEIVED -> Pipeline queued'], | |
| ]; | |
| var html=''; | |
| rows.forEach(function(r){ | |
| html+='<div style="display:flex;justify-content:space-between;font-size:11px;padding:4px 0;border-bottom:1px solid rgba(255,255,255,.05)">'; | |
| html+='<span style="color:var(--dim)">'+r[0]+'</span><span style="color:var(--text);font-weight:500;font-family:Share Tech Mono,monospace;font-size:10px">'+r[1]+'</span></div>'; | |
| }); | |
| var sd=document.getElementById('pb-suc-detail'); if(sd) sd.innerHTML=html; | |
| } | |
| </script> | |
| <!-- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| FLASK API SERVER β save as api_server.py in your agents/ folder | |
| Run with: python api_server.py | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| from flask import Flask, jsonify, request | |
| from flask_cors import CORS | |
| import json, sys, os | |
| sys.path.insert(0, os.path.dirname(__file__)) | |
| from agent1_ssn_kyc import run_kyc_agent | |
| from agent2_property_risk import run_property_risk_agent | |
| from agent3_underwriting import run_underwriting_agent | |
| from agent4_pricing import run_pricing_agent | |
| from agent5_issuance_orchestrator import run_issuance_agent | |
| import mysql.connector | |
| from urllib.parse import quote_plus as qp | |
| from sqlalchemy import create_engine | |
| import pandas as pd | |
| app = Flask(__name__) | |
| CORS(app) | |
| DB = dict(host='localhost', port=3306, user='root', password='root@123', database='bronze') | |
| def get_conn(): | |
| pwd = qp(DB['password']) | |
| eng = create_engine(f"mysql+mysqlconnector://{DB['user']}:{pwd}@{DB['host']}:{DB['port']}/{DB['database']}") | |
| return eng.connect() | |
| # In-memory pipeline context per submission | |
| pipeline_ctx = {} | |
| @app.route('/status') | |
| def status(): | |
| try: | |
| conn = get_conn() | |
| df = pd.read_sql("SELECT COUNT(*) as c FROM bronze.submissions", conn) | |
| conn.close() | |
| return jsonify({'status':'ok', 'submissions_count': int(df.iloc[0]['c'])}) | |
| except Exception as e: | |
| return jsonify({'status':'error','error':str(e)}), 500 | |
| @app.route('/submissions/recent') | |
| def recent_submissions(): | |
| limit = request.args.get('limit', 10, type=int) | |
| try: | |
| conn = get_conn() | |
| df = pd.read_sql(f""" | |
| SELECT s.submission_id, s.coverage_type_code, s.requested_coverage_limit, | |
| s.requested_deductible, s.final_outcome, s.pipeline_status, | |
| s.halt_reason, s.submitted_at, s.raw_payload, | |
| b.broker_code | |
| FROM bronze.submissions s | |
| LEFT JOIN bronze.brokers b ON s.broker_id = b.broker_id | |
| ORDER BY s.submitted_at DESC LIMIT {limit} | |
| """, conn) | |
| conn.close() | |
| subs = [] | |
| for _, row in df.iterrows(): | |
| d = row.to_dict() | |
| d['raw_payload'] = json.loads(d['raw_payload']) if d['raw_payload'] else {} | |
| d['submitted_at'] = str(d['submitted_at']) | |
| subs.append(d) | |
| return jsonify({'submissions': subs}) | |
| except Exception as e: | |
| return jsonify({'error': str(e)}), 500 | |
| def _load_submission(sub_id): | |
| conn = get_conn() | |
| df = pd.read_sql(f"SELECT raw_payload FROM bronze.submissions WHERE submission_id='{sub_id}' LIMIT 1", conn) | |
| conn.close() | |
| if df.empty: return None | |
| return json.loads(df.iloc[0]['raw_payload']) | |
| @app.route('/agent/kyc', methods=['POST']) | |
| def agent_kyc(): | |
| sub_id = request.json.get('submission_id') | |
| sub = _load_submission(sub_id) | |
| if not sub: return jsonify({'error':'Not found'}), 404 | |
| result = run_kyc_agent(sub) | |
| pipeline_ctx[sub_id] = {'kyc': result, 'submission': sub} | |
| return jsonify(result) | |
| @app.route('/agent/property', methods=['POST']) | |
| def agent_property(): | |
| sub_id = request.json.get('submission_id') | |
| ctx = pipeline_ctx.get(sub_id, {}) | |
| sub = ctx.get('submission') or _load_submission(sub_id) | |
| kyc = ctx.get('kyc', {'status':'KYC_PASS'}) | |
| result = run_property_risk_agent(kyc, sub) | |
| pipeline_ctx.setdefault(sub_id, {}).update({'property': result, 'submission': sub}) | |
| return jsonify(result) | |
| @app.route('/agent/underwriting', methods=['POST']) | |
| def agent_underwriting(): | |
| sub_id = request.json.get('submission_id') | |
| ctx = pipeline_ctx.get(sub_id, {}) | |
| sub = ctx.get('submission') or _load_submission(sub_id) | |
| prop = ctx.get('property', {'status':'RISK_ACCEPTABLE'}) | |
| result = run_underwriting_agent(prop, sub) | |
| pipeline_ctx.setdefault(sub_id, {}).update({'underwriting': result}) | |
| return jsonify(result) | |
| @app.route('/agent/pricing', methods=['POST']) | |
| def agent_pricing(): | |
| sub_id = request.json.get('submission_id') | |
| ctx = pipeline_ctx.get(sub_id, {}) | |
| sub = ctx.get('submission') or _load_submission(sub_id) | |
| uw = ctx.get('underwriting', {'status':'UW_APPROVED'}) | |
| prop = ctx.get('property', {}) | |
| result = run_pricing_agent(uw, prop, sub) | |
| pipeline_ctx.setdefault(sub_id, {}).update({'pricing': result}) | |
| return jsonify(result) | |
| @app.route('/agent/issuance', methods=['POST']) | |
| def agent_issuance(): | |
| sub_id = request.json.get('submission_id') | |
| ctx = pipeline_ctx.get(sub_id, {}) | |
| sub = ctx.get('submission') or _load_submission(sub_id) | |
| result = run_issuance_agent( | |
| ctx.get('pricing', {'status':'PRICED', 'final_premium':0}), | |
| ctx.get('underwriting', {}), | |
| ctx.get('property', {}), | |
| sub | |
| ) | |
| pipeline_ctx.pop(sub_id, None) # clear context after issuance | |
| return jsonify(result) | |
| if __name__ == '__main__': | |
| app.run(host='0.0.0.0', port=5000, debug=True) | |
| --> | |
| <!-- ββ AXIOM AUDIT MODAL ββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="audit-modal-overlay" id="auditModalOverlay" onclick="if(event.target===this)closeAuditModal()"> | |
| <div class="audit-modal" id="auditModal"> | |
| <div class="audit-modal-header"> | |
| <div> | |
| <div class="audit-modal-title">π AXIOM β Audit Decision Trail</div> | |
| <div class="audit-modal-sub" id="auditModalSub">Agent 6 Β· gold_audit_log Β· Full Pipeline Analysis</div> | |
| </div> | |
| <button class="audit-modal-close" onclick="closeAuditModal()">β</button> | |
| </div> | |
| <div class="audit-modal-body" id="auditModalBody"> | |
| <div style="text-align:center;padding:2rem;color:rgba(255,255,255,.3);font-family:'Share Tech Mono',monospace;font-size:12px;">Loading audit data...</div> | |
| </div> | |
| </div> | |
| </div> | |
| </body> | |
| </html> | |