Download site.html from opsecsystems/BEYOND: direct link, hf CLI and curl.
- Browser
- Download file 91.3 kB
-
https://huggingface.co/opsecsystems/BEYOND/resolve/main/site.html
- Command line
-
hf download hf://opsecsystems/BEYOND/site.html
-
curl -L -o site.html https://huggingface.co/opsecsystems/BEYOND/resolve/main/site.html
91.3 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Neural State Classifier · EEG Mental Imagery</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=DM+Sans:wght@200;300;400;500&display=swap'); | |
| :root { | |
| /* Semantic tokens (referenced throughout) */ | |
| --color-background-primary: #FAFAF8; | |
| --color-background-secondary: #F3F2EE; | |
| --color-background-tertiary: #EAE8E3; | |
| --color-text-primary: #1C1B19; | |
| --color-text-secondary: #4D4C47; | |
| --color-text-tertiary: #8E8C86; | |
| --color-border-tertiary: #DAD7D1; | |
| --color-border-secondary: #C5C2BB; | |
| --font-sans: 'DM Sans', system-ui, -apple-system, 'Segoe UI', sans-serif; | |
| --border-radius-lg: 10px; | |
| --border-radius-md: 8px; | |
| --shadow-sm: 0 1px 2px rgba(24, 22, 18, 0.06); | |
| --shadow-md: 0 8px 32px rgba(24, 22, 18, 0.08); | |
| --focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent); | |
| --bg: var(--color-background-primary); | |
| --bg2: var(--color-background-secondary); | |
| --bg3: var(--color-background-tertiary); | |
| --txt: var(--color-text-primary); | |
| --txt2: var(--color-text-secondary); | |
| --txt3: var(--color-text-tertiary); | |
| --border: var(--color-border-tertiary); | |
| --border2: var(--color-border-secondary); | |
| --accent: #185FA5; | |
| --accent-light: #E6F1FB; | |
| --teal: #0F6E56; | |
| --teal-light: #E1F5EE; | |
| --amber: #854F0B; | |
| --amber-light: #FAEEDA; | |
| --coral: #993C1D; | |
| --coral-light: #FAECE7; | |
| --purple: #3C3489; | |
| --purple-light: #EEEDFE; | |
| --mono: 'DM Mono', ui-monospace, 'Cascadia Mono', monospace; | |
| --sans: var(--font-sans); | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| html { | |
| height: 100%; | |
| -webkit-text-size-adjust: 100%; | |
| } | |
| body { | |
| font-family: var(--sans); | |
| min-height: 100%; | |
| background: linear-gradient(165deg, var(--color-background-secondary) 0%, var(--color-background-primary) 45%, #EFECE6 100%); | |
| background-attachment: fixed; | |
| color: var(--txt); | |
| font-size: 13px; | |
| line-height: 1.45; | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| ::selection { | |
| background: var(--accent-light); | |
| color: var(--txt); | |
| } | |
| .app { | |
| display: grid; | |
| grid-template-columns: minmax(180px, 220px) 1fr; | |
| grid-template-rows: auto 1fr; | |
| min-height: min(720px, 100vh); | |
| max-width: 1400px; | |
| margin: 12px auto; | |
| padding: 0 12px 12px; | |
| border: 0.5px solid var(--border); | |
| border-radius: var(--border-radius-lg); | |
| overflow: hidden; | |
| background: var(--bg); | |
| box-shadow: var(--shadow-md); | |
| } | |
| /* HEADER */ | |
| .header { | |
| grid-column: 1 / -1; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 12px 20px; | |
| border-bottom: 0.5px solid var(--border); | |
| background: var(--bg); | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .logo-mark { | |
| width: 28px; | |
| height: 28px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .header-title { | |
| font-size: 12px; | |
| font-weight: 500; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| color: var(--txt2); | |
| } | |
| .header-sub { | |
| font-size: 11px; | |
| color: var(--txt3); | |
| font-family: var(--mono); | |
| margin-top: 1px; | |
| } | |
| .status-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| } | |
| .status-pill { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| font-size: 11px; | |
| font-family: var(--mono); | |
| color: var(--txt3); | |
| } | |
| .dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background: var(--color-border-secondary); | |
| } | |
| .dot.active { | |
| background: #1D9E75; | |
| animation: pulse 2s ease-in-out infinite; | |
| } | |
| .dot.warn { | |
| background: #BA7517; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.4; } | |
| } | |
| /* SIDEBAR */ | |
| .sidebar { | |
| border-right: 0.5px solid var(--border); | |
| background: var(--bg2); | |
| padding: 0; | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 0; | |
| } | |
| .sidebar-section { | |
| padding: 14px 14px 8px; | |
| border-bottom: 0.5px solid var(--border); | |
| } | |
| .sidebar-label { | |
| font-size: 10px; | |
| letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| color: var(--txt3); | |
| font-family: var(--mono); | |
| margin-bottom: 8px; | |
| } | |
| .phase-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 8px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| margin-bottom: 2px; | |
| transition: background 0.15s; | |
| } | |
| .phase-item:hover { background: var(--bg3); } | |
| .phase-item.active { background: var(--accent-light); } | |
| .phase-num { | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 4px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 10px; | |
| font-family: var(--mono); | |
| font-weight: 500; | |
| background: var(--border); | |
| color: var(--txt2); | |
| flex-shrink: 0; | |
| } | |
| .phase-item.active .phase-num { | |
| background: var(--accent); | |
| color: white; | |
| } | |
| .phase-item.done .phase-num { | |
| background: #1D9E75; | |
| color: white; | |
| } | |
| .phase-text { | |
| font-size: 11px; | |
| color: var(--txt2); | |
| line-height: 1.3; | |
| } | |
| .phase-item.active .phase-text { color: var(--accent); font-weight: 500; } | |
| .class-pair { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| margin-bottom: 8px; | |
| } | |
| .class-tag { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 5px 8px; | |
| border-radius: 6px; | |
| border: 0.5px solid var(--border2); | |
| background: var(--bg); | |
| cursor: pointer; | |
| } | |
| .class-tag.a { border-color: #B5D4F4; background: #E6F1FB; } | |
| .class-tag.b { border-color: #9FE1CB; background: #E1F5EE; } | |
| .class-name { | |
| font-size: 11px; | |
| font-weight: 500; | |
| color: var(--txt); | |
| } | |
| .class-tag.a .class-name { color: #0C447C; } | |
| .class-tag.b .class-name { color: #085041; } | |
| .class-trials { | |
| font-size: 10px; | |
| font-family: var(--mono); | |
| color: var(--txt3); | |
| } | |
| .class-tag.a .class-trials { color: #185FA5; } | |
| .class-tag.b .class-trials { color: #0F6E56; } | |
| .metric-mini { | |
| display: flex; | |
| flex-direction: column; | |
| padding: 6px 8px; | |
| border-radius: 6px; | |
| background: var(--bg); | |
| border: 0.5px solid var(--border); | |
| margin-bottom: 4px; | |
| } | |
| .metric-mini-label { font-size: 10px; color: var(--txt3); font-family: var(--mono); } | |
| .metric-mini-value { font-size: 15px; font-weight: 500; color: var(--txt); margin-top: 1px; } | |
| .lib-entry { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 4px 6px; | |
| border-radius: 4px; | |
| margin-bottom: 2px; | |
| cursor: pointer; | |
| } | |
| .lib-entry:hover { background: var(--bg3); } | |
| .lib-dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| flex-shrink: 0; | |
| } | |
| .lib-text { font-size: 11px; color: var(--txt2); } | |
| .lib-score { font-size: 10px; font-family: var(--mono); color: var(--txt3); margin-left: auto; } | |
| /* MAIN CONTENT */ | |
| .main { | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| /* TAB BAR */ | |
| .tabbar { | |
| display: flex; | |
| border-bottom: 0.5px solid var(--border); | |
| background: var(--bg); | |
| padding: 0 16px; | |
| gap: 0; | |
| } | |
| .tab { | |
| padding: 10px 14px; | |
| font-size: 12px; | |
| color: var(--txt3); | |
| cursor: pointer; | |
| border-bottom: 2px solid transparent; | |
| margin-bottom: -0.5px; | |
| transition: color 0.15s; | |
| white-space: nowrap; | |
| } | |
| .tab:hover { color: var(--txt2); } | |
| .tab.active { color: var(--txt); border-bottom-color: var(--txt); font-weight: 500; } | |
| /* PANELS */ | |
| .panel { display: none; flex: 1; overflow-y: auto; } | |
| .panel.active { display: flex; flex-direction: column; } | |
| /* ACQUISITION PANEL */ | |
| .acq-layout { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px; | |
| padding: 16px; | |
| flex: 1; | |
| } | |
| .acq-protocol-card { | |
| grid-column: 1 / -1; | |
| } | |
| .acq-settings-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px 16px; | |
| align-items: start; | |
| } | |
| @media (max-width: 900px) { | |
| .acq-settings-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .field-hint { | |
| display: block; | |
| font-size: 10px; | |
| color: var(--txt3); | |
| font-family: var(--mono); | |
| margin-top: 4px; | |
| line-height: 1.35; | |
| } | |
| .check-row { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 8px; | |
| font-size: 12px; | |
| color: var(--txt2); | |
| margin-bottom: 8px; | |
| cursor: pointer; | |
| } | |
| .check-row input { | |
| margin-top: 2px; | |
| flex-shrink: 0; | |
| accent-color: var(--accent); | |
| } | |
| .acq-details { | |
| margin-top: 10px; | |
| border: 0.5px solid var(--border); | |
| border-radius: var(--border-radius-md); | |
| background: var(--bg2); | |
| padding: 0; | |
| overflow: hidden; | |
| } | |
| .acq-details > summary { | |
| list-style: none; | |
| cursor: pointer; | |
| padding: 8px 12px; | |
| font-size: 11px; | |
| font-weight: 500; | |
| font-family: var(--mono); | |
| color: var(--txt2); | |
| user-select: none; | |
| } | |
| .acq-details > summary::-webkit-details-marker { | |
| display: none; | |
| } | |
| .acq-details > summary::before { | |
| content: '▸ '; | |
| color: var(--txt3); | |
| } | |
| .acq-details[open] > summary::before { | |
| content: '▾ '; | |
| } | |
| .acq-details .acq-details-body { | |
| padding: 0 12px 12px; | |
| border-top: 0.5px solid var(--border); | |
| } | |
| .protocol-desc { | |
| font-size: 11px; | |
| color: var(--txt2); | |
| line-height: 1.45; | |
| padding: 8px 10px; | |
| border-radius: var(--border-radius-md); | |
| background: var(--accent-light); | |
| border-left: 3px solid var(--accent); | |
| margin-top: 6px; | |
| } | |
| .acq-section-label { | |
| font-size: 10px; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| font-family: var(--mono); | |
| color: var(--txt3); | |
| margin: 14px 0 8px; | |
| grid-column: 1 / -1; | |
| } | |
| .acq-section-label:first-child { | |
| margin-top: 0; | |
| } | |
| .card { | |
| border: 0.5px solid var(--border); | |
| border-radius: var(--border-radius-lg); | |
| background: var(--bg); | |
| overflow: hidden; | |
| } | |
| .card-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 10px 14px; | |
| border-bottom: 0.5px solid var(--border); | |
| background: var(--bg2); | |
| } | |
| .card-title { | |
| font-size: 11px; | |
| font-weight: 500; | |
| text-transform: uppercase; | |
| letter-spacing: 0.07em; | |
| color: var(--txt2); | |
| font-family: var(--mono); | |
| } | |
| .card-badge { | |
| font-size: 10px; | |
| font-family: var(--mono); | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| background: var(--accent-light); | |
| color: var(--accent); | |
| } | |
| .card-body { padding: 14px; } | |
| /* EEG topography */ | |
| .topo-container { | |
| display: flex; | |
| justify-content: center; | |
| padding: 8px 0; | |
| } | |
| /* Trial timeline */ | |
| .trial-row { | |
| display: grid; | |
| grid-template-columns: 28px 1fr auto auto; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 0; | |
| border-bottom: 0.5px solid var(--border); | |
| } | |
| .trial-row:last-child { border-bottom: none; } | |
| .trial-num { | |
| font-size: 11px; | |
| font-family: var(--mono); | |
| color: var(--txt3); | |
| text-align: right; | |
| } | |
| .trial-bar-wrap { | |
| position: relative; | |
| height: 8px; | |
| border-radius: 4px; | |
| background: var(--bg3); | |
| overflow: hidden; | |
| } | |
| .trial-bar { | |
| height: 100%; | |
| border-radius: 4px; | |
| transition: width 0.4s ease; | |
| } | |
| .trial-score { | |
| font-size: 11px; | |
| font-family: var(--mono); | |
| font-weight: 500; | |
| min-width: 16px; | |
| text-align: center; | |
| } | |
| .trial-badge { | |
| font-size: 9px; | |
| font-family: var(--mono); | |
| padding: 1px 5px; | |
| border-radius: 3px; | |
| min-width: 28px; | |
| text-align: center; | |
| } | |
| .trial-badge.keep { background: #E1F5EE; color: #085041; } | |
| .trial-badge.drop { background: #F1EFE8; color: #5F5E5A; } | |
| /* Rating stars */ | |
| .rating-row { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin: 10px 0; | |
| } | |
| .star-group { | |
| display: flex; | |
| gap: 6px; | |
| } | |
| .star { | |
| width: 32px; | |
| height: 32px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 6px; | |
| border: 0.5px solid var(--border2); | |
| cursor: pointer; | |
| font-size: 16px; | |
| transition: all 0.15s; | |
| background: var(--bg); | |
| } | |
| .star:hover, .star.sel { | |
| background: #FAEEDA; | |
| border-color: #FAC775; | |
| transform: scale(1.05); | |
| } | |
| .current-trial-info { | |
| background: var(--bg2); | |
| border-radius: 8px; | |
| padding: 12px; | |
| margin-bottom: 12px; | |
| } | |
| .trial-class-label { | |
| font-size: 10px; | |
| font-family: var(--mono); | |
| color: var(--txt3); | |
| margin-bottom: 4px; | |
| } | |
| .trial-object { | |
| font-size: 20px; | |
| font-weight: 300; | |
| color: var(--txt); | |
| letter-spacing: 0.02em; | |
| } | |
| .trial-timer { | |
| font-size: 11px; | |
| font-family: var(--mono); | |
| color: var(--txt3); | |
| margin-top: 4px; | |
| } | |
| /* Progress bar */ | |
| .progress-wrap { | |
| height: 3px; | |
| background: var(--bg3); | |
| border-radius: 2px; | |
| margin: 10px 0; | |
| overflow: hidden; | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| background: var(--accent); | |
| border-radius: 2px; | |
| transition: width 0.3s; | |
| } | |
| /* Neurofeedback panel */ | |
| .nf-layout { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px; | |
| padding: 16px; | |
| } | |
| .nf-full { grid-column: 1 / -1; } | |
| .convergence-track { | |
| position: relative; | |
| height: 60px; | |
| border: 0.5px solid var(--border); | |
| border-radius: 8px; | |
| overflow: hidden; | |
| background: var(--bg2); | |
| } | |
| .conv-fill { | |
| position: absolute; | |
| top: 0; left: 0; bottom: 0; | |
| border-radius: 8px 0 0 8px; | |
| transition: width 1s ease; | |
| opacity: 0.6; | |
| } | |
| .conv-label { | |
| position: absolute; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| right: 10px; | |
| font-size: 11px; | |
| font-family: var(--mono); | |
| color: var(--txt2); | |
| z-index: 2; | |
| } | |
| .conv-pct { | |
| position: absolute; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| left: 10px; | |
| font-size: 13px; | |
| font-weight: 500; | |
| font-family: var(--mono); | |
| color: var(--txt); | |
| z-index: 2; | |
| } | |
| .audio-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 8px; | |
| border-radius: 6px; | |
| background: var(--bg2); | |
| margin-bottom: 6px; | |
| } | |
| .audio-icon { | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 5px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| } | |
| .audio-icon.pos { background: #E1F5EE; } | |
| .audio-icon.neg { background: #FAECE7; } | |
| .audio-text { | |
| flex: 1; | |
| font-size: 11px; | |
| color: var(--txt2); | |
| } | |
| .audio-freq { | |
| font-size: 10px; | |
| font-family: var(--mono); | |
| color: var(--txt3); | |
| } | |
| /* Library panel */ | |
| .lib-layout { | |
| padding: 16px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .lib-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 8px; | |
| } | |
| .lib-card { | |
| border: 0.5px solid var(--border); | |
| border-radius: var(--border-radius-lg); | |
| padding: 12px; | |
| background: var(--bg); | |
| cursor: pointer; | |
| transition: border-color 0.15s, background 0.15s; | |
| } | |
| .lib-card:hover { background: var(--bg2); border-color: var(--border2); } | |
| .lib-card-name { | |
| font-size: 13px; | |
| font-weight: 500; | |
| margin-bottom: 6px; | |
| color: var(--txt); | |
| } | |
| .lib-card-meta { | |
| font-size: 10px; | |
| font-family: var(--mono); | |
| color: var(--txt3); | |
| margin-bottom: 8px; | |
| line-height: 1.6; | |
| } | |
| .lib-card-bar { | |
| height: 3px; | |
| border-radius: 2px; | |
| background: var(--bg3); | |
| overflow: hidden; | |
| } | |
| .lib-card-fill { | |
| height: 100%; | |
| border-radius: 2px; | |
| } | |
| .subject-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 8px 10px; | |
| border-radius: 6px; | |
| background: var(--bg2); | |
| margin-bottom: 4px; | |
| } | |
| .subject-avatar { | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 10px; | |
| font-weight: 500; | |
| flex-shrink: 0; | |
| } | |
| .subject-info { flex: 1; } | |
| .subject-name { font-size: 12px; font-weight: 500; color: var(--txt); } | |
| .subject-meta { font-size: 10px; font-family: var(--mono); color: var(--txt3); } | |
| .subject-acc { | |
| font-size: 12px; | |
| font-family: var(--mono); | |
| font-weight: 500; | |
| } | |
| /* Action buttons */ | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 7px 14px; | |
| border-radius: 6px; | |
| border: 0.5px solid var(--border2); | |
| background: var(--bg); | |
| color: var(--txt); | |
| font-size: 12px; | |
| cursor: pointer; | |
| transition: background 0.15s; | |
| font-family: var(--sans); | |
| } | |
| .btn:hover { background: var(--bg2); } | |
| .btn.primary { background: var(--accent); color: white; border-color: var(--accent); } | |
| .btn.primary:hover { opacity: 0.9; } | |
| /* Bottom actions */ | |
| .panel-footer { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 10px 16px; | |
| border-top: 0.5px solid var(--border); | |
| background: var(--bg2); | |
| } | |
| /* Waveform */ | |
| .waveform-wrap { | |
| height: 80px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| canvas.waveform { | |
| width: 100%; | |
| height: 80px; | |
| } | |
| /* General info */ | |
| .info-row { | |
| display: flex; | |
| align-items: baseline; | |
| justify-content: space-between; | |
| padding: 5px 0; | |
| border-bottom: 0.5px solid var(--border); | |
| font-size: 12px; | |
| } | |
| .info-row:last-child { border-bottom: none; } | |
| .info-key { color: var(--txt3); font-family: var(--mono); } | |
| .info-val { color: var(--txt); font-weight: 500; } | |
| .section-title { | |
| font-size: 11px; | |
| font-weight: 500; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| color: var(--txt3); | |
| font-family: var(--mono); | |
| margin-bottom: 8px; | |
| } | |
| /* ─── Forms & inputs ─── */ | |
| input[type="text"], | |
| input[type="number"], | |
| select, | |
| textarea { | |
| font-family: var(--sans); | |
| font-size: 12px; | |
| padding: 7px 10px; | |
| border-radius: 6px; | |
| border: 0.5px solid var(--border2); | |
| background: var(--bg); | |
| color: var(--txt); | |
| width: 100%; | |
| max-width: 100%; | |
| transition: border-color 0.15s, box-shadow 0.15s; | |
| } | |
| input:focus-visible, | |
| select:focus-visible, | |
| textarea:focus-visible { | |
| outline: none; | |
| border-color: var(--accent); | |
| box-shadow: var(--focus-ring); | |
| } | |
| label.field-label { | |
| display: block; | |
| font-size: 10px; | |
| font-family: var(--mono); | |
| letter-spacing: 0.06em; | |
| text-transform: uppercase; | |
| color: var(--txt3); | |
| margin-bottom: 4px; | |
| } | |
| .field-group { | |
| margin-bottom: 10px; | |
| } | |
| /* ─── Sidebar scroll & flex ─── */ | |
| .sidebar-section:last-child { | |
| flex: 1; | |
| min-height: 0; | |
| overflow-y: auto; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--border2) transparent; | |
| } | |
| .sidebar-section:last-child::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .sidebar-section:last-child::-webkit-scrollbar-thumb { | |
| background: var(--border2); | |
| border-radius: 4px; | |
| } | |
| /* ─── Panels scroll ─── */ | |
| .panel { | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--border2) transparent; | |
| } | |
| .panel::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .panel::-webkit-scrollbar-thumb { | |
| background: var(--border2); | |
| border-radius: 4px; | |
| } | |
| /* ─── Buttons ─── */ | |
| button.btn { | |
| appearance: none; | |
| border: 0.5px solid var(--border2); | |
| } | |
| button.btn:focus-visible { | |
| outline: none; | |
| box-shadow: var(--focus-ring); | |
| } | |
| button.btn:disabled { | |
| opacity: 0.45; | |
| cursor: not-allowed; | |
| } | |
| .btn.danger { | |
| border-color: #E8B4A8; | |
| background: var(--coral-light); | |
| color: var(--coral); | |
| } | |
| .btn.danger:hover { | |
| background: #F5DCD4; | |
| } | |
| .btn.ghost { | |
| background: transparent; | |
| border-color: transparent; | |
| } | |
| .btn.ghost:hover { | |
| background: var(--bg2); | |
| border-color: var(--border); | |
| } | |
| .btn.sm { | |
| padding: 5px 10px; | |
| font-size: 11px; | |
| } | |
| /* ─── Tabs accessibility ─── */ | |
| .tab:focus-visible { | |
| outline: none; | |
| box-shadow: inset 0 -2px 0 var(--accent); | |
| color: var(--txt); | |
| } | |
| .phase-item:focus-visible { | |
| outline: none; | |
| box-shadow: 0 0 0 2px var(--accent-light); | |
| } | |
| /* ─── Cards & canvases ─── */ | |
| .card-body canvas { | |
| display: block; | |
| max-width: 100%; | |
| height: auto; | |
| } | |
| #distCanvas { | |
| width: 100%; | |
| max-width: 100%; | |
| height: auto; | |
| min-height: 80px; | |
| } | |
| /* ─── Links ─── */ | |
| a { | |
| color: var(--accent); | |
| text-decoration: none; | |
| } | |
| a:hover { | |
| text-decoration: underline; | |
| } | |
| /* ─── Utility ─── */ | |
| .sr-only { | |
| position: absolute; | |
| width: 1px; | |
| height: 1px; | |
| padding: 0; | |
| margin: -1px; | |
| overflow: hidden; | |
| clip: rect(0, 0, 0, 0); | |
| white-space: nowrap; | |
| border: 0; | |
| } | |
| .muted { color: var(--txt3); } | |
| .mono { font-family: var(--mono); } | |
| .stack { display: flex; flex-direction: column; gap: 8px; } | |
| /* ─── Responsive ─── */ | |
| @media (max-width: 900px) { | |
| .app { | |
| grid-template-columns: 1fr; | |
| grid-template-rows: auto auto 1fr; | |
| margin: 0; | |
| padding: 0; | |
| border-radius: 0; | |
| border-left: none; | |
| border-right: none; | |
| min-height: 100vh; | |
| max-width: none; | |
| } | |
| .header { | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| } | |
| .sidebar { | |
| border-right: none; | |
| border-bottom: 0.5px solid var(--border); | |
| flex-direction: row; | |
| flex-wrap: wrap; | |
| max-height: none; | |
| } | |
| .sidebar-section { | |
| flex: 1 1 140px; | |
| border-bottom: 0.5px solid var(--border); | |
| } | |
| .sidebar-section:last-child { | |
| flex: 1 1 100%; | |
| max-height: 160px; | |
| } | |
| .acq-layout, | |
| .nf-layout { | |
| grid-template-columns: 1fr; | |
| } | |
| .lib-grid { | |
| grid-template-columns: repeat(2, 1fr); | |
| } | |
| .tabbar { | |
| overflow-x: auto; | |
| -webkit-overflow-scrolling: touch; | |
| } | |
| } | |
| @media (max-width: 520px) { | |
| .lib-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .status-row { | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| } | |
| .star { | |
| width: 28px; | |
| height: 28px; | |
| font-size: 14px; | |
| } | |
| } | |
| @media (prefers-reduced-motion: reduce) { | |
| *, | |
| *::before, | |
| *::after { | |
| animation-duration: 0.01ms ; | |
| animation-iteration-count: 1 ; | |
| transition-duration: 0.01ms ; | |
| } | |
| .dot.active { | |
| animation: none; | |
| } | |
| } | |
| /* ─── Print ─── */ | |
| @media print { | |
| body { | |
| background: white; | |
| } | |
| .app { | |
| box-shadow: none; | |
| margin: 0; | |
| max-width: none; | |
| min-height: auto; | |
| } | |
| .panel-footer, | |
| .tabbar .tab:not(.active), | |
| .btn { | |
| display: none ; | |
| } | |
| .panel { | |
| display: flex ; | |
| overflow: visible; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h2 class="sr-only">EEG mental-imagery classification paradigm — acquisition, neurofeedback, and object library dashboard</h2> | |
| <div class="app"> | |
| <!-- HEADER --> | |
| <div class="header"> | |
| <div class="header-left"> | |
| <div class="logo-mark"> | |
| <svg width="28" height="28" viewBox="0 0 28 28" fill="none"> | |
| <circle cx="14" cy="14" r="13" stroke="var(--border2)" stroke-width="0.5"/> | |
| <circle cx="14" cy="14" r="9" stroke="var(--accent)" stroke-width="0.5" stroke-dasharray="2 2"/> | |
| <circle cx="14" cy="14" r="3" fill="var(--accent)"/> | |
| <circle cx="14" cy="6" r="1.5" fill="var(--txt3)"/> | |
| <circle cx="22" cy="10" r="1.5" fill="var(--txt3)"/> | |
| <circle cx="22" cy="18" r="1.5" fill="var(--txt3)"/> | |
| <circle cx="14" cy="22" r="1.5" fill="var(--txt3)"/> | |
| <circle cx="6" cy="18" r="1.5" fill="var(--txt3)"/> | |
| <circle cx="6" cy="10" r="1.5" fill="var(--txt3)"/> | |
| </svg> | |
| </div> | |
| <div> | |
| <div class="header-title">Neural State Classifier</div> | |
| <div class="header-sub">EEG Mental Imagery Paradigm · 16ch Parieto-Occipital</div> | |
| </div> | |
| </div> | |
| <div class="status-row"> | |
| <div class="status-pill"><span class="dot" id="dot-phase"></span> <span id="txt-phase">IDLE</span></div> | |
| <div class="status-pill"><span class="dot" id="dot-session"></span> <span id="txt-session">— · —</span></div> | |
| <div class="status-pill"><span class="dot" id="dot-board"></span> <span id="txt-hz">250 Hz · 16 ch</span></div> | |
| </div> | |
| </div> | |
| <!-- SIDEBAR --> | |
| <div class="sidebar"> | |
| <div class="sidebar-section"> | |
| <div class="sidebar-label">Backend</div> | |
| <div id="backend-status" style="font-size:10px;font-family:var(--mono);color:var(--txt3);line-height:1.5;margin-bottom:10px">REST: … · WS: … · Board: …</div> | |
| <label class="field-label" for="cfg-subject">Subject ID</label> | |
| <input id="cfg-subject" type="text" value="S001" autocomplete="off"> | |
| <label class="field-label" for="cfg-class-a">Class A label</label> | |
| <input id="cfg-class-a" type="text" value="Chair" autocomplete="off"> | |
| <label class="field-label" for="cfg-class-b">Class B label</label> | |
| <input id="cfg-class-b" type="text" value="Spiral" autocomplete="off"> | |
| <label class="field-label" for="cfg-min-q">Min quality (1–5)</label> | |
| <input id="cfg-min-q" type="number" min="1" max="5" value="4"> | |
| <label class="field-label" for="cfg-port">Serial port (hardware)</label> | |
| <input id="cfg-port" type="text" placeholder="/dev/ttyUSB0 or COM3" autocomplete="off"> | |
| <div style="display:flex;flex-direction:column;gap:6px;margin-top:10px"> | |
| <button type="button" class="btn primary sm" onclick="applyConfiguration()">Apply configuration</button> | |
| <button type="button" class="btn sm" onclick="connectBoard(true)">Connect · simulate</button> | |
| <button type="button" class="btn sm" onclick="connectBoard(false)">Connect · hardware</button> | |
| <button type="button" class="btn sm" onclick="disconnectBoard()">Disconnect board</button> | |
| <button type="button" class="btn ghost sm" onclick="resetSession()">Reset session</button> | |
| </div> | |
| </div> | |
| <div class="sidebar-section"> | |
| <div class="sidebar-label">Phases</div> | |
| <div class="phase-item done" onclick="showTab('acq')"> | |
| <div class="phase-num">1</div> | |
| <div class="phase-text">Acquisition + self-rating</div> | |
| </div> | |
| <div class="phase-item done" onclick="showTab('acq')"> | |
| <div class="phase-num">2</div> | |
| <div class="phase-text">Filtering · stable state</div> | |
| </div> | |
| <div class="phase-item active" onclick="showTab('nf')"> | |
| <div class="phase-num">3</div> | |
| <div class="phase-text">Real-time neurofeedback</div> | |
| </div> | |
| <div class="phase-item" onclick="showTab('lib')"> | |
| <div class="phase-num">4</div> | |
| <div class="phase-text">Multi-class object library</div> | |
| </div> | |
| <div class="phase-item" onclick="showTab('lib')"> | |
| <div class="phase-num">5</div> | |
| <div class="phase-text">Cross-subject generalization</div> | |
| </div> | |
| </div> | |
| <div class="sidebar-section"> | |
| <div class="sidebar-label">Current session</div> | |
| <div class="class-pair"> | |
| <div class="class-tag a"> | |
| <span class="class-name" id="ui-class-a-name">Chair</span> | |
| <span class="class-trials" id="ui-class-a-n">0 trials</span> | |
| </div> | |
| <div class="class-tag b"> | |
| <span class="class-name" id="ui-class-b-name">Spiral</span> | |
| <span class="class-trials" id="ui-class-b-n">0 trials</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="sidebar-section"> | |
| <div class="sidebar-label">Metrics</div> | |
| <div class="metric-mini"> | |
| <span class="metric-mini-label">Convergence (mean)</span> | |
| <span class="metric-mini-value" id="metric-conv">—</span> | |
| </div> | |
| <div class="metric-mini"> | |
| <span class="metric-mini-label">Trials kept</span> | |
| <span class="metric-mini-value" id="metric-kept">—</span> | |
| </div> | |
| <div class="metric-mini"> | |
| <span class="metric-mini-label">Mean score</span> | |
| <span class="metric-mini-value" id="metric-mean">—</span> | |
| </div> | |
| <div class="metric-mini"> | |
| <span class="metric-mini-label">Classifier</span> | |
| <span class="metric-mini-value" id="metric-lda">—</span> | |
| </div> | |
| </div> | |
| <div class="sidebar-section" style="flex:1; overflow-y:auto;"> | |
| <div class="sidebar-label">Neural states</div> | |
| <div id="sidebar-library-list"></div> | |
| </div> | |
| </div><!-- /sidebar --> | |
| <!-- MAIN --> | |
| <div class="main"> | |
| <!-- TABBAR --> | |
| <div class="tabbar"> | |
| <div class="tab active" id="tab-acq" onclick="showTab('acq')">Acquisition</div> | |
| <div class="tab" id="tab-nf" onclick="showTab('nf')">Neurofeedback</div> | |
| <div class="tab" id="tab-model" onclick="showTab('model')">Model</div> | |
| <div class="tab" id="tab-lib" onclick="showTab('lib')">Library</div> | |
| </div> | |
| <!-- ======== PANEL: ACQUISITION ======== --> | |
| <div class="panel active" id="panel-acq"> | |
| <div class="acq-layout"> | |
| <!-- Protocol & stimulus (index.html / index-modular parity) --> | |
| <div class="card acq-protocol-card"> | |
| <div class="card-header"> | |
| <span class="card-title">Protocol & stimulus</span> | |
| <span class="card-badge">Timing · markers</span> | |
| </div> | |
| <div class="card-body"> | |
| <div class="acq-settings-grid"> | |
| <div> | |
| <label class="field-label" for="acq-protocol-mode">Protocol mode</label> | |
| <select id="acq-protocol-mode" aria-describedby="acq-protocol-desc"> | |
| <option value="visual">Visual stimuli (real colors)</option> | |
| <option value="imagery">Mental visualization (imagery)</option> | |
| <option value="custom">Custom configuration</option> | |
| </select> | |
| <div id="acq-protocol-desc" class="protocol-desc" role="note"> | |
| <strong>Visual:</strong> short stimulus + gray ISI; align band powers with onsets. | |
| </div> | |
| </div> | |
| <div> | |
| <label class="field-label" for="acq-stimulus-type">Stimulus type</label> | |
| <select id="acq-stimulus-type"> | |
| <option value="color">Color baseline</option> | |
| <option value="shape">3D primitives</option> | |
| <option value="combined" selected>Combined protocol</option> | |
| </select> | |
| <span class="field-hint">Same taxonomy as the standalone stimulus pages (color / shape / both).</span> | |
| </div> | |
| <div class="acq-section-label">Timing</div> | |
| <div> | |
| <label class="field-label" for="acq-prep-sec">Prep countdown before recording</label> | |
| <input id="acq-prep-sec" type="number" min="0" max="120" step="0.5" value="3"> | |
| <span class="field-hint">Seconds before the backend trial starts (LSL marker + beep fire at trial onset).</span> | |
| </div> | |
| <div> | |
| <label class="field-label" for="acq-stim-duration-ms">Stimulus duration (ms)</label> | |
| <input id="acq-stim-duration-ms" type="number" min="100" max="30000" step="100" value="300"> | |
| <span class="field-hint">Displayed & sent in LSL marker (external VR/stimulus app).</span> | |
| </div> | |
| <div> | |
| <label class="field-label" for="acq-isi-ms">Inter-stimulus interval / gray mask (ms)</label> | |
| <input id="acq-isi-ms" type="number" min="200" max="10000" step="100" value="2500"> | |
| </div> | |
| <div> | |
| <label class="field-label" for="acq-repetitions">Repetitions per block</label> | |
| <input id="acq-repetitions" type="number" min="1" max="200" value="30"> | |
| </div> | |
| <div> | |
| <label class="field-label" for="acq-epoch-sec">Recording epoch (display)</label> | |
| <input id="acq-epoch-sec" type="number" min="1" max="30" step="0.5" value="4"> | |
| <span class="field-hint">Progress bar only; backend epoch extraction stays at 4 s unless the server is updated.</span> | |
| </div> | |
| <div> | |
| <label class="field-label" for="acq-analysis-window-ms">EEG analysis window (ms post-onset)</label> | |
| <input id="acq-analysis-window-ms" type="number" min="200" max="3000" step="100" value="1000"> | |
| </div> | |
| <div id="acq-imagery-block" style="display:none;grid-column:1/-1;width:100%"> | |
| <div class="acq-settings-grid" style="margin-top:4px"> | |
| <div> | |
| <label class="check-row"> | |
| <input type="checkbox" id="acq-show-imagery-instructions" checked> | |
| <span>Show imagery instructions on external display</span> | |
| </label> | |
| <label class="field-label" for="acq-instruction-duration-ms">Instruction duration (ms)</label> | |
| <input id="acq-instruction-duration-ms" type="number" min="500" max="8000" step="100" value="2000"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="acq-section-label">Recording & cues</div> | |
| <div> | |
| <label class="check-row"> | |
| <input type="checkbox" id="acq-lsl-markers"> | |
| <span>Send LSL markers via backend (<code style="font-size:10px">POST /lsl/marker</code>)</span> | |
| </label> | |
| <label class="check-row"> | |
| <input type="checkbox" id="acq-audio-cue" checked> | |
| <span>Audio cue at trial onset (short beep)</span> | |
| </label> | |
| </div> | |
| <div> | |
| <label class="field-label" for="acq-angular-size">Angular size (visual °)</label> | |
| <select id="acq-angular-size"> | |
| <option value="5">5° · central</option> | |
| <option value="7" selected>7° · standard</option> | |
| <option value="10">10° · larger field</option> | |
| <option value="33">33° · wide field</option> | |
| <option value="full">Full screen</option> | |
| </select> | |
| </div> | |
| <div class="acq-section-label">Blocks & modular options</div> | |
| <div> | |
| <label class="check-row"> | |
| <input type="checkbox" id="acq-enable-breaks" checked> | |
| <span>Enable breaks between series</span> | |
| </label> | |
| <label class="field-label" for="acq-trials-per-series">Trials per series (before break)</label> | |
| <input id="acq-trials-per-series" type="number" min="5" max="100" step="5" value="15"> | |
| </div> | |
| <div> | |
| <label class="check-row"> | |
| <input type="checkbox" id="acq-randomize"> | |
| <span>Randomize trial order (logged locally)</span> | |
| </label> | |
| <label class="check-row"> | |
| <input type="checkbox" id="acq-enable-vr"> | |
| <span>WebXR / VR session (use external fullscreen stimulus page)</span> | |
| </label> | |
| </div> | |
| <div> | |
| <label class="field-label" for="acq-rotation-speed">3D rotation speed (rad/s)</label> | |
| <input id="acq-rotation-speed" type="number" min="0" max="2" step="0.1" value="0.5"> | |
| <label class="check-row" style="margin-top:8px"> | |
| <input type="checkbox" id="acq-tracking-dots"> | |
| <span>Tracking dots (pilot testing)</span> | |
| </label> | |
| </div> | |
| </div> | |
| <details class="acq-details"> | |
| <summary>Ganzfeld pre-experiment (optional)</summary> | |
| <div class="acq-details-body"> | |
| <label class="check-row"> | |
| <input type="checkbox" id="acq-ganzfeld-enable"> | |
| <span>Enable Ganzfeld sequence before color trials (long run-in)</span> | |
| </label> | |
| <span class="field-hint">Durations are stored for your external runner; not executed in this dashboard.</span> | |
| <div id="acq-ganzfeld-fields" style="margin-top:10px;opacity:0.55;pointer-events:none"> | |
| <div class="field-group"> | |
| <label class="field-label" for="acq-ganzfeld-color-sec">Duration per color (s)</label> | |
| <input id="acq-ganzfeld-color-sec" type="number" min="60" max="600" step="30" value="300"> | |
| </div> | |
| <div class="field-group"> | |
| <label class="field-label" for="acq-ganzfeld-transition-sec">HSV transition (s)</label> | |
| <input id="acq-ganzfeld-transition-sec" type="number" min="5" max="60" step="5" value="15"> | |
| </div> | |
| <div class="field-group"> | |
| <label class="field-label" for="acq-ganzfeld-black-sec">Final black (s)</label> | |
| <input id="acq-ganzfeld-black-sec" type="number" min="30" max="180" step="10" value="60"> | |
| </div> | |
| </div> | |
| </div> | |
| </details> | |
| <div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:14px;align-items:center"> | |
| <button type="button" class="btn sm danger" id="cancel-prep-btn" onclick="cancelPrep()" style="display:none">Cancel countdown</button> | |
| <span id="acq-settings-status" class="muted" style="font-size:10px;font-family:var(--mono)">Settings saved in this browser</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Current trial --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <span class="card-title">Current trial</span> | |
| <span class="card-badge" id="badge-trial-id">—</span> | |
| </div> | |
| <div class="card-body"> | |
| <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px"> | |
| <button type="button" class="btn sm primary" onclick="startTrialClass('a')">Start · Class A</button> | |
| <button type="button" class="btn sm primary" onclick="startTrialClass('b')">Start · Class B</button> | |
| </div> | |
| <div class="current-trial-info"> | |
| <div class="trial-class-label" id="trial-class-heading">—</div> | |
| <div class="trial-object" id="trial-object-name">—</div> | |
| <div class="trial-timer" id="timer-display">No active trial — start Class A or B</div> | |
| </div> | |
| <div class="progress-wrap"> | |
| <div class="progress-bar" id="trial-progress" style="width:0%"></div> | |
| </div> | |
| <div class="section-title" style="margin-top:12px">Self-rating</div> | |
| <div class="rating-row"> | |
| <div class="star-group" id="stars"> | |
| <div class="star" data-v="1" onclick="rateTrial(1)">1</div> | |
| <div class="star" data-v="2" onclick="rateTrial(2)">2</div> | |
| <div class="star" data-v="3" onclick="rateTrial(3)">3</div> | |
| <div class="star" data-v="4" onclick="rateTrial(4)">4</div> | |
| <div class="star sel" data-v="5" onclick="rateTrial(5)">5</div> | |
| </div> | |
| <span id="rate-label" style="font-size:11px;font-family:var(--mono);color:var(--txt3)">Excellent</span> | |
| </div> | |
| <div style="display:flex;gap:8px;margin-top:10px"> | |
| <button class="btn" onclick="sendPrompt('How can I improve EEG trial quality in a mental-imagery paradigm?')">↗ Help</button> | |
| <button class="btn primary" onclick="confirmTrial()">Confirm trial →</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Topographie EEG --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <span class="card-title">EEG topomap</span> | |
| <span class="card-badge">Live</span> | |
| </div> | |
| <div class="card-body"> | |
| <div class="topo-container"> | |
| <canvas id="topoCanvas" width="160" height="160" style="border-radius:50%"></canvas> | |
| </div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:10px"> | |
| <div class="info-row"><span class="info-key">Alpha</span><span class="info-val" id="live-alpha" style="color:#185FA5">—</span></div> | |
| <div class="info-row"><span class="info-key">Beta</span><span class="info-val" id="live-beta" style="color:#0F6E56">—</span></div> | |
| <div class="info-row"><span class="info-key">Theta</span><span class="info-val" id="live-theta">—</span></div> | |
| <div class="info-row"><span class="info-key">Gamma</span><span class="info-val" id="live-gamma">—</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Trial history --> | |
| <div class="card" style="grid-column:1/-1"> | |
| <div class="card-header"> | |
| <span class="card-title" id="history-card-title">History · Class A</span> | |
| <span class="card-badge" id="history-card-badge">0 trials · 0 kept</span> | |
| </div> | |
| <div class="card-body"> | |
| <div id="trial-history"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel-footer"> | |
| <span id="acq-footer-summary" style="font-size:11px;font-family:var(--mono);color:var(--txt3)">Prep 3 s · stim 300 ms · ISI 2500 ms · epoch display 4 s</span> | |
| <div style="display:flex;gap:8px"> | |
| <button class="btn" onclick="showTab('nf')">→ Neurofeedback</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ======== PANEL: NEUROFEEDBACK ======== --> | |
| <div class="panel" id="panel-nf"> | |
| <div class="nf-layout"> | |
| <!-- Convergence tracking --> | |
| <div class="card nf-full"> | |
| <div class="card-header"> | |
| <span class="card-title">Convergence to stable state</span> | |
| <span class="card-badge">Phase 3 · Active</span> | |
| </div> | |
| <div class="card-body"> | |
| <div style="margin-bottom:10px"> | |
| <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px"> | |
| <span class="section-title" id="nf-a-title">Class A</span> | |
| <span id="nf-a-pct" style="font-size:11px;font-family:var(--mono);color:#185FA5">—</span> | |
| </div> | |
| <div class="convergence-track"> | |
| <div class="conv-fill" id="nf-a-fill" style="width:0%;background:linear-gradient(to right,#E6F1FB,#B5D4F4)"></div> | |
| <div class="conv-pct" id="nf-a-pct-bar">0%</div> | |
| <div class="conv-label">Target</div> | |
| </div> | |
| </div> | |
| <div> | |
| <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px"> | |
| <span class="section-title" id="nf-b-title">Class B</span> | |
| <span id="nf-b-pct" style="font-size:11px;font-family:var(--mono);color:#0F6E56">—</span> | |
| </div> | |
| <div class="convergence-track"> | |
| <div class="conv-fill" id="nf-b-fill" style="width:0%;background:linear-gradient(to right,#E1F5EE,#9FE1CB)"></div> | |
| <div class="conv-pct" id="nf-b-pct-bar">0%</div> | |
| <div class="conv-label">Target</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Signal waveform --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <span class="card-title" id="wave-card-title">Raw signal · preview</span> | |
| <span class="card-badge">250 Hz</span> | |
| </div> | |
| <div class="card-body" style="padding:8px"> | |
| <div class="waveform-wrap"> | |
| <canvas id="waveCanvas" width="300" height="80"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Audio feedback --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <span class="card-title">Audio feedback (NF)</span> | |
| </div> | |
| <div class="card-body"> | |
| <div class="audio-row"> | |
| <div class="audio-icon pos"> | |
| <svg width="14" height="14" viewBox="0 0 14 14" fill="none"> | |
| <path d="M3 7h8M7 3l4 4-4 4" stroke="#0F6E56" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| </div> | |
| <div> | |
| <div class="audio-text" id="nf-audio-target-title" style="color:#085041;font-weight:500">On target</div> | |
| <div class="audio-freq" id="nf-audio-target-detail">—</div> | |
| </div> | |
| </div> | |
| <div class="audio-row"> | |
| <div class="audio-icon neg"> | |
| <svg width="14" height="14" viewBox="0 0 14 14" fill="none"> | |
| <path d="M11 7H3M7 3L3 7l4 4" stroke="#993C1D" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| </div> | |
| <div> | |
| <div class="audio-text" id="nf-audio-neg-title" style="color:#712B13;font-weight:500">Divergence detected</div> | |
| <div class="audio-freq" id="nf-audio-neg-detail">—</div> | |
| </div> | |
| </div> | |
| <div class="audio-row"> | |
| <div class="audio-icon" style="background:#EEEDFE"> | |
| <svg width="14" height="14" viewBox="0 0 14 14" fill="none"> | |
| <circle cx="7" cy="7" r="4" stroke="#534AB7" stroke-width="1.2"/> | |
| <circle cx="7" cy="7" r="1.5" fill="#534AB7"/> | |
| </svg> | |
| </div> | |
| <div> | |
| <div class="audio-text" id="nf-audio-neutral-title" style="color:#3C3489;font-weight:500">Neutral band</div> | |
| <div class="audio-freq" id="nf-audio-neutral-detail">Last trial NF · —</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Similarity score history --> | |
| <div class="card nf-full"> | |
| <div class="card-header"> | |
| <span class="card-title">Distance to reference model · history</span> | |
| </div> | |
| <div class="card-body" style="padding:8px"> | |
| <canvas id="distCanvas" width="580" height="80"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel-footer"> | |
| <span style="font-size:11px;font-family:var(--mono);color:var(--txt3)">Metric: cosine distance · 8D CSP space</span> | |
| <button class="btn primary" onclick="sendPrompt('Which algorithms do you recommend for EEG neurofeedback in mental imagery?')">↗ NF algorithm notes</button> | |
| </div> | |
| </div> | |
| <!-- ======== PANEL: MODEL ======== --> | |
| <div class="panel" id="panel-model"> | |
| <div style="padding:16px;display:flex;flex-direction:column;gap:12px;flex:1"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <span class="card-title">Feature extraction pipeline</span> | |
| </div> | |
| <div class="card-body"> | |
| <div style="display:flex;align-items:center;gap:0;overflow-x:auto;padding-bottom:4px"> | |
| <div style="text-align:center;min-width:90px"> | |
| <div style="width:60px;height:44px;border-radius:8px;background:var(--accent-light);border:0.5px solid #B5D4F4;display:flex;align-items:center;justify-content:center;margin:0 auto 5px;font-size:10px;font-family:var(--mono);color:#185FA5;text-align:center;padding:4px">EEG raw<br>16 ch</div> | |
| </div> | |
| <div style="flex:0 0 20px;text-align:center;color:var(--txt3);font-size:12px">→</div> | |
| <div style="text-align:center;min-width:90px"> | |
| <div style="width:60px;height:44px;border-radius:8px;background:#E1F5EE;border:0.5px solid #9FE1CB;display:flex;align-items:center;justify-content:center;margin:0 auto 5px;font-size:10px;font-family:var(--mono);color:#085041;text-align:center;padding:4px">Filter<br>1–40 Hz</div> | |
| </div> | |
| <div style="flex:0 0 20px;text-align:center;color:var(--txt3);font-size:12px">→</div> | |
| <div style="text-align:center;min-width:90px"> | |
| <div style="width:60px;height:44px;border-radius:8px;background:#EEEDFE;border:0.5px solid #CECBF6;display:flex;align-items:center;justify-content:center;margin:0 auto 5px;font-size:10px;font-family:var(--mono);color:#3C3489;text-align:center;padding:4px">CSP<br>8 comp.</div> | |
| </div> | |
| <div style="flex:0 0 20px;text-align:center;color:var(--txt3);font-size:12px">→</div> | |
| <div style="text-align:center;min-width:90px"> | |
| <div style="width:60px;height:44px;border-radius:8px;background:#FAEEDA;border:0.5px solid #FAC775;display:flex;align-items:center;justify-content:center;margin:0 auto 5px;font-size:10px;font-family:var(--mono);color:#633806;text-align:center;padding:4px">Log-var<br>features</div> | |
| </div> | |
| <div style="flex:0 0 20px;text-align:center;color:var(--txt3);font-size:12px">→</div> | |
| <div style="text-align:center;min-width:90px"> | |
| <div style="width:60px;height:44px;border-radius:8px;background:#FAECE7;border:0.5px solid #F5C4B3;display:flex;align-items:center;justify-content:center;margin:0 auto 5px;font-size:10px;font-family:var(--mono);color:#712B13;text-align:center;padding:4px">LDA<br>classifier</div> | |
| </div> | |
| <div style="flex:0 0 20px;text-align:center;color:var(--txt3);font-size:12px">→</div> | |
| <div style="text-align:center;min-width:90px"> | |
| <div style="width:60px;height:44px;border-radius:8px;background:#EAF3DE;border:0.5px solid #C0DD97;display:flex;align-items:center;justify-content:center;margin:0 auto 5px;font-size:10px;font-family:var(--mono);color:#27500A;text-align:center;padding:4px">Stable<br>state</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px"> | |
| <div class="card"> | |
| <div class="card-header"><span class="card-title">CSP parameters</span></div> | |
| <div class="card-body"> | |
| <div class="info-row"><span class="info-key">n_components</span><span class="info-val">8</span></div> | |
| <div class="info-row"><span class="info-key">reg</span><span class="info-val">0.05 (Ledoit-Wolf)</span></div> | |
| <div class="info-row"><span class="info-key">log</span><span class="info-val">True</span></div> | |
| <div class="info-row"><span class="info-key">norm_trace</span><span class="info-val">False</span></div> | |
| <div class="info-row"><span class="info-key">epoch window</span><span class="info-val">2–8 s</span></div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"><span class="card-title">Convergence criterion</span></div> | |
| <div class="card-body"> | |
| <div class="info-row"><span class="info-key">Metric</span><span class="info-val">Cosine distance</span></div> | |
| <div class="info-row"><span class="info-key">Convergence threshold</span><span class="info-val">d < 0.12</span></div> | |
| <div class="info-row"><span class="info-key">Rolling window</span><span class="info-val">5 trials</span></div> | |
| <div class="info-row"><span class="info-key">Min. trials kept</span><span class="info-val">8</span></div> | |
| <div class="info-row"><span class="info-key">Min. score</span><span class="info-val">≥ 4 / 5</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <button class="btn" style="align-self:flex-start" onclick="sendPrompt('Beyond CSP+LDA, which EEG mental-imagery classifiers work well with few channels?')">↗ Algorithm alternatives</button> | |
| </div> | |
| </div> | |
| <!-- ======== PANEL: LIBRARY ======== --> | |
| <div class="panel" id="panel-lib"> | |
| <div class="lib-layout"> | |
| <div class="section-title" id="lib-section-title">Library · session</div> | |
| <div class="lib-grid"> | |
| <div class="lib-card" onclick="sendPrompt('How do you compare stable mental states for two imagined objects in a BCI?')"> | |
| <div class="lib-card-name">Apple</div> | |
| <div class="lib-card-meta">Session 1 · 23 trials<br>11 kept · 3 NF sessions</div> | |
| <div class="lib-card-bar"><div class="lib-card-fill" style="width:94%;background:#1D9E75"></div></div> | |
| </div> | |
| <div class="lib-card"> | |
| <div class="lib-card-name">Cube</div> | |
| <div class="lib-card-meta">Session 1 · 20 trials<br>9 kept · 2 NF sessions</div> | |
| <div class="lib-card-bar"><div class="lib-card-fill" style="width:88%;background:#185FA5"></div></div> | |
| </div> | |
| <div class="lib-card"> | |
| <div class="lib-card-name">Flame</div> | |
| <div class="lib-card-meta">Session 2 · 25 trials<br>13 kept · 4 NF sessions</div> | |
| <div class="lib-card-bar"><div class="lib-card-fill" style="width:91%;background:#534AB7"></div></div> | |
| </div> | |
| <div class="lib-card" style="border-color:#FAC775;background:#FAEEDA"> | |
| <div class="lib-card-name" style="color:#633806">Chair</div> | |
| <div class="lib-card-meta" style="color:#854F0B">Session 3 · in progress<br>18 trials · NF active</div> | |
| <div class="lib-card-bar"><div class="lib-card-fill" style="width:76%;background:#BA7517"></div></div> | |
| </div> | |
| <div class="lib-card" style="opacity:0.5;cursor:default"> | |
| <div class="lib-card-name">Spiral</div> | |
| <div class="lib-card-meta">Session 3 · in progress<br>17 trials · phase 2</div> | |
| <div class="lib-card-bar"><div class="lib-card-fill" style="width:31%;background:#888"></div></div> | |
| </div> | |
| <div class="lib-card" style="border-style:dashed;cursor:pointer;display:flex;align-items:center;justify-content:center;min-height:80px" onclick="sendPrompt('Which visual objects work best for a mental-imagery BCI paradigm?')"> | |
| <div style="text-align:center;color:var(--txt3)"> | |
| <div style="font-size:20px;margin-bottom:4px">+</div> | |
| <div style="font-size:11px">New class</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="section-title" style="margin-top:4px">Cross-subject generalization</div> | |
| <div> | |
| <div class="subject-row"> | |
| <div class="subject-avatar" style="background:#E6F1FB;color:#0C447C">S01</div> | |
| <div class="subject-info"> | |
| <div class="subject-name">Subject 01</div> | |
| <div class="subject-meta">4 classes · 86 trials · Ref</div> | |
| </div> | |
| <span class="subject-acc" style="color:#185FA5">91%</span> | |
| </div> | |
| <div class="subject-row"> | |
| <div class="subject-avatar" style="background:#E1F5EE;color:#085041">S02</div> | |
| <div class="subject-info"> | |
| <div class="subject-name">Subject 02</div> | |
| <div class="subject-meta">4 classes · 72 trials · Transfer</div> | |
| </div> | |
| <span class="subject-acc" style="color:#0F6E56">78%</span> | |
| </div> | |
| <div class="subject-row" style="opacity:0.5"> | |
| <div class="subject-avatar" style="background:#F1EFE8;color:#5F5E5A">S03</div> | |
| <div class="subject-info"> | |
| <div class="subject-name">Subject 03</div> | |
| <div class="subject-meta">In progress · session 1</div> | |
| </div> | |
| <span class="subject-acc" style="color:#888">—</span> | |
| </div> | |
| </div> | |
| <button class="btn" style="align-self:flex-start" onclick="sendPrompt('How should I implement transfer learning for cross-subject EEG BCI generalization?')">↗ Cross-subject transfer learning</button> | |
| </div> | |
| </div> | |
| </div><!-- /main --> | |
| </div><!-- /app --> | |
| <script> | |
| </script> | |
| </body> | |
| </html> | |