| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> |
| <title>CyberSecQwen-4B</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=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500;600&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet"> |
|
|
| |
| |
| |
| |
| <script async src="https://unpkg.com/es-module-shims@1.7.0/dist/es-module-shims.js"></script> |
| <script type="importmap"> |
| { |
| "imports": { |
| "@huggingface/hub": "https://cdn.jsdelivr.net/npm/@huggingface/hub@0.21.0/+esm" |
| } |
| } |
| </script> |
|
|
| <style> |
| |
| :root{ |
| --bg: #08080a; |
| --surface: #0e0e11; |
| --surface-2: #16161a; |
| --border: #232326; |
| --border-soft: #1a1a1d; |
| |
| --fg: #fafafa; |
| --fg-2: #e4e4e7; |
| --fg-3: #a1a1aa; |
| --fg-4: #71717a; |
| --fg-5: #52525b; |
| |
| --accent: #22d3ee; |
| --accent-soft: rgba(34,211,238,0.08); |
| --accent-border: rgba(34,211,238,0.28); |
| --accent-glow: rgba(34,211,238,0.18); |
| |
| --font-sans: 'Geist', ui-sans-serif, system-ui, -apple-system, sans-serif; |
| --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace; |
| --font-serif: 'Instrument Serif', ui-serif, Georgia, serif; |
| |
| --ease-out: cubic-bezier(.22,.61,.36,1); |
| } |
| |
| *,*::before,*::after{box-sizing:border-box} |
| html,body{height:100%} |
| body{ |
| margin:0; |
| color:var(--fg-2); |
| background: var(--bg); |
| font-family:var(--font-sans); |
| font-size:14px; |
| line-height:1.5; |
| -webkit-font-smoothing:antialiased; |
| text-rendering:optimizeLegibility; |
| overflow:hidden; |
| } |
| ::selection{background:var(--accent-soft);color:#fff} |
| a{color:inherit;text-decoration:none} |
| button{font:inherit;color:inherit;background:none;border:0;cursor:pointer} |
| |
| |
| .bg-mesh{ |
| position:fixed;inset:0;pointer-events:none;z-index:0; |
| background: |
| radial-gradient(900px 540px at 92% -8%, rgba(34,211,238,.16), transparent 60%), |
| radial-gradient(620px 420px at 6% 108%, rgba(34,211,238,.10), transparent 60%); |
| } |
| .bg-grain{ |
| position:fixed;inset:0;pointer-events:none;z-index:1; |
| opacity:.035;mix-blend-mode:overlay; |
| background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.6 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>"); |
| } |
| |
| |
| .app{ |
| position:relative;z-index:2; |
| display:grid; |
| grid-template-columns: 240px 1fr; |
| height:100vh; |
| } |
| |
| |
| .sidebar{ |
| border-right:1px solid var(--border-soft); |
| background: rgba(14,14,17,.6); |
| backdrop-filter: blur(8px); |
| display:flex;flex-direction:column; |
| padding:18px 14px; |
| min-height:0; |
| } |
| .brand{display:flex;align-items:center;gap:10px;padding:4px 6px 18px} |
| .brand-mark{ |
| width:32px;height:32px;border-radius:8px; |
| border:1px solid var(--accent-border); |
| display:grid;place-items:center; |
| background:var(--accent-soft); |
| overflow:hidden; |
| } |
| .brand-mark img{ |
| width:22px;height:22px; |
| |
| filter: brightness(0) saturate(100%) invert(82%) sepia(34%) saturate(2576%) hue-rotate(140deg) brightness(101%) contrast(96%); |
| display:block; |
| } |
| .brand-name{font-weight:600;letter-spacing:-.005em;color:var(--fg);font-size:14px} |
| .brand-tag{font-family:var(--font-mono);font-size:10.5px;color:var(--fg-4);letter-spacing:.02em} |
| |
| .nav{display:flex;flex-direction:column;gap:2px;margin-top:6px} |
| .nav a{ |
| display:flex;align-items:center;gap:10px; |
| padding:8px 10px;border-radius:8px;color:var(--fg-3);font-size:13.5px; |
| transition:background .15s, color .15s; |
| } |
| .nav a:hover{color:var(--fg-2)} |
| .nav a.active{background:var(--surface-2);color:var(--fg)} |
| .nav a svg{width:15px;height:15px;flex:none;opacity:.85} |
| |
| .side-foot{ |
| margin-top:auto;display:flex;flex-direction:column;gap:4px; |
| padding-top:14px;border-top:1px solid var(--border-soft); |
| } |
| .side-foot a{ |
| padding:6px 10px;border-radius:6px;color:var(--fg-4);font-size:12.5px; |
| display:flex;align-items:center;gap:8px; |
| } |
| .side-foot a:hover{color:var(--fg-2);background:var(--surface)} |
| .side-foot a svg{width:13px;height:13px;opacity:.8} |
| .status-pill{ |
| margin-top:6px;align-self:flex-start; |
| padding:3px 9px;border:1px solid var(--border);border-radius:999px; |
| font-family:var(--font-mono);font-size:10.5px;color:var(--fg-4); |
| display:inline-flex;align-items:center;gap:6px; |
| } |
| .status-dot{width:5px;height:5px;border-radius:50%;background:var(--accent);box-shadow:0 0 6px var(--accent)} |
| |
| |
| .main{display:flex;flex-direction:column;min-width:0;min-height:0;overflow-y:auto;scroll-behavior:smooth;position:relative} |
| .topbar{ |
| height:56px;flex:none; |
| padding:0 28px;border-bottom:1px solid var(--border-soft); |
| display:flex;align-items:center;justify-content:space-between; |
| background:rgba(8,8,10,.72);backdrop-filter:blur(10px); |
| position:sticky;top:0;z-index:10; |
| } |
| .topbar h1.section-title{ |
| margin:0;font-size:13.5px;font-weight:500;color:var(--fg-2);letter-spacing:.005em; |
| } |
| .top-status{ |
| font-family:var(--font-mono);font-size:11px;color:var(--fg-4); |
| padding:4px 10px;border:1px solid var(--border);border-radius:999px; |
| display:inline-flex;align-items:center;gap:7px; |
| } |
| .top-actions{display:flex;align-items:center;gap:10px} |
| .top-link{ |
| font-family:var(--font-mono);font-size:11px;color:var(--accent); |
| padding:4px 10px;border:1px solid var(--accent);border-radius:999px; |
| text-decoration:none;letter-spacing:.005em; |
| transition:background .15s ease; |
| } |
| .top-link:hover{background:rgba(34,211,238,.08)} |
| |
| |
| .page-section{display:none;position:relative} |
| .page-section.is-active{display:block} |
| .page-section.chat-section.is-active{display:flex;flex-direction:column;min-height:calc(100vh - 56px)} |
| .page-section.chat-section.is-active > .chat-active{flex:1;display:flex;flex-direction:column} |
| .hidden{display:none !important} |
| |
| |
| .cta-block{ |
| margin:64px auto 0;max-width:560px; |
| padding:36px 32px; |
| border:1px solid var(--accent-border);border-radius:16px; |
| background:radial-gradient(120% 100% at 50% 0%, var(--accent-soft), transparent 70%); |
| text-align:center; |
| } |
| .cta-block h3{ |
| margin:0 0 8px; |
| font-family:var(--font-serif);font-weight:400; |
| font-size:30px;line-height:1.15;letter-spacing:-.01em;color:var(--fg); |
| } |
| .cta-block h3 em{font-style:italic;color:var(--accent)} |
| .cta-block p{margin:0 0 22px;color:var(--fg-4);font-size:14.5px;max-width:42ch;margin-left:auto;margin-right:auto} |
| .cta-primary{ |
| display:inline-flex;align-items:center;gap:9px; |
| padding:11px 22px;border-radius:10px; |
| background:var(--accent);color:#06181c;font-weight:600;font-size:14px; |
| transition:transform .2s var(--ease-out), filter .15s, box-shadow .2s var(--ease-out); |
| box-shadow: 0 0 0 1px var(--accent-border), 0 0 28px var(--accent-glow); |
| } |
| .cta-primary:hover{transform:translateY(-1px);filter:brightness(1.05)} |
| .cta-primary svg{transition:transform .2s var(--ease-out)} |
| .cta-primary:hover svg{transform:translateY(2px)} |
| |
| |
| .status-text{ |
| margin-top:6px; |
| font-family:var(--font-mono);font-size:11px;color:var(--fg-5); |
| letter-spacing:.02em; |
| } |
| .status-text.warn{color:var(--accent)} |
| |
| |
| .signin-gate{ |
| display:flex;flex-direction:column;align-items:center;justify-content:center; |
| padding:96px 28px 80px;gap:18px; |
| } |
| .signin-gate.hidden{display:none} |
| .signin-card{ |
| max-width:520px;width:100%; |
| background:linear-gradient(180deg, var(--surface) 0%, rgba(34,211,238,0.03) 100%); |
| border:1px solid var(--border);border-radius:18px; |
| padding:28px 28px 24px; |
| text-align:center; |
| box-shadow: 0 10px 40px rgba(0,0,0,0.35); |
| } |
| .signin-card h3{ |
| margin:0 0 8px; |
| font-family:var(--font-serif);font-weight:400; |
| font-size:30px;line-height:1.1;letter-spacing:-.01em;color:var(--fg); |
| } |
| .signin-card h3 em{font-style:italic;color:var(--accent);font-family:var(--font-serif)} |
| .signin-card .lede{margin:0 0 20px;color:var(--fg-4);font-size:14px;line-height:1.55} |
| .signin-btn{ |
| display:inline-flex;align-items:center;gap:10px; |
| padding:12px 22px;border-radius:11px; |
| background:#fafafa;color:#08080a; |
| font-weight:600;font-size:14px;letter-spacing:.01em; |
| border:1px solid rgba(255,255,255,0.05); |
| transition:transform .15s var(--ease-out), filter .15s var(--ease-out); |
| box-shadow: 0 6px 18px rgba(0,0,0,0.4); |
| } |
| .signin-btn:hover{transform:translateY(-1px);filter:brightness(1.03)} |
| .signin-btn:active{transform:translateY(0)} |
| .signin-btn .hf-emoji{font-size:18px;line-height:1} |
| .signin-meta{ |
| margin-top:14px;color:var(--fg-5);font-size:12px;font-family:var(--font-mono); |
| letter-spacing:.02em; |
| } |
| .signin-meta a{color:var(--accent);border-bottom:1px dotted var(--accent-border)} |
| |
| |
| .user-pill{ |
| display:flex;align-items:center;gap:8px; |
| padding:7px 9px; |
| background:var(--accent-soft);border:1px solid var(--accent-border); |
| border-radius:9px; |
| color:var(--fg-2);font-size:12px; |
| font-family:var(--font-mono); |
| } |
| .user-pill .who{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--accent)} |
| .user-pill button{color:var(--fg-4);font-size:11px;letter-spacing:.05em;text-transform:uppercase} |
| .user-pill button:hover{color:var(--fg-2)} |
| |
| |
| .chat-empty{ |
| display:flex;flex-direction:column;align-items:center;justify-content:center; |
| padding:64px 28px 80px; |
| gap:24px; |
| } |
| .hero-icon{ |
| color:var(--accent); |
| width:36px;height:36px; |
| display:grid;place-items:center; |
| animation: fadeUp .6s var(--ease-out) both; |
| } |
| .hero-h1{ |
| margin:0; |
| font-family:var(--font-serif);font-weight:400; |
| font-size:clamp(40px, 6vw, 56px); |
| line-height:1.05;letter-spacing:-.01em;color:var(--fg); |
| text-align:center; |
| animation: fadeUp .6s var(--ease-out) both;animation-delay:.08s; |
| } |
| .hero-h1 em{font-style:italic;color:var(--accent);font-family:var(--font-serif)} |
| .hero-sub{ |
| margin:0;font-size:15px;color:var(--fg-4);max-width:54ch;text-align:center; |
| animation: fadeUp .6s var(--ease-out) both;animation-delay:.16s; |
| } |
| |
| .composer{ |
| width:100%;max-width:640px; |
| background:var(--surface); |
| border:1px solid var(--border);border-radius:14px; |
| padding:14px 14px 10px; |
| display:flex;flex-direction:column;gap:10px; |
| transition:border-color .2s var(--ease-out), box-shadow .2s var(--ease-out); |
| animation: fadeUp .6s var(--ease-out) both;animation-delay:.24s; |
| } |
| .composer:focus-within{ |
| border-color:var(--accent-border); |
| box-shadow: 0 0 0 4px var(--accent-soft), 0 0 32px var(--accent-glow); |
| } |
| .composer textarea{ |
| width:100%;min-height:24px;max-height:240px; |
| background:transparent;color:var(--fg); |
| border:0;outline:none;resize:none; |
| font:inherit;font-size:15px;line-height:1.5; |
| } |
| .composer textarea::placeholder{color:var(--fg-5)} |
| .composer-row{display:flex;align-items:center;justify-content:space-between;gap:10px} |
| .send-hint{font-family:var(--font-mono);font-size:10.5px;color:var(--fg-5)} |
| .send-btn{ |
| display:inline-flex;align-items:center;gap:7px; |
| padding:8px 14px;border-radius:8px; |
| background:var(--accent);color:#06181c;font-weight:600;font-size:13px; |
| transition:transform .2s var(--ease-out), filter .15s, box-shadow .2s var(--ease-out); |
| box-shadow: 0 0 0 1px var(--accent-border), 0 0 24px var(--accent-glow); |
| } |
| .send-btn:hover:not(:disabled){transform:translateY(-1px);filter:brightness(1.05)} |
| .send-btn:disabled{opacity:.4;cursor:not-allowed;box-shadow:none} |
| |
| .chips{ |
| display:flex;flex-wrap:wrap;gap:8px;justify-content:center; |
| max-width:640px; |
| animation: fadeUp .6s var(--ease-out) both;animation-delay:.32s; |
| } |
| .chip{ |
| padding:6px 12px;border:1px solid var(--border);border-radius:999px; |
| font-size:12.5px;color:var(--fg-3); |
| display:inline-flex;align-items:center;gap:7px; |
| transition:transform .2s var(--ease-out), border-color .15s, color .15s; |
| } |
| .chip .kind{font-family:var(--font-mono);font-size:10.5px;color:var(--accent)} |
| .chip:hover{transform:translateY(-1px);border-color:var(--accent-border);color:var(--fg-2)} |
| |
| |
| .chat-active{ |
| display:flex;flex-direction:column; |
| width:100%; |
| } |
| .thread{ |
| padding: 28px 28px 32px; |
| display:flex;flex-direction:column;align-items:center; |
| } |
| .thread-inner{width:100%;max-width:720px;display:flex;flex-direction:column;gap:18px} |
| |
| .msg{display:flex;flex-direction:column;gap:6px;max-width:100%} |
| .msg .who{ |
| font-family:var(--font-mono);font-size:10.5px;color:var(--fg-5); |
| letter-spacing:.04em;text-transform:uppercase; |
| } |
| .msg.user .bubble{ |
| align-self:flex-end;max-width:80%; |
| background:var(--surface-2);border:1px solid var(--border); |
| color:var(--fg); |
| padding:10px 14px; |
| border-radius:12px 12px 4px 12px; |
| white-space:pre-wrap; |
| font-size:14.5px; |
| } |
| .msg.user .who{align-self:flex-end} |
| .msg.bot .body{ |
| color:var(--fg-2);font-size:14.5px;line-height:1.65; |
| white-space:pre-wrap; |
| } |
| .msg.bot .body strong{color:var(--fg)} |
| .msg.bot .body code{ |
| font-family:var(--font-mono);font-size:13px; |
| background:var(--surface);border:1px solid var(--border-soft); |
| padding:1px 6px;border-radius:5px;color:var(--fg-2); |
| } |
| |
| |
| .msg-actions{ |
| display:flex;gap:6px;margin-top:6px;align-items:center;flex-wrap:wrap; |
| opacity:.55;transition:opacity .15s var(--ease-out); |
| } |
| .msg-actions:hover, .msg.bot:hover .msg-actions, .msg-actions:focus-within{opacity:1} |
| .msg-action{ |
| display:inline-flex;align-items:center;gap:5px; |
| padding:4px 8px;border-radius:6px; |
| border:1px solid var(--border-soft);background:transparent; |
| color:var(--fg-4);font-size:11.5px;font-family:var(--font-mono); |
| letter-spacing:.02em; |
| transition:color .15s, border-color .15s, background .15s; |
| cursor:pointer; |
| } |
| .msg-action:hover{color:var(--fg-2);border-color:var(--border);background:var(--surface)} |
| .msg-action svg{width:11px;height:11px;opacity:.85} |
| .msg-action.is-flash{color:var(--accent);border-color:var(--accent-border)} |
| |
| |
| .msg-action.continue{ |
| color:var(--accent);border:1px solid var(--accent-border); |
| background:var(--accent-soft); |
| padding:5px 10px;font-weight:500; |
| } |
| .msg-action.continue:hover{background:rgba(34,211,238,0.14);color:var(--fg)} |
| .truncation-note{ |
| margin-top:8px;font-family:var(--font-mono);font-size:10.5px; |
| color:var(--fg-5);letter-spacing:.02em; |
| } |
| .truncation-note .dot{color:var(--accent)} |
| |
| .typing{display:inline-flex;gap:5px;padding:8px 0} |
| .typing i{ |
| width:5px;height:5px;border-radius:50%;background:var(--fg-5); |
| animation:blink 1.2s infinite ease-in-out; |
| } |
| .typing i:nth-child(2){animation-delay:.15s} |
| .typing i:nth-child(3){animation-delay:.30s} |
| @keyframes blink{0%,80%,100%{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}} |
| |
| |
| .dock{ |
| flex:none; |
| padding:14px 28px 22px; |
| background:linear-gradient(to bottom, transparent, rgba(8,8,10,.85) 22%, var(--bg) 60%); |
| display:flex;justify-content:center; |
| position:sticky;bottom:0;z-index:4; |
| pointer-events:none; |
| } |
| .dock > .dock-inner{ |
| pointer-events:auto;width:100%;max-width:720px; |
| display:flex;align-items:flex-end;gap:10px; |
| } |
| .dock .composer{margin:0;flex:1;animation:none} |
| .new-chat{ |
| flex:none;height:46px;padding:0 14px; |
| border:1px solid var(--border);border-radius:10px; |
| color:var(--fg-3);font-size:13px; |
| display:inline-flex;align-items:center;gap:7px; |
| transition:border-color .15s, color .15s; |
| } |
| .new-chat:hover{color:var(--fg);border-color:var(--accent-border)} |
| |
| |
| .about{padding:0} |
| .about-inner{max-width:880px;margin:0 auto;padding:48px 32px 120px} |
| .subnav{ |
| position:sticky;top:56px;z-index:5; |
| background:rgba(8,8,10,.78);backdrop-filter:blur(10px); |
| border-bottom:1px solid var(--border-soft); |
| padding:12px 32px; |
| display:flex;justify-content:center; |
| } |
| .subnav-inner{display:flex;gap:6px;flex-wrap:wrap;max-width:880px;width:100%} |
| .subnav a{ |
| font-family:var(--font-mono);font-size:11.5px;color:var(--fg-4); |
| padding:5px 10px;border-radius:6px;letter-spacing:.02em; |
| transition:color .15s, background .15s; |
| } |
| .subnav a:hover{color:var(--fg-2);background:var(--surface)} |
| .subnav a.is-current{color:var(--accent);background:var(--accent-soft)} |
| |
| .about h2{ |
| font-family:var(--font-serif);font-weight:400; |
| font-size:36px;line-height:1.1;letter-spacing:-.01em;color:var(--fg); |
| margin:0 0 14px; |
| } |
| .about h3{ |
| font-size:13px;font-weight:600;color:var(--fg-4); |
| text-transform:uppercase;letter-spacing:.12em; |
| margin:0 0 18px; |
| } |
| .about p{color:var(--fg-3);font-size:15px;line-height:1.7;margin:0 0 14px;max-width:64ch} |
| .about p strong{color:var(--fg-2)} |
| .about section{padding:48px 0;border-top:1px solid var(--border-soft)} |
| .about section:first-of-type{border-top:0;padding-top:24px} |
| .tagline{ |
| font-family:var(--font-serif);font-style:italic;font-weight:400; |
| font-size:28px;line-height:1.25;color:var(--fg); |
| border-left:1px solid var(--accent-border); |
| padding:6px 0 6px 18px;margin:18px 0 0;max-width:42ch; |
| } |
| .tagline em{font-style:italic;color:var(--accent)} |
| |
| |
| .video-card{ |
| border:1px solid var(--border);border-radius:14px; |
| background:var(--surface); |
| padding:6px 6px 14px; |
| margin:18px 0 12px; |
| box-shadow: 0 8px 32px rgba(0,0,0,0.32); |
| overflow:hidden; |
| } |
| .video-card video{ |
| width:100%;height:auto;display:block; |
| border-radius:9px; |
| background:#000; |
| } |
| .video-card .vc-meta{ |
| display:flex;align-items:center;justify-content:space-between;gap:12px; |
| padding:10px 10px 4px; |
| font-family:var(--font-mono);font-size:11.5px;color:var(--fg-4); |
| letter-spacing:.02em; |
| } |
| .video-card .vc-meta .vc-tag{ |
| color:var(--accent); |
| background:var(--accent-soft); |
| border:1px solid var(--accent-border); |
| padding:3px 8px;border-radius:6px; |
| font-size:10.5px;letter-spacing:.06em;text-transform:uppercase; |
| } |
| |
| .stat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:22px 0 28px} |
| .stat{ |
| border:1px solid var(--border);border-radius:12px; |
| background:var(--surface);padding:18px; |
| display:flex;flex-direction:column;gap:6px; |
| } |
| .stat .lbl{font-family:var(--font-mono);font-size:10.5px;color:var(--fg-5);text-transform:uppercase;letter-spacing:.08em} |
| .stat .num{ |
| font-family:var(--font-serif);font-size:38px;line-height:1;color:var(--fg); |
| font-feature-settings:"tnum";letter-spacing:-.01em; |
| } |
| .stat.accent .num{color:var(--accent)} |
| .stat .sub{font-family:var(--font-mono);font-size:11.5px;color:var(--fg-4)} |
| |
| table.cmp{ |
| width:100%;border-collapse:collapse;font-size:13px; |
| border:1px solid var(--border);border-radius:12px;overflow:hidden; |
| background:var(--surface); |
| } |
| table.cmp th, table.cmp td{ |
| padding:10px 14px;border-bottom:1px solid var(--border-soft);text-align:left; |
| font-variant-numeric:tabular-nums; |
| } |
| table.cmp th{ |
| font-family:var(--font-mono);font-size:10.5px;letter-spacing:.06em; |
| text-transform:uppercase;color:var(--fg-5);font-weight:500; |
| background:var(--surface-2); |
| } |
| table.cmp td{color:var(--fg-3)} |
| table.cmp td.model{color:var(--fg-2);font-weight:500} |
| table.cmp tr.us td{background:var(--accent-soft);color:var(--fg)} |
| table.cmp tr.us td.model{color:var(--accent);font-weight:600} |
| table.cmp tr:last-child td{border-bottom:0} |
| |
| .lift-callout{ |
| margin-top:22px;padding:16px 18px;border:1px solid var(--accent-border); |
| border-radius:12px;background:var(--accent-soft); |
| display:flex;align-items:center;gap:18px;flex-wrap:wrap; |
| } |
| .lift-callout .lift{ |
| font-family:var(--font-serif);font-size:32px;line-height:1;color:var(--accent); |
| } |
| .lift-callout .desc{color:var(--fg-2);font-size:14px} |
| .lift-callout .desc small{display:block;color:var(--fg-4);font-family:var(--font-mono);font-size:11px;margin-top:4px} |
| |
| .card-row{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:18px 0 26px} |
| .card{ |
| border:1px solid var(--border);border-radius:12px;background:var(--surface); |
| padding:18px;display:flex;flex-direction:column;gap:8px; |
| } |
| .card .pill{ |
| font-family:var(--font-mono);font-size:10.5px;color:var(--accent); |
| text-transform:uppercase;letter-spacing:.08em; |
| } |
| .card h4{margin:0;color:var(--fg);font-size:15px;font-weight:600;letter-spacing:-.005em} |
| .card p{margin:0;color:var(--fg-3);font-size:13.5px;line-height:1.6} |
| |
| .spec{ |
| display:grid;grid-template-columns:auto 1fr;gap:0; |
| border:1px solid var(--border);border-radius:12px;overflow:hidden;background:var(--surface); |
| margin:14px 0; |
| } |
| .spec dt, .spec dd{padding:9px 14px;border-bottom:1px solid var(--border-soft)} |
| .spec dt{ |
| font-family:var(--font-mono);font-size:11.5px;color:var(--fg-4); |
| background:var(--surface-2);border-right:1px solid var(--border-soft); |
| text-transform:uppercase;letter-spacing:.04em; |
| } |
| .spec dd{margin:0;color:var(--fg-2);font-size:13.5px;font-family:var(--font-mono)} |
| .spec dt:nth-last-of-type(1), .spec dd:nth-last-of-type(1){border-bottom:0} |
| |
| pre.code{ |
| margin:14px 0;background:var(--surface);border:1px solid var(--border-soft); |
| border-radius:10px;padding:14px 16px; |
| font-family:var(--font-mono);font-size:12.5px;color:var(--fg-2); |
| overflow:auto;line-height:1.55; |
| } |
| pre.code .c{color:var(--fg-5)} |
| pre.code .k{color:var(--accent)} |
| |
| ul.bullets{padding-left:18px;color:var(--fg-3);font-size:14.5px;line-height:1.7;margin:0 0 14px} |
| ul.bullets li{margin:4px 0} |
| ul.bullets li strong{color:var(--fg-2)} |
| |
| .twocol{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:18px 0} |
| |
| |
| *::-webkit-scrollbar{width:10px;height:10px} |
| *::-webkit-scrollbar-track{background:transparent} |
| *::-webkit-scrollbar-thumb{background:#1d1d20;border-radius:99px;border:2px solid transparent;background-clip:content-box} |
| *::-webkit-scrollbar-thumb:hover{background:#2a2a2e;background-clip:content-box;border:2px solid transparent} |
| |
| @keyframes fadeUp{ |
| from{opacity:0;transform:translateY(8px)} |
| to {opacity:1;transform:translateY(0)} |
| } |
| |
| |
| @media (max-width: 860px){ |
| .app{grid-template-columns: 1fr} |
| .sidebar{display:none} |
| .stat-grid, .card-row, .twocol{grid-template-columns:1fr} |
| } |
| </style> |
| </head> |
|
|
| <body> |
| <div class="bg-mesh" aria-hidden="true"></div> |
| <div class="bg-grain" aria-hidden="true"></div> |
|
|
| <div class="app"> |
| |
| <aside class="sidebar"> |
| <div class="brand"> |
| <div class="brand-mark"><img src="data:image/png;base64,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" alt="Qwen" /></div> |
| <div> |
| <div class="brand-name">CyberSecQwen</div> |
| <div class="brand-tag">4B · CTI</div> |
| </div> |
| </div> |
|
|
| <nav class="nav" id="nav"> |
| <a href="#about" data-section="about" class="active"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8h.01M11 12h1v5h1"/></svg> |
| About |
| </a> |
| <a href="#chat" data-section="chat"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> |
| Try the Chat |
| </a> |
| </nav> |
|
|
| <div class="side-foot"> |
| <a href="https://huggingface.co/athena129/CyberSecQwen-4B" target="_blank" rel="noopener"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/></svg> |
| Model card |
| </a> |
| <a href="https://github.com/GPT-64590/CyberSecQwen-4B" target="_blank" rel="noopener"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-4 1.5-4-2-6-2"/><path d="M15 22v-4a3 3 0 0 0-1-2.3c3 0 6-2 6-5.5a4 4 0 0 0-1-2.8 4 4 0 0 0 0-2.8s-1 0-3 1.3a10 10 0 0 0-5 0C7.9 2.6 7 2.6 7 2.6a4 4 0 0 0 0 2.8 4 4 0 0 0-1 2.8C6 11.7 9 13.7 12 13.7a3 3 0 0 0-1 2.3v4"/></svg> |
| GitHub |
| </a> |
| <a href="https://lablab.ai/ai-hackathons/amd-developer/athena19/cybersecqwen-4b-cti-specialist-fine-tuned-on-amd" target="_blank" rel="noopener" title="AMD Developer Hackathon submission on lablab.ai"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2z"/></svg> |
| Hackathon entry |
| </a> |
| <span class="status-pill" id="statusPill"><span class="status-dot"></span> Trained on MI300X</span> |
| <div class="user-pill" id="userPill" style="display:none"> |
| <span class="who" id="userPillWho">@user</span> |
| <button id="signoutBtn" type="button" title="Sign out">Out</button> |
| </div> |
| </div> |
| </aside> |
|
|
| |
| <main class="main" id="main"> |
| <header class="topbar"> |
| <h1 class="section-title" id="topTitle">CyberSecQwen-4B</h1> |
| <div class="top-actions"> |
| <a class="top-link" href="https://huggingface.co/athena129/CyberSecQwen-4B" target="_blank" rel="noopener" title="Open the Hugging Face model card">Model card ↗</a> |
| <span class="top-status">4B · CTI specialist · temp 0.3</span> |
| </div> |
| </header> |
|
|
| |
| <section class="page-section about is-active" id="about"> |
| <div class="about"> |
| <div class="subnav"> |
| <div class="subnav-inner" id="subnav"> |
| <a href="#walkthrough">Walkthrough</a> |
| <a href="#performance">Performance</a> |
| <a href="#recipe">Recipe</a> |
| <a href="#hardware">Hardware</a> |
| <a href="#limitations">Limitations</a> |
| <a href="#companion">Companion</a> |
| <a href="#citation">Citation</a> |
| </div> |
| </div> |
|
|
| <div class="about-inner"> |
|
|
| <section id="overview"> |
| <h3>Overview</h3> |
| <h2>CyberSecQwen-4B</h2> |
| <p>A Qwen3-4B base fine-tuned for cyber threat intelligence — CWE classification, CVE-to-CWE mapping, and code-pattern reasoning. Trained on AMD MI300X with a recipe designed to preserve instruction-tuned behavior while shifting the model's distribution onto the CTI domain.</p> |
| <p class="tagline">Beating an 8B Cisco specialist <em>at half the size.</em></p> |
| </section> |
|
|
| <section id="walkthrough"> |
| <h3>Walkthrough</h3> |
| <h2>Research, in 5 minutes</h2> |
| <p>A walkthrough of the training methodology, AMD MI300X workflow, and the benchmark results — context for the numbers below.</p> |
| <div class="video-card"> |
| <video controls preload="metadata" playsinline> |
| <source src="research.mp4" type="video/mp4" /> |
| Your browser doesn't support inline video — <a href="research.mp4">download the file</a>. |
| </video> |
| <div class="vc-meta"> |
| <span><span class="vc-tag">Video</span> AMD MI300X · Qwen3-4B fine-tune</span> |
| <span>5:33 · 720p</span> |
| </div> |
| </div> |
| </section> |
|
|
| <section id="performance"> |
| <h3>Performance</h3> |
| <h2>CTI-Bench, n=5, temp 0.3</h2> |
| <p>Evaluated on the public CTI-Bench multi-trial protocol. Scores below are mean ± 1σ over five runs.</p> |
|
|
| <div class="stat-grid"> |
| <div class="stat"> |
| <span class="lbl">CTI-RCM</span> |
| <span class="num">0.6664</span> |
| <span class="sub">± 0.0023 · CWE root-cause mapping</span> |
| </div> |
| <div class="stat accent"> |
| <span class="lbl">CTI-MCQ</span> |
| <span class="num">0.5868</span> |
| <span class="sub">± 0.0029 · +8.7 pp vs Cisco-Instruct-8B</span> |
| </div> |
| <div class="stat"> |
| <span class="lbl">Param ratio</span> |
| <span class="num">2×</span> |
| <span class="sub">smaller than Cisco-Foundation-Sec-8B</span> |
| </div> |
| </div> |
|
|
| <table class="cmp"> |
| <thead> |
| <tr><th>Model</th><th>Params</th><th>CTI-RCM</th><th>CTI-MCQ</th></tr> |
| </thead> |
| <tbody> |
| <tr><td class="model">Foundation-Sec-8B (base, 5-shot)</td><td>8B</td><td>0.7450</td><td>0.6552</td></tr> |
| <tr><td class="model">Cisco-Foundation-Sec-Instruct-8B</td><td>8B</td><td>0.6850</td><td>0.4996</td></tr> |
| <tr><td class="model">CyberPal-2.0-20B</td><td>20B</td><td>0.7280</td><td>0.7384</td></tr> |
| <tr class="us"><td class="model">CyberSecQwen-4B</td><td>4B</td><td>0.6664</td><td>0.5868</td></tr> |
| <tr><td class="model">Gemma4Defense-2B (companion)</td><td>2B</td><td>0.6754</td><td>0.6042</td></tr> |
| <tr><td class="model">Qwen3-4B-Instruct-2507 (raw)</td><td>4B</td><td>0.5190</td><td>0.4732</td></tr> |
| <tr><td class="model">Qwen3-4B-Base (5-shot)</td><td>4B</td><td>0.5170</td><td>0.6672</td></tr> |
| <tr><td class="model">Gemma-4-E2B-it (raw)</td><td>2B</td><td>0.5800</td><td>0.5780</td></tr> |
| </tbody> |
| </table> |
|
|
| <div class="lift-callout"> |
| <span class="lift">+15.1 pp</span> |
| <div class="desc">RCM lift over our Qwen3-4B base |
| <small>and +12.0 pp on MCQ — same architecture, recipe alone</small> |
| </div> |
| </div> |
| </section> |
|
|
| <section id="recipe"> |
| <h3>Recipe</h3> |
| <h2>What changed, in three ideas.</h2> |
|
|
| <div class="card-row"> |
| <div class="card"> |
| <span class="pill">01 · Decontamination</span> |
| <h4>Strict eval-set scrub</h4> |
| <p>Training corpus is filtered against CTI-Bench prompts and near-duplicates before any update sees a gradient. No leakage, no shortcutting.</p> |
| </div> |
| <div class="card"> |
| <span class="pill">02 · IT-base preservation</span> |
| <h4>Keep the instruction-tuned voice</h4> |
| <p>Loss is balanced so the model adopts CTI domain knowledge without losing Qwen3's general instruction-following. Tone, formatting, and refusal behavior remain intact.</p> |
| </div> |
| <div class="card"> |
| <span class="pill">03 · Recipe portability</span> |
| <h4>Same recipe, different base</h4> |
| <p>The same procedure applied to Gemma-4-E2B-it produces Gemma4Defense-2B with comparable lift — evidence the gains come from the recipe, not the base.</p> |
| </div> |
| </div> |
|
|
| <h3>Corpus · ~14,776 supervised records</h3> |
| <ul class="bullets"> |
| <li><strong>rcm-2021 (decontaminated)</strong> — ~6,776 CVE → CWE classification examples from MITRE/NVD 2021 cohort, with all CTI-Bench overlap items removed.</li> |
| <li><strong>cve_cti synthetic Q&A</strong> — ~8,000 defensive-analyst-style Q&A pairs grounded in CVE descriptions.</li> |
| </ul> |
| <p>An earlier internal CPT corpus had <strong>72.3% test-set overlap</strong> with CTI-Bench. The released model trains exclusively on the 2021 cohort with overlap removed — released numbers are post-fix.</p> |
|
|
| <h3>Hyperparameters</h3> |
| <dl class="spec"> |
| <dt>Base</dt> <dd>Qwen3-4B-Instruct-2507</dd> |
| <dt>Adapter</dt> <dd>LoRA r=64, α=64, dropout=0.05</dd> |
| <dt>Targets</dt> <dd>q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj</dd> |
| <dt>Optimizer</dt> <dd>AdamW · cosine schedule · warmup_ratio=0.05 · weight_decay=0.01</dd> |
| <dt>LR · peak</dt> <dd>5e-5</dd> |
| <dt>Batch · effective</dt><dd>2 per device · grad_accum 8 · effective batch 16</dd> |
| <dt>Schedule</dt> <dd>10 epochs · max_seq_len 4096</dd> |
| <dt>Precision</dt> <dd>bfloat16 throughout</dd> |
| <dt>Attention</dt> <dd>flash_attention_2 (FA2)</dd> |
| <dt>Wall · MI300X</dt> <dd>173 min · 1,290 steps · 7.85 s/step</dd> |
| <dt>Eval protocol</dt> <dd>Foundation-Sec arXiv:2504.21039 §B.3-B.4 · n=5 · temp 0.3</dd> |
| </dl> |
| </section> |
|
|
| <section id="hardware"> |
| <h3>Hardware</h3> |
| <h2>Trained on AMD MI300X.</h2> |
| <p>The recipe was developed on a single-node MI300X stack. Optimizations are aimed at ROCm; the recipe ports cleanly to other datacenter-class GPUs (40 GB+ VRAM) with the FA2 caveat noted below.</p> |
|
|
| <h3>Stack</h3> |
| <dl class="spec"> |
| <dt>Accelerator</dt> <dd>AMD Instinct MI300X · 192 GB HBM3 (gfx942)</dd> |
| <dt>Runtime</dt> <dd>ROCm 7.0</dd> |
| <dt>Docker image</dt> <dd>vllm/vllm-openai-rocm:latest</dd> |
| <dt>PyTorch</dt> <dd>2.6.0 (ROCm)</dd> |
| <dt>flash-attn</dt> <dd>2.8.3 (preinstalled in vLLM ROCm image)</dd> |
| <dt>vLLM</dt> <dd>0.10.1</dd> |
| </dl> |
|
|
| <h3>FA2 viability per model family</h3> |
| <p style="font-size:13.5px;color:var(--fg-4);margin:0 0 10px;">FA2 via the ROCm/flash-attention Composable-Kernels backend is supported on MI300X but bounded at <strong style="color:var(--fg-2)">head_dim ≤ 256</strong> by the LDS shared-memory budget.</p> |
| <table class="cmp"> |
| <thead><tr><th>Family</th><th>head_dim</th><th>FA2</th><th>Note</th></tr></thead> |
| <tbody> |
| <tr class="us"><td class="model">Qwen3 (this work)</td><td>128</td><td>✓ enabled</td><td>fits LDS budget — ~1.6× faster than sdpa</td></tr> |
| <tr><td class="model">Llama-3 / Mistral</td><td>128</td><td>✓ works</td><td>same head_dim class</td></tr> |
| <tr><td class="model">Gemma-2</td><td>256</td><td>✓ boundary</td><td>at LDS limit, viable</td></tr> |
| <tr><td class="model">Gemma-4 (global layers)</td><td>512</td><td>✗ disabled</td><td>exceeds LDS — fallback to sdpa</td></tr> |
| </tbody> |
| </table> |
|
|
| <h3>Optimizations</h3> |
| <ul class="bullets"> |
| <li><strong>FA2 enabled in training</strong> — Qwen3-4B head_dim=128 fits the gfx942 LDS budget; ~7.85 s/step at LoRA r=64 / max_seq_len=4096 — <strong>~1.6× faster</strong> than the same recipe on Gemma-4 (sdpa fallback).</li> |
| <li><strong>TRITON_ATTN backend for vLLM inference</strong> — recommended on MI300X for Qwen3-class models.</li> |
| <li><strong>bf16 throughout</strong> — native MI300X precision; no mixed-precision dance.</li> |
| <li><strong>AITER kernels for matmul</strong> — <code>VLLM_ROCM_USE_AITER=1</code> + <code>TORCH_BLAS_PREFER_HIPBLASLT=1</code>. Note: this works for Qwen3 dense — does NOT work for gpt-oss MoE (AITER=0 required there).</li> |
| <li><strong>Prefix caching</strong> — vLLM <code>--enable-prefix-caching</code> for shared system-prompt batches.</li> |
| <li><strong>HF Transfer for push/pull</strong> — <code>HF_HUB_ENABLE_HF_TRANSFER=1</code> saturates ~240 MB/s; 8 GB merged model uploads in ~36 s.</li> |
| </ul> |
|
|
| <h3>ROCm environment (verified-working)</h3> |
| <pre class="code"><span class="c"># env exported inside the vLLM ROCm Docker container</span> |
| <span class="k">export</span> VLLM_ROCM_USE_AITER=1 |
| <span class="k">export</span> TORCH_BLAS_PREFER_HIPBLASLT=1 |
| <span class="k">export</span> HF_HUB_DISABLE_XET=1 |
| <span class="k">export</span> PYTORCH_ROCM_ARCH=<span class="c">'gfx90a;gfx942;gfx950'</span> |
| <span class="k">export</span> AITER_ROCM_ARCH=<span class="c">'gfx942;gfx950'</span> |
| <span class="k">export</span> HIP_FORCE_DEV_KERNARG=1</pre> |
|
|
| <p><strong>Portability:</strong> the recipe runs on other 40 GB+ datacenter GPUs with the AMD-specific env vars dropped (no-ops elsewhere). FA2 via <code>pip install flash-attn --no-build-isolation</code>. Same VRAM minimums: 24 GB+ for training, 12 GB+ for inference.</p> |
| </section> |
|
|
| <section id="limitations"> |
| <h3>Limitations</h3> |
| <h2>What it's good at, and where it isn't.</h2> |
|
|
| <h3>In-distribution · verified strong</h3> |
| <div class="card-row"> |
| <div class="card"> |
| <span class="pill">Strong</span> |
| <h4>CWE classification</h4> |
| <p>Mapping a described weakness or code pattern to a CWE id with a short rationale. Calibrated on CTI-Bench RCM.</p> |
| </div> |
| <div class="card"> |
| <span class="pill">Strong</span> |
| <h4>CVE-to-CWE on trained CVEs</h4> |
| <p>Returns the underlying CWE for CVEs whose descriptions are in-distribution. Confidence drops for CVEs disclosed after the corpus cutoff.</p> |
| </div> |
| <div class="card"> |
| <span class="pill">Strong</span> |
| <h4>Code-pattern reasoning</h4> |
| <p>Identifying the weakness class behind a small snippet (string-format SQL, unchecked path joins, inline HTML rendering, etc.).</p> |
| </div> |
| </div> |
|
|
| <h3>Out-of-distribution · known failure modes</h3> |
| <div class="card-row"> |
| <div class="card"> |
| <span class="pill">Weak</span> |
| <h4>MITRE ATT&CK technique IDs</h4> |
| <p>Wrong T-numbers (returned T1543/003 — Windows Service — for scheduled-task persistence, vs the correct T1053.005). Wrong technique names (called LSASS dumping "Extract Web Credentials"; correct is T1003.001 OS Credential Dumping: LSASS Memory).</p> |
| </div> |
| <div class="card"> |
| <span class="pill">Weak</span> |
| <h4>CVE implementation specifics</h4> |
| <p>Fabricates implementation details. Cited a non-existent <code>pgp</code> binary path when asked about CVE-2024-3400 (PAN-OS GlobalProtect — actual root cause is session-ID handling). Top-level CWE call usually correct; deep mechanics often invented.</p> |
| </div> |
| <div class="card"> |
| <span class="pill">Weak</span> |
| <h4>Tool categorization</h4> |
| <p>Misclassifies offensive tooling. Listed Mimikatz as a "ransomware loader" — Mimikatz is a credential-dumping utility, not a ransomware loader.</p> |
| </div> |
| </div> |
|
|
| <h3>Validated empirically · we tested 14, kept 7</h3> |
| <p>We ran 14 candidate demo prompts through the deployed model and graded each output for factual accuracy. <strong>7 passed</strong>, <strong>7 were cut</strong>: Log4Shell vs Spring4Shell exploit-primitive comparison; CVE-2024-3400 explanation; LSASS ATT&CK mapping; schtasks ATT&CK mapping; Dockerfile <code>curl|bash</code> review; ransomware detection signals; Python dynamic-code-execution CWE. Most cuts were OOD hallucinations: invented technique numbers, fabricated CVE specifics, mislabeled mitigations.</p> |
|
|
| <h3>Out-of-scope use</h3> |
| <ul class="bullets"> |
| <li>Generating exploit code, weaponized PoC, or attacker tradecraft.</li> |
| <li>Critical security decisions without qualified human review.</li> |
| <li>Legal, medical, or other regulated-advice contexts.</li> |
| <li>Tasks outside cybersecurity (general chat, code generation, summarization).</li> |
| <li>Violation of laws (CFAA, GDPR, etc.).</li> |
| </ul> |
|
|
| <h3>Recommendations</h3> |
| <ul class="bullets"> |
| <li><strong>Pair with retrieval.</strong> Ground CVE / advisory queries against an authoritative source before quoting specifics.</li> |
| <li><strong>Sample at low temperature.</strong> CWE selection benefits from determinism (temp 0.2–0.3).</li> |
| <li><strong>Treat output as a triage hint,</strong> not a verdict — keep an analyst in the loop.</li> |
| </ul> |
| </section> |
|
|
| <section id="companion"> |
| <h3>Companion</h3> |
| <h2>Gemma4Defense-2B</h2> |
| <p>The same recipe applied to Gemma-4-E2B-it produces a 2B companion that scores RCM <strong>0.6754 ± 0.0035</strong> and MCQ <strong>0.6042 ± 0.0090</strong> — slightly higher MCQ than CyberSecQwen-4B at half the parameters again. Use the 2B when memory is tight; use the 4B when you want stronger general instruction-following and longer-form rationales.</p> |
| </section> |
|
|
| <section id="citation"> |
| <h3>Citation</h3> |
| <h2>Cite & license.</h2> |
| <pre class="code"><span class="c">% bibtex</span> |
| @misc{cybersecqwen2026, |
| title = {CyberSecQwen-4B: A Compact CTI Specialist Fine-Tuned from |
| Qwen3-4B-Instruct-2507 on AMD MI300X}, |
| author = {Mulia, Samuel}, |
| year = {2026}, |
| publisher = {Hugging Face}, |
| url = {https://huggingface.co/athena129/CyberSecQwen-4B} |
| }</pre> |
| <p><strong>License:</strong> Apache 2.0, end-to-end — weights, training code, and the synthetic CVE/CTI Q&A corpus. The decontaminated 2021 CVE→CWE mappings derive from public MITRE/NVD records. Evaluation protocol: <a href="https://arxiv.org/abs/2504.21039" target="_blank" style="color:var(--accent)">Foundation-Sec-8B (arXiv:2504.21039)</a>. Benchmark: <a href="https://github.com/xashru/cti-bench" target="_blank" style="color:var(--accent)">CTI-Bench</a>.</p> |
| </section> |
|
|
| |
| <div class="cta-block"> |
| <h3>Ready to <em>try it?</em></h3> |
| <p>Test CyberSecQwen-4B on your own prompts below. ZeroGPU; cold start ~10–20 s, then warm.</p> |
| <a href="#chat" class="cta-primary" id="ctaTry"> |
| Try the chat |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12l7 7 7-7"/></svg> |
| </a> |
| </div> |
|
|
| </div> |
| </div> |
| </section> |
|
|
| |
| <section class="page-section chat-section" id="chat"> |
| |
| <div class="signin-gate hidden" id="signinGate"> |
| <div class="signin-card"> |
| <h3>Sign in to <em>start chatting.</em></h3> |
| <p class="lede"> |
| The model runs on Hugging Face ZeroGPU — a free, shared GPU pool. |
| Sign in with your HF account so each call uses your own quota |
| <span style="color:var(--fg-3)">(free tier: 3.5 min/day · Pro: 25 min/day)</span> |
| instead of the 2 min/day anonymous IP cap. |
| </p> |
| <button class="signin-btn" id="signinBtn" type="button"> |
| <span class="hf-emoji">🤗</span> |
| <span>Sign in with Hugging Face</span> |
| </button> |
| <div class="signin-meta"> |
| No data stored — only your access token, kept locally. |
| <br/>Don't have an account? <a href="https://huggingface.co/join" target="_blank" rel="noopener">Create one (free)</a>. |
| </div> |
| </div> |
| </div> |
|
|
| <div class="chat-empty" id="chatEmpty"> |
| <div class="hero-icon" aria-hidden="true"> |
| <svg viewBox="0 0 24 24" width="36" height="36" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/><path d="M9 12l2 2 4-4"/></svg> |
| </div> |
| <h2 class="hero-h1">Map a vulnerability<br/><em>to a CWE.</em></h2> |
| <p class="hero-sub">Paste a snippet, log line, or CVE. The model returns a Common Weakness Enumeration with a one-paragraph rationale.</p> |
|
|
| <form class="composer" id="composerEmpty"> |
| <textarea rows="1" id="taEmpty" placeholder="Describe the issue, paste code, or drop a CVE id…"></textarea> |
| <div class="composer-row"> |
| <span class="send-hint">↵ to send · ⇧↵ for newline</span> |
| <button class="send-btn" type="submit" id="sendEmpty" disabled> |
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l14-7-7 14-2-5-5-2z"/></svg> |
| Send |
| </button> |
| </div> |
| </form> |
|
|
| <div class="chips" id="chips"> |
| <button class="chip" data-prompt="What CWE applies to using strcpy without bounds checking? Give a one-paragraph rationale."> |
| <span class="kind">CWE</span> Buffer overflow |
| </button> |
| <button class="chip" data-prompt="What CWE underlies the MOVEit Transfer SQL injection chain (CVE-2023-34362)?"> |
| <span class="kind">CVE</span> MOVEit |
| </button> |
| <button class="chip" data-prompt="What CWE applies to: query = 'SELECT * FROM users WHERE id=' + user_id"> |
| <span class="kind">CWE</span> SQL injection |
| </button> |
| <button class="chip" data-prompt="Log line: GET /static/../../../etc/passwd HTTP/1.1 200 — what weakness, and how to fix?"> |
| <span class="kind">LOG</span> path traversal |
| </button> |
| </div> |
| </div> |
|
|
| <div class="chat-active hidden" id="chatActive"> |
| <div class="thread"><div class="thread-inner" id="threadInner"></div></div> |
| <div class="dock"> |
| <div class="dock-inner"> |
| <form class="composer" id="composerDock"> |
| <textarea rows="1" id="taDock" placeholder="Continue the conversation…"></textarea> |
| <div class="composer-row"> |
| <span class="send-hint">↵ to send · ⇧↵ for newline</span> |
| <button class="send-btn" type="submit" id="sendDock" disabled> |
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l14-7-7 14-2-5-5-2z"/></svg> |
| Send |
| </button> |
| </div> |
| </form> |
| <button class="new-chat" id="newChat" type="button" title="Start a new chat"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg> |
| New chat |
| </button> |
| </div> |
| </div> |
| </div> |
| </section> |
|
|
| </main> |
| </div> |
|
|
| <script type="module"> |
| |
| |
| |
| |
| |
| |
| |
| |
| import { oauthLoginUrl, oauthHandleRedirectIfPresent } from "@huggingface/hub"; |
| |
| let oauthResult = null; |
| try { |
| const stored = localStorage.getItem("hf_oauth"); |
| if (stored) oauthResult = JSON.parse(stored); |
| } catch { oauthResult = null; } |
| |
| |
| const fromRedirect = await oauthHandleRedirectIfPresent(); |
| if (fromRedirect) { |
| oauthResult = fromRedirect; |
| localStorage.setItem("hf_oauth", JSON.stringify(oauthResult)); |
| |
| const cleanHash = location.hash || "#chat"; |
| history.replaceState(null, "", location.pathname + cleanHash); |
| } |
| |
| function getOAuthToken(){ |
| return oauthResult?.accessToken || null; |
| } |
| function getUsername(){ |
| return oauthResult?.userInfo?.preferredUsername || oauthResult?.userInfo?.name || ""; |
| } |
| async function startSignIn(){ |
| |
| const scopes = (window.huggingface?.variables?.OAUTH_SCOPES) || "openid profile"; |
| window.location.href = (await oauthLoginUrl({ scopes })) + "&prompt=consent"; |
| } |
| function signOut(){ |
| oauthResult = null; |
| localStorage.removeItem("hf_oauth"); |
| |
| _client = null; |
| applyAuthUI(); |
| } |
| |
| |
| |
| |
| |
| |
| |
| const BACKEND_SPACE = "athena129/cybersecqwen-demo"; |
| let _client = null; |
| let _clientToken = null; |
| async function getClient(){ |
| const tok = getOAuthToken(); |
| if (_client && _clientToken === tok) return _client; |
| const { Client } = await import("https://esm.sh/@gradio/client@2.2.0"); |
| const opts = { events: ["data", "status"] }; |
| if (tok) opts.hf_token = tok; |
| _client = await Client.connect(BACKEND_SPACE, opts); |
| _clientToken = tok; |
| return _client; |
| } |
| |
| |
| |
| |
| |
| const FINISH_MARKER_RE = /\n?<!--finish:(\w+)-->\s*$/; |
| function stripFinishMarker(text){ |
| if (!text) return { text: "", finishReason: null }; |
| const m = text.match(FINISH_MARKER_RE); |
| if (m) return { text: text.slice(0, text.length - m[0].length), finishReason: m[1] }; |
| return { text, finishReason: null }; |
| } |
| |
| async function askModel(prompt, history, onChunk, onStatus){ |
| const client = await getClient(); |
| const job = client.submit("/chat", [prompt, JSON.stringify(history || [])]); |
| let lastContent = ""; |
| let lastFinishReason = null; |
| let errorPayload = null; |
| |
| |
| |
| |
| |
| const INACTIVITY_MS = 90_000; |
| let watchdog = null; |
| let timedOut = false; |
| const arm = () => { |
| if (watchdog) clearTimeout(watchdog); |
| watchdog = setTimeout(() => { |
| timedOut = true; |
| try { job.cancel?.(); } catch {} |
| }, INACTIVITY_MS); |
| }; |
| arm(); |
| |
| try { |
| for await (const msg of job){ |
| arm(); |
| if (timedOut) break; |
| if (msg.type === "data" && msg.data && Array.isArray(msg.data)){ |
| const raw = msg.data[0]; |
| if (typeof raw === "string" && raw !== lastContent){ |
| const { text: cleaned, finishReason } = stripFinishMarker(raw); |
| if (finishReason) lastFinishReason = finishReason; |
| if (cleaned !== lastContent){ |
| lastContent = cleaned; |
| if (onChunk) onChunk(lastContent); |
| } |
| } |
| } else if (msg.type === "status"){ |
| if (msg.stage === "error"){ |
| errorPayload = { title: msg.title || "", message: msg.message || "" }; |
| } |
| if (onStatus) onStatus(msg); |
| |
| |
| |
| |
| |
| if (msg.stage === "complete" || msg.stage === "error") { |
| try { job.cancel?.(); } catch {} |
| break; |
| } |
| } |
| } |
| } finally { |
| if (watchdog) clearTimeout(watchdog); |
| } |
| |
| if (timedOut){ |
| const err = new Error("The streaming connection went quiet for 90 s — likely a backend hang, dropped SSE, or ZeroGPU duration cap reached."); |
| err.title = "Stream timeout"; |
| err.partial = lastContent; |
| throw err; |
| } |
| if (errorPayload){ |
| const err = new Error(errorPayload.message || errorPayload.title || "Backend returned an error."); |
| err.title = errorPayload.title; |
| err.detail = errorPayload.message; |
| err.partial = lastContent; |
| throw err; |
| } |
| return { |
| answer: lastContent || "The model returned no output. Try again.", |
| finishReason: lastFinishReason, |
| }; |
| } |
| |
| |
| function explainError(e){ |
| const msg = (e && (e.message || String(e))) || ""; |
| const title = (e && e.title) || ""; |
| const lower = (title + " " + msg).toLowerCase(); |
| if (lower.includes("zerogpu quota") || lower.includes("quota exceeded")){ |
| if (!getOAuthToken()){ |
| return ( |
| "Anonymous quota exhausted (2 minutes per day per IP). " + |
| "Sign in with Hugging Face for your own quota — free tier 3.5 min/day, Pro 25 min/day." |
| ); |
| } |
| return ( |
| "Your HF quota is exhausted for today. Pro accounts get 25 min/day; " + |
| "free accounts get 3.5 min/day. The quota resets 24 h after first use.\n\n" + msg |
| ); |
| } |
| if (lower.includes("cold") || lower.includes("starting") || lower.includes("loading")){ |
| return "ZeroGPU is cold-starting (~10–20 seconds after idle). Try again in a moment."; |
| } |
| return "The backend isn't reachable right now. " + msg; |
| } |
| |
| |
| |
| |
| |
| function applyAuthUI(){ |
| const signed = !!getOAuthToken(); |
| const gate = document.getElementById('signinGate'); |
| const empty = document.getElementById('chatEmpty'); |
| const active = document.getElementById('chatActive'); |
| const statusPill = document.getElementById('statusPill'); |
| const userPill = document.getElementById('userPill'); |
| const userPillWho = document.getElementById('userPillWho'); |
| |
| if (signed){ |
| gate.classList.add('hidden'); |
| |
| if (convo.length === 0){ |
| empty.classList.remove('hidden'); |
| active.classList.add('hidden'); |
| } |
| if (statusPill) statusPill.style.display = 'none'; |
| if (userPill){ |
| userPill.style.display = 'flex'; |
| userPillWho.textContent = '@' + (getUsername() || 'user'); |
| } |
| } else { |
| |
| empty.classList.add('hidden'); |
| active.classList.add('hidden'); |
| gate.classList.remove('hidden'); |
| if (statusPill) statusPill.style.display = ''; |
| if (userPill) userPill.style.display = 'none'; |
| } |
| } |
| |
| |
| const main = document.getElementById('main'); |
| const navLinks = document.querySelectorAll('#nav a'); |
| const aboutSection = document.getElementById('about'); |
| const chatSection = document.getElementById('chat'); |
| const chatEmpty = document.getElementById('chatEmpty'); |
| const chatActive = document.getElementById('chatActive'); |
| const threadInner = document.getElementById('threadInner'); |
| |
| let convo = []; |
| |
| |
| |
| |
| |
| |
| const CONVO_STORAGE_KEY = 'cybersecqwen_convo_v1'; |
| function saveConvo(){ |
| try { |
| |
| |
| const stable = convo.map(m => { |
| if (m.pending) return null; |
| const { pending, statusText, ...keep } = m; |
| void pending; void statusText; |
| return keep; |
| }).filter(Boolean); |
| |
| |
| |
| while (stable.length && stable[stable.length - 1].role === 'user'){ |
| stable.pop(); |
| } |
| localStorage.setItem(CONVO_STORAGE_KEY, JSON.stringify(stable)); |
| } catch { } |
| } |
| function loadConvo(){ |
| try { |
| const raw = localStorage.getItem(CONVO_STORAGE_KEY); |
| if (!raw) return []; |
| const parsed = JSON.parse(raw); |
| if (!Array.isArray(parsed)) return []; |
| |
| |
| const out = [...parsed]; |
| while (out.length && (out[out.length - 1].pending || out[out.length - 1].error)){ |
| out.pop(); |
| |
| if (out.length && out[out.length - 1].role === 'user') out.pop(); |
| } |
| return out; |
| } catch { return []; } |
| } |
| function clearConvoStorage(){ |
| try { localStorage.removeItem(CONVO_STORAGE_KEY); } catch {} |
| } |
| |
| |
| function el(tag, opts){ |
| const e = document.createElement(tag); |
| if (opts){ |
| if (opts.cls) e.className = opts.cls; |
| if (opts.text != null) e.textContent = opts.text; |
| } |
| return e; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| function renderMdToFragment(s){ |
| const frag = document.createDocumentFragment(); |
| let i = 0; |
| while (i < s.length){ |
| const startI = i; |
| |
| if (s[i] === '*' && s[i+1] === '*'){ |
| const end = s.indexOf('**', i + 2); |
| if (end !== -1){ |
| frag.appendChild(el('strong', { text: s.slice(i + 2, end) })); |
| i = end + 2; |
| continue; |
| } |
| |
| frag.appendChild(document.createTextNode('**')); |
| i += 2; |
| continue; |
| } |
| |
| if (s[i] === '`'){ |
| const end = s.indexOf('`', i + 1); |
| if (end !== -1){ |
| frag.appendChild(el('code', { text: s.slice(i + 1, end) })); |
| i = end + 1; |
| continue; |
| } |
| |
| frag.appendChild(document.createTextNode('`')); |
| i += 1; |
| continue; |
| } |
| |
| let next = i; |
| while (next < s.length){ |
| if (s[next] === '`') break; |
| if (s[next] === '*' && s[next + 1] === '*') break; |
| next++; |
| } |
| frag.appendChild(document.createTextNode(s.slice(i, next))); |
| i = next; |
| |
| if (i === startI) i++; |
| } |
| return frag; |
| } |
| |
| |
| |
| |
| let _inflight = false; |
| const _composerUpdaters = []; |
| |
| function refreshComposers(){ _composerUpdaters.forEach(fn => fn()); } |
| |
| function bindComposer(form, ta, btn, onSend){ |
| const update = () => { btn.disabled = _inflight || ta.value.trim().length === 0; }; |
| _composerUpdaters.push(update); |
| ta.addEventListener('input', () => { |
| ta.style.height = 'auto'; |
| ta.style.height = Math.min(ta.scrollHeight, 240) + 'px'; |
| update(); |
| }); |
| ta.addEventListener('keydown', e => { |
| if (e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); if (!btn.disabled) form.requestSubmit(); } |
| }); |
| form.addEventListener('submit', e => { e.preventDefault(); const v = ta.value.trim(); if (!v || _inflight) return; onSend(v); }); |
| update(); |
| } |
| |
| bindComposer( |
| document.getElementById('composerEmpty'), |
| document.getElementById('taEmpty'), |
| document.getElementById('sendEmpty'), |
| sendMessage, |
| ); |
| bindComposer( |
| document.getElementById('composerDock'), |
| document.getElementById('taDock'), |
| document.getElementById('sendDock'), |
| sendMessage, |
| ); |
| |
| document.querySelectorAll('#chips .chip').forEach(c => { |
| c.addEventListener('click', () => { |
| const ta = document.getElementById('taEmpty'); |
| ta.value = c.dataset.prompt; |
| ta.dispatchEvent(new Event('input')); |
| ta.focus(); |
| }); |
| }); |
| |
| document.getElementById('newChat').addEventListener('click', () => { |
| convo = []; |
| clearConvoStorage(); |
| showEmptyState(); |
| chatSection.scrollIntoView({ behavior:'smooth', block:'start' }); |
| }); |
| |
| |
| function showEmptyState(){ |
| chatActive.classList.add('hidden'); |
| chatEmpty.classList.remove('hidden'); |
| while (threadInner.firstChild) threadInner.removeChild(threadInner.firstChild); |
| setTimeout(() => document.getElementById('taEmpty')?.focus(), 30); |
| } |
| |
| function showActiveState(){ |
| chatEmpty.classList.add('hidden'); |
| chatActive.classList.remove('hidden'); |
| } |
| |
| |
| |
| function svgIcon(d, opts){ |
| const NS = 'http://www.w3.org/2000/svg'; |
| const s = document.createElementNS(NS, 'svg'); |
| s.setAttribute('viewBox', '0 0 24 24'); |
| s.setAttribute('fill', 'none'); |
| s.setAttribute('stroke', 'currentColor'); |
| s.setAttribute('stroke-width', (opts && opts.sw) || '1.8'); |
| s.setAttribute('stroke-linecap', 'round'); |
| s.setAttribute('stroke-linejoin', 'round'); |
| const p = document.createElementNS(NS, 'path'); |
| p.setAttribute('d', d); |
| s.appendChild(p); |
| return s; |
| } |
| |
| function makeActionBtn(action, label, iconPath, extraCls){ |
| const b = el('button', { cls: 'msg-action' + (extraCls ? ' ' + extraCls : '') }); |
| b.type = 'button'; |
| b.dataset.action = action; |
| b.appendChild(svgIcon(iconPath)); |
| b.appendChild(el('span', { text: label })); |
| return b; |
| } |
| |
| function buildMsg(m){ |
| const wrap = el('div', { cls: 'msg ' + (m.role === 'user' ? 'user' : 'bot') }); |
| wrap.appendChild(el('span', { cls: 'who', text: m.role === 'user' ? 'You' : 'CyberSecQwen' })); |
| |
| if (m.role === 'user'){ |
| wrap.appendChild(el('div', { cls: 'bubble', text: m.text })); |
| return wrap; |
| } |
| |
| const body = el('div', { cls: 'body' }); |
| if (m.pending){ |
| const typing = el('span', { cls: 'typing' }); |
| typing.appendChild(el('i')); |
| typing.appendChild(el('i')); |
| typing.appendChild(el('i')); |
| body.appendChild(typing); |
| body.appendChild(el('div', { cls: 'status-text', text: m.statusText || 'Connecting…' })); |
| } else if (m.error){ |
| body.appendChild(el('div', { cls: 'status-text warn', text: m.text })); |
| } else { |
| body.appendChild(renderMdToFragment(m.text)); |
| |
| |
| |
| |
| const actions = el('div', { cls: 'msg-actions' }); |
| actions.appendChild(makeActionBtn( |
| 'copy', 'Copy', |
| 'M9 9h10v12H9zM5 5h10v2M5 5v10h2', |
| )); |
| |
| const isLastBot = (m === convo[convo.length - 1]); |
| if (isLastBot){ |
| actions.appendChild(makeActionBtn( |
| 'regenerate', 'Regenerate', |
| 'M3 12a9 9 0 0 1 15.5-6.3L21 8M21 3v5h-5M21 12a9 9 0 0 1-15.5 6.3L3 16M3 21v-5h5', |
| )); |
| } |
| |
| if (m.finishReason === 'length' && isLastBot){ |
| actions.appendChild(makeActionBtn( |
| 'continue', 'Continue', |
| 'M5 12h14M13 6l6 6-6 6', |
| 'continue', |
| )); |
| const note = el('div', { cls: 'truncation-note' }); |
| note.appendChild(el('span', { cls: 'dot', text: '● ' })); |
| note.appendChild(document.createTextNode('Response was capped at the token limit. Click Continue to extend.')); |
| body.appendChild(note); |
| } |
| body.appendChild(actions); |
| } |
| wrap.appendChild(body); |
| return wrap; |
| } |
| |
| |
| |
| |
| |
| |
| |
| const PIN_THRESHOLD_PX = 96; |
| function isPinnedToBottom(){ |
| return (main.scrollHeight - main.scrollTop - main.clientHeight) <= PIN_THRESHOLD_PX; |
| } |
| function scrollToBottom(){ |
| main.scrollTo({ top: main.scrollHeight }); |
| } |
| |
| function replaceLastMsg(){ |
| const wasPinned = isPinnedToBottom(); |
| const last = threadInner.lastElementChild; |
| if (last) last.replaceWith(buildMsg(convo[convo.length - 1])); |
| if (wasPinned) scrollToBottom(); |
| } |
| |
| function appendToThread(m){ |
| const wasPinned = isPinnedToBottom(); |
| threadInner.appendChild(buildMsg(m)); |
| |
| |
| |
| scrollToBottom(); |
| void wasPinned; |
| } |
| |
| |
| |
| |
| |
| |
| function isTransientError(e){ |
| const lower = ((e && (e.title || '')) + ' ' + (e && (e.message || ''))).toLowerCase(); |
| if (lower.includes('quota')) return false; |
| if (lower.includes('cold') || lower.includes('starting') || lower.includes('loading')) return false; |
| if (lower.includes('sign in')) return false; |
| return true; |
| } |
| |
| async function sendMessage(text){ |
| if (_inflight) return; |
| _inflight = true; |
| refreshComposers(); |
| |
| const wasEmpty = convo.length === 0; |
| if (wasEmpty){ showActiveState(); } |
| |
| convo.push({ role:'user', text }); |
| convo.push({ role:'bot', text:'', pending:true, statusText:'Connecting to backend…' }); |
| appendToThread(convo[convo.length - 2]); |
| appendToThread(convo[convo.length - 1]); |
| saveConvo(); |
| |
| |
| for (const id of ['taEmpty', 'taDock']){ |
| const ta = document.getElementById(id); |
| if (ta){ ta.value = ''; ta.style.height='auto'; ta.dispatchEvent(new Event('input')); } |
| } |
| |
| |
| const hist = convo.slice(0, -2) |
| .filter(m => !m.pending && !m.error) |
| .map(m => ({ role: m.role === 'bot' ? 'assistant' : 'user', content: m.text })); |
| |
| |
| const t0 = Date.now(); |
| let statusTimer = setInterval(() => { |
| const last = convo[convo.length - 1]; |
| if (!last.pending) return; |
| const elapsed = (Date.now() - t0) / 1000; |
| let copy = last.statusText; |
| if (elapsed > 5 && elapsed <= 12) copy = 'Waking ZeroGPU (cold start ~10–20 s)…'; |
| else if (elapsed > 12 && elapsed <= 25) copy = 'Cold start in progress — almost there…'; |
| else if (elapsed > 25) copy = 'Still warming up — ZeroGPU shared pool can be slow.'; |
| if (copy !== last.statusText){ |
| last.statusText = copy; |
| replaceLastMsg(); |
| } |
| }, 1500); |
| |
| const onChunk = (partial) => { |
| if (statusTimer){ clearInterval(statusTimer); statusTimer = null; } |
| convo[convo.length - 1] = { role:'bot', text: partial }; |
| replaceLastMsg(); |
| }; |
| |
| const onStatus = (s) => { |
| const last = convo[convo.length - 1]; |
| if (!last.pending) return; |
| let copy = null; |
| if (s.stage === 'pending') copy = 'Queued…'; |
| else if (s.stage === 'processing') copy = 'Generating…'; |
| else if (s.stage === 'iterating') copy = 'Generating…'; |
| if (copy && copy !== last.statusText){ |
| last.statusText = copy; |
| replaceLastMsg(); |
| } |
| }; |
| |
| |
| |
| |
| |
| |
| const runOnce = async (isRetry) => { |
| if (isRetry){ |
| _client = null; |
| _clientToken = null; |
| |
| convo[convo.length - 1] = { role:'bot', text:'', pending:true, statusText:'Reconnecting…' }; |
| replaceLastMsg(); |
| } |
| return await askModel(text, hist, onChunk, onStatus); |
| }; |
| |
| let result = null; |
| let firstError = null; |
| try{ |
| result = await runOnce(false); |
| } catch (e){ |
| firstError = e; |
| const noPartial = !(e && typeof e.partial === 'string' && e.partial.length > 0); |
| if (noPartial && isTransientError(e)){ |
| try { |
| result = await runOnce(true); |
| firstError = null; |
| } catch (e2){ |
| firstError = e2; |
| } |
| } |
| } |
| |
| if (result){ |
| convo[convo.length - 1] = { |
| role:'bot', |
| text: result.answer, |
| finishReason: result.finishReason || null, |
| }; |
| } else { |
| const e = firstError; |
| const errMsg = explainError(e); |
| const partial = (e && typeof e.partial === 'string') ? e.partial : ''; |
| convo[convo.length - 1] = { |
| role:'bot', |
| error: true, |
| text: partial ? `${partial}\n\n— ${errMsg}` : errMsg, |
| }; |
| |
| |
| |
| |
| _client = null; |
| _clientToken = null; |
| } |
| |
| if (statusTimer){ clearInterval(statusTimer); statusTimer = null; } |
| _inflight = false; |
| refreshComposers(); |
| replaceLastMsg(); |
| saveConvo(); |
| setTimeout(() => document.getElementById('taDock')?.focus(), 30); |
| } |
| |
| |
| |
| |
| |
| |
| async function regenerateLast(){ |
| if (_inflight) return; |
| if (convo.length < 2) return; |
| |
| const last = convo[convo.length - 1]; |
| if (last.role !== 'bot' || last.pending) return; |
| const userTurn = convo[convo.length - 2]; |
| if (!userTurn || userTurn.role !== 'user') return; |
| |
| |
| |
| |
| convo.pop(); |
| convo.pop(); |
| |
| if (threadInner.lastElementChild) threadInner.removeChild(threadInner.lastElementChild); |
| if (threadInner.lastElementChild) threadInner.removeChild(threadInner.lastElementChild); |
| saveConvo(); |
| await sendMessage(userTurn.text); |
| } |
| |
| async function continueTruncated(){ |
| if (_inflight) return; |
| if (convo.length === 0) return; |
| const last = convo[convo.length - 1]; |
| if (last.role !== 'bot' || last.pending || last.finishReason !== 'length') return; |
| |
| |
| |
| |
| |
| |
| await sendMessage('Continue.'); |
| } |
| |
| async function copyText(text){ |
| try { |
| await navigator.clipboard.writeText(text || ''); |
| return true; |
| } catch { |
| |
| try { |
| const ta = document.createElement('textarea'); |
| ta.value = text || ''; |
| ta.style.position = 'fixed'; |
| ta.style.opacity = '0'; |
| document.body.appendChild(ta); |
| ta.select(); |
| document.execCommand('copy'); |
| document.body.removeChild(ta); |
| return true; |
| } catch { return false; } |
| } |
| } |
| |
| |
| |
| threadInner.addEventListener('click', async (e) => { |
| const btn = e.target.closest('.msg-action'); |
| if (!btn) return; |
| const action = btn.dataset.action; |
| if (action === 'regenerate'){ |
| await regenerateLast(); |
| } else if (action === 'continue'){ |
| await continueTruncated(); |
| } else if (action === 'copy'){ |
| |
| |
| |
| const msgEl = btn.closest('.msg'); |
| const idx = msgEl ? Array.prototype.indexOf.call(threadInner.children, msgEl) : -1; |
| const m = idx >= 0 ? convo[idx] : null; |
| const text = (m && m.text) || ''; |
| if (await copyText(text)){ |
| btn.classList.add('is-flash'); |
| const span = btn.querySelector('span'); |
| const orig = span ? span.textContent : 'Copy'; |
| if (span) span.textContent = 'Copied'; |
| setTimeout(() => { |
| btn.classList.remove('is-flash'); |
| if (span) span.textContent = orig; |
| }, 900); |
| } |
| } |
| }); |
| |
| |
| function setTab(name){ |
| if (name !== 'about' && name !== 'chat') name = 'about'; |
| aboutSection.classList.toggle('is-active', name === 'about'); |
| chatSection.classList.toggle('is-active', name === 'chat'); |
| navLinks.forEach(a => a.classList.toggle('active', a.dataset.section === name)); |
| |
| main.scrollTo({ top: 0 }); |
| |
| const target = '#' + name; |
| if (location.hash !== target){ |
| history.replaceState(null, '', target); |
| } |
| |
| |
| if (name === 'chat'){ |
| applyAuthUI(); |
| if (getOAuthToken() && convo.length === 0){ |
| setTimeout(() => document.getElementById('taEmpty')?.focus(), 30); |
| } |
| } |
| } |
| |
| navLinks.forEach(a => { |
| a.addEventListener('click', e => { |
| e.preventDefault(); |
| setTab(a.dataset.section); |
| }); |
| }); |
| |
| document.getElementById('ctaTry').addEventListener('click', e => { |
| e.preventDefault(); |
| setTab('chat'); |
| }); |
| |
| window.addEventListener('hashchange', () => { |
| setTab(location.hash === '#chat' ? 'chat' : 'about'); |
| }); |
| |
| |
| const subnavLinks = document.querySelectorAll('#subnav a'); |
| const aboutSubsections = document.querySelectorAll('#about .about-inner > section'); |
| |
| subnavLinks.forEach(a => { |
| a.addEventListener('click', e => { |
| e.preventDefault(); |
| const id = a.getAttribute('href').slice(1); |
| const target = document.getElementById(id); |
| if (target) main.scrollTo({ top: target.offsetTop - 110, behavior:'smooth' }); |
| }); |
| }); |
| |
| function setCurrentSubnav(){ |
| if (!aboutSection.classList.contains('is-active')) return; |
| const top = main.scrollTop + 140; |
| let current = aboutSubsections[0]?.id; |
| aboutSubsections.forEach(s => { if (s.offsetTop <= top) current = s.id; }); |
| subnavLinks.forEach(a => a.classList.toggle('is-current', a.getAttribute('href') === '#' + current)); |
| } |
| main.addEventListener('scroll', setCurrentSubnav, { passive:true }); |
| |
| |
| document.getElementById('signinBtn')?.addEventListener('click', startSignIn); |
| document.getElementById('signoutBtn')?.addEventListener('click', signOut); |
| |
| |
| |
| |
| |
| |
| function rehydrateConvoIfAny(){ |
| const restored = loadConvo(); |
| if (!restored.length) return; |
| convo = restored; |
| |
| |
| |
| |
| while (threadInner.firstChild) threadInner.removeChild(threadInner.firstChild); |
| for (const m of convo) threadInner.appendChild(buildMsg(m)); |
| if (getOAuthToken()){ |
| chatEmpty.classList.add('hidden'); |
| chatActive.classList.remove('hidden'); |
| } |
| |
| setTimeout(scrollToBottom, 0); |
| } |
| |
| |
| rehydrateConvoIfAny(); |
| applyAuthUI(); |
| setTab(location.hash === '#chat' ? 'chat' : 'about'); |
| setCurrentSubnav(); |
| </script> |
| </body> |
| </html> |
|
|