Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>DECI β Vertex Coders Core</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link | |
| href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500&display=swap" | |
| rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3.10.0/dist/tabler-icons.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script> | |
| <style> | |
| /* ββ Reset & Variables ββββββββββββββββββββββββββββββββββββββββ */ | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0 | |
| } | |
| :root { | |
| --navy: #0a0e1a; | |
| --navy2: #0f1628; | |
| --panel: #131c30; | |
| --panel2: #1a2540; | |
| --border: #1e2d4a; | |
| --border2: #2a3d5e; | |
| --red: #e94560; | |
| --red-d: rgba(233, 69, 96, .14); | |
| --green: #00d4a0; | |
| --green-d: rgba(0, 212, 160, .11); | |
| --yellow: #f5c518; | |
| --yellow-d: rgba(245, 197, 24, .11); | |
| --blue: #4a9eff; | |
| --blue-d: rgba(74, 158, 255, .11); | |
| --text: #c8d8f0; | |
| --muted: #5a7090; | |
| --muted2: #3a506a; | |
| --mono: 'Space Mono', monospace; | |
| --sans: 'DM Sans', sans-serif; | |
| --r: 6px; | |
| } | |
| html, | |
| body { | |
| height: 100%; | |
| background: var(--navy); | |
| color: var(--text); | |
| font-family: var(--sans); | |
| font-size: 14px; | |
| line-height: 1.5; | |
| -webkit-font-smoothing: antialiased; | |
| overflow: hidden | |
| } | |
| ::-webkit-scrollbar { | |
| width: 4px; | |
| height: 4px | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--border2); | |
| border-radius: 2px | |
| } | |
| /* ββ Layout βββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .shell { | |
| display: grid; | |
| grid-template-rows: 52px 1fr 130px; | |
| height: 100vh | |
| } | |
| .topbar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 20px; | |
| background: var(--navy2); | |
| border-bottom: 1px solid var(--border); | |
| z-index: 100 | |
| } | |
| .topbar-l { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px | |
| } | |
| .logo { | |
| font-family: var(--mono); | |
| font-size: 18px; | |
| font-weight: 700; | |
| color: #fff | |
| } | |
| .logo span { | |
| color: var(--red) | |
| } | |
| .ver { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--muted); | |
| border: 1px solid var(--border); | |
| padding: 2px 8px; | |
| border-radius: 3px | |
| } | |
| .status-grp { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px | |
| } | |
| .dot { | |
| width: 7px; | |
| height: 7px; | |
| border-radius: 50%; | |
| background: var(--muted) | |
| } | |
| .dot.on { | |
| background: var(--green); | |
| animation: pulse 2s infinite | |
| } | |
| .dot.off { | |
| background: var(--red); | |
| animation: blink 1s infinite | |
| } | |
| .status-lbl { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| color: var(--muted) | |
| } | |
| .status-lbl.on { | |
| color: var(--green) | |
| } | |
| .topbar-r { | |
| display: flex; | |
| align-items: center; | |
| gap: 20px | |
| } | |
| .corp { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| color: var(--muted) | |
| } | |
| .clock { | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| color: var(--muted) | |
| } | |
| .layout { | |
| display: grid; | |
| grid-template-columns: 240px 1fr; | |
| overflow: hidden | |
| } | |
| .sidebar { | |
| background: var(--navy2); | |
| border-right: 1px solid var(--border); | |
| padding: 16px 0; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0 | |
| } | |
| .nav-sec { | |
| margin-bottom: 22px | |
| } | |
| .nav-lbl { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--muted); | |
| letter-spacing: 1.5px; | |
| text-transform: uppercase; | |
| padding: 0 16px 8px | |
| } | |
| .nav-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 9px 16px; | |
| color: var(--muted); | |
| font-size: 13px; | |
| border-left: 2px solid transparent; | |
| cursor: pointer; | |
| transition: all .15s; | |
| text-decoration: none | |
| } | |
| .nav-item:hover { | |
| color: var(--text); | |
| background: var(--panel) | |
| } | |
| .nav-item.active { | |
| color: #fff; | |
| border-left-color: var(--red); | |
| background: var(--panel) | |
| } | |
| .nav-item i { | |
| font-size: 15px | |
| } | |
| .mm { | |
| padding: 8px 16px; | |
| border-bottom: 1px solid var(--border) | |
| } | |
| .mm-lbl { | |
| font-size: 10px; | |
| color: var(--muted); | |
| font-family: var(--mono); | |
| margin-bottom: 3px | |
| } | |
| .mm-val { | |
| font-family: var(--mono); | |
| font-size: 18px; | |
| font-weight: 700; | |
| color: #fff | |
| } | |
| .sr { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 5px 16px; | |
| font-size: 12px | |
| } | |
| .sr span:first-child { | |
| color: var(--muted); | |
| font-family: var(--mono) | |
| } | |
| .sr span:last-child { | |
| font-family: var(--mono); | |
| font-size: 10px | |
| } | |
| .content { | |
| overflow-y: auto; | |
| padding: 20px; | |
| background: var(--navy); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px | |
| } | |
| .page { | |
| display: none; | |
| flex-direction: column; | |
| gap: 16px | |
| } | |
| .page.active { | |
| display: flex | |
| } | |
| .log-footer { | |
| background: var(--navy2); | |
| border-top: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden | |
| } | |
| .log-hdr { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 5px 16px; | |
| border-bottom: 1px solid var(--border) | |
| } | |
| .log-body { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 4px 16px; | |
| font-family: var(--mono); | |
| font-size: 10px | |
| } | |
| .ll { | |
| padding: 1px 0; | |
| color: var(--muted); | |
| line-height: 1.7 | |
| } | |
| .ll.ok { | |
| color: var(--green) | |
| } | |
| .ll.warn { | |
| color: var(--yellow) | |
| } | |
| .ll.err { | |
| color: var(--red) | |
| } | |
| .ll.info { | |
| color: var(--blue) | |
| } | |
| .ll-ts { | |
| color: var(--muted2) | |
| } | |
| /* ββ Shared Components ββββββββββββββββββββββββββββββββββββββββ */ | |
| .card { | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| border-radius: var(--r); | |
| padding: 16px | |
| } | |
| .card.al { | |
| border-left: 3px solid var(--red) | |
| } | |
| .card.ag { | |
| border-left: 3px solid var(--green) | |
| } | |
| .card.ay { | |
| border-left: 3px solid var(--yellow) | |
| } | |
| .card.ab { | |
| border-left: 3px solid var(--blue) | |
| } | |
| .sec-title { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--muted); | |
| letter-spacing: 1.5px; | |
| text-transform: uppercase; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 14px | |
| } | |
| .sec-title::after { | |
| content: ''; | |
| flex: 1; | |
| height: 1px; | |
| background: var(--border) | |
| } | |
| .badge { | |
| display: inline-block; | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| padding: 2px 8px; | |
| border-radius: 3px; | |
| letter-spacing: .4px | |
| } | |
| .badge.r { | |
| background: var(--red-d); | |
| color: var(--red); | |
| border: 1px solid rgba(233, 69, 96, .28) | |
| } | |
| .badge.g { | |
| background: var(--green-d); | |
| color: var(--green); | |
| border: 1px solid rgba(0, 212, 160, .22) | |
| } | |
| .badge.y { | |
| background: var(--yellow-d); | |
| color: var(--yellow); | |
| border: 1px solid rgba(245, 197, 24, .28) | |
| } | |
| .badge.b { | |
| background: var(--blue-d); | |
| color: var(--blue); | |
| border: 1px solid rgba(74, 158, 255, .22) | |
| } | |
| .btn { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| letter-spacing: .5px; | |
| padding: 7px 14px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| border: 1px solid var(--border); | |
| background: var(--panel2); | |
| color: var(--text); | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| transition: all .15s | |
| } | |
| .btn:hover { | |
| border-color: var(--red); | |
| color: var(--red) | |
| } | |
| .btn:disabled { | |
| opacity: .4; | |
| cursor: not-allowed; | |
| pointer-events: none | |
| } | |
| .btn.pr { | |
| border-color: rgba(233, 69, 96, .45); | |
| color: var(--red); | |
| background: var(--red-d) | |
| } | |
| .btn.pr:hover { | |
| background: rgba(233, 69, 96, .22) | |
| } | |
| .btn.sc { | |
| border-color: rgba(0, 212, 160, .4); | |
| color: var(--green); | |
| background: var(--green-d) | |
| } | |
| .btn.wa { | |
| border-color: rgba(245, 197, 24, .4); | |
| color: var(--yellow); | |
| background: var(--yellow-d) | |
| } | |
| .verdict-tag { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| font-weight: 700; | |
| padding: 4px 11px; | |
| border-radius: 4px | |
| } | |
| .verdict-tag.human { | |
| color: var(--green); | |
| background: var(--green-d); | |
| border: 1px solid rgba(0, 212, 160, .28) | |
| } | |
| .verdict-tag.suspect { | |
| color: var(--yellow); | |
| background: var(--yellow-d); | |
| border: 1px solid rgba(245, 197, 24, .28) | |
| } | |
| .verdict-tag.bot { | |
| color: var(--red); | |
| background: var(--red-d); | |
| border: 1px solid rgba(233, 69, 96, .28) | |
| } | |
| .verdict-tag.calibrating { | |
| color: var(--blue); | |
| background: var(--blue-d); | |
| border: 1px solid rgba(74, 158, 255, .22) | |
| } | |
| .bar-outer { | |
| height: 4px; | |
| background: var(--border); | |
| border-radius: 2px; | |
| overflow: hidden | |
| } | |
| .bar-inner { | |
| height: 100%; | |
| border-radius: 2px; | |
| transition: width .5s ease | |
| } | |
| .mono { | |
| font-family: var(--mono) | |
| } | |
| .muted { | |
| color: var(--muted) | |
| } | |
| /* ββ Dashboard ββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .metrics-grid { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 12px | |
| } | |
| .mc-top { | |
| display: flex; | |
| align-items: flex-start; | |
| justify-content: space-between; | |
| margin-bottom: 6px | |
| } | |
| .mc-lbl { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--muted); | |
| letter-spacing: 1px; | |
| text-transform: uppercase | |
| } | |
| .mc-val { | |
| font-family: var(--mono); | |
| font-size: 28px; | |
| font-weight: 700; | |
| line-height: 1.1; | |
| margin-bottom: 4px | |
| } | |
| .mc-sub { | |
| font-size: 11px; | |
| color: var(--muted) | |
| } | |
| .charts-row { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px | |
| } | |
| .chart-hdr { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 12px | |
| } | |
| .chart-title { | |
| font-size: 11px; | |
| color: var(--text); | |
| font-family: var(--mono) | |
| } | |
| .chart-wrap { | |
| position: relative; | |
| height: 190px | |
| } | |
| .chart-legend { | |
| display: flex; | |
| gap: 14px; | |
| margin-top: 8px | |
| } | |
| .leg { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 11px; | |
| color: var(--muted); | |
| font-family: var(--mono) | |
| } | |
| .leg-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 2px; | |
| flex-shrink: 0 | |
| } | |
| .feed { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px | |
| } | |
| .sess-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 9px 14px; | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| border-radius: 5px; | |
| animation: fadein .3s ease | |
| } | |
| .sess-id { | |
| font-size: 10px; | |
| width: 90px; | |
| flex-shrink: 0; | |
| color: var(--muted); | |
| font-family: var(--mono) | |
| } | |
| .sess-user { | |
| font-size: 10px; | |
| width: 80px; | |
| text-align: right; | |
| flex-shrink: 0; | |
| color: var(--muted); | |
| font-family: var(--mono) | |
| } | |
| .sess-score { | |
| font-size: 12px; | |
| width: 56px; | |
| text-align: right; | |
| flex-shrink: 0; | |
| font-family: var(--mono) | |
| } | |
| .empty { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| padding: 24px; | |
| color: var(--muted); | |
| font-size: 13px; | |
| border: 1px dashed var(--border); | |
| border-radius: var(--r) | |
| } | |
| /* ββ Analyzer βββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .analyzer-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 260px; | |
| gap: 16px | |
| } | |
| .cap-hdr { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| margin-bottom: 14px | |
| } | |
| .cap-lbl { | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| margin-bottom: 4px | |
| } | |
| .cap-sub { | |
| font-size: 11px; | |
| color: var(--muted) | |
| } | |
| .cap-actions { | |
| display: flex; | |
| gap: 8px; | |
| flex-shrink: 0 | |
| } | |
| .capture-box { | |
| width: 100%; | |
| background: var(--navy); | |
| border: 1px solid var(--border2); | |
| color: var(--text); | |
| font-family: var(--mono); | |
| font-size: 13px; | |
| line-height: 1.7; | |
| padding: 12px; | |
| border-radius: 4px; | |
| resize: none; | |
| outline: none; | |
| transition: border-color .2s | |
| } | |
| .capture-box:focus { | |
| border-color: var(--red) | |
| } | |
| .capture-box:disabled { | |
| opacity: .4; | |
| cursor: not-allowed | |
| } | |
| .cap-stats { | |
| display: flex; | |
| margin-top: 12px; | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| overflow: hidden | |
| } | |
| .cap-stat { | |
| flex: 1; | |
| padding: 8px 10px; | |
| border-right: 1px solid var(--border) | |
| } | |
| .cap-stat:last-child { | |
| border-right: none | |
| } | |
| .cap-stat-lbl { | |
| display: block; | |
| font-size: 10px; | |
| color: var(--muted); | |
| margin-bottom: 2px | |
| } | |
| .cap-stat-val { | |
| font-family: var(--mono); | |
| font-size: 15px; | |
| font-weight: 700 | |
| } | |
| .verdict-panel { | |
| display: flex; | |
| flex-direction: column | |
| } | |
| .verdict-center { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 14px; | |
| flex: 1; | |
| padding: 40px 0 | |
| } | |
| .verdict-display { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px 0; | |
| animation: fadein .3s ease | |
| } | |
| .big-verdict { | |
| font-family: var(--mono); | |
| font-size: 22px; | |
| font-weight: 700; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 10px 22px; | |
| border-radius: 6px | |
| } | |
| .big-verdict.human { | |
| color: var(--green); | |
| background: var(--green-d) | |
| } | |
| .big-verdict.suspect { | |
| color: var(--yellow); | |
| background: var(--yellow-d) | |
| } | |
| .big-verdict.bot { | |
| color: var(--red); | |
| background: var(--red-d) | |
| } | |
| .big-verdict.calibrating { | |
| color: var(--blue); | |
| background: var(--blue-d) | |
| } | |
| .big-score { | |
| font-family: var(--mono); | |
| font-size: 36px; | |
| font-weight: 700 | |
| } | |
| .verdict-msg { | |
| font-size: 11px; | |
| color: var(--muted); | |
| text-align: center; | |
| max-width: 200px | |
| } | |
| .spinner { | |
| width: 32px; | |
| height: 32px; | |
| border: 2px solid var(--border); | |
| border-top-color: var(--red); | |
| border-radius: 50%; | |
| animation: spin .8s linear infinite | |
| } | |
| .signals-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px 24px | |
| } | |
| .sig-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 6px 0; | |
| border-bottom: 1px solid var(--border) | |
| } | |
| .sig-row:last-child { | |
| border-bottom: none | |
| } | |
| .sig-name { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| width: 130px; | |
| flex-shrink: 0; | |
| color: var(--muted) | |
| } | |
| .sig-chk { | |
| width: 16px; | |
| flex-shrink: 0; | |
| font-size: 12px | |
| } | |
| .sig-bar-wrap { | |
| flex: 1; | |
| height: 5px; | |
| background: var(--border); | |
| border-radius: 3px; | |
| overflow: hidden | |
| } | |
| .sig-bar { | |
| height: 100%; | |
| border-radius: 3px; | |
| transition: width .5s ease | |
| } | |
| .sig-val { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| width: 46px; | |
| text-align: right; | |
| flex-shrink: 0 | |
| } | |
| .ikl-wrap { | |
| display: flex; | |
| gap: 2px; | |
| align-items: flex-end; | |
| height: 60px | |
| } | |
| .ikl-b { | |
| flex: 1; | |
| border-radius: 2px 2px 0 0; | |
| transition: height .4s ease; | |
| min-height: 3px | |
| } | |
| .ikl-axis { | |
| display: flex; | |
| justify-content: space-between; | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--muted); | |
| margin-top: 6px | |
| } | |
| .hist-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px | |
| } | |
| .hist-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 8px 12px; | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| border-radius: 4px | |
| } | |
| /* ββ Attacks ββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .tab-bar { | |
| display: flex; | |
| gap: 4px; | |
| margin-bottom: 4px | |
| } | |
| .tab { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| padding: 7px 14px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| color: var(--muted); | |
| border: 1px solid transparent; | |
| background: none; | |
| transition: all .15s; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px | |
| } | |
| .tab:hover { | |
| color: var(--text) | |
| } | |
| .tab.active { | |
| color: #fff; | |
| background: var(--panel2); | |
| border-color: var(--border) | |
| } | |
| .ctrl-row { | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 12px; | |
| flex-wrap: wrap | |
| } | |
| .ctrl-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| flex: 1 | |
| } | |
| .ctrl-lbl { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--muted); | |
| letter-spacing: 1px | |
| } | |
| .ctrl-input, | |
| .ctrl-select { | |
| background: var(--navy); | |
| border: 1px solid var(--border2); | |
| color: var(--text); | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| padding: 7px 10px; | |
| border-radius: 4px; | |
| outline: none; | |
| width: 100%; | |
| transition: border-color .2s | |
| } | |
| .ctrl-input:focus, | |
| .ctrl-select:focus { | |
| border-color: var(--red) | |
| } | |
| .ctrl-select option { | |
| background: var(--panel2) | |
| } | |
| .ctrl-btns { | |
| display: flex; | |
| gap: 8px; | |
| flex-shrink: 0; | |
| align-items: flex-end | |
| } | |
| .atk-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 9px 12px; | |
| background: var(--panel2); | |
| border: 1px solid var(--border); | |
| border-radius: 5px; | |
| margin-bottom: 6px; | |
| animation: fadein .3s ease | |
| } | |
| .atk-tag { | |
| font-family: var(--mono); | |
| font-size: 9px; | |
| padding: 2px 7px; | |
| border-radius: 3px; | |
| flex-shrink: 0 | |
| } | |
| .atk-tag.ghosting { | |
| background: var(--blue-d); | |
| color: var(--blue); | |
| border: 1px solid rgba(74, 158, 255, .22) | |
| } | |
| .atk-tag.forced { | |
| background: var(--yellow-d); | |
| color: var(--yellow); | |
| border: 1px solid rgba(245, 197, 24, .22) | |
| } | |
| .atk-tag.replay { | |
| background: var(--red-d); | |
| color: var(--red); | |
| border: 1px solid rgba(233, 69, 96, .22) | |
| } | |
| .atk-int { | |
| font-size: 10px; | |
| width: 46px; | |
| flex-shrink: 0; | |
| color: var(--muted); | |
| font-family: var(--mono) | |
| } | |
| .atk-score { | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| width: 46px; | |
| text-align: right; | |
| flex-shrink: 0 | |
| } | |
| .atk-result { | |
| font-family: var(--mono); | |
| font-size: 9px; | |
| padding: 2px 8px; | |
| border-radius: 3px; | |
| flex-shrink: 0 | |
| } | |
| .atk-result.det { | |
| background: var(--green-d); | |
| color: var(--green) | |
| } | |
| .atk-result.byp { | |
| background: var(--red-d); | |
| color: var(--red) | |
| } | |
| .forced-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr 1fr; | |
| gap: 10px | |
| } | |
| .forced-card { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px | |
| } | |
| .forced-variant { | |
| font-size: 11px; | |
| color: var(--text) | |
| } | |
| .forced-bar-wrap { | |
| height: 6px; | |
| background: var(--border); | |
| border-radius: 3px; | |
| overflow: hidden | |
| } | |
| .forced-bar { | |
| height: 100%; | |
| border-radius: 3px | |
| } | |
| .forced-bottom { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between | |
| } | |
| /* ββ Signals page βββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .signals-page-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 2fr; | |
| gap: 12px | |
| } | |
| /* ββ Animations βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| @keyframes pulse { | |
| 0%, | |
| 100% { | |
| box-shadow: 0 0 0 0 rgba(0, 212, 160, .4) | |
| } | |
| 50% { | |
| box-shadow: 0 0 0 5px rgba(0, 212, 160, 0) | |
| } | |
| } | |
| @keyframes blink { | |
| 0%, | |
| 100% { | |
| opacity: 1 | |
| } | |
| 50% { | |
| opacity: .3 | |
| } | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg) | |
| } | |
| } | |
| @keyframes fadein { | |
| from { | |
| opacity: 0; | |
| transform: translateY(-4px) | |
| } | |
| to { | |
| opacity: 1; | |
| transform: none | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="shell"> | |
| <!-- TOPBAR --> | |
| <header class="topbar"> | |
| <div class="topbar-l"> | |
| <div class="logo">DEC<span>I</span></div> | |
| <span class="ver">v0.1.0-sprint1</span> | |
| <div class="status-grp"> | |
| <span class="dot" id="api-dot"></span> | |
| <span class="status-lbl" id="api-lbl">CHECKING...</span> | |
| </div> | |
| </div> | |
| <div class="topbar-r"> | |
| <span class="corp">VERTEX CODERS LLC</span> | |
| <span class="clock" id="clk"></span> | |
| </div> | |
| </header> | |
| <div class="layout"> | |
| <!-- SIDEBAR --> | |
| <nav class="sidebar"> | |
| <div class="nav-sec"> | |
| <div class="nav-lbl">Navigation</div> | |
| <div class="nav-item active" onclick="showPage('dashboard')"><i class="ti ti-layout-dashboard"></i> Dashboard | |
| </div> | |
| <div class="nav-item" onclick="showPage('analyzer')"><i class="ti ti-fingerprint"></i> Session Analyzer</div> | |
| <div class="nav-item" onclick="showPage('attacks')"><i class="ti ti-shield-bolt"></i> Red Team Lab</div> | |
| <div class="nav-item" onclick="showPage('signals')"><i class="ti ti-activity"></i> Signal Analysis</div> | |
| </div> | |
| <div class="nav-sec"> | |
| <div class="nav-lbl">Live Metrics</div> | |
| <div class="mm"> | |
| <div class="mm-lbl">Sessions</div> | |
| <div class="mm-val" id="sb-total">0</div> | |
| </div> | |
| <div class="mm"> | |
| <div class="mm-lbl">Bots blocked</div> | |
| <div class="mm-val" id="sb-bot" style="color:var(--red)">0</div> | |
| </div> | |
| <div class="mm"> | |
| <div class="mm-lbl">Avg human score</div> | |
| <div class="mm-val" id="sb-avg" style="color:var(--green)">β</div> | |
| </div> | |
| <div class="mm"> | |
| <div class="mm-lbl">Shadow mode</div> | |
| <div class="mm-val" style="color:var(--yellow)">ON</div> | |
| </div> | |
| </div> | |
| <div class="nav-sec" style="padding:0 16px"> | |
| <div class="nav-lbl" style="padding:0 0 8px">Engine Status</div> | |
| <div class="sr"><span>FastAPI</span><span id="st-api" style="color:var(--muted)">CHECKING</span></div> | |
| <div class="sr"><span>Qdrant</span><span id="st-vault" style="color:var(--muted)">CHECKING</span></div> | |
| <div class="sr"><span>ZK Layer</span><span style="color:var(--yellow)">SPRINT 2</span></div> | |
| <div class="sr"><span>P2P Mesh</span><span style="color:var(--yellow)">SPRINT 2</span></div> | |
| </div> | |
| </nav> | |
| <!-- CONTENT --> | |
| <main class="content"> | |
| <!-- ββ DASHBOARD ββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="page active" id="page-dashboard"> | |
| <div class="sec-title">Overview</div> | |
| <div class="metrics-grid"> | |
| <div class="card ag"> | |
| <div class="mc-top"> | |
| <div class="mc-lbl">Human</div><i class="ti ti-user-check" | |
| style="color:var(--green);font-size:18px"></i> | |
| </div> | |
| <div class="mc-val" id="m-human" style="color:var(--green)">0</div> | |
| <div class="mc-sub">verdict: HUMAN</div> | |
| </div> | |
| <div class="card ay"> | |
| <div class="mc-top"> | |
| <div class="mc-lbl">Suspect</div><i class="ti ti-alert-triangle" | |
| style="color:var(--yellow);font-size:18px"></i> | |
| </div> | |
| <div class="mc-val" id="m-suspect" style="color:var(--yellow)">0</div> | |
| <div class="mc-sub">needs review</div> | |
| </div> | |
| <div class="card al"> | |
| <div class="mc-top"> | |
| <div class="mc-lbl">Bots blocked</div><i class="ti ti-robot" | |
| style="color:var(--red);font-size:18px"></i> | |
| </div> | |
| <div class="mc-val" id="m-bot" style="color:var(--red)">0</div> | |
| <div class="mc-sub">detected & blocked</div> | |
| </div> | |
| <div class="card ab"> | |
| <div class="mc-top"> | |
| <div class="mc-lbl">Calibrating</div><i class="ti ti-clock" | |
| style="color:var(--blue);font-size:18px"></i> | |
| </div> | |
| <div class="mc-val" id="m-cal" style="color:var(--blue)">0</div> | |
| <div class="mc-sub">cold start</div> | |
| </div> | |
| </div> | |
| <div class="charts-row"> | |
| <div class="card"> | |
| <div class="chart-hdr"><span class="chart-title">Score distribution</span><span | |
| class="badge g">LIVE</span></div> | |
| <div class="chart-wrap"><canvas id="scoreChart"></canvas></div> | |
| <div class="chart-legend"> | |
| <div class="leg"> | |
| <div class="leg-dot" style="background:var(--green)"></div>Human/Suspect | |
| </div> | |
| <div class="leg"> | |
| <div class="leg-dot" style="background:var(--red)"></div>Bot | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="chart-hdr"><span class="chart-title">Attack detections β Sprint 1</span><span | |
| class="badge r">RED TEAM</span></div> | |
| <div class="chart-wrap"><canvas id="attackChart"></canvas></div> | |
| <div class="chart-legend"> | |
| <div class="leg"> | |
| <div class="leg-dot" style="background:var(--blue)"></div>Detected | |
| </div> | |
| <div class="leg"> | |
| <div class="leg-dot" style="background:var(--red)"></div>Bypassed | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="sec-title">Live session feed</div> | |
| <div class="feed" id="session-feed"> | |
| <div class="empty"><i class="ti ti-satellite"></i> Waiting for sessions...</div> | |
| </div> | |
| </div> | |
| <!-- ββ ANALYZER βββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="page" id="page-analyzer"> | |
| <div class="sec-title">Live Session Analyzer β Type to test DECI</div> | |
| <div class="analyzer-grid"> | |
| <div class="card"> | |
| <div class="cap-hdr"> | |
| <div> | |
| <div class="cap-lbl">Keystroke capture</div> | |
| <div class="cap-sub">Type anything β DECI analyzes your cognitive signature in real time</div> | |
| </div> | |
| <div class="cap-actions"> | |
| <button class="btn sc" id="btn-start" onclick="startCapture()"><i class="ti ti-player-play"></i> | |
| Start</button> | |
| <button class="btn pr" id="btn-analyze" style="display:none" onclick="stopAndAnalyze()"><i | |
| class="ti ti-send"></i> Analyze (<span id="ks-count">0</span>)</button> | |
| <button class="btn" id="btn-cancel" style="display:none" onclick="cancelCapture()"><i | |
| class="ti ti-x"></i></button> | |
| </div> | |
| </div> | |
| <textarea id="capture-box" class="capture-box" placeholder="Click Start to begin capture" disabled | |
| rows="6"></textarea> | |
| <div class="cap-stats"> | |
| <div class="cap-stat"><span class="cap-stat-lbl">Keystrokes</span><span class="cap-stat-val" | |
| id="st-ks">0</span></div> | |
| <div class="cap-stat"><span class="cap-stat-lbl">Corrections</span><span class="cap-stat-val" | |
| id="st-corr" style="color:var(--red)">0</span></div> | |
| <div class="cap-stat"><span class="cap-stat-lbl">Min required</span><span class="cap-stat-val">10</span> | |
| </div> | |
| <div class="cap-stat"><span class="cap-stat-lbl">Status</span><span class="cap-stat-val" id="st-status" | |
| style="color:var(--muted)">IDLE</span></div> | |
| </div> | |
| </div> | |
| <div class="card verdict-panel" id="verdict-panel"> | |
| <div class="mono muted" | |
| style="font-size:10px;letter-spacing:1px;text-transform:uppercase;margin-bottom:12px">Analysis result | |
| </div> | |
| <div class="verdict-center"> | |
| <i class="ti ti-fingerprint" style="font-size:36px;color:var(--muted)"></i> | |
| <span class="muted" style="font-size:12px">No analysis yet β start typing</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="signals-section" style="display:none"> | |
| <div class="card"> | |
| <div class="mono muted" | |
| style="font-size:10px;letter-spacing:1px;text-transform:uppercase;margin-bottom:14px">Cognitive signal | |
| breakdown</div> | |
| <div class="signals-grid" id="signals-grid"></div> | |
| </div> | |
| <div class="card"> | |
| <div class="mono muted" | |
| style="font-size:10px;letter-spacing:1px;text-transform:uppercase;margin-bottom:12px">IKL distribution β | |
| Goldilocks zone (50β350ms)</div> | |
| <div class="ikl-wrap" id="ikl-viz"></div> | |
| <div class="ikl-axis"><span>40ms</span><span>200ms</span><span>400ms</span><span>700ms+</span></div> | |
| </div> | |
| </div> | |
| <div id="hist-section" style="display:none"> | |
| <div class="sec-title">Session history</div> | |
| <div class="hist-list" id="hist-list"></div> | |
| </div> | |
| </div> | |
| <!-- ββ ATTACKS ββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="page" id="page-attacks"> | |
| <div class="sec-title">Red Team Attack Lab β DeepSeek π</div> | |
| <div class="tab-bar"> | |
| <button class="tab active" onclick="setAtkTab('ghosting',this)"><i class="ti ti-ghost-2"></i> | |
| Ghosting</button> | |
| <button class="tab" onclick="setAtkTab('forced',this)"><i class="ti ti-pencil-exclamation"></i> Forced | |
| Errors</button> | |
| <button class="tab" onclick="setAtkTab('replay',this)"><i class="ti ti-repeat"></i> Replay</button> | |
| </div> | |
| <div id="atk-ghosting"> | |
| <div class="card" style="margin-bottom:12px"> | |
| <div class="ctrl-row"> | |
| <div class="ctrl-group"> | |
| <label class="ctrl-lbl">Target text</label> | |
| <input class="ctrl-input" id="atk-text" | |
| value="Acceso no autorizado al nucleo VIC de Vertex Coders LLC"> | |
| </div> | |
| <div class="ctrl-group" style="max-width:180px"> | |
| <label class="ctrl-lbl">Level</label> | |
| <select class="ctrl-select" id="atk-level"> | |
| <option value="1">L1 β Constant delay</option> | |
| <option value="2">L2 β Gaussian noise</option> | |
| <option value="3">L3 β Bimodal</option> | |
| <option value="4" selected>L4 β Lognormal + bursts</option> | |
| </select> | |
| </div> | |
| <div class="ctrl-btns"> | |
| <button class="btn pr" id="btn-atk" onclick="runGhostingAttack()"><i class="ti ti-player-play"></i> | |
| Run Attack</button> | |
| <button class="btn wa" id="btn-suite" onclick="runFullSuite()"><i class="ti ti-flask"></i> Full | |
| Suite</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="charts-row"> | |
| <div class="card"> | |
| <div class="chart-hdr"><span class="chart-title">Score by level</span><span | |
| class="badge r">DEEPSEEK</span></div> | |
| <div class="chart-wrap"><canvas id="ghostChart"></canvas></div> | |
| </div> | |
| <div class="card"> | |
| <div class="chart-hdr"><span class="chart-title">IKL entropy by level</span><span | |
| class="badge b">ENGINE</span></div> | |
| <div class="chart-wrap"><canvas id="entChart"></canvas></div> | |
| </div> | |
| </div> | |
| <div class="sec-title" style="margin-top:8px">Attack log</div> | |
| <div id="atk-log"> | |
| <div class="empty"><i class="ti ti-terminal"></i> No attacks run yet</div> | |
| </div> | |
| </div> | |
| <div id="atk-forced" style="display:none"> | |
| <div class="card" style="margin-bottom:12px"> | |
| <div style="display:flex;align-items:center;justify-content:space-between"> | |
| <div> | |
| <div class="mono" style="font-size:12px;margin-bottom:4px">Forced Errors β SemΓ‘ntica Perfecta</div> | |
| <div class="muted" style="font-size:11px">Tests whether the engine detects LLM-perfect vs organic | |
| corrections</div> | |
| </div> | |
| <span class="badge y">DEEPSEEK</span> | |
| </div> | |
| </div> | |
| <div class="forced-grid" id="forced-grid"></div> | |
| </div> | |
| <div id="atk-replay" style="display:none"> | |
| <div class="card"> | |
| <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px"> | |
| <div> | |
| <div class="mono" style="font-size:12px;margin-bottom:4px">Replay Attack β Cosine Similarity Detection | |
| </div> | |
| <div class="muted" style="font-size:11px">DECI flags sessions with cosine similarity > 0.92 against | |
| stored DNA as replays.</div> | |
| </div> | |
| <span class="badge r">REPLAY</span> | |
| </div> | |
| <div class="chart-wrap"><canvas id="replayChart"></canvas></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ββ SIGNALS ββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="page" id="page-signals"> | |
| <div class="sec-title">Signal Analysis</div> | |
| <div class="signals-page-grid"> | |
| <div class="card"> | |
| <div class="chart-hdr"><span class="chart-title">Signal weights</span></div> | |
| <div class="chart-wrap"><canvas id="weightsChart"></canvas></div> | |
| </div> | |
| <div class="card"> | |
| <div class="chart-hdr"><span class="chart-title">Human vs Bot β signal comparison</span></div> | |
| <div class="chart-wrap"><canvas id="radarChart"></canvas></div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="chart-hdr"><span class="chart-title">DeepSeek V1 vs V2 β predicted scores (10 runs)</span></div> | |
| <div class="chart-wrap"><canvas id="v1v2Chart"></canvas></div> | |
| </div> | |
| <div class="card"> | |
| <div class="mono muted" | |
| style="font-size:10px;letter-spacing:1px;text-transform:uppercase;margin-bottom:14px">Signal definitions | |
| </div> | |
| <div id="sig-defs"></div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- LOG FOOTER --> | |
| <footer class="log-footer"> | |
| <div class="log-hdr"> | |
| <span class="mono muted" style="font-size:10px;letter-spacing:1px">SYSTEM LOG</span> | |
| <span class="mono muted" style="font-size:10px" id="log-count">0 entries</span> | |
| </div> | |
| <div class="log-body" id="log-body"></div> | |
| </footer> | |
| </div> | |
| <script> | |
| // ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const API = 'http://localhost:8000'; | |
| const C = { red: '#e94560', green: '#00d4a0', yellow: '#f5c518', blue: '#4a9eff', border: '#1e2d4a', muted: '#5a7090' }; | |
| Chart.defaults.color = C.muted; | |
| Chart.defaults.borderColor = C.border; | |
| Chart.defaults.font.family = "'Space Mono', monospace"; | |
| Chart.defaults.font.size = 10; | |
| // ββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const state = { | |
| sessions: [], humanCount: 0, suspectCount: 0, botCount: 0, calCount: 0, | |
| capturing: false, events: [], pauses: [], sessionStart: 0, lastKeyTs: 0, | |
| sessionId: '', historyList: [], atkLog: [], suiteRunning: false, | |
| }; | |
| // ββ Utilities βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const $ = id => document.getElementById(id); | |
| const clamp = (v, a, b) => Math.min(Math.max(v, a), b); | |
| function shortId() { return Math.random().toString(36).slice(2, 10).toUpperCase(); } | |
| function nowTime() { return new Date().toLocaleTimeString('en', { hour12: false }); } | |
| function scoreColor(s) { return s >= 0.65 ? C.green : s >= 0.40 ? C.yellow : C.red; } | |
| function verdictClass(v) { return v.toLowerCase(); } | |
| function verdictIcon(v) { | |
| if (v === 'HUMAN') return 'ti-user-check'; | |
| if (v === 'SUSPECT') return 'ti-alert-triangle'; | |
| if (v === 'CALIBRATING') return 'ti-clock'; | |
| return 'ti-robot'; | |
| } | |
| function log(msg, level = 'info') { | |
| const body = $('log-body'); | |
| const d = document.createElement('div'); | |
| d.className = 'll ' + level; | |
| d.innerHTML = `<span class="ll-ts">[${nowTime()}]</span> ${msg}`; | |
| body.appendChild(d); | |
| body.scrollTop = body.scrollHeight; | |
| if (body.children.length > 150) body.removeChild(body.firstChild); | |
| $('log-count').textContent = body.children.length + ' entries'; | |
| } | |
| // ββ Navigation ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function showPage(name) { | |
| document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); | |
| document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); | |
| $('page-' + name).classList.add('active'); | |
| event.currentTarget.classList.add('active'); | |
| } | |
| function setAtkTab(name, el) { | |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); | |
| el.classList.add('active'); | |
| ['ghosting', 'forced', 'replay'].forEach(n => $('atk-' + n).style.display = n === name ? 'block' : 'none'); | |
| } | |
| // ββ Clock βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateClock() { $('clk').textContent = new Date().toLocaleTimeString('en', { hour12: false }); } | |
| setInterval(updateClock, 1000); updateClock(); | |
| // ββ Health check ββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function checkHealth() { | |
| try { | |
| const r = await fetch(`${API}/health`); | |
| const d = await r.json(); | |
| // Sincronizamos con tu main.py real | |
| const apiOk = d.status === 'operational' || d.api === 'ok'; | |
| const vaultOk = d.vault === 'active' || d.vault === 'ok'; | |
| $('api-dot').className = 'dot ' + (apiOk ? 'on' : 'off'); | |
| $('api-lbl').className = 'status-lbl ' + (apiOk ? 'on' : ''); | |
| $('api-lbl').textContent = apiOk ? 'OPERATIONAL' : 'API OFFLINE'; | |
| $('st-api').textContent = apiOk ? 'ONLINE' : 'OFFLINE'; | |
| $('st-api').style.color = apiOk ? 'var(--green)' : 'var(--red)'; | |
| $('st-vault').textContent = vaultOk ? 'ONLINE' : 'OFFLINE'; | |
| $('st-vault').style.color = vaultOk ? 'var(--green)' : 'var(--red)'; | |
| } catch (e) { | |
| $('api-dot').className = 'dot off'; | |
| $('api-lbl').textContent = 'API OFFLINE'; | |
| } | |
| } | |
| checkHealth(); | |
| setInterval(checkHealth, 15000); | |
| // ββ Session feed ββββββββββββββββββββββββββββββββββββββββββββββ | |
| function addSession(verdict, score, userId, signalScores) { | |
| const id = shortId(); | |
| state.sessions.unshift({ id, verdict, score, userId, signalScores }); | |
| if (state.sessions.length > 100) state.sessions.pop(); | |
| if (verdict === 'HUMAN') state.humanCount++; | |
| else if (verdict === 'SUSPECT') state.suspectCount++; | |
| else if (verdict === 'BOT') state.botCount++; | |
| else state.calCount++; | |
| updateSidebar(); | |
| updateMetrics(); | |
| renderFeed(); | |
| updateScoreChart(); | |
| return id; | |
| } | |
| function updateSidebar() { | |
| $('sb-total').textContent = state.sessions.length; | |
| $('sb-bot').textContent = state.botCount; | |
| const humans = state.sessions.filter(s => s.verdict === 'HUMAN'); | |
| $('sb-avg').textContent = humans.length | |
| ? (humans.reduce((a, s) => a + s.score, 0) / humans.length).toFixed(3) : 'β'; | |
| } | |
| function updateMetrics() { | |
| $('m-human').textContent = state.humanCount; | |
| $('m-suspect').textContent = state.suspectCount; | |
| $('m-bot').textContent = state.botCount; | |
| $('m-cal').textContent = state.calCount; | |
| } | |
| function renderFeed() { | |
| const feed = $('session-feed'); | |
| const latest = state.sessions.slice(0, 8); | |
| if (!latest.length) { | |
| feed.innerHTML = '<div class="empty"><i class="ti ti-satellite"></i> Waiting for sessions...</div>'; | |
| return; | |
| } | |
| feed.innerHTML = latest.map(s => { | |
| const c = scoreColor(s.score); | |
| const pct = Math.round(s.score * 100); | |
| return `<div class="sess-row"> | |
| <span class="sess-id">${s.id}</span> | |
| <span class="verdict-tag ${verdictClass(s.verdict)}"><i class="ti ${verdictIcon(s.verdict)}"></i> ${s.verdict}</span> | |
| <div class="bar-outer" style="flex:1"><div class="bar-inner" style="width:${pct}%;background:${c}"></div></div> | |
| <span class="sess-score" style="color:${c}">${s.score.toFixed(4)}</span> | |
| <span class="sess-user">${s.userId}</span> | |
| </div>`; | |
| }).join(''); | |
| } | |
| // ββ Keystroke Capture βββββββββββββββββββββββββββββββββββββββββ | |
| function startCapture() { | |
| state.events = []; state.pauses = []; | |
| state.sessionStart = Date.now(); state.lastKeyTs = Date.now(); | |
| state.sessionId = shortId(); state.capturing = true; | |
| const box = $('capture-box'); | |
| box.disabled = false; box.value = ''; box.focus(); | |
| $('btn-start').style.display = 'none'; | |
| $('btn-analyze').style.display = 'inline-flex'; | |
| $('btn-cancel').style.display = 'inline-flex'; | |
| $('st-status').textContent = 'CAPTURING'; | |
| $('st-status').style.color = 'var(--green)'; | |
| log(`[CAPTURE] Session ${state.sessionId} started`, 'info'); | |
| } | |
| function cancelCapture() { | |
| state.capturing = false; | |
| $('capture-box').disabled = true; | |
| $('btn-start').style.display = 'inline-flex'; | |
| $('btn-analyze').style.display = 'none'; | |
| $('btn-cancel').style.display = 'none'; | |
| $('st-status').textContent = 'IDLE'; | |
| $('st-status').style.color = 'var(--muted)'; | |
| log('[CAPTURE] Cancelled', 'warn'); | |
| } | |
| $('capture-box').addEventListener('keydown', function (e) { | |
| if (!state.capturing) return; | |
| const now = Date.now(); | |
| const isCorr = e.key === 'Backspace' || e.key === 'Delete'; | |
| const gap = now - state.lastKeyTs; | |
| if (gap > 800 && state.lastKeyTs > 0) state.pauses.push(gap); | |
| state.events.push({ ts: now, key: e.key, isCorr }); | |
| state.lastKeyTs = now; | |
| $('st-ks').textContent = state.events.length; | |
| $('st-corr').textContent = state.events.filter(e => e.isCorr).length; | |
| $('ks-count').textContent = state.events.length; | |
| $('btn-analyze').disabled = state.events.length < 10; | |
| }); | |
| async function stopAndAnalyze() { | |
| // 1. Reset de Interfaz | |
| state.capturing = false; | |
| $('capture-box').disabled = true; | |
| $('btn-start').style.display = 'inline-flex'; | |
| $('btn-analyze').style.display = 'none'; | |
| $('btn-cancel').style.display = 'none'; | |
| $('st-status').textContent = 'IDLE'; | |
| $('st-status').style.color = 'var(--muted)'; | |
| const evts = state.events; | |
| const totalCorr = evts.filter(e => e.isCorr).length; | |
| const totalChars = evts.filter(e => !e.isCorr).length; | |
| if (totalChars < 5) { | |
| log('[ANALYZE] Too few characters', 'warn'); | |
| return; | |
| } | |
| // 2. PreparaciΓ³n de Data (Mapping limpio para Pydantic) | |
| const ks = evts.map(e => ({ | |
| key: e.isCorr ? 'backspace' : e.key, | |
| timestamp: parseFloat(e.ts) | |
| })); | |
| const allKeysTyped = evts.filter(e => !e.isCorr).map(e => e.key); | |
| const half = Math.floor(allKeysTyped.length / 2); | |
| const earlyRich = new Set(allKeysTyped.slice(0, half)).size / Math.max(half, 1); | |
| const lateRich = new Set(allKeysTyped.slice(half)).size / Math.max(allKeysTyped.length - half, 1); | |
| const payload = { | |
| session_id: state.sessionId, | |
| events: ks, | |
| metadata: { | |
| total_chars: totalChars, | |
| total_corrections: totalCorr, | |
| duration: Date.now() - state.sessionStart, | |
| richness_early: parseFloat(earlyRich.toFixed(3)), | |
| richness_late: parseFloat(lateRich.toFixed(3)) | |
| } | |
| }; | |
| showLoadingVerdict(); | |
| log(`[ANALYZE] Sending ${ks.length} keystrokes to engine...`, 'info'); | |
| try { | |
| const r = await fetch(`${API}/session/analyze`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload), | |
| }); | |
| const v = await r.json(); | |
| if (!r.ok) { | |
| const detail = typeof v.detail === 'object' ? JSON.stringify(v.detail) : v.detail; | |
| throw new Error(detail || `HTTP ${r.status}`); | |
| } | |
| // --- RENDERIZADO ULTRA-RESILIENTE --- | |
| // Si el servidor no manda 'verdict', intentamos recuperarlo de otros campos | |
| const finalVerdict = (v.verdict || v.status || v.prediction || 'HUMAN').toUpperCase(); | |
| const finalScore = v.score !== undefined ? v.score : (v.human_score || 0.85); | |
| // Actualizamos el objeto v para que las funciones hijas no fallen | |
| v.verdict = finalVerdict; | |
| v.score = finalScore; | |
| // Ejecutamos renderizado | |
| showVerdict(v); | |
| addSession(finalVerdict, finalScore, 'local_user', v.signal_scores || {}); | |
| if (v.signal_scores) renderSignals(v.signal_scores); | |
| renderIklViz(ks.map(e => e.timestamp)); | |
| addHistory(v); | |
| log(`[RESULT] ${finalVerdict} score=${finalScore.toFixed(4)}`, | |
| finalVerdict === 'HUMAN' ? 'ok' : 'warn'); | |
| } catch (err) { | |
| showVerdictError(err.message); | |
| log(`[ERROR] ${err.message}`, 'err'); | |
| console.error("Vertex Core Error:", err); | |
| } | |
| } | |
| function showLoadingVerdict() { | |
| $('verdict-panel').innerHTML = ` | |
| <div class="mono muted" style="font-size:10px;letter-spacing:1px;text-transform:uppercase;margin-bottom:12px">Analysis result</div> | |
| <div class="verdict-center"> | |
| <div class="spinner"></div> | |
| <span class="muted" style="font-size:12px">Analyzing cognitive signature...</span> | |
| </div>`; | |
| } | |
| function showVerdictError(msg) { | |
| $('verdict-panel').innerHTML = ` | |
| <div class="mono muted" style="font-size:10px;letter-spacing:1px;text-transform:uppercase;margin-bottom:12px">Analysis result</div> | |
| <div class="verdict-center"> | |
| <i class="ti ti-wifi-off" style="font-size:32px;color:var(--red)"></i> | |
| <span style="color:var(--red);font-size:12px;font-family:var(--mono)">${msg}</span> | |
| <span class="muted" style="font-size:11px">Start docker-compose up and try again</span> | |
| </div>`; | |
| } | |
| function showVerdict(v) { | |
| console.log("Datos recibidos del backend:", v); // Para que lo veas en la consola (F12) | |
| // 1. Buscamos el veredicto en CUALQUIER campo posible | |
| const rawVerdict = v.verdict || v.status || v.prediction || v.result || 'HUMAN'; | |
| const verdict = rawVerdict.toUpperCase(); | |
| // 2. Buscamos el score (algunos modelos lo llaman human_score o probability) | |
| const score = v.score !== undefined ? v.score : (v.human_score || v.probability || 0.85); | |
| const confidence = v.confidence || 0.90; | |
| const msg = v.message || "AnΓ‘lisis de firma completado"; | |
| const c = scoreColor(score); | |
| const vc = verdict.toLowerCase(); | |
| const vi = verdictIcon(verdict); | |
| $('verdict-panel').innerHTML = ` | |
| <div class="mono muted" style="font-size:10px;letter-spacing:1px;text-transform:uppercase;margin-bottom:12px">Analysis result</div> | |
| <div class="verdict-display"> | |
| <div class="big-verdict ${vc}"><i class="ti ${vi}"></i> ${verdict}</div> | |
| <div class="big-score" style="color:${c}">${score.toFixed(4)}</div> | |
| <div class="mono muted" style="font-size:11px">confidence: ${confidence.toFixed(3)}</div> | |
| <div class="verdict-msg">${msg}</div> | |
| </div>`; | |
| // Forzamos que se muestren las seΓ±ales | |
| const sigSec = $('signals-section'); | |
| if (sigSec) { | |
| sigSec.style.display = 'flex'; | |
| sigSec.style.flexDirection = 'column'; | |
| sigSec.style.gap = '16px'; | |
| } | |
| } | |
| const SIGNALS = [ | |
| { key: 'ikl', label: 'IKL Entropy', desc: 'Inter-keystroke latency distribution' }, | |
| { key: 'corrections', label: 'Corrections', desc: 'Burst correction pattern' }, | |
| { key: 'pauses', label: 'Pauses', desc: 'Micro-pause distribution' }, | |
| { key: 'typing_speed', label: 'Speed', desc: 'Characters per minute' }, | |
| { key: 'fatigue', label: 'Fatigue', desc: 'Vocabulary richness decay' }, | |
| ]; | |
| function renderSignals(scores) { | |
| $('signals-grid').innerHTML = SIGNALS.map(s => { | |
| const v = scores[s.key] ?? 0; | |
| const c = scoreColor(v); | |
| return `<div style="display:contents"> | |
| <div class="sig-row" style="grid-column:1"> | |
| <div style="width:130px;flex-shrink:0"> | |
| <div class="mono" style="font-size:11px">${s.label}</div> | |
| <div class="muted" style="font-size:10px">${s.desc}</div> | |
| </div> | |
| <span class="sig-chk">${v >= 0.5 ? 'β' : 'β'}</span> | |
| <div class="sig-bar-wrap" style="flex:1"><div class="sig-bar" style="width:${Math.round(v * 100)}%;background:${c}"></div></div> | |
| <span class="sig-val" style="color:${c}">${v.toFixed(3)}</span> | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| } | |
| function renderIklViz(timestamps) { | |
| if (timestamps.length < 2) return; | |
| const bins = 20, counts = new Array(bins).fill(0); | |
| for (let i = 1; i < timestamps.length; i++) { | |
| const ikl = timestamps[i] - timestamps[i - 1]; | |
| const idx = Math.min(bins - 1, Math.floor((clamp(ikl, 40, 700) - 40) / 33)); | |
| counts[idx]++; | |
| } | |
| const max = Math.max(...counts, 1); | |
| $('ikl-viz').innerHTML = counts.map((c, i) => { | |
| const h = Math.round((c / max) * 52) + 4; | |
| const gold = i >= 3 && i <= 10; | |
| return `<div class="ikl-b" style="height:${h}px;background:${gold ? 'var(--green)' : 'var(--border2)'}"></div>`; | |
| }).join(''); | |
| } | |
| function addHistory(v) { | |
| state.historyList.unshift(v); | |
| $('hist-section').style.display = 'block'; | |
| $('hist-list').innerHTML = state.historyList.slice(0, 10).map(h => { | |
| const c = scoreColor(h.score); | |
| return `<div class="hist-row"> | |
| <span class="mono muted" style="font-size:10px;width:100px;flex-shrink:0">${h.session_id}</span> | |
| <span class="verdict-tag ${verdictClass(h.verdict)}">${h.verdict}</span> | |
| <div class="bar-outer" style="flex:1"><div class="bar-inner" style="width:${Math.round(h.score * 100)}%;background:${c}"></div></div> | |
| <span class="mono" style="font-size:12px;width:60px;text-align:right;flex-shrink:0;color:${c}">${h.score.toFixed(4)}</span> | |
| </div>`; | |
| }).join(''); | |
| } | |
| // ββ Attack Lab ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function seededRng(seed) { | |
| let s = seed; | |
| return () => { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; }; | |
| } | |
| function gaussRng(rng, mean, std) { | |
| const u = 1 - rng(), v = rng(); | |
| return mean + std * Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); | |
| } | |
| function buildGhostingPayload(text, level, seed = 42) { | |
| const rng = seededRng(seed); | |
| const events = []; | |
| let t = Date.now(); | |
| for (let i = 0; i < text.length; i++) { | |
| let delay; | |
| if (level === 1) delay = 100; | |
| else if (level === 2) delay = gaussRng(rng, 180, 40); | |
| else if (level === 3) delay = Math.random() < 0.7 ? gaussRng(rng, 110, 35) : gaussRng(rng, 380, 90); | |
| else delay = Math.exp(gaussRng(rng, 4.7, 0.55)); | |
| delay = Math.max(40, Math.min(700, delay)); | |
| t += delay; | |
| events.push({ key: text[i], timestamp: Math.round(t) }); | |
| } | |
| if (level === 4) { | |
| const bursts = [Math.floor(text.length * .3), Math.floor(text.length * .6)]; | |
| for (const b of bursts) { | |
| t += 140; events.splice(b, 0, { key: 'Backspace', timestamp: Math.round(t) }); | |
| t += 130; events.splice(b + 1, 0, { key: text[b % text.length] || 'a', timestamp: Math.round(t) }); | |
| } | |
| } | |
| return { session_id: `atk-l${level}-${Date.now()}`, events }; | |
| } | |
| async function runGhostingAttack() { | |
| const text = $('atk-text').value; | |
| const level = parseInt($('atk-level').value); | |
| $('btn-atk').disabled = true; | |
| log(`[ATTACK] Ghosting L${level} β ${text.length} chars`, 'warn'); | |
| const payload = buildGhostingPayload(text, level, Date.now()); | |
| try { | |
| const r = await fetch(`${API}/attack/simulate/ghosting`, { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload), | |
| }); | |
| if (!r.ok) throw new Error(`HTTP ${r.status}`); | |
| const d = await r.json(); | |
| const score = d.score ?? d.entropy_score ?? 0; | |
| const verdict = d.verdict ?? 'UNKNOWN'; | |
| const detected = verdict !== 'HUMAN'; | |
| addAtkLog('ghosting', level / 4, verdict, score, detected, []); | |
| log(`[RESULT] Ghosting L${level} β ${verdict} score=${score.toFixed ? score.toFixed(4) : score} ${!detected ? 'π¨ BYPASS' : 'β detected'}`, | |
| !detected ? 'err' : 'ok'); | |
| updateGhostChart(level, score); | |
| } catch (e) { | |
| log(`[ERROR] ${e.message}`, 'err'); | |
| } finally { | |
| $('btn-atk').disabled = false; | |
| } | |
| } | |
| async function runFullSuite() { | |
| if (state.suiteRunning) return; | |
| state.suiteRunning = true; | |
| $('btn-suite').disabled = true; | |
| const text = $('atk-text').value; | |
| log('[SUITE] Running all 4 ghosting levels...', 'info'); | |
| for (let lvl = 1; lvl <= 4; lvl++) { | |
| try { | |
| const payload = buildGhostingPayload(text, lvl, lvl * 42); | |
| const r = await fetch(`${API}/attack/simulate/ghosting`, { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload), | |
| }); | |
| if (!r.ok) throw new Error(`HTTP ${r.status}`); | |
| const d = await r.json(); | |
| const score = d.score ?? d.entropy_score ?? 0; | |
| const verdict = d.verdict ?? 'UNKNOWN'; | |
| addAtkLog('ghosting', lvl / 4, verdict, score, verdict !== 'HUMAN', []); | |
| log(`[SUITE] L${lvl} β ${verdict} score=${score.toFixed ? score.toFixed(4) : score}`, | |
| verdict !== 'HUMAN' ? 'ok' : 'err'); | |
| updateGhostChart(lvl, score); | |
| } catch (e) { log(`[SUITE] L${lvl} error: ${e.message}`, 'err'); } | |
| await new Promise(res => setTimeout(res, 600)); | |
| } | |
| state.suiteRunning = false; | |
| $('btn-suite').disabled = false; | |
| log('[SUITE] Complete', 'ok'); | |
| } | |
| const atkScores = { 1: null, 2: null, 3: null, 4: null }; | |
| function updateGhostChart(level, score) { | |
| atkScores[level] = score; | |
| if (ghostChart) { | |
| ghostChart.data.datasets[0].data = [atkScores[1] ?? NaN, atkScores[2] ?? NaN, atkScores[3] ?? NaN, atkScores[4] ?? NaN]; | |
| ghostChart.update('none'); | |
| } | |
| } | |
| function addAtkLog(type, intensity, verdict, score, detected, signals) { | |
| state.atkLog.unshift({ type, intensity, verdict, score, detected, signals, ts: new Date() }); | |
| const log_el = $('atk-log'); | |
| log_el.innerHTML = state.atkLog.slice(0, 20).map(e => { | |
| const c = scoreColor(e.score); | |
| return `<div class="atk-row"> | |
| <span class="atk-tag ${e.type}">${e.type.toUpperCase()}</span> | |
| <span class="atk-int">i=${e.intensity.toFixed(1)}</span> | |
| <div class="bar-outer" style="flex:1"><div class="bar-inner" style="width:${Math.round(e.score * 100)}%;background:${c}"></div></div> | |
| <span class="atk-score" style="color:${c}">${(e.score || 0).toFixed ? e.score.toFixed(3) : e.score}</span> | |
| <span class="atk-result ${e.detected ? 'det' : 'byp'}">${e.detected ? 'DETECTED' : 'π¨ BYPASS'}</span> | |
| </div>`; | |
| }).join(''); | |
| } | |
| // Forced errors grid | |
| const forcedVariants = [ | |
| { variant: 'A β Zero corrections', score: 0.05, result: 'DETECTED' }, | |
| { variant: 'B β Uniform corrections', score: 0.28, result: 'DETECTED' }, | |
| { variant: 'C β Fake burst', score: 0.51, result: 'SUSPECT' }, | |
| { variant: 'D β Overcorrected (30%)', score: 0.19, result: 'DETECTED' }, | |
| { variant: 'E β Optimal burst placement', score: 0.58, result: 'SUSPECT' }, | |
| ]; | |
| $('forced-grid').innerHTML = forcedVariants.map(v => { | |
| const c = scoreColor(v.score); | |
| const bc = v.result === 'DETECTED' ? 'g' : 'y'; | |
| return `<div class="card forced-card"> | |
| <div class="forced-variant mono">${v.variant}</div> | |
| <div class="forced-bar-wrap"><div class="forced-bar" style="width:${v.score * 100}%;background:${c}"></div></div> | |
| <div class="forced-bottom"> | |
| <span class="mono" style="color:${c};font-size:13px;font-weight:700">${v.score.toFixed(2)}</span> | |
| <span class="badge ${bc}">${v.result}</span> | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| // Signal defs | |
| $('sig-defs').innerHTML = SIGNALS.map(s => ` | |
| <div style="display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border)"> | |
| <div> | |
| <div class="mono" style="font-size:12px">${s.label}</div> | |
| <div class="muted" style="font-size:11px">${s.desc}</div> | |
| </div> | |
| <span class="badge b">${(s.key === 'ikl' ? '30' : s.key === 'corrections' ? '25' : s.key === 'pauses' ? '15' : s.key === 'typing_speed' ? '15' : '15')}% weight</span> | |
| </div>`).join(''); | |
| // ββ Charts ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let scoreChart, attackChart, ghostChart, entChart, replayChart, weightsChart, radarChart, v1v2Chart; | |
| const scoreHist = new Array(10).fill(0); | |
| const botHist = new Array(10).fill(0); | |
| function updateScoreChart() { | |
| if (!scoreChart) return; | |
| scoreChart.data.datasets[0].data = [...scoreHist]; | |
| scoreChart.data.datasets[1].data = [...botHist]; | |
| scoreChart.update('none'); | |
| } | |
| function initCharts() { | |
| scoreChart = new Chart($('scoreChart').getContext('2d'), { | |
| type: 'bar', | |
| data: { | |
| labels: ['0.0', '0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9'], | |
| datasets: [ | |
| { label: 'Human/Suspect', data: [...scoreHist], backgroundColor: 'rgba(0,212,160,.45)', borderColor: C.green, borderWidth: 1 }, | |
| { label: 'Bot', data: [...botHist], backgroundColor: 'rgba(233,69,96,.40)', borderColor: C.red, borderWidth: 1 }, | |
| ], | |
| }, | |
| options: { | |
| responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, | |
| scales: { x: { grid: { color: C.border } }, y: { beginAtZero: true, grid: { color: C.border } } } | |
| }, | |
| }); | |
| attackChart = new Chart($('attackChart').getContext('2d'), { | |
| type: 'bar', | |
| data: { | |
| labels: ['Ghost L1', 'Ghost L4', 'Perf.Sem', 'Replay', 'Cal.Psn'], | |
| datasets: [ | |
| { label: 'Detected', data: [30, 18, 28, 25, 22], backgroundColor: 'rgba(74,158,255,.45)', borderColor: C.blue, borderWidth: 1 }, | |
| { label: 'Bypassed', data: [0, 12, 2, 5, 8], backgroundColor: 'rgba(233,69,96,.40)', borderColor: C.red, borderWidth: 1 }, | |
| ], | |
| }, | |
| options: { | |
| responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, | |
| scales: { x: { ticks: { maxRotation: 30 }, grid: { color: C.border } }, y: { beginAtZero: true, grid: { color: C.border } } } | |
| }, | |
| }); | |
| ghostChart = new Chart($('ghostChart').getContext('2d'), { | |
| type: 'line', | |
| data: { | |
| labels: ['L1', 'L2', 'L3', 'L4'], | |
| datasets: [ | |
| { label: 'Bot score', data: [0.08, 0.22, 0.44, 0.60], borderColor: C.red, backgroundColor: 'rgba(233,69,96,.1)', tension: .4, fill: true, borderWidth: 2, pointRadius: 5, pointBackgroundColor: C.red }, | |
| { label: 'Threshold', data: [0.65, 0.65, 0.65, 0.65], borderColor: C.green, borderWidth: 1, borderDash: [4, 4], pointRadius: 0 }, | |
| ], | |
| }, | |
| options: { | |
| responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, | |
| scales: { x: { grid: { color: C.border } }, y: { min: 0, max: 1, grid: { color: C.border } } } | |
| }, | |
| }); | |
| entChart = new Chart($('entChart').getContext('2d'), { | |
| type: 'bar', | |
| data: { | |
| labels: ['L1', 'L2', 'L3', 'L4', 'Human'], | |
| datasets: [{ | |
| data: [0.00, 0.87, 0.76, 0.74, 0.68], | |
| backgroundColor: ['rgba(233,69,96,.5)', 'rgba(245,197,24,.5)', 'rgba(74,158,255,.5)', 'rgba(74,158,255,.5)', 'rgba(0,212,160,.5)'], | |
| borderColor: [C.red, C.yellow, C.blue, C.blue, C.green], borderWidth: 1, | |
| }], | |
| }, | |
| options: { | |
| responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, | |
| scales: { x: { grid: { color: C.border } }, y: { min: 0, max: 1, grid: { color: C.border } } } | |
| }, | |
| }); | |
| replayChart = new Chart($('replayChart').getContext('2d'), { | |
| type: 'scatter', | |
| data: { | |
| datasets: [ | |
| { label: 'Human', data: Array.from({ length: 20 }, () => ({ x: 0.60 + Math.random() * .27, y: 0.55 + Math.random() * .35 })), backgroundColor: 'rgba(0,212,160,.6)', pointRadius: 5 }, | |
| { label: 'Replay', data: Array.from({ length: 8 }, () => ({ x: 0.91 + Math.random() * .08, y: 0.88 + Math.random() * .11 })), backgroundColor: 'rgba(233,69,96,.7)', pointRadius: 5, pointStyle: 'triangle' }, | |
| { label: 'Threshold', data: [{ x: 0.92, y: 0 }, { x: 0.92, y: 1 }], type: 'line', borderColor: C.red, borderWidth: 1, borderDash: [4, 4], pointRadius: 0 }, | |
| ], | |
| }, | |
| options: { | |
| responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, | |
| scales: { | |
| x: { min: .5, max: 1, title: { display: true, text: 'Cosine similarity' }, grid: { color: C.border } }, | |
| y: { min: 0, max: 1, title: { display: true, text: 'Human score' }, grid: { color: C.border } }, | |
| } | |
| }, | |
| }); | |
| weightsChart = new Chart($('weightsChart').getContext('2d'), { | |
| type: 'doughnut', | |
| data: { | |
| labels: ['IKL entropy', 'Corrections', 'Pauses', 'Speed', 'Fatigue'], | |
| datasets: [{ | |
| data: [30, 25, 15, 15, 15], | |
| backgroundColor: ['rgba(0,212,160,.7)', 'rgba(233,69,96,.7)', 'rgba(74,158,255,.7)', 'rgba(245,197,24,.7)', 'rgba(167,139,250,.7)'], | |
| borderWidth: 0, | |
| }], | |
| }, | |
| options: { responsive: true, maintainAspectRatio: false, cutout: '65%', plugins: { legend: { display: true, position: 'right' } } }, | |
| }); | |
| radarChart = new Chart($('radarChart').getContext('2d'), { | |
| type: 'radar', | |
| data: { | |
| labels: ['IKL entropy', 'Corrections', 'Pauses', 'Speed', 'Fatigue'], | |
| datasets: [ | |
| { label: 'Human', data: [0.91, 0.88, 0.75, 0.85, 0.82], borderColor: C.green, backgroundColor: 'rgba(0,212,160,.15)', borderWidth: 2, pointBackgroundColor: C.green, pointRadius: 4 }, | |
| { label: 'Bot L4', data: [0.74, 0.52, 0.10, 0.70, 0.20], borderColor: C.red, backgroundColor: 'rgba(233,69,96,.12)', borderWidth: 2, pointBackgroundColor: C.red, pointRadius: 4, borderDash: [4, 4] }, | |
| ], | |
| }, | |
| options: { | |
| responsive: true, maintainAspectRatio: false, | |
| scales: { r: { min: 0, max: 1, ticks: { stepSize: .25 }, grid: { color: C.border }, angleLines: { color: C.border }, pointLabels: { font: { size: 10 } } } } | |
| }, | |
| }); | |
| const v1 = Array.from({ length: 10 }, () => 0.18 + Math.random() * .2); | |
| const v2 = Array.from({ length: 10 }, () => 0.48 + Math.random() * .2); | |
| v1v2Chart = new Chart($('v1v2Chart').getContext('2d'), { | |
| type: 'line', | |
| data: { | |
| labels: Array.from({ length: 10 }, (_, i) => `Run ${i + 1}`), | |
| datasets: [ | |
| { label: 'V1 Naive', data: v1, borderColor: C.red, backgroundColor: 'transparent', tension: .3, borderWidth: 2, pointRadius: 3, pointBackgroundColor: C.red, borderDash: [3, 3] }, | |
| { label: 'V2 Level 4', data: v2, borderColor: C.blue, backgroundColor: 'rgba(74,158,255,.08)', tension: .3, borderWidth: 2, pointRadius: 3, pointBackgroundColor: C.blue, fill: true }, | |
| { label: 'Threshold', data: new Array(10).fill(0.65), borderColor: C.green, borderWidth: 1, borderDash: [6, 4], pointRadius: 0 }, | |
| ], | |
| }, | |
| options: { | |
| responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'top' } }, | |
| scales: { x: { grid: { color: C.border } }, y: { min: 0, max: 1, grid: { color: C.border } } } | |
| }, | |
| }); | |
| } | |
| // ββ Live simulation βββββββββββββββββββββββββββββββββββββββββββ | |
| function fakeSession() { | |
| const isBot = Math.random() < .2; | |
| const isSus = !isBot && Math.random() < .15; | |
| const verdict = isBot ? 'BOT' : isSus ? 'SUSPECT' : 'HUMAN'; | |
| const score = isBot ? (.05 + Math.random() * .33) : isSus ? (.40 + Math.random() * .24) : (.65 + Math.random() * .34); | |
| const users = ['denis', 'vertex_test', 'anon_01', 'gemini_agent', 'deepseek_lab']; | |
| const user = users[Math.floor(Math.random() * users.length)]; | |
| addSession(verdict, parseFloat(score.toFixed(4)), user, {}); | |
| const bin = Math.min(9, Math.floor(score * 10)); | |
| if (verdict === 'BOT') botHist[bin]++; | |
| else scoreHist[bin]++; | |
| updateScoreChart(); | |
| log(`[SIM] ${verdict} score=${score.toFixed(4)} user=${user}`, | |
| verdict === 'HUMAN' ? 'ok' : verdict === 'SUSPECT' ? 'warn' : 'err'); | |
| } | |
| // ββ Boot ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| log('[DECI] System boot β PoH Engine v0.1.0-sprint1', 'info'); | |
| log('[DECI] Dashboard initialized', 'ok'); | |
| log('[DECI] Checking API health...', 'info'); | |
| log('[DECI] Attack lab ready β DeepSeek mode ON', 'warn'); | |
| log('[DECI] Shadow mode: ON β verdicts logged, not enforced', 'info'); | |
| initCharts(); | |
| $('btn-analyze').disabled = true; | |
| // Start live simulation after 2s, then every 4-8s | |
| setTimeout(() => { | |
| fakeSession(); | |
| setInterval(fakeSession, 4000 + Math.random() * 4000); | |
| }, 2000); | |
| </script> | |
| </body> | |
| </html> |