Spaces:
Running
Running
Download index.html from SMKBK1/STREAM_PREDICTOR: direct link, hf CLI and curl.
- Browser
- Download file 39.3 kB
-
https://huggingface.co/spaces/SMKBK1/STREAM_PREDICTOR/resolve/main/index.html
- Command line
-
hf download hf://spaces/SMKBK1/STREAM_PREDICTOR/index.html
-
curl -L -o index.html https://huggingface.co/spaces/SMKBK1/STREAM_PREDICTOR/resolve/main/index.html
39.3 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AuraStudy // Placement Console</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;600;700&family=Share+Tech+Mono&family=Rajdhani:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #05080f; | |
| --bg-2: #0a101c; | |
| --panel: #0d1524; | |
| --panel-2: #111b2e; | |
| --line: rgba(0, 245, 255, 0.12); | |
| --line-strong: rgba(0, 245, 255, 0.28); | |
| --cyan: #00f5ff; | |
| --cyan-dim: rgba(0, 245, 255, 0.12); | |
| --cyan-glow: rgba(0, 245, 255, 0.45); | |
| --magenta: #ff2bd6; | |
| --magenta-dim: rgba(255, 43, 214, 0.12); | |
| --lime: #b8ff3c; | |
| --lime-dim: rgba(184, 255, 60, 0.12); | |
| --amber: #ffb020; | |
| --amber-dim: rgba(255, 176, 32, 0.12); | |
| --red: #ff4d6d; | |
| --red-dim: rgba(255, 77, 109, 0.14); | |
| --violet: #a78bfa; | |
| --violet-dim: rgba(167, 139, 250, 0.14); | |
| --text: #e6f7ff; | |
| --text-2: #8ba3b8; | |
| --text-3: #4d6478; | |
| --font: "Rajdhani", system-ui, sans-serif; | |
| --display: "Orbitron", sans-serif; | |
| --mono: "Share Tech Mono", ui-monospace, monospace; | |
| --r: 4px; | |
| } | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| html, body { height: 100%; } | |
| body { | |
| font-family: var(--font); | |
| font-size: 14.5px; | |
| color: var(--text); | |
| background: var(--bg); | |
| line-height: 1.4; | |
| overflow: hidden; | |
| } | |
| /* cyber grid + scan */ | |
| body::before { | |
| content: ""; | |
| position: fixed; | |
| inset: 0; | |
| background: | |
| linear-gradient(rgba(0, 245, 255, 0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0, 245, 255, 0.03) 1px, transparent 1px); | |
| background-size: 48px 48px; | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| body::after { | |
| content: ""; | |
| position: fixed; | |
| inset: 0; | |
| background: repeating-linear-gradient( | |
| 0deg, | |
| transparent, | |
| transparent 2px, | |
| rgba(0, 0, 0, 0.12) 2px, | |
| rgba(0, 0, 0, 0.12) 4px | |
| ); | |
| pointer-events: none; | |
| z-index: 50; | |
| opacity: 0.35; | |
| } | |
| .app { | |
| position: relative; | |
| z-index: 1; | |
| display: grid; | |
| grid-template-rows: 54px 1fr; | |
| height: 100vh; | |
| min-width: 1100px; | |
| } | |
| /* ── Titlebar ── */ | |
| .titlebar { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| padding: 0 18px; | |
| background: linear-gradient(180deg, #0e1628 0%, #0a101c 100%); | |
| border-bottom: 1px solid var(--line-strong); | |
| box-shadow: 0 0 24px rgba(0, 245, 255, 0.06); | |
| z-index: 20; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| min-width: 260px; | |
| } | |
| .logo { | |
| width: 32px; | |
| height: 32px; | |
| clip-path: polygon(20% 0%, 100% 0%, 100% 80%, 80% 100%, 0% 100%, 0% 20%); | |
| background: linear-gradient(135deg, var(--cyan), #007a88); | |
| color: #05080f; | |
| font-family: var(--display); | |
| font-weight: 700; | |
| font-size: 11px; | |
| display: grid; | |
| place-items: center; | |
| box-shadow: 0 0 16px var(--cyan-glow); | |
| } | |
| .brand-text { | |
| font-family: var(--display); | |
| font-size: 13px; | |
| font-weight: 600; | |
| letter-spacing: 0.08em; | |
| color: var(--cyan); | |
| text-shadow: 0 0 12px var(--cyan-glow); | |
| } | |
| .brand-text span { | |
| display: block; | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| font-weight: 400; | |
| letter-spacing: 0.16em; | |
| color: var(--text-3); | |
| text-shadow: none; | |
| margin-top: 1px; | |
| } | |
| .title-center { flex: 1; display: flex; justify-content: center; } | |
| .seg { | |
| display: inline-flex; | |
| gap: 0; | |
| border: 1px solid var(--line-strong); | |
| background: var(--bg); | |
| } | |
| .seg button { | |
| border: none; | |
| background: transparent; | |
| padding: 8px 22px; | |
| font: 600 13px var(--font); | |
| letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| color: var(--text-3); | |
| cursor: pointer; | |
| position: relative; | |
| } | |
| .seg button + button { border-left: 1px solid var(--line); } | |
| .seg button:hover { color: var(--cyan); } | |
| .seg button.on { | |
| color: var(--bg); | |
| background: var(--cyan); | |
| box-shadow: 0 0 20px var(--cyan-glow); | |
| } | |
| .title-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| min-width: 260px; | |
| justify-content: flex-end; | |
| } | |
| .status { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 7px; | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| letter-spacing: 0.06em; | |
| color: var(--text-3); | |
| } | |
| .status i { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 1px; | |
| background: var(--text-3); | |
| box-shadow: 0 0 0 1px rgba(0,0,0,.5); | |
| } | |
| .status.ok { color: var(--lime); } | |
| .status.ok i { | |
| background: var(--lime); | |
| box-shadow: 0 0 10px var(--lime), 0 0 0 1px rgba(0,0,0,.4); | |
| animation: blink 1.6s step-end infinite; | |
| } | |
| .status.warn { color: var(--amber); } | |
| .status.warn i { background: var(--amber); box-shadow: 0 0 8px var(--amber); } | |
| @keyframes blink { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.25; } | |
| } | |
| /* ── Body ── */ | |
| .body { | |
| display: grid; | |
| grid-template-columns: 300px 1fr; | |
| min-height: 0; | |
| overflow: hidden; | |
| } | |
| .sidebar { | |
| background: var(--panel); | |
| border-right: 1px solid var(--line); | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 0; | |
| position: relative; | |
| } | |
| .sidebar::before { | |
| content: ""; | |
| position: absolute; | |
| top: 0; right: 0; bottom: 0; | |
| width: 1px; | |
| background: linear-gradient(180deg, var(--cyan), transparent 40%, var(--magenta)); | |
| opacity: 0.35; | |
| pointer-events: none; | |
| } | |
| .sidebar-scroll { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 16px 14px 28px; | |
| } | |
| .side-label { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| letter-spacing: 0.18em; | |
| text-transform: uppercase; | |
| color: var(--cyan); | |
| margin-bottom: 10px; | |
| padding-left: 2px; | |
| text-shadow: 0 0 8px var(--cyan-dim); | |
| } | |
| .side-section { margin-bottom: 22px; } | |
| .drop { | |
| position: relative; | |
| border: 1px dashed var(--line-strong); | |
| background: var(--bg); | |
| padding: 26px 12px; | |
| text-align: center; | |
| cursor: pointer; | |
| clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px)); | |
| transition: border-color .15s, box-shadow .15s; | |
| } | |
| .drop:hover, .drop.drag { | |
| border-color: var(--cyan); | |
| box-shadow: inset 0 0 24px var(--cyan-dim), 0 0 16px var(--cyan-dim); | |
| } | |
| .drop input { position: absolute; inset: 0; opacity: 0; cursor: pointer; } | |
| .drop-ico { | |
| font-family: var(--display); | |
| font-size: 18px; | |
| color: var(--cyan); | |
| margin-bottom: 8px; | |
| text-shadow: 0 0 12px var(--cyan-glow); | |
| } | |
| .drop-title { | |
| font-weight: 600; | |
| font-size: 14px; | |
| letter-spacing: 0.04em; | |
| color: var(--text); | |
| } | |
| .drop-sub { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| color: var(--text-3); | |
| margin-top: 4px; | |
| } | |
| .file-row { | |
| display: none; | |
| align-items: center; | |
| gap: 8px; | |
| margin-top: 10px; | |
| padding: 8px 10px; | |
| background: var(--cyan-dim); | |
| border: 1px solid var(--line-strong); | |
| } | |
| .file-row.show { display: flex; } | |
| .file-row .fn { | |
| flex: 1; | |
| font-family: var(--mono); | |
| font-size: 11.5px; | |
| color: var(--cyan); | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .file-row .fs { font-family: var(--mono); font-size: 11px; color: var(--text-3); } | |
| .field { margin-bottom: 12px; } | |
| .field label { | |
| display: block; | |
| font-family: var(--mono); | |
| font-size: 10.5px; | |
| letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| color: var(--text-3); | |
| margin-bottom: 5px; | |
| } | |
| .field input { | |
| width: 100%; | |
| height: 34px; | |
| padding: 0 10px; | |
| border: 1px solid var(--line); | |
| background: var(--bg); | |
| font: 500 13px var(--mono); | |
| color: var(--text); | |
| border-radius: 0; | |
| } | |
| .field input:focus { | |
| outline: none; | |
| border-color: var(--cyan); | |
| box-shadow: 0 0 0 1px var(--cyan), 0 0 12px var(--cyan-dim); | |
| } | |
| .field input::placeholder { color: var(--text-3); } | |
| .switch-row { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px 0; | |
| font-size: 13.5px; | |
| font-weight: 500; | |
| color: var(--text-2); | |
| } | |
| .switch { | |
| position: relative; | |
| width: 38px; | |
| height: 18px; | |
| } | |
| .switch input { opacity: 0; width: 0; height: 0; } | |
| .switch span { | |
| position: absolute; | |
| inset: 0; | |
| background: var(--bg); | |
| border: 1px solid var(--line-strong); | |
| cursor: pointer; | |
| transition: .15s; | |
| } | |
| .switch span::after { | |
| content: ""; | |
| position: absolute; | |
| width: 10px; | |
| height: 10px; | |
| left: 3px; | |
| top: 3px; | |
| background: var(--text-3); | |
| transition: .15s; | |
| } | |
| .switch input:checked + span { | |
| border-color: var(--cyan); | |
| box-shadow: 0 0 10px var(--cyan-dim); | |
| } | |
| .switch input:checked + span::after { | |
| transform: translateX(18px); | |
| background: var(--cyan); | |
| box-shadow: 0 0 8px var(--cyan); | |
| } | |
| .stack { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; } | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| height: 36px; | |
| padding: 0 14px; | |
| font: 600 13px var(--font); | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| cursor: pointer; | |
| border: 1px solid transparent; | |
| transition: .12s; | |
| width: 100%; | |
| clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 8px 100%, 0 calc(100% - 8px)); | |
| } | |
| .btn:disabled { opacity: .35; cursor: not-allowed; } | |
| .btn-primary { | |
| background: var(--cyan); | |
| color: #05080f; | |
| border-color: var(--cyan); | |
| box-shadow: 0 0 18px var(--cyan-dim); | |
| } | |
| .btn-primary:hover:not(:disabled) { | |
| box-shadow: 0 0 28px var(--cyan-glow); | |
| filter: brightness(1.08); | |
| } | |
| .btn-secondary { | |
| background: transparent; | |
| color: var(--amber); | |
| border-color: rgba(255, 176, 32, 0.45); | |
| } | |
| .btn-secondary:hover:not(:disabled) { | |
| background: var(--amber-dim); | |
| box-shadow: 0 0 14px var(--amber-dim); | |
| } | |
| .btn-ghost { | |
| background: transparent; | |
| color: var(--text-2); | |
| border-color: var(--line); | |
| } | |
| .btn-ghost:hover:not(:disabled) { | |
| color: var(--cyan); | |
| border-color: var(--line-strong); | |
| } | |
| .hint-box { | |
| font-size: 12px; | |
| color: var(--text-3); | |
| line-height: 1.5; | |
| padding: 10px 12px; | |
| background: var(--bg); | |
| border: 1px solid var(--line); | |
| margin-bottom: 12px; | |
| } | |
| .hint-box strong { color: var(--magenta); font-weight: 600; letter-spacing: 0.06em; } | |
| .hint-box code { | |
| font-family: var(--mono); | |
| font-size: 10.5px; | |
| color: var(--cyan); | |
| background: var(--cyan-dim); | |
| padding: 1px 4px; | |
| } | |
| .hint-box ol { margin: 6px 0 0 14px; } | |
| .hint-box li { margin-bottom: 3px; } | |
| .col-guide { | |
| font-size: 12px; | |
| color: var(--text-2); | |
| line-height: 1.55; | |
| font-family: var(--mono); | |
| } | |
| .col-guide .k { | |
| display: inline-block; | |
| font-size: 10.5px; | |
| color: var(--cyan); | |
| border: 1px solid var(--line); | |
| padding: 1px 5px; | |
| margin: 1px 2px 1px 0; | |
| background: var(--bg); | |
| } | |
| .col-guide .h { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| letter-spacing: 0.14em; | |
| text-transform: uppercase; | |
| color: var(--text-3); | |
| margin: 10px 0 4px; | |
| } | |
| .col-guide .h:first-child { margin-top: 0; } | |
| /* ── Main ── */ | |
| .main { | |
| display: flex; | |
| flex-direction: column; | |
| min-width: 0; | |
| min-height: 0; | |
| background: var(--bg-2); | |
| } | |
| .main-toolbar { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 10px 18px; | |
| background: var(--panel); | |
| border-bottom: 1px solid var(--line); | |
| flex-shrink: 0; | |
| } | |
| .main-toolbar h2 { | |
| font-family: var(--display); | |
| font-size: 12px; | |
| font-weight: 600; | |
| letter-spacing: 0.14em; | |
| color: var(--text); | |
| margin-right: auto; | |
| } | |
| .main-toolbar .meta { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| color: var(--text-3); | |
| letter-spacing: 0.06em; | |
| } | |
| .progress { | |
| height: 2px; | |
| background: transparent; | |
| flex-shrink: 0; | |
| overflow: hidden; | |
| } | |
| .progress.on { background: rgba(0, 245, 255, 0.15); } | |
| .progress.on::after { | |
| content: ""; | |
| display: block; | |
| height: 100%; | |
| width: 28%; | |
| background: linear-gradient(90deg, transparent, var(--cyan), var(--magenta), transparent); | |
| box-shadow: 0 0 12px var(--cyan); | |
| animation: scan 1.1s linear infinite; | |
| } | |
| @keyframes scan { | |
| 0% { transform: translateX(-120%); } | |
| 100% { transform: translateX(450%); } | |
| } | |
| .main-body { | |
| flex: 1; | |
| overflow: auto; | |
| padding: 18px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 14px; | |
| } | |
| .empty { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| text-align: center; | |
| min-height: 340px; | |
| background: var(--panel); | |
| border: 1px solid var(--line); | |
| position: relative; | |
| } | |
| .empty::before { | |
| content: ""; | |
| position: absolute; | |
| inset: 0; | |
| background: | |
| linear-gradient(var(--line) 1px, transparent 1px), | |
| linear-gradient(90deg, var(--line) 1px, transparent 1px); | |
| background-size: 32px 32px; | |
| opacity: 0.5; | |
| pointer-events: none; | |
| } | |
| .empty-ico { | |
| position: relative; | |
| font-family: var(--display); | |
| font-size: 28px; | |
| color: var(--cyan); | |
| text-shadow: 0 0 20px var(--cyan-glow); | |
| margin-bottom: 14px; | |
| } | |
| .empty h3 { | |
| position: relative; | |
| font-family: var(--display); | |
| font-size: 14px; | |
| letter-spacing: 0.12em; | |
| margin-bottom: 8px; | |
| } | |
| .empty p { | |
| position: relative; | |
| font-size: 13.5px; | |
| color: var(--text-3); | |
| max-width: 360px; | |
| line-height: 1.5; | |
| } | |
| .kpis { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 12px; | |
| } | |
| .kpi { | |
| background: var(--panel); | |
| border: 1px solid var(--line); | |
| padding: 14px 16px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .kpi::after { | |
| content: ""; | |
| position: absolute; | |
| top: 0; right: 0; | |
| width: 24px; height: 24px; | |
| border-top: 1px solid var(--line-strong); | |
| border-right: 1px solid var(--line-strong); | |
| } | |
| .kpi .l { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| letter-spacing: 0.14em; | |
| text-transform: uppercase; | |
| color: var(--text-3); | |
| margin-bottom: 6px; | |
| } | |
| .kpi .v { | |
| font-family: var(--display); | |
| font-size: 24px; | |
| font-weight: 700; | |
| letter-spacing: 0.04em; | |
| color: var(--text); | |
| text-shadow: 0 0 16px rgba(230, 247, 255, 0.15); | |
| } | |
| .kpi .v.g { color: var(--lime); text-shadow: 0 0 14px rgba(184,255,60,.4); } | |
| .kpi .v.r { color: var(--red); text-shadow: 0 0 14px rgba(255,77,109,.35); } | |
| .kpi .v.t { color: var(--cyan); text-shadow: 0 0 14px var(--cyan-glow); } | |
| .panel { | |
| background: var(--panel); | |
| border: 1px solid var(--line); | |
| overflow: hidden; | |
| } | |
| .panel-h { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 10px 14px; | |
| border-bottom: 1px solid var(--line); | |
| background: var(--panel-2); | |
| } | |
| .panel-h h3 { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| letter-spacing: 0.16em; | |
| text-transform: uppercase; | |
| color: var(--cyan); | |
| } | |
| .panel-b { padding: 14px; } | |
| .dist { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 16px; | |
| } | |
| .dist-item .name { | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: var(--text-2); | |
| margin-bottom: 6px; | |
| display: flex; | |
| justify-content: space-between; | |
| letter-spacing: 0.03em; | |
| } | |
| .dist-item .name b { | |
| font-family: var(--mono); | |
| font-weight: 500; | |
| color: var(--text); | |
| } | |
| .dist-bar { | |
| height: 5px; | |
| background: var(--bg); | |
| border: 1px solid var(--line); | |
| overflow: hidden; | |
| } | |
| .dist-bar i { | |
| display: block; | |
| height: 100%; | |
| transition: width .55s cubic-bezier(.22,1,.36,1); | |
| } | |
| .dist-bar i.s { background: var(--cyan); box-shadow: 0 0 10px var(--cyan); } | |
| .dist-bar i.r { background: #3d9eff; box-shadow: 0 0 10px #3d9eff; } | |
| .dist-bar i.a { background: var(--violet); box-shadow: 0 0 10px var(--violet); } | |
| .dist-bar i.o { background: var(--amber); box-shadow: 0 0 10px var(--amber); } | |
| .tools { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 10px 14px; | |
| border-bottom: 1px solid var(--line); | |
| background: var(--panel-2); | |
| } | |
| .search { position: relative; flex: 1; max-width: 260px; } | |
| .search input { | |
| width: 100%; | |
| height: 30px; | |
| padding: 0 10px 0 28px; | |
| border: 1px solid var(--line); | |
| background: var(--bg); | |
| font: 500 12.5px var(--mono); | |
| color: var(--text); | |
| } | |
| .search input:focus { | |
| outline: none; | |
| border-color: var(--cyan); | |
| box-shadow: 0 0 10px var(--cyan-dim); | |
| } | |
| .search::before { | |
| content: "›"; | |
| position: absolute; | |
| left: 10px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--cyan); | |
| font-family: var(--mono); | |
| font-size: 14px; | |
| } | |
| .select { | |
| height: 30px; | |
| padding: 0 10px; | |
| border: 1px solid var(--line); | |
| background: var(--bg); | |
| font: 500 12.5px var(--mono); | |
| color: var(--text); | |
| cursor: pointer; | |
| } | |
| .select:focus { outline: none; border-color: var(--cyan); } | |
| .link { | |
| margin-left: auto; | |
| font-family: var(--mono); | |
| font-size: 11.5px; | |
| letter-spacing: 0.06em; | |
| color: var(--cyan); | |
| text-decoration: none; | |
| text-shadow: 0 0 8px var(--cyan-dim); | |
| } | |
| .link:hover { text-decoration: underline; } | |
| .table-wrap { | |
| overflow: auto; | |
| max-height: calc(100vh - 400px); | |
| min-height: 180px; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 12.5px; | |
| } | |
| thead th { | |
| position: sticky; | |
| top: 0; | |
| z-index: 2; | |
| background: #0a1220; | |
| text-align: left; | |
| padding: 9px 12px; | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| font-weight: 500; | |
| letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| color: var(--text-3); | |
| border-bottom: 1px solid var(--line-strong); | |
| white-space: nowrap; | |
| } | |
| tbody td { | |
| padding: 8px 12px; | |
| border-bottom: 1px solid var(--line); | |
| color: var(--text); | |
| white-space: nowrap; | |
| max-width: 150px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| } | |
| tbody tr:hover td { | |
| background: rgba(0, 245, 255, 0.04); | |
| } | |
| tbody tr:last-child td { border-bottom: none; } | |
| .tag { | |
| display: inline-flex; | |
| align-items: center; | |
| padding: 2px 8px; | |
| font-size: 10.5px; | |
| font-weight: 500; | |
| font-family: var(--mono); | |
| letter-spacing: 0.04em; | |
| border: 1px solid transparent; | |
| } | |
| .tag-ok { background: var(--lime-dim); color: var(--lime); border-color: rgba(184,255,60,.3); } | |
| .tag-fail { background: var(--red-dim); color: var(--red); border-color: rgba(255,77,109,.3); } | |
| .tag-s { background: var(--cyan-dim); color: var(--cyan); border-color: var(--line-strong); } | |
| .tag-r { background: rgba(61,158,255,.12); color: #6bb3ff; border-color: rgba(61,158,255,.3); } | |
| .tag-a { background: var(--violet-dim); color: var(--violet); border-color: rgba(167,139,250,.3); } | |
| .tag-o { background: var(--amber-dim); color: var(--amber); border-color: rgba(255,176,32,.3); } | |
| .tag-high { background: var(--lime-dim); color: var(--lime); border-color: rgba(184,255,60,.3); } | |
| .tag-med { background: var(--amber-dim); color: var(--amber); border-color: rgba(255,176,32,.3); } | |
| .tag-low { background: var(--red-dim); color: var(--red); border-color: rgba(255,77,109,.3); } | |
| .table-foot { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 8px 14px; | |
| border-top: 1px solid var(--line); | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| letter-spacing: 0.06em; | |
| color: var(--text-3); | |
| background: var(--panel-2); | |
| } | |
| .toasts { | |
| position: fixed; | |
| bottom: 18px; | |
| right: 18px; | |
| z-index: 100; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| max-width: 360px; | |
| } | |
| .toast { | |
| display: flex; | |
| gap: 10px; | |
| align-items: flex-start; | |
| padding: 12px 14px; | |
| background: var(--panel); | |
| border: 1px solid var(--line-strong); | |
| box-shadow: 0 8px 32px rgba(0,0,0,.5), 0 0 20px var(--cyan-dim); | |
| font-size: 13px; | |
| animation: up .2s ease; | |
| } | |
| @keyframes up { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: none; } | |
| } | |
| .toast.ok { border-left: 3px solid var(--lime); } | |
| .toast.err { border-left: 3px solid var(--red); } | |
| .toast .msg { flex: 1; color: var(--text); line-height: 1.4; font-family: var(--mono); font-size: 12px; } | |
| .toast button { | |
| border: none; | |
| background: none; | |
| color: var(--text-3); | |
| cursor: pointer; | |
| font-size: 14px; | |
| line-height: 1; | |
| } | |
| /* Google Sheets tutorial */ | |
| .sheet-tutorial { margin-top: 12px; border: 1px solid var(--line-strong); background: var(--panel-2); } | |
| .sheet-tutorial summary { cursor: pointer; list-style: none; padding: 13px 14px; color: var(--cyan); font-family: var(--mono); font-size: 14px; font-weight: 700; letter-spacing: .04em; display: flex; align-items: center; justify-content: space-between; gap: 8px; } | |
| .sheet-tutorial summary::-webkit-details-marker { display: none; } | |
| .sheet-tutorial summary::after { content: '+'; font-size: 20px; color: var(--lime); } | |
| .sheet-tutorial[open] summary::after { content: '−'; } | |
| .sheet-tutorial-content { padding: 0 14px 14px; color: var(--text-2); font-size: 14px; line-height: 1.5; } | |
| .sheet-tutorial-content .tutorial-step { padding: 11px 0; border-top: 1px solid var(--line); } | |
| .sheet-tutorial-content .tutorial-step b { display: block; margin-bottom: 4px; color: var(--text); font-size: 14px; } | |
| .sheet-tutorial-content code { overflow-wrap: anywhere; } | |
| .sheet-tutorial-content a { color: var(--cyan); } | |
| .sheet-tutorial-content .warning { margin-top: 8px; padding: 9px; border-left: 2px solid var(--lime); background: rgba(0,245,255,.04); color: var(--text); font-size: 12px; } | |
| .service-email-row { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:8px; padding:10px; border:1px solid var(--line-strong); background:rgba(0,245,255,.04); } | |
| .service-email { flex:1; min-width:180px; color:var(--cyan); font-family:var(--mono); font-size:13px; overflow-wrap:anywhere; user-select:all; } | |
| .copy-email-btn { border:1px solid var(--line-strong); background:var(--cyan-dim); color:var(--cyan); padding:7px 10px; font:700 12px var(--font); cursor:pointer; white-space:nowrap; } | |
| .copy-email-btn:hover { background:var(--cyan); color:var(--bg); } | |
| .hidden { display: none ; } | |
| .credit { | |
| position: fixed; | |
| bottom: 6px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| font-family: var(--mono); | |
| font-size: 8px; | |
| letter-spacing: 0.2em; | |
| text-transform: lowercase; | |
| color: rgba(0, 245, 255, 0.06); | |
| pointer-events: none; | |
| user-select: none; | |
| z-index: 1; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <header class="titlebar"> | |
| <div class="brand"> | |
| <div class="logo">AS</div> | |
| <div class="brand-text">AURASTUDY<span>PLACEMENT // CONSOLE</span></div> | |
| </div> | |
| <div class="title-center"> | |
| <div class="seg"> | |
| <button type="button" class="on" data-src="csv">CSV</button> | |
| <button type="button" data-src="sheet">Sheet</button> | |
| </div> | |
| </div> | |
| <div class="title-right"> | |
| <div class="status" id="apiStatus"><i></i> API</div> | |
| <div class="status" id="modelStatus"><i></i> MODEL</div> | |
| </div> | |
| </header> | |
| <div class="body"> | |
| <aside class="sidebar"> | |
| <div class="sidebar-scroll"> | |
| <div id="pane-csv"> | |
| <div class="side-section"> | |
| <div class="side-label">// Import</div> | |
| <div class="drop" id="dropZone"> | |
| <input type="file" id="csvFile" accept=".csv,text/csv"> | |
| <div class="drop-ico">↑</div> | |
| <div class="drop-title">DROP CSV</div> | |
| <div class="drop-sub">UTF-8 · ≥5 subject cols</div> | |
| </div> | |
| <div class="file-row" id="fileRow"> | |
| <span class="fn" id="fileName">—</span> | |
| <span class="fs" id="fileSize"></span> | |
| </div> | |
| <div class="stack"> | |
| <button type="button" class="btn btn-ghost" id="btnTemplate">Template</button> | |
| <button type="button" class="btn btn-primary" id="btnRunCsv" disabled>Run predictions</button> | |
| <button type="button" class="btn btn-secondary" id="btnDownload" disabled>Export CSV</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="pane-sheet" class="hidden"> | |
| <div class="side-section"> | |
| <div class="side-label">// Google Sheet</div> | |
| <div class="hint-box"> | |
| <strong>SETUP</strong> | |
| <ol> | |
| <li>Share sheet with the app as Editor</li> | |
| <li>Header: Name + scores</li> | |
| </ol> | |
| </div> | |
| <details class="sheet-tutorial" open> | |
| <summary>GOOGLE SHEETS · QUICK GUIDE</summary> | |
| <div class="sheet-tutorial-content"> | |
| <div class="tutorial-step"> | |
| <b>1. Share your sheet with the app</b> | |
| Click <b>Share</b> in Google Sheets, add this service email, and set access to <b>Editor</b>: | |
| <div class="service-email-row"> | |
| <span class="service-email" id="serviceEmail">streampredictor-sheets@solid-pact-472307-k4.iam.gserviceaccount.com</span> | |
| <button type="button" class="copy-email-btn" id="copyServiceEmail">COPY EMAIL</button> | |
| </div> | |
| <div style="margin-top:6px">Keep the sheet restricted; do not make it public.</div> | |
| </div> | |
| <div class="tutorial-step"> | |
| <b>2. Prepare your student data</b> | |
| Use one student per row, a header row, a student-name column (e.g. <code>Name</code>), and at least five recognised subject-score columns. Enter scores as numbers. | |
| </div> | |
| <div class="tutorial-step"> | |
| <b>3. Run and save predictions</b> | |
| Paste your Google Sheet URL above, optionally enter the worksheet tab name, keep <b>Write results back</b> enabled, then click <b>Run on sheet</b>. Reopen the sheet to check the new prediction columns. | |
| </div> | |
| <div class="warning"><b>Before using real student data:</b> test with a copy first and only process data you are authorised to use.</div> | |
| </div> | |
| </details> | |
| <div class="field"> | |
| <label for="sheetUrl">Spreadsheet URL / ID</label> | |
| <input type="url" id="sheetUrl" placeholder="docs.google.com/spreadsheets/d/…"> | |
| </div> | |
| <div class="field"> | |
| <label for="worksheet">Worksheet tab</label> | |
| <input type="text" id="worksheet" placeholder="optional"> | |
| </div> | |
| <div class="switch-row"> | |
| <span>Write results back</span> | |
| <label class="switch"><input type="checkbox" id="writeBack" checked><span></span></label> | |
| </div> | |
| <div class="stack"> | |
| <button type="button" class="btn btn-primary" id="btnRunSheet">Run on sheet</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="side-section"> | |
| <div class="side-label">// Columns</div> | |
| <div class="col-guide"> | |
| <div class="h">Subjects ≥5</div> | |
| <span class="k">BM</span><span class="k">BI</span><span class="k">Math</span><span class="k">Science</span><br> | |
| <span class="k">Sejarah</span><span class="k">Geo</span><span class="k">RBT</span><span class="k">PSV</span> | |
| <div class="h">Identity</div> | |
| <span class="k">Name</span><span class="k">Class</span><span class="k">ID</span> | |
| <div class="h">Aliases</div> | |
| F3_BM · Bahasa Melayu · Sains | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <main class="main"> | |
| <div class="main-toolbar"> | |
| <h2>RESULTS</h2> | |
| <span class="meta" id="runMeta">STANDBY</span> | |
| </div> | |
| <div class="progress" id="progress"></div> | |
| <div class="main-body"> | |
| <div class="empty" id="empty"> | |
| <div class="empty-ico">◈</div> | |
| <h3>NO BATCH LOADED</h3> | |
| <p>Import class CSV or link a Google Sheet from the left console to run stream placement.</p> | |
| </div> | |
| <div id="results" class="hidden"> | |
| <div class="kpis"> | |
| <div class="kpi"><div class="l">Students</div><div class="v" id="kpiTotal">0</div></div> | |
| <div class="kpi"><div class="l">Predicted</div><div class="v g" id="kpiOk">0</div></div> | |
| <div class="kpi"><div class="l">Failed</div><div class="v r" id="kpiFail">0</div></div> | |
| <div class="kpi"><div class="l">Subjects</div><div class="v t" id="kpiSubj">—</div></div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-h"><h3>Stream distribution</h3></div> | |
| <div class="panel-b"><div class="dist" id="dist"></div></div> | |
| </div> | |
| <div class="panel"> | |
| <div class="tools"> | |
| <div class="search"><input type="search" id="search" placeholder="filter rows…"></div> | |
| <select class="select" id="streamFilter"> | |
| <option value="">ALL STREAMS</option> | |
| <option>Science</option> | |
| <option>Reka Cipta</option> | |
| <option>Account</option> | |
| <option>Art / Other</option> | |
| </select> | |
| <a class="link hidden" id="sheetLink" href="#" target="_blank" rel="noopener">OPEN SHEET →</a> | |
| </div> | |
| <div class="table-wrap"> | |
| <table> | |
| <thead id="thead"></thead> | |
| <tbody id="tbody"></tbody> | |
| </table> | |
| </div> | |
| <div class="table-foot"> | |
| <span id="footLeft">—</span> | |
| <span id="footRight"></span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| </div> | |
| <div class="credit" aria-hidden="true">develop by syuan</div> | |
| <div class="toasts" id="toasts"></div> | |
| <script> | |
| (() => { | |
| const API = "https://syuanteng315-spm-predictor.hf.space"; | |
| const $ = (id) => document.getElementById(id); | |
| let file = null, rows = [], cols = []; | |
| $("copyServiceEmail").addEventListener("click", async () => { | |
| const email = $("serviceEmail").textContent.trim(); | |
| const button = $("copyServiceEmail"); | |
| try { | |
| await navigator.clipboard.writeText(email); | |
| button.textContent = "COPIED!"; | |
| } catch { | |
| const temp = document.createElement("textarea"); | |
| temp.value = email; | |
| temp.style.position = "fixed"; | |
| temp.style.opacity = "0"; | |
| document.body.appendChild(temp); | |
| temp.select(); | |
| const copied = document.execCommand("copy"); | |
| temp.remove(); | |
| button.textContent = copied ? "COPIED!" : "SELECT EMAIL"; | |
| } | |
| setTimeout(() => { button.textContent = "COPY EMAIL"; }, 1800); | |
| }); | |
| async function ping() { | |
| try { | |
| const r = await fetch(API + "/"); | |
| const d = await r.json(); | |
| if (r.ok) { | |
| $("apiStatus").classList.add("ok"); | |
| $("apiStatus").innerHTML = "<i></i> API ONLINE"; | |
| } | |
| if (d.model_loaded) { | |
| $("modelStatus").classList.add("ok"); | |
| $("modelStatus").innerHTML = "<i></i> MODEL READY"; | |
| } else { | |
| $("modelStatus").classList.add("warn"); | |
| $("modelStatus").innerHTML = "<i></i> MODEL OFF"; | |
| } | |
| } catch { | |
| $("apiStatus").classList.add("warn"); | |
| $("apiStatus").innerHTML = "<i></i> API OFF"; | |
| } | |
| } | |
| ping(); | |
| document.querySelectorAll(".seg button").forEach((b) => { | |
| b.addEventListener("click", () => { | |
| document.querySelectorAll(".seg button").forEach((x) => x.classList.remove("on")); | |
| b.classList.add("on"); | |
| const src = b.dataset.src; | |
| $("pane-csv").classList.toggle("hidden", src !== "csv"); | |
| $("pane-sheet").classList.toggle("hidden", src !== "sheet"); | |
| }); | |
| }); | |
| function setFile(f) { | |
| if (!f) return; | |
| file = f; | |
| $("fileName").textContent = f.name; | |
| $("fileSize").textContent = (f.size / 1024).toFixed(1) + " KB"; | |
| $("fileRow").classList.add("show"); | |
| $("btnRunCsv").disabled = false; | |
| $("btnDownload").disabled = false; | |
| } | |
| $("csvFile").addEventListener("change", () => setFile($("csvFile").files[0])); | |
| const dz = $("dropZone"); | |
| ["dragenter", "dragover"].forEach((e) => dz.addEventListener(e, (ev) => { ev.preventDefault(); dz.classList.add("drag"); })); | |
| ["dragleave", "drop"].forEach((e) => dz.addEventListener(e, (ev) => { ev.preventDefault(); dz.classList.remove("drag"); })); | |
| dz.addEventListener("drop", (ev) => { const f = ev.dataTransfer.files[0]; if (f) setFile(f); }); | |
| $("btnTemplate").onclick = () => { location.href = API + "/bulk/template.csv"; }; | |
| function toast(msg, type = "ok") { | |
| const el = document.createElement("div"); | |
| el.className = "toast " + type; | |
| el.innerHTML = '<span class="msg">' + esc(msg) + '</span><button type="button">×</button>'; | |
| el.querySelector("button").onclick = () => el.remove(); | |
| $("toasts").appendChild(el); | |
| setTimeout(() => el.remove(), 5500); | |
| } | |
| function load(on) { | |
| $("progress").classList.toggle("on", on); | |
| $("runMeta").textContent = on ? "PROCESSING…" : "STANDBY"; | |
| } | |
| function esc(s) { | |
| return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""); | |
| } | |
| function streamTag(n) { | |
| const x = (n || "").toLowerCase(); | |
| if (x.includes("science") && !x.includes("reka")) return "tag-s"; | |
| if (x.includes("reka")) return "tag-r"; | |
| if (x.includes("account")) return "tag-a"; | |
| if (x.includes("art")) return "tag-o"; | |
| return "tag-s"; | |
| } | |
| function confTag(c) { | |
| const x = (c || "").toLowerCase(); | |
| if (x === "high") return "tag-high"; | |
| if (x === "medium") return "tag-med"; | |
| if (x === "borderline") return "tag-low"; | |
| return ""; | |
| } | |
| function barClass(n) { | |
| const x = (n || "").toLowerCase(); | |
| if (x.includes("science") && !x.includes("reka")) return "s"; | |
| if (x.includes("reka")) return "r"; | |
| if (x.includes("account")) return "a"; | |
| return "o"; | |
| } | |
| function show(data) { | |
| $("empty").classList.add("hidden"); | |
| $("results").classList.remove("hidden"); | |
| const list = data.rows || data.rows_preview || []; | |
| rows = list; | |
| $("kpiTotal").textContent = data.total ?? list.length; | |
| $("kpiOk").textContent = data.ok ?? 0; | |
| $("kpiFail").textContent = data.failed ?? 0; | |
| $("kpiSubj").textContent = (data.subjects_detected || []).length || "—"; | |
| $("runMeta").textContent = "LAST // " + new Date().toLocaleTimeString(); | |
| const counts = {}; | |
| list.forEach((r) => { | |
| const s = r["Recommended Stream"] || ""; | |
| if (s) counts[s] = (counts[s] || 0) + 1; | |
| }); | |
| const tot = Object.values(counts).reduce((a, b) => a + b, 0) || 1; | |
| const order = ["Science", "Reka Cipta", "Account", "Art / Other"]; | |
| $("dist").innerHTML = order.map((name) => { | |
| const n = counts[name] || 0; | |
| const pct = Math.round((n / tot) * 100); | |
| return '<div class="dist-item"><div class="name">' + esc(name) + "<b>" + n + "</b></div>" + | |
| '<div class="dist-bar"><i class="' + barClass(name) + '" style="width:' + pct + '%"></i></div></div>'; | |
| }).join(""); | |
| if (data.sheet_url) { | |
| $("sheetLink").href = data.sheet_url; | |
| $("sheetLink").classList.remove("hidden"); | |
| } else $("sheetLink").classList.add("hidden"); | |
| const prefer = ["Name","Nama","Class","Kelas","ID","Recommended Stream","Second Stream","Confidence","Prob Science %","Prob Reka Cipta %","Prob Account %","Prob Art / Other %","Key Subjects","Status"]; | |
| let all = data.columns || (list[0] ? Object.keys(list[0]) : []); | |
| const ordered = []; | |
| prefer.forEach((c) => { | |
| const real = all.find((x) => x === c || x.toLowerCase() === c.toLowerCase()); | |
| if (real && !ordered.includes(real)) ordered.push(real); | |
| }); | |
| all.forEach((c) => { if (!ordered.includes(c)) ordered.push(c); }); | |
| cols = ordered.slice(0, 12); | |
| filter(); | |
| } | |
| function filter() { | |
| const q = ($("search").value || "").toLowerCase().trim(); | |
| const stream = $("streamFilter").value; | |
| let list = rows; | |
| if (stream) list = list.filter((r) => (r["Recommended Stream"] || "") === stream); | |
| if (q) list = list.filter((r) => cols.some((c) => String(r[c] ?? "").toLowerCase().includes(q))); | |
| $("thead").innerHTML = "<tr>" + cols.map((c) => "<th>" + esc(c) + "</th>").join("") + "</tr>"; | |
| $("tbody").innerHTML = list.length ? list.map((r) => { | |
| return "<tr>" + cols.map((c) => { | |
| const v = r[c] ?? ""; | |
| if (c === "Status") { | |
| const ok = String(v) === "OK"; | |
| return '<td><span class="tag ' + (ok ? "tag-ok" : "tag-fail") + '">' + esc(String(v)) + "</span></td>"; | |
| } | |
| if (c === "Recommended Stream") { | |
| return '<td><span class="tag ' + streamTag(String(v)) + '">' + esc(String(v)) + "</span></td>"; | |
| } | |
| if (c === "Confidence") { | |
| return '<td><span class="tag ' + confTag(String(v)) + '">' + esc(String(v)) + "</span></td>"; | |
| } | |
| return '<td title="' + esc(String(v)) + '">' + esc(String(v)) + "</td>"; | |
| }).join("") + "</tr>"; | |
| }).join("") : '<tr><td colspan="' + cols.length + '" style="text-align:center;color:var(--text-3);padding:28px">NO MATCH</td></tr>'; | |
| $("footLeft").textContent = list.length + " SHOWN · " + rows.length + " TOTAL"; | |
| $("footRight").textContent = new Date().toLocaleTimeString(); | |
| } | |
| $("search").addEventListener("input", filter); | |
| $("streamFilter").addEventListener("change", filter); | |
| async function runCsv() { | |
| if (!file) return; | |
| load(true); | |
| $("btnRunCsv").disabled = true; | |
| $("btnRunCsv").textContent = "Running…"; | |
| try { | |
| const fd = new FormData(); | |
| fd.append("file", file); | |
| const res = await fetch(API + "/bulk/preview", { method: "POST", body: fd }); | |
| const data = await res.json().catch(() => ({})); | |
| if (!res.ok) throw new Error(typeof data.detail === "string" ? data.detail : JSON.stringify(data.detail || data)); | |
| show(data); | |
| toast("COMPLETE — " + data.ok + " / " + data.total + " predicted.", "ok"); | |
| } catch (e) { | |
| toast(e.message || String(e), "err"); | |
| } finally { | |
| load(false); | |
| $("btnRunCsv").disabled = false; | |
| $("btnRunCsv").textContent = "Run predictions"; | |
| } | |
| } | |
| async function downloadCsv() { | |
| if (!file) return; | |
| load(true); | |
| $("btnDownload").disabled = true; | |
| $("btnDownload").textContent = "Exporting…"; | |
| try { | |
| const fd = new FormData(); | |
| fd.append("file", file); | |
| const res = await fetch(API + "/bulk/csv", { method: "POST", body: fd }); | |
| if (!res.ok) { | |
| const data = await res.json().catch(() => ({})); | |
| throw new Error(typeof data.detail === "string" ? data.detail : "HTTP " + res.status); | |
| } | |
| const blob = await res.blob(); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = "aurastudy_predictions.csv"; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| toast("EXPORT OK", "ok"); | |
| } catch (e) { | |
| toast(e.message || String(e), "err"); | |
| } finally { | |
| load(false); | |
| $("btnDownload").disabled = false; | |
| $("btnDownload").textContent = "Export CSV"; | |
| } | |
| } | |
| async function runSheet() { | |
| const url = $("sheetUrl").value.trim(); | |
| if (!url) { toast("Paste a Google Sheet URL or ID.", "err"); return; } | |
| load(true); | |
| $("btnRunSheet").disabled = true; | |
| $("btnRunSheet").textContent = "Running…"; | |
| try { | |
| const res = await fetch(API + "/bulk/sheet", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| sheet_url: url, | |
| worksheet: $("worksheet").value.trim() || null, | |
| write_back: $("writeBack").checked, | |
| }), | |
| }); | |
| const data = await res.json().catch(() => ({})); | |
| if (!res.ok) throw new Error(typeof data.detail === "string" ? data.detail : JSON.stringify(data.detail || data)); | |
| show(data); | |
| toast(data.written_back | |
| ? "SHEET UPDATED — " + data.ok + "/" + data.total | |
| : "DONE — " + data.ok + "/" + data.total, "ok"); | |
| } catch (e) { | |
| toast(e.message || String(e), "err"); | |
| } finally { | |
| load(false); | |
| $("btnRunSheet").disabled = false; | |
| $("btnRunSheet").textContent = "Run on sheet"; | |
| } | |
| } | |
| $("btnRunCsv").onclick = runCsv; | |
| $("btnDownload").onclick = downloadCsv; | |
| $("btnRunSheet").onclick = runSheet; | |
| })(); | |
| </script> | |
| </body> | |
| </html> |