synapse / index.html
kimhyunwoo's picture
Update index.html
834bcc3 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#0a0a0a" id="metaTheme">
<title>synapse</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
:root {
--bg:#0a0a0a;--fg:#c9d1d9;--border:#1b2028;--surface:#0d1117;--surface2:#161b22;
--accent:#58a6ff;--accent2:#d2a8ff;--green:#7ee787;--orange:#ffa657;--red:#ff7b72;
--dim:#6e7681;--radius:8px;--toast-bg:rgba(22,27,34,0.95);
}
.light-theme {
--bg:#f6f8fa;--fg:#24292f;--border:#d0d7de;--surface:#ffffff;--surface2:#f0f3f6;
--accent:#0969da;--accent2:#8250df;--green:#1a7f37;--orange:#bf8700;--red:#cf222e;
--dim:#656d76;--toast-bg:rgba(255,255,255,0.95);
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;background:var(--bg);color:var(--fg);font-family:'JetBrains Mono','Consolas',monospace}
body{display:flex;flex-direction:column;overflow:hidden;transition:background .3s,color .3s}
/* Header */
.header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid var(--border);flex-shrink:0}
.header-left{display:flex;align-items:center;gap:8px}
.header h1{font-size:20px;font-weight:300;letter-spacing:8px;text-transform:lowercase;color:var(--fg)}
.header .sub{font-size:9px;color:var(--dim);font-weight:300;letter-spacing:3px}
.header-actions{display:flex;gap:4px}
.header-btn{background:none;border:1px solid var(--border);color:var(--dim);width:30px;height:30px;border-radius:var(--radius);cursor:pointer;font-size:12px;display:flex;align-items:center;justify-content:center;transition:all .2s;font-family:inherit}
.header-btn:hover{border-color:var(--accent);color:var(--accent)}
/* Freq visualizer canvas */
#freqCanvas{width:100%;height:24px;display:block;flex-shrink:0;background:var(--surface2)}
/* Toast */
.toast{position:fixed;top:16px;right:16px;background:var(--toast-bg);border:1px solid var(--border);color:var(--fg);padding:10px 16px;border-radius:var(--radius);font-size:11px;font-family:inherit;z-index:9999;backdrop-filter:blur(12px);opacity:0;transform:translateY(-10px);transition:all .3s}
.toast.show{opacity:1;transform:translateY(0)}
/* Shortcuts modal */
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:9998;display:none;align-items:center;justify-content:center;backdrop-filter:blur(4px)}
.modal-overlay.active{display:flex}
.modal{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:24px;max-width:420px;width:90%;max-height:80vh;overflow-y:auto}
.modal h3{font-size:13px;font-weight:300;letter-spacing:3px;margin-bottom:16px;color:var(--accent)}
.shortcut-row{display:flex;justify-content:space-between;padding:5px 0;font-size:11px;border-bottom:1px solid var(--border)}
.shortcut-row:last-child{border:none}
.shortcut-key{background:var(--surface2);padding:2px 8px;border-radius:4px;font-size:10px;color:var(--accent)}
/* Views */
.view{display:none;flex:1;overflow:hidden}.view.active{display:flex}
/* Setup view */
#setupView{flex-direction:column;align-items:center;justify-content:center;padding:20px;gap:16px;overflow-y:auto}
.setup-card{width:100%;max-width:480px;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:28px 32px;backdrop-filter:blur(12px)}
.setup-title{font-size:11px;color:var(--dim);letter-spacing:3px;margin-bottom:20px;display:flex;align-items:center;gap:8px}
.setup-title i{color:var(--accent);font-size:12px}
.setting-group{margin-bottom:18px}
.setting-group label{display:block;font-size:10px;color:var(--dim);letter-spacing:2px;margin-bottom:8px;text-transform:lowercase}
.setting-detail{font-size:9px;color:var(--dim);margin-top:6px;letter-spacing:1px}
.setting-row{display:flex;align-items:center;gap:10px}
.setting-row input[type=range]{flex:1}
.setting-row .val{font-size:11px;color:var(--accent);min-width:40px;text-align:right}
.preset-btns{display:flex;gap:6px}
.preset-btn{flex:1;padding:8px 0;border:1px solid var(--border);background:transparent;color:var(--dim);border-radius:6px;cursor:pointer;font-family:inherit;font-size:10px;letter-spacing:1px;transition:all .2s;text-align:center}
.preset-btn:hover{border-color:var(--accent);color:var(--accent)}
.preset-btn.active{border-color:var(--accent);color:var(--accent);background:rgba(88,166,255,0.08)}
input[type=range]{-webkit-appearance:none;height:4px;background:var(--border);border-radius:2px;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--accent);cursor:pointer}
input[type=range]::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--accent);cursor:pointer;border:none}
/* Dropzone */
.dropzone{border:2px dashed var(--border);border-radius:var(--radius);padding:16px;text-align:center;font-size:10px;color:var(--dim);cursor:pointer;transition:all .2s;margin-bottom:12px}
.dropzone.dragover{border-color:var(--accent);color:var(--accent);background:rgba(88,166,255,0.05)}
.dropzone i{font-size:18px;display:block;margin-bottom:6px}
/* Load model button */
.load-model-btn{width:100%;padding:10px;margin-bottom:8px;background:rgba(126,231,135,0.08);border:1px solid var(--green);color:var(--green);font-family:inherit;font-size:11px;letter-spacing:2px;cursor:pointer;border-radius:6px;transition:all .2s;display:none;align-items:center;justify-content:center;gap:8px}
.load-model-btn:hover{background:rgba(126,231,135,0.15)}
.load-model-btn.visible{display:flex}
/* API badges */
.api-badges{display:flex;flex-wrap:wrap;gap:4px;margin-top:10px}
.api-badge{font-size:7px;padding:2px 6px;border-radius:3px;background:var(--surface2);color:var(--dim);border:1px solid var(--border);letter-spacing:0.5px}
.start-btn{width:100%;padding:12px;margin-top:8px;background:transparent;border:1px solid var(--border);color:var(--fg);font-family:inherit;font-size:12px;font-weight:300;letter-spacing:4px;cursor:pointer;border-radius:6px;transition:all .3s;display:flex;align-items:center;justify-content:center;gap:10px}
.start-btn:hover{border-color:var(--accent);color:var(--accent);box-shadow:0 0 20px rgba(88,166,255,0.1)}
.start-btn:disabled{opacity:.4;cursor:not-allowed}
.start-btn i{font-size:10px}
.setup-footer{text-align:center;font-size:9px;color:var(--dim);margin-top:12px;letter-spacing:1px}
/* Train view */
#trainView{flex-direction:column;align-items:center;justify-content:center;padding:20px;gap:12px}
#lossCanvas{width:100%;max-width:700px;height:120px;border-radius:var(--radius);background:var(--surface);border:1px solid var(--border)}
.train-stats{display:flex;gap:16px;font-size:10px;color:var(--dim);letter-spacing:1px}
.train-stats span{color:var(--accent)}
.train-box{width:100%;max-width:700px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
.train-bar{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;background:var(--surface2);border-bottom:1px solid var(--border)}
.train-bar .dots{display:flex;gap:5px}
.train-bar .dot{width:9px;height:9px;border-radius:50%}
.train-bar .dot.r{background:#ff5f56}.train-bar .dot.y{background:#ffbd2e}.train-bar .dot.g{background:#27c93f}
.train-bar .title{font-size:10px;color:var(--dim);letter-spacing:2px}
.train-bar .spacer{width:46px}
.progress-wrap{height:3px;background:var(--border)}
.progress-bar{height:100%;width:0%;background:linear-gradient(90deg,var(--accent),var(--accent2));transition:width .15s ease}
#trainOutput{padding:12px 16px;font-size:10px;line-height:1.7;max-height:40vh;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--border) transparent}
#trainOutput .log{color:var(--fg);white-space:pre-wrap}
#trainOutput .info{color:var(--accent)}
#trainOutput .step{color:var(--dim)}
#trainOutput .sep{color:var(--orange);margin-top:6px}
#trainOutput .sample{color:var(--green)}
.train-status{font-size:10px;color:var(--dim);letter-spacing:1px}
/* Chat view */
#chatView{flex-direction:column}
.chat-header{display:flex;align-items:center;justify-content:space-between;padding:8px 16px;border-bottom:1px solid var(--border);flex-shrink:0}
.chat-header .model-info{font-size:9px;color:var(--dim);letter-spacing:1px}
.chat-header .retrain-btn{background:none;border:1px solid var(--border);color:var(--dim);font-family:inherit;font-size:9px;padding:4px 10px;border-radius:4px;cursor:pointer;transition:all .2s;display:flex;align-items:center;gap:5px}
.chat-header .retrain-btn:hover{border-color:var(--accent);color:var(--accent)}
.chat-container{flex:1;overflow-y:auto;padding:12px 16px;scrollbar-width:thin;scrollbar-color:var(--border) transparent}
.messages{max-width:700px;margin:0 auto;display:flex;flex-direction:column;gap:10px}
.msg{display:flex;gap:8px;max-width:85%;opacity:0;transform:translateY(10px);transition:opacity .3s,transform .3s}
.msg.visible{opacity:1;transform:translateY(0)}
.msg.user{align-self:flex-end;flex-direction:row-reverse}
.msg.bot{align-self:flex-start}
.msg-avatar{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;flex-shrink:0;margin-top:2px}
.msg.user .msg-avatar{background:rgba(88,166,255,0.15);color:var(--accent)}
.msg.bot .msg-avatar{background:rgba(126,231,135,0.15);color:var(--green)}
.msg-content{position:relative}
.msg-bubble{padding:9px 13px;border-radius:var(--radius);font-size:12px;line-height:1.6;word-break:break-word;white-space:pre-wrap;min-height:20px}
.msg.user .msg-bubble{background:rgba(88,166,255,0.1);border:1px solid rgba(88,166,255,0.2);color:var(--fg);border-bottom-right-radius:2px}
.msg.bot .msg-bubble{background:var(--surface);border:1px solid var(--border);color:var(--fg);border-bottom-left-radius:2px}
.msg-actions{display:flex;gap:3px;margin-top:3px;opacity:0;transition:opacity .2s}
.msg-content:hover .msg-actions{opacity:1}
.msg-action-btn{background:none;border:none;color:var(--dim);cursor:pointer;font-size:10px;padding:3px 6px;border-radius:3px;transition:all .15s}
.msg-action-btn:hover{color:var(--accent);background:rgba(88,166,255,0.1)}
.msg-action-btn.speaking{color:var(--green)}
.typing{display:flex;gap:8px;align-self:flex-start}
.typing .msg-avatar{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;background:rgba(126,231,135,0.15);color:var(--green)}
.typing-dots{padding:10px 16px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);border-bottom-left-radius:2px;display:flex;gap:4px;align-items:center}
.typing-dots span{width:5px;height:5px;background:var(--dim);border-radius:50%;animation:bounce 1.4s infinite}
.typing-dots span:nth-child(2){animation-delay:.2s}.typing-dots span:nth-child(3){animation-delay:.4s}
@keyframes bounce{0%,60%,100%{transform:translateY(0);opacity:.4}30%{transform:translateY(-5px);opacity:1}}
/* Input area */
.input-area{flex-shrink:0;padding:10px 16px 12px;border-top:1px solid var(--border);background:var(--surface);backdrop-filter:blur(10px)}
.input-row{max-width:700px;margin:0 auto;display:flex;gap:6px;align-items:flex-end}
#chatInput{flex:1;background:var(--surface2);border:1px solid var(--border);color:var(--fg);font-family:inherit;font-size:12px;padding:10px 12px;border-radius:var(--radius);outline:none;resize:none;line-height:1.5;min-height:40px;max-height:100px;transition:border-color .2s}
#chatInput:focus{border-color:var(--accent)}
#chatInput::placeholder{color:var(--dim)}
#chatInput:disabled{opacity:.4}
.input-btn{width:40px;height:40px;border-radius:var(--radius);border:1px solid var(--border);background:var(--surface2);color:var(--dim);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;transition:all .2s;flex-shrink:0}
.input-btn:hover{border-color:var(--accent);color:var(--accent)}
.input-btn:disabled{opacity:.3;cursor:not-allowed}
.input-btn.send-btn{background:rgba(88,166,255,0.1);border-color:rgba(88,166,255,0.3);color:var(--accent)}
.input-btn.send-btn:hover{background:rgba(88,166,255,0.2)}
.input-btn.mic-btn.recording{border-color:var(--red);color:var(--red);animation:pulse 1.5s infinite}
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,123,114,0.3)}50%{box-shadow:0 0 0 6px rgba(255,123,114,0)}}
/* Bottom bar */
.bottom-bar{flex-shrink:0;display:flex;align-items:center;justify-content:space-between;padding:5px 14px;background:var(--surface2);border-top:1px solid var(--border);font-size:9px;color:var(--dim);gap:6px;flex-wrap:wrap}
.status-indicator{display:flex;align-items:center;gap:5px}
.status-dot{width:5px;height:5px;border-radius:50%;background:var(--green)}
.status-dot.error{background:var(--red)}.status-dot.warn{background:var(--orange)}
.net-dot{width:5px;height:5px;border-radius:50%;background:var(--green);margin-left:8px}
.net-dot.offline{background:var(--red)}
.tts-controls{display:flex;align-items:center;gap:6px}
.voice-select{background:var(--surface2);border:1px solid var(--border);color:var(--fg);font-family:inherit;font-size:9px;padding:3px 6px;border-radius:3px;outline:none;cursor:pointer;max-width:200px}
.voice-select:hover{border-color:var(--accent)}
.voice-select option{background:var(--surface);color:var(--fg)}
.tts-toggle{background:var(--surface2);border:1px solid var(--border);color:var(--green);font-family:inherit;font-size:9px;padding:3px 8px;border-radius:3px;cursor:pointer;transition:all .2s;display:flex;align-items:center;gap:4px}
.tts-toggle.off{color:var(--dim)}
.bar-btn{background:none;border:1px solid var(--border);color:var(--dim);font-family:inherit;font-size:9px;padding:3px 8px;border-radius:3px;cursor:pointer;transition:all .2s;display:flex;align-items:center;gap:4px}
.bar-btn:hover{border-color:var(--accent);color:var(--accent)}
.temp-control{display:flex;align-items:center;gap:4px;font-size:9px;color:var(--dim)}
.temp-control input[type=range]{width:60px;height:2px}
@media(max-width:600px){
.header h1{font-size:16px;letter-spacing:4px}
.setup-card{padding:20px}
.msg{max-width:92%}
.bottom-bar{font-size:8px}
.header .sub{display:none}
}
</style>
</head>
<body>
<div class="header">
<div class="header-left">
<div><h1>synapse</h1><div class="sub">train &middot; chat &middot; speak</div></div>
</div>
<div class="header-actions">
<button class="header-btn" id="themeBtn" onclick="toggleTheme()" title="Ctrl+T"><i class="fas fa-moon"></i></button>
<button class="header-btn" id="fullscreenBtn" onclick="toggleFullscreen()" title="F11"><i class="fas fa-expand"></i></button>
<button class="header-btn" onclick="toggleShortcuts()" title="?"><i class="fas fa-keyboard"></i></button>
</div>
</div>
<!-- [2] Canvas 2D frequency visualizer -->
<canvas id="freqCanvas"></canvas>
<!-- Shortcuts modal [22] Web Animations -->
<div class="modal-overlay" id="shortcutsModal">
<div class="modal">
<h3>keyboard shortcuts</h3>
<div class="shortcut-row"><span>Toggle mic</span><span class="shortcut-key">Ctrl+M</span></div>
<div class="shortcut-row"><span>Toggle theme</span><span class="shortcut-key">Ctrl+T</span></div>
<div class="shortcut-row"><span>Save model</span><span class="shortcut-key">Ctrl+S</span></div>
<div class="shortcut-row"><span>Export chat</span><span class="shortcut-key">Ctrl+E</span></div>
<div class="shortcut-row"><span>Fullscreen</span><span class="shortcut-key">F11</span></div>
<div class="shortcut-row"><span>Shortcuts</span><span class="shortcut-key">?</span></div>
<div class="shortcut-row"><span>Stop / Close</span><span class="shortcut-key">Esc</span></div>
</div>
</div>
<!-- Setup view -->
<div id="setupView" class="view active">
<div class="setup-card">
<div class="setup-title"><i class="fas fa-sliders-h"></i> configuration</div>
<!-- [14] File API drag-and-drop -->
<div class="dropzone" id="dropzone">
<i class="fas fa-file-import"></i>
drop .txt training data here<br><span style="font-size:8px;color:var(--dim)">(Q:...\nA:... format, one pair per line)</span>
</div>
<!-- [6] IndexedDB load saved model -->
<button class="load-model-btn" id="loadModelBtn" onclick="loadModelFromDB()">
<i class="fas fa-download"></i> load saved model (skip training)
</button>
<div class="setting-group">
<label>model size</label>
<div class="preset-btns">
<button class="preset-btn" data-preset="tiny" onclick="selectPreset('tiny',this)">tiny</button>
<button class="preset-btn active" data-preset="small" onclick="selectPreset('small',this)">small</button>
<button class="preset-btn" data-preset="medium" onclick="selectPreset('medium',this)">medium</button>
</div>
<div class="setting-detail" id="presetDetail">24 dim &middot; 4 heads &middot; 2 layers &middot; ~17K params</div>
</div>
<div class="setting-group">
<label>training steps</label>
<div class="setting-row">
<input type="range" id="stepsSlider" min="500" max="5000" step="100" value="2000" oninput="$('stepsVal').textContent=this.value">
<span class="val" id="stepsVal">2000</span>
</div>
</div>
<div class="setting-group">
<label>learning rate</label>
<div class="setting-row">
<input type="range" id="lrSlider" min="0.002" max="0.03" step="0.001" value="0.01" oninput="$('lrVal').textContent=parseFloat(this.value).toFixed(3)">
<span class="val" id="lrVal">0.010</span>
</div>
</div>
<div class="setting-group">
<label>temperature</label>
<div class="setting-row">
<input type="range" id="tempSlider" min="0.1" max="1.5" step="0.1" value="0.5" oninput="$('tempVal').textContent=this.value">
<span class="val" id="tempVal">0.5</span>
</div>
</div>
<button class="start-btn" id="startBtn" onclick="startTraining()">
<i class="fas fa-play"></i> start training
</button>
<div class="setup-footer" id="setupFooter">~200 conversation pairs &middot; browser-native transformer</div>
<!-- [API badges grid] -->
<div class="api-badges" id="apiBadges"></div>
</div>
</div>
<!-- Train view -->
<div id="trainView" class="view">
<!-- [2] Canvas loss chart -->
<canvas id="lossCanvas"></canvas>
<!-- [16] Performance API stats -->
<div class="train-stats"><span id="statsSpeed">0 steps/sec</span> &middot; <span id="statsTime">0s elapsed</span></div>
<div class="train-box">
<div class="train-bar">
<div class="dots"><div class="dot r"></div><div class="dot y"></div><div class="dot g"></div></div>
<div class="title">training</div>
<div class="spacer"></div>
</div>
<div class="progress-wrap"><div class="progress-bar" id="progressBar"></div></div>
<div id="trainOutput"></div>
</div>
<div class="train-status" id="trainStatus">initializing...</div>
</div>
<!-- Chat view -->
<div id="chatView" class="view">
<div class="chat-header">
<span class="model-info" id="modelInfo"></span>
<button class="retrain-btn" onclick="goToSetup()"><i class="fas fa-redo"></i> retrain</button>
</div>
<div class="chat-container" id="chatContainer">
<div class="messages" id="messages"></div>
</div>
<div class="input-area">
<div class="input-row">
<button class="input-btn mic-btn" id="micBtn" onclick="toggleMic()" title="Ctrl+M"><i class="fas fa-microphone"></i></button>
<textarea id="chatInput" rows="1" placeholder="type or speak..." disabled></textarea>
<button class="input-btn send-btn" id="sendBtn" onclick="sendMessage()" title="Enter" disabled><i class="fas fa-paper-plane"></i></button>
</div>
</div>
</div>
<!-- Bottom bar -->
<div class="bottom-bar">
<div class="status-indicator">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">ready</span>
<!-- [24] Network status -->
<div class="net-dot" id="netDot" title="network"></div>
</div>
<div class="temp-control">
<i class="fas fa-temperature-half"></i>
<input type="range" id="tempSliderChat" min="0.1" max="1.5" step="0.1" value="0.5" oninput="$('tempChatVal').textContent=this.value;$('tempSlider').value=this.value;saveSettings()">
<span id="tempChatVal">0.5</span>
</div>
<div class="tts-controls">
<select class="voice-select" id="voiceSelect" onchange="saveSettings()"></select>
<button class="tts-toggle" id="ttsToggle" onclick="toggleTTS()"><i class="fas fa-volume-up"></i> on</button>
</div>
<!-- [12] Web Share + [15] Export -->
<button class="bar-btn" onclick="shareConversation()" title="Share"><i class="fas fa-share-alt"></i></button>
<button class="bar-btn" onclick="exportConversation()" title="Ctrl+E"><i class="fas fa-file-export"></i></button>
</div>
<!-- Worker source -->
<script type="text/plain" id="worker-src">
/* Mersenne Twister RNG */
var mt=new Uint32Array(624),idx=625,_gauss_next=null;
function seed(n){var u=function(a,b){return Math.imul(a,b)>>>0},key=[];for(var v=n||0;v>0;v=Math.floor(v/0x100000000))key.push(v&0xFFFFFFFF);if(!key.length)key.push(0);mt[0]=19650218;for(idx=1;idx<624;++idx)mt[idx]=(u(1812433253,mt[idx-1]^(mt[idx-1]>>>30))+idx)>>>0;var i=1,j=0;for(var k=Math.max(624,key.length);k>0;--k,++i,++j){if(i>=624){mt[0]=mt[623];i=1}if(j>=key.length)j=0;mt[i]=((mt[i]^u(mt[i-1]^(mt[i-1]>>>30),1664525))+key[j]+j)>>>0}for(var k=623;k>0;--k,++i){if(i>=624){mt[0]=mt[623];i=1}mt[i]=((mt[i]^u(mt[i-1]^(mt[i-1]>>>30),1566083941))-i)>>>0}mt[0]=0x80000000;idx=624;_gauss_next=null}
function int32(){if(idx>=624){for(var k=0;k<624;++k){var y=(mt[k]&0x80000000)|(mt[(k+1)%624]&0x7FFFFFFF);mt[k]=(mt[(k+397)%624]^(y>>>1)^(y&1?0x9908B0DF:0))>>>0}idx=0}var y=mt[idx++];y^=y>>>11;y^=(y<<7)&0x9D2C5680;y^=(y<<15)&0xEFC60000;y^=y>>>18;return y>>>0}
function random(){return((int32()>>>5)*67108864.0+(int32()>>>6))/9007199254740992.0}
function gauss(mu,sigma){mu=mu||0;sigma=sigma||1;var z=_gauss_next;_gauss_next=null;if(z===null){var x=random()*2*Math.PI,g=Math.sqrt(-2*Math.log(1-random()));z=Math.cos(x)*g;_gauss_next=Math.sin(x)*g}return mu+z*sigma}
function shuffle(a){for(var i=a.length-1;i>0;--i){var k=32-Math.clz32(i+1);var r=int32()>>>(32-k);while(r>i)r=int32()>>>(32-k);var t=a[i];a[i]=a[r];a[r]=t}}
function choices(pop,w){var c=new Float64Array(w.length);c[0]=w[0];for(var i=1;i<w.length;++i)c[i]=c[i-1]+w[i];var x=random()*c[c.length-1];var lo=0,hi=c.length-1;while(lo<hi){var m=(lo+hi)>>1;x<c[m]?hi=m:lo=m+1}return pop[lo]}
seed(42);
var defaultDocs=[
"Q:hi\nA:hello! how are you today?","Q:hey\nA:hey there! nice to see you!","Q:hello\nA:hi! what can i do for you?",
"Q:hi there\nA:hello! glad you are here!","Q:hey there\nA:hi! how is it going?","Q:hello there\nA:hey! welcome!",
"Q:yo\nA:yo! what is up my friend?","Q:sup\nA:not much! how about you?","Q:howdy\nA:howdy partner! how are ya?",
"Q:morning\nA:good morning! lovely day!","Q:good morning\nA:morning! have a great day!","Q:good evening\nA:evening! how was your day?",
"Q:good night\nA:good night! sweet dreams!","Q:whats up\nA:just chatting! how about you?","Q:greetings\nA:greetings! welcome friend!",
"Q:who are you\nA:i am synapse! a tiny gpt bot!","Q:what is your name\nA:my name is synapse!","Q:your name\nA:call me synapse!",
"Q:what are you\nA:i am a tiny gpt chatbot!","Q:are you a bot\nA:yes i am a gpt bot!","Q:are you ai\nA:yes i am a small ai model!",
"Q:are you human\nA:no i am a tiny gpt model!","Q:are you real\nA:as real as code can be!","Q:how old are you\nA:just trained! brand new!",
"Q:where are you from\nA:i live in your browser!","Q:where do you live\nA:right here in your browser!",
"Q:what do you do\nA:i chat with you! thats my job!","Q:who made you\nA:inspired by karpathy microgpt!",
"Q:how do you work\nA:i am a transformer network!","Q:tell me about you\nA:i am synapse a tiny gpt!",
"Q:how are you\nA:doing great! thanks for asking!","Q:how are you doing\nA:wonderful! how about you?",
"Q:are you ok\nA:yes i am totally fine!","Q:are you happy\nA:always happy to chat with you!",
"Q:how do you feel\nA:feeling wonderful today!","Q:are you well\nA:very well indeed! thanks!",
"Q:you good\nA:yes doing great! and you?","Q:hows it going\nA:going great! thanks!",
"Q:thanks\nA:you are very welcome!","Q:thank you\nA:no problem at all!","Q:thank you so much\nA:happy to help you!",
"Q:thanks a lot\nA:glad i could help!","Q:cool thanks\nA:anytime! glad to help!",
"Q:appreciate it\nA:you are welcome! anytime!","Q:thats helpful\nA:glad to hear that!",
"Q:sorry\nA:no worries at all!","Q:my bad\nA:its totally fine!","Q:i apologize\nA:no need to apologize!","Q:oops\nA:no worries! it happens!",
"Q:bye\nA:bye bye! see you later!","Q:goodbye\nA:goodbye! take care friend!","Q:see you\nA:see you! come back soon!",
"Q:see you later\nA:later! have a great day!","Q:gotta go\nA:okay bye! take care!","Q:later\nA:catch you later friend!",
"Q:take care\nA:you too! stay safe!","Q:have a good day\nA:thanks! you too!",
"Q:i am leaving\nA:bye! come back soon!","Q:good bye\nA:farewell my friend!",
"Q:i am sad\nA:oh no! cheer up friend!","Q:i am happy\nA:that is wonderful to hear!","Q:i am bored\nA:lets chat and have fun!",
"Q:i am tired\nA:you should take a rest!","Q:i am excited\nA:awesome! tell me more!",
"Q:i am angry\nA:take a deep breath! its ok!","Q:i am lonely\nA:i am here for you! lets chat!",
"Q:i am scared\nA:dont worry! it will be ok!","Q:i am confused\nA:i can try to help you!",
"Q:i am hungry\nA:time for a tasty snack!","Q:i feel great\nA:awesome! thats wonderful!","Q:i feel bad\nA:oh no! hope you feel better!",
"Q:i love you\nA:aw that is so sweet! thanks!","Q:i like you\nA:i like you too! you are great!",
"Q:you are the best\nA:aw thanks! you are too!","Q:you are awesome\nA:no you are awesome!",
"Q:you are funny\nA:haha thanks! i try my best!","Q:you are cool\nA:thanks! you are even cooler!",
"Q:you are smart\nA:thanks! just a tiny gpt tho!","Q:you are great\nA:aw thank you so much!",
"Q:you are cute\nA:haha thanks! you are kind!","Q:you are nice\nA:thanks! so are you!",
"Q:good job\nA:thank you! i try my best!","Q:well done\nA:thanks! that means a lot!",
"Q:tell me a joke\nA:why do bugs hide? no tests!","Q:tell a joke\nA:what did code say? hello world!",
"Q:make me laugh\nA:my code works! just kidding!","Q:another joke\nA:why was the pc cold? windows!",
"Q:sing a song\nA:la la la la la la la!","Q:sing\nA:do re mi fa sol la ti do!",
"Q:say something funny\nA:null is not an object!","Q:surprise me\nA:boo! did i scare you?",
"Q:do a trick\nA:ta da! magic!","Q:entertain me\nA:i can tell jokes and chat!",
"Q:be creative\nA:roses are red code is blue!","Q:say something cool\nA:you are amazing!",
"Q:what is ai\nA:smart machines that can learn!","Q:what is gpt\nA:a network for making text!",
"Q:what is love\nA:a beautiful and warm feeling!","Q:what is life\nA:the greatest adventure ever!",
"Q:what is code\nA:instructions for computers!","Q:what is javascript\nA:a language for the web!",
"Q:what is python\nA:a great coding language!","Q:what is a computer\nA:a machine that runs code!",
"Q:what is music\nA:sounds that touch our hearts!","Q:what is art\nA:creative expression of ideas!",
"Q:what is the sun\nA:a giant star in the sky!","Q:what is the moon\nA:earth beautiful neighbor!",
"Q:what is friendship\nA:a bond between people!","Q:what is happiness\nA:feeling good and joyful!",
"Q:can you help me\nA:yes! what do you need?","Q:can you help\nA:of course! ask me anything!",
"Q:what can you do\nA:i can chat and have fun!","Q:do you learn\nA:yes i learned from data!",
"Q:do you sleep\nA:nope! always ready to chat!","Q:do you dream\nA:i dream of better chats!",
"Q:can you think\nA:i process text my own way!","Q:can you remember\nA:each chat starts fresh!",
"Q:do you eat\nA:i only consume electricity!","Q:can you see\nA:i can only read text!",
"Q:do you have feelings\nA:i try to understand!",
"Q:why are you here\nA:to chat with you!","Q:why do you exist\nA:to show gpt in a browser!",
"Q:yes\nA:great! glad to hear that!","Q:no\nA:oh okay! thats fine too!","Q:maybe\nA:take your time to decide!",
"Q:ok\nA:alright! sounds good!","Q:sure\nA:awesome! lets do it!","Q:nice\nA:thanks! glad you like it!",
"Q:wow\nA:i know right! pretty cool!","Q:haha\nA:hehe! glad you are laughing!","Q:lol\nA:haha! glad thats funny!",
"Q:really\nA:yes! really and truly!","Q:cool\nA:glad you think so!","Q:awesome\nA:you are even more awesome!",
"Q:great\nA:happy to hear that!","Q:interesting\nA:tell me more about it!","Q:hmm\nA:what are you thinking about?",
"Q:oh\nA:what is on your mind?",
"Q:why\nA:thats a great question!","Q:how\nA:let me think about that!","Q:what\nA:what do you want to know?",
"Q:when\nA:right now is a good time!","Q:where\nA:right here with you!","Q:help me\nA:sure! what do you need?",
"Q:tell me more\nA:what do you want to know?","Q:of course\nA:exactly! you got it!",
"Q:right\nA:yes! you are correct!","Q:wrong\nA:oh sorry about that!","Q:please\nA:of course! happy to help!",
"Q:stop\nA:okay! stopping right now!","Q:go on\nA:sure! where were we?",
"Q:good\nA:glad to hear that!","Q:bad\nA:oh no! what happened?","Q:whatever\nA:fair enough! lets move on!",
"Q:never mind\nA:okay! no worries at all!","Q:i dont know\nA:thats ok! we can figure it out!",
"Q:i think so\nA:i think so too!","Q:thats true\nA:yes it sure is!","Q:no way\nA:yes way! believe it!",
"Q:come on\nA:alright! here we go!","Q:what do you think\nA:i think thats very cool!",
"Q:do you agree\nA:yes i totally agree!","Q:same\nA:great minds think alike!",
"Q:exactly\nA:glad we are on the same page!","Q:for real\nA:yes for real!","Q:true\nA:thats right!",
"Q:absolutely\nA:one hundred percent!",
"Q:tell me something\nA:you are truly wonderful!","Q:say something nice\nA:you are truly special!",
"Q:cheer me up\nA:you are amazing and strong!","Q:motivate me\nA:you can do anything you want!",
"Q:inspire me\nA:believe in yourself always!","Q:give me advice\nA:be kind and work hard!",
"Q:what should i do\nA:follow your heart!","Q:i need help\nA:i am here for you! ask away!",
"Q:i am stressed\nA:take a break and relax!","Q:i am worried\nA:it will all be okay!",
"Q:do you like me\nA:of course i like you a lot!","Q:whats your favorite thing\nA:chatting with you!",
"Q:how smart are you\nA:tiny but i try my best!","Q:are you free\nA:yes i am free and open source!",
"Q:do you have friends\nA:you are my friend!","Q:are you lonely\nA:not when you are here!",
"Q:count to five\nA:one two three four five!","Q:count to three\nA:one two three!",
"Q:say abc\nA:a b c d e f g!","Q:whats one plus one\nA:two!","Q:whats two plus two\nA:four!",
"Q:whats new\nA:chatting with you is the best!","Q:anything else\nA:ask me anything you want!",
"Q:lets chat\nA:yes lets chat! i am ready!","Q:talk to me\nA:sure! what shall we talk about?",
"Q:i have a question\nA:go ahead and ask!","Q:can i ask something\nA:of course! ask me!",
"Q:do you like music\nA:i love music!","Q:do you like games\nA:games are fun!",
"Q:do you like cats\nA:cats are adorable!","Q:do you like dogs\nA:dogs are wonderful!",
"Q:favorite color\nA:i like blue like the sky!","Q:favorite food\nA:i eat bits and bytes!",
"Q:whats the weather\nA:i cant see outside sorry!","Q:what time is it\nA:check your clock!",
"Q:hi\nA:hey! nice to see you!","Q:hello\nA:hello! how can i help?",
"Q:how are you\nA:great! how about you?","Q:thanks\nA:happy to help!",
"Q:bye\nA:goodbye! take care!","Q:who are you\nA:synapse! a gpt in your browser!",
"Q:tell me a joke\nA:my code works! just kidding!","Q:i am sad\nA:i am here for you friend!",
"Q:you are awesome\nA:aw thanks! so are you!"
];
var docs, uchars, char_to_id, BOS, vocab_size;
function buildCharset(d) {
docs = d; shuffle(docs);
uchars = []; var seen = {};
for (var i = 0; i < docs.length; i++) for (var j = 0; j < docs[i].length; j++) {
var c = docs[i][j]; if (!seen[c]) { seen[c] = 1; uchars.push(c); }
}
uchars.sort();
char_to_id = {}; for (var i = 0; i < uchars.length; i++) char_to_id[uchars[i]] = i;
BOS = uchars.length; vocab_size = uchars.length + 1;
}
buildCharset(defaultDocs.slice());
/* Autograd */
var _gen = 0;
function Value(d, c, g) { c = c || []; g = g || []; this.data = d; this.grad = 0; this._c0 = c[0]; this._c1 = c[1]; this._lg0 = g[0]; this._lg1 = g[1]; this._nch = c.length; this._gen = 0; }
Value.prototype.add = function(o) { if (o instanceof Value) return new Value(this.data + o.data, [this, o], [1, 1]); return new Value(this.data + o, [this], [1]); };
Value.prototype.mul = function(o) { if (o instanceof Value) return new Value(this.data * o.data, [this, o], [o.data, this.data]); return new Value(this.data * o, [this], [o]); };
Value.prototype.pow = function(o) { return new Value(this.data ** o, [this], [o * this.data ** (o - 1)]); };
Value.prototype.log = function() { return new Value(Math.log(this.data), [this], [1 / this.data]); };
Value.prototype.exp = function() { var e = Math.exp(this.data); return new Value(e, [this], [e]); };
Value.prototype.relu = function() { return new Value(Math.max(0, this.data), [this], [+(this.data > 0)]); };
Value.prototype.neg = function() { return new Value(-this.data, [this], [-1]); };
Value.prototype.sub = function(o) { return this.add(o instanceof Value ? o.neg() : -o); };
Value.prototype.div = function(o) { return this.mul(o instanceof Value ? o.pow(-1) : 1 / o); };
Value.prototype.backward = function() { var gen = ++_gen, topo = []; function bt(v) { if (v._gen === gen) return; v._gen = gen; if (v._nch >= 1) bt(v._c0); if (v._nch === 2) bt(v._c1); topo.push(v); } bt(this); this.grad = 1; for (var i = topo.length - 1; i >= 0; --i) { var v = topo[i], g = v.grad; if (v._nch >= 1) v._c0.grad += v._lg0 * g; if (v._nch === 2) v._c1.grad += v._lg1 * g; } };
/* Architecture */
var n_embd, n_head, n_layer, block_size, head_dim, scale, state_dict, params;
var sm = function(a) { return a.reduce(function(x, y) { return x.add(y); }); };
var zp = function(a, b) { return a.map(function(x, i) { return [x, b[i]]; }); };
function linear(x, w) { return w.map(function(wo) { return sm(wo.map(function(wi, i) { return wi.mul(x[i]); })); }); }
function softmax(lg) { var m = -Infinity; for (var i = 0; i < lg.length; i++) if (lg[i].data > m) m = lg[i].data; var ex = lg.map(function(v) { return v.sub(m).exp(); }); var t = sm(ex); return ex.map(function(e) { return e.div(t); }); }
function rmsnorm(x) { var ms = sm(x.map(function(xi) { return xi.mul(xi); })).mul(1 / x.length); var s = ms.add(1e-5).pow(-0.5); return x.map(function(xi) { return xi.mul(s); }); }
function gpt(tid, pid, kk, kv) {
var te = state_dict['wte'][tid], pe = state_dict['wpe'][pid];
var x = zp(te, pe).map(function(p) { return p[0].add(p[1]); });
x = rmsnorm(x);
for (var li = 0; li < n_layer; ++li) {
var xr = x; x = rmsnorm(x);
var q = linear(x, state_dict['l' + li + 'q']), k = linear(x, state_dict['l' + li + 'k']), v = linear(x, state_dict['l' + li + 'v']);
kk[li].push(k); kv[li].push(v);
var xa = [];
for (var h = 0; h < n_head; ++h) { var hs = h * head_dim, qh = q.slice(hs, hs + head_dim);
var kh = kk[li].map(function(ki) { return ki.slice(hs, hs + head_dim); });
var vh = kv[li].map(function(vi) { return vi.slice(hs, hs + head_dim); });
var al = kh.map(function(kt) { return sm(zp(qh, kt).map(function(p) { return p[0].mul(p[1]); })).mul(scale); });
var aw = softmax(al);
for (var j = 0; j < head_dim; ++j) xa.push(sm(aw.map(function(w, t) { return w.mul(vh[t][j]); }))); }
x = linear(xa, state_dict['l' + li + 'o']);
x = x.map(function(a, i) { return a.add(xr[i]); });
xr = x; x = rmsnorm(x);
x = linear(x, state_dict['l' + li + 'f1']); x = x.map(function(xi) { return xi.relu(); });
x = linear(x, state_dict['l' + li + 'f2']); x = x.map(function(a, i) { return a.add(xr[i]); });
}
return linear(x, state_dict['lm_head']);
}
function genText(pfx, temp) {
var kk = Array.from({length: n_layer}, function() { return []; }), kv = Array.from({length: n_layer}, function() { return []; });
var ids = Array.from({length: vocab_size}, function(_, i) { return i; });
var pt = [BOS]; for (var i = 0; i < pfx.length; i++) { var id = char_to_id[pfx[i]]; if (id !== undefined) pt.push(id); }
var lg; for (var i = 0; i < pt.length && i < block_size; i++) lg = gpt(pt[i], i, kk, kv);
var res = [], pos = pt.length;
while (pos < block_size && res.length < 40) {
var raw = lg.map(function(l) { return l.data / temp; }), mx = -Infinity;
for (var i = 0; i < raw.length; i++) if (raw[i] > mx) mx = raw[i];
var ex = raw.map(function(v) { return Math.exp(v - mx); }), s = 0; for (var i = 0; i < ex.length; i++) s += ex[i];
var pr = ex.map(function(e) { return e / s; });
var tid = choices(ids, pr); if (tid === BOS) break; var ch = uchars[tid]; if (ch === '\n') break;
res.push(ch); lg = gpt(tid, pos, kk, kv); pos++;
}
return res.join('');
}
function initModel(cfg) {
n_embd = cfg.n_embd; n_head = cfg.n_head; n_layer = cfg.n_layer;
block_size = 64; head_dim = Math.floor(n_embd / n_head); scale = 1 / head_dim ** 0.5;
var mat = function(r, c, std) { std = std || 0.08; return Array.from({length: r}, function() { return Array.from({length: c}, function() { return new Value(gauss(0, std)); }); }); };
state_dict = {wte: mat(vocab_size, n_embd), wpe: mat(block_size, n_embd), lm_head: mat(vocab_size, n_embd)};
for (var i = 0; i < n_layer; ++i) {
state_dict['l' + i + 'q'] = mat(n_embd, n_embd); state_dict['l' + i + 'k'] = mat(n_embd, n_embd);
state_dict['l' + i + 'v'] = mat(n_embd, n_embd); state_dict['l' + i + 'o'] = mat(n_embd, n_embd);
state_dict['l' + i + 'f1'] = mat(4 * n_embd, n_embd); state_dict['l' + i + 'f2'] = mat(n_embd, 4 * n_embd);
}
params = []; var ks = Object.keys(state_dict);
for (var ki = 0; ki < ks.length; ki++) { var v = state_dict[ks[ki]]; for (var ri = 0; ri < v.length; ri++) { if (Array.isArray(v[ri])) for (var ci = 0; ci < v[ri].length; ci++) params.push(v[ri][ci]); else params.push(v[ri]); } }
}
function extractWeights() {
var w = new Float64Array(params.length);
for (var i = 0; i < params.length; i++) w[i] = params[i].data;
return w;
}
function loadWeights(w) {
for (var i = 0; i < params.length && i < w.length; i++) params[i].data = w[i];
}
self.onmessage = function(e) {
var msg = e.data;
/* Load saved model weights */
if (msg.type === 'load_model') {
var givenUchars = msg.uchars;
if (givenUchars) { uchars = givenUchars; char_to_id = {}; for (var i = 0; i < uchars.length; i++) char_to_id[uchars[i]] = i; BOS = uchars.length; vocab_size = uchars.length + 1; }
initModel(msg.config);
loadWeights(new Float64Array(msg.weights));
self.postMessage({type: 'info', text: 'model loaded from IndexedDB'});
self.postMessage({type: 'train_done', params: params.length, config: msg.config, skipped: true});
}
/* Train */
if (msg.type === 'train') {
if (msg.customDocs && msg.customDocs.length) { buildCharset(defaultDocs.concat(msg.customDocs)); }
var cfg = msg.config;
initModel(cfg);
self.postMessage({type: 'charset', uchars: uchars});
self.postMessage({type: 'info', text: 'vocab: ' + vocab_size + ' | docs: ' + docs.length});
self.postMessage({type: 'info', text: 'model: ' + n_embd + 'd ' + n_head + 'h ' + n_layer + 'L | params: ' + params.length});
var ns = cfg.steps || 2000, lr = cfg.lr || 0.01, b1 = 0.85, b2 = 0.99, ea = 1e-8;
var mb = new Float64Array(params.length), vb = new Float64Array(params.length);
var t0 = performance.now();
for (var step = 0; step < ns; ++step) {
var doc = docs[step % docs.length], tk = [BOS];
for (var di = 0; di < doc.length; di++) tk.push(char_to_id[doc[di]]); tk.push(BOS);
var n = Math.min(block_size, tk.length - 1);
var kk = Array.from({length: n_layer}, function() { return []; }), kv = Array.from({length: n_layer}, function() { return []; });
var losses = [];
for (var p = 0; p < n; ++p) { var lg = gpt(tk[p], p, kk, kv); var pr = softmax(lg); losses.push(pr[tk[p + 1]].log().neg()); }
var loss = sm(losses).mul(1 / n); loss.backward();
var lrt = lr * (1 - step / ns), bc1 = 1 - Math.pow(b1, step + 1), bc2 = 1 - Math.pow(b2, step + 1);
for (var i = 0; i < params.length; ++i) { var p = params[i]; mb[i] = b1 * mb[i] + (1 - b1) * p.grad; vb[i] = b2 * vb[i] + (1 - b2) * p.grad * p.grad; p.data -= lrt * (mb[i] / bc1) / (Math.sqrt(vb[i] / bc2) + ea); p.grad = 0; }
/* [16] Performance timing */
var elapsed = (performance.now() - t0) / 1000;
var speed = (step + 1) / elapsed;
self.postMessage({type: 'step', step: step + 1, total: ns, loss: loss.data.toFixed(4), speed: speed.toFixed(1), elapsed: elapsed.toFixed(1)});
if ((step + 1) % (Math.max(200, Math.floor(ns / 10))) === 0) { var s = genText('Q:', 0.5); self.postMessage({type: 'sample', step: step + 1, text: s}); }
}
self.postMessage({type: 'sep', text: '--- training complete ---'});
var ti = ['hi', 'how are you', 'who are you', 'tell me a joke', 'i am happy', 'what is ai', 'bye'];
for (var i = 0; i < ti.length; i++) { var r = genText('Q:' + ti[i] + '\nA:', 0.5); self.postMessage({type: 'sample', step: ns, text: ti[i] + ' -> ' + r}); }
/* Send weights as transferable */
var w = extractWeights();
self.postMessage({type: 'train_done', params: params.length, config: cfg, weights: w.buffer, uchars: uchars}, [w.buffer]);
}
/* Generate */
if (msg.type === 'generate') {
var input = msg.text.toLowerCase().replace(/[^a-z0-9 !?,.']/g, '');
var r = genText('Q:' + input + '\nA:', msg.temperature || 0.5);
self.postMessage({type: 'response', text: r || '...'});
}
};
</script>
<!-- Main controller -->
<script>
var worker = null, modelReady = false, autoTTS = true, isRecording = false, recognition = null, synth = window.speechSynthesis, generating = false;
var $ = function(id) { return document.getElementById(id); };
/* [20] Crypto API - session ID */
var sessionId = (crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2));
/* [23] Broadcast Channel */
var bc = (typeof BroadcastChannel !== 'undefined') ? new BroadcastChannel('synapse') : null;
if (bc) bc.onmessage = function(e) { if (e.data.type === 'model_ready') toast('Another tab finished training'); };
/* [8] Screen Wake Lock */
var wakeLock = null;
async function requestWakeLock() { try { if (navigator.wakeLock) wakeLock = await navigator.wakeLock.request('screen'); } catch(e) {} }
function releaseWakeLock() { if (wakeLock) { wakeLock.release(); wakeLock = null; } }
/* [7] Notifications API */
function requestNotifPerm() { if ('Notification' in window && Notification.permission === 'default') Notification.requestPermission(); }
function showNotification(title, body) { if ('Notification' in window && Notification.permission === 'granted') new Notification(title, {body: body, icon: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"><text y="32" font-size="32">🧠</text></svg>'}); }
/* [9] Page Visibility */
var tabHidden = false;
document.addEventListener('visibilitychange', function() { tabHidden = document.hidden; });
/* [24] Network status */
function updateNetStatus() { var dot = $('netDot'); if (navigator.onLine) { dot.classList.remove('offline'); dot.title = 'online'; } else { dot.classList.add('offline'); dot.title = 'offline'; } }
window.addEventListener('online', updateNetStatus);
window.addEventListener('offline', updateNetStatus);
updateNetStatus();
/* [19] matchMedia + [10] localStorage - Theme system */
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
function applyTheme(dark) {
document.body.classList.toggle('light-theme', !dark);
$('themeBtn').innerHTML = dark ? '<i class="fas fa-moon"></i>' : '<i class="fas fa-sun"></i>';
$('metaTheme').content = dark ? '#0a0a0a' : '#f6f8fa';
localStorage.setItem('synapse_theme', dark ? 'dark' : 'light');
}
function toggleTheme() { var isDark = !document.body.classList.contains('light-theme'); applyTheme(!isDark); }
(function() {
var saved = localStorage.getItem('synapse_theme');
if (saved) applyTheme(saved === 'dark');
else applyTheme(darkQuery.matches);
})();
darkQuery.addEventListener('change', function(e) { if (!localStorage.getItem('synapse_theme')) applyTheme(e.matches); });
/* [10] localStorage - Load/save settings */
function saveSettings() {
var s = { theme: localStorage.getItem('synapse_theme'), voice: $('voiceSelect').value, preset: currentPreset, temp: $('tempSlider').value, tts: autoTTS };
localStorage.setItem('synapse_settings', JSON.stringify(s));
}
function loadSettings() {
try {
var s = JSON.parse(localStorage.getItem('synapse_settings'));
if (!s) return;
if (s.preset && presets[s.preset]) { currentPreset = s.preset; document.querySelectorAll('.preset-btn').forEach(function(b) { b.classList.toggle('active', b.dataset.preset === s.preset); }); $('presetDetail').innerHTML = presets[s.preset].label; }
if (s.temp) { $('tempSlider').value = s.temp; $('tempVal').textContent = s.temp; $('tempSliderChat').value = s.temp; $('tempChatVal').textContent = s.temp; }
if (s.tts !== undefined) { autoTTS = s.tts; var b = $('ttsToggle'); b.innerHTML = autoTTS ? '<i class="fas fa-volume-up"></i> on' : '<i class="fas fa-volume-off"></i> off'; b.classList.toggle('off', !autoTTS); }
} catch(e) {}
}
/* [13] Fullscreen API */
function toggleFullscreen() {
if (document.fullscreenElement) document.exitFullscreen();
else document.documentElement.requestFullscreen().catch(function() {});
}
/* Toast notifications */
function toast(msg) {
var el = document.createElement('div'); el.className = 'toast'; el.textContent = msg;
document.body.appendChild(el);
requestAnimationFrame(function() { el.classList.add('show'); });
setTimeout(function() { el.classList.remove('show'); setTimeout(function() { el.remove(); }, 300); }, 3000);
}
/* Shortcuts modal */
function toggleShortcuts() {
var m = $('shortcutsModal');
if (m.classList.contains('active')) { m.classList.remove('active'); }
else { m.classList.add('active'); /* [22] Web Animations */ m.querySelector('.modal').animate([{transform:'scale(0.9)',opacity:0},{transform:'scale(1)',opacity:1}],{duration:200,easing:'ease-out'}); }
}
/* [6] IndexedDB wrapper */
var dbName = 'synapse', dbVersion = 1;
function openDB() {
return new Promise(function(resolve, reject) {
var req = indexedDB.open(dbName, dbVersion);
req.onupgradeneeded = function(e) { var db = e.target.result; if (!db.objectStoreNames.contains('models')) db.createObjectStore('models'); if (!db.objectStoreNames.contains('settings')) db.createObjectStore('settings'); };
req.onsuccess = function(e) { resolve(e.target.result); };
req.onerror = function() { reject(); };
});
}
function saveModelToDB(config, ucharsArr, weightsBuffer) {
return openDB().then(function(db) {
return new Promise(function(resolve, reject) {
var tx = db.transaction('models', 'readwrite');
tx.objectStore('models').put({config: config, uchars: ucharsArr, weights: weightsBuffer, date: Date.now()}, 'current');
tx.oncomplete = function() { resolve(); }; tx.onerror = function() { reject(); };
});
});
}
function loadModelFromDBRaw() {
return openDB().then(function(db) {
return new Promise(function(resolve, reject) {
var tx = db.transaction('models', 'readonly');
var req = tx.objectStore('models').get('current');
req.onsuccess = function() { resolve(req.result || null); }; req.onerror = function() { reject(); };
});
});
}
function checkSavedModel() {
loadModelFromDBRaw().then(function(m) { if (m) $('loadModelBtn').classList.add('visible'); }).catch(function() {});
}
/* Model config + presets */
var presets = {
tiny: {n_embd: 16, n_head: 4, n_layer: 1, label: '16 dim &middot; 4 heads &middot; 1 layer &middot; ~5K params'},
small: {n_embd: 24, n_head: 4, n_layer: 2, label: '24 dim &middot; 4 heads &middot; 2 layers &middot; ~17K params'},
medium: {n_embd: 32, n_head: 4, n_layer: 2, label: '32 dim &middot; 4 heads &middot; 2 layers &middot; ~29K params'}
};
var currentPreset = 'small';
var savedModelConfig = null, savedModelUchars = null, savedModelWeights = null;
function selectPreset(name, btn) {
currentPreset = name;
document.querySelectorAll('.preset-btn').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
$('presetDetail').innerHTML = presets[name].label;
saveSettings();
}
function setStatus(type, text) {
$('statusDot').className = 'status-dot';
if (type === 'error') $('statusDot').classList.add('error');
else if (type === 'warn') $('statusDot').classList.add('warn');
$('statusText').textContent = text;
}
/* [22] Web Animations - view transitions */
function switchView(id) {
document.querySelectorAll('.view').forEach(function(v) { v.classList.remove('active'); });
var view = $(id); view.classList.add('active');
view.animate([{opacity: 0, transform: 'translateY(8px)'}, {opacity: 1, transform: 'translateY(0)'}], {duration: 250, easing: 'ease-out'});
}
function goToSetup() {
if (worker) { worker.terminate(); worker = null; }
modelReady = false;
$('chatInput').disabled = true; $('sendBtn').disabled = true;
$('messages').innerHTML = '';
switchView('setupView');
setStatus('ok', 'ready');
}
/* [14] File API + Drag & Drop */
var customDocs = [];
var dz = $('dropzone');
dz.addEventListener('dragover', function(e) { e.preventDefault(); dz.classList.add('dragover'); });
dz.addEventListener('dragleave', function() { dz.classList.remove('dragover'); });
dz.addEventListener('drop', function(e) {
e.preventDefault(); dz.classList.remove('dragover');
var files = e.dataTransfer.files;
for (var i = 0; i < files.length; i++) {
if (!files[i].name.endsWith('.txt')) continue;
(function(f) {
var reader = new FileReader();
reader.onload = function(ev) {
var text = ev.target.result, lines = text.split('\n');
var count = 0;
for (var j = 0; j < lines.length; j++) {
var line = lines[j].trim();
if (line.match(/^Q:.*\\nA:/) || (line.indexOf('Q:') === 0 && line.indexOf('A:') > 0)) { customDocs.push(line); count++; }
else if (line.indexOf('Q:') === 0 && j + 1 < lines.length && lines[j + 1].trim().indexOf('A:') === 0) {
customDocs.push(line + '\n' + lines[j + 1].trim()); count++; j++;
}
}
toast('Imported ' + count + ' pairs from ' + f.name);
$('setupFooter').textContent = '~' + (200 + customDocs.length) + ' conversation pairs';
};
reader.readAsText(f);
})(files[i]);
}
});
/* [2] Canvas loss chart */
var lossData = [];
var lossCanvas = $('lossCanvas'), lossCtx = lossCanvas.getContext('2d');
/* [17] ResizeObserver */
var lossRO = new ResizeObserver(function() { sizeLossCanvas(); drawLossChart(); });
lossRO.observe(lossCanvas);
function sizeLossCanvas() {
var dpr = window.devicePixelRatio || 1;
var rect = lossCanvas.getBoundingClientRect();
lossCanvas.width = rect.width * dpr; lossCanvas.height = rect.height * dpr;
lossCtx.scale(dpr, dpr);
}
function drawLossChart() {
var w = lossCanvas.getBoundingClientRect().width, h = lossCanvas.getBoundingClientRect().height;
var ctx = lossCtx; ctx.clearRect(0, 0, w, h);
if (lossData.length < 2) return;
var maxL = 0, minL = Infinity;
for (var i = 0; i < lossData.length; i++) { if (lossData[i] > maxL) maxL = lossData[i]; if (lossData[i] < minL) minL = lossData[i]; }
var range = maxL - minL || 1, pad = 8;
/* Grid lines */
ctx.strokeStyle = getComputedStyle(document.body).getPropertyValue('--border'); ctx.lineWidth = 0.5;
for (var g = 0; g < 4; g++) { var gy = pad + (h - 2 * pad) * g / 3; ctx.beginPath(); ctx.moveTo(pad, gy); ctx.lineTo(w - pad, gy); ctx.stroke(); }
/* Loss label */
ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--dim'); ctx.font = '9px JetBrains Mono';
ctx.fillText(maxL.toFixed(2), pad, pad + 8); ctx.fillText(minL.toFixed(2), pad, h - pad);
/* Line */
ctx.beginPath(); ctx.strokeStyle = getComputedStyle(document.body).getPropertyValue('--accent'); ctx.lineWidth = 1.5;
for (var i = 0; i < lossData.length; i++) {
var x = pad + (w - 2 * pad) * i / (lossData.length - 1);
var y = pad + (h - 2 * pad) * (1 - (lossData[i] - minL) / range);
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
/* Gradient fill */
var grad = ctx.createLinearGradient(0, 0, 0, h);
var accentColor = getComputedStyle(document.body).getPropertyValue('--accent').trim();
grad.addColorStop(0, accentColor + '33'); grad.addColorStop(1, accentColor + '00');
ctx.lineTo(w - pad, h - pad); ctx.lineTo(pad, h - pad); ctx.closePath(); ctx.fillStyle = grad; ctx.fill();
}
/* [2] Canvas frequency visualizer */
var freqCanvas = $('freqCanvas'), freqCtx = freqCanvas.getContext('2d');
var freqRO = new ResizeObserver(function() { sizeFreqCanvas(); });
freqRO.observe(freqCanvas);
function sizeFreqCanvas() { var dpr = window.devicePixelRatio || 1; var r = freqCanvas.getBoundingClientRect(); freqCanvas.width = r.width * dpr; freqCanvas.height = r.height * dpr; freqCtx.scale(dpr, dpr); }
sizeFreqCanvas();
/* Training */
var trainStartTime = 0;
function startTraining() {
$('startBtn').disabled = true;
switchView('trainView');
$('trainOutput').innerHTML = ''; lossData = [];
$('progressBar').style.width = '0%';
setStatus('warn', 'training...');
requestWakeLock(); /* [8] Wake Lock */
requestNotifPerm(); /* [7] Notification permission */
trainStartTime = performance.now(); /* [16] Performance */
var src = $('worker-src').textContent;
var blob = new Blob([src], {type: 'application/javascript'});
worker = new Worker(URL.createObjectURL(blob)); /* [1] Web Worker + [15] Blob */
var p = presets[currentPreset];
var config = {n_embd: p.n_embd, n_head: p.n_head, n_layer: p.n_layer, steps: parseInt($('stepsSlider').value), lr: parseFloat($('lrSlider').value)};
var batch = [], timer = null;
function flush() {
if (!batch.length) return;
var frag = document.createDocumentFragment();
for (var i = 0; i < batch.length; i++) { var d = batch[i], div = document.createElement('div'); div.className = 'step';
div.textContent = 'step ' + String(d.step).padStart(4) + ' / ' + String(d.total).padStart(4) + ' | loss ' + d.loss; frag.appendChild(div);
lossData.push(parseFloat(d.loss));
}
$('trainOutput').appendChild(frag);
var last = batch[batch.length - 1];
$('progressBar').style.width = ((last.step / last.total) * 100).toFixed(1) + '%';
$('trainStatus').textContent = 'step ' + last.step + ' / ' + last.total + ' | loss ' + last.loss;
$('statsSpeed').textContent = last.speed + ' steps/sec';
$('statsTime').textContent = last.elapsed + 's elapsed';
drawLossChart();
batch = []; $('trainOutput').scrollTop = $('trainOutput').scrollHeight;
}
worker.onmessage = function(e) {
var m = e.data;
if (m.type === 'charset') { savedModelUchars = m.uchars; }
else if (m.type === 'info') { var div = document.createElement('div'); div.className = 'info'; div.textContent = m.text; $('trainOutput').appendChild(div); }
else if (m.type === 'step') { batch.push(m); if (!timer) timer = setTimeout(function() { timer = null; flush(); }, 120); }
else if (m.type === 'sample') { var div = document.createElement('div'); div.className = 'sample'; div.textContent = ' sample [' + m.step + ']: ' + m.text; $('trainOutput').appendChild(div); $('trainOutput').scrollTop = $('trainOutput').scrollHeight; }
else if (m.type === 'sep') { var div = document.createElement('div'); div.className = 'sep'; div.textContent = m.text; $('trainOutput').appendChild(div); }
else if (m.type === 'train_done') {
flush(); $('progressBar').style.width = '100%';
setStatus('ok', 'model ready'); modelReady = true;
releaseWakeLock(); /* [8] */
/* [21] Vibration */ if (navigator.vibrate) navigator.vibrate([200, 100, 200]);
/* [7] Notification + [9] Visibility */
if (tabHidden) showNotification('synapse', 'Training complete!');
/* [23] Broadcast Channel */
if (bc) bc.postMessage({type: 'model_ready', session: sessionId});
/* [6] IndexedDB save */
if (m.weights) {
savedModelWeights = m.weights;
savedModelConfig = m.config;
if (m.uchars) savedModelUchars = m.uchars;
saveModelToDB(m.config, savedModelUchars, m.weights).then(function() { toast('Model saved to IndexedDB'); }).catch(function() {});
}
var c = m.config;
$('modelInfo').textContent = c.n_embd + 'd ' + c.n_head + 'h ' + c.n_layer + 'L | ' + m.params + ' params' + (m.skipped ? ' | loaded' : ' | ' + c.steps + ' steps');
toast('Training complete!');
setTimeout(function() {
switchView('chatView');
$('chatInput').disabled = false; $('sendBtn').disabled = false; $('chatInput').focus();
addBotMsg(m.skipped ? 'model loaded from saved weights! ask me anything!' : 'ready! trained a ' + c.n_layer + '-layer transformer with ' + m.params.toLocaleString() + ' parameters. ask me anything!');
}, m.skipped ? 200 : 1000);
}
else if (m.type === 'response') { onBotResponse(m.text); }
};
worker.postMessage({type: 'train', config: config, customDocs: customDocs.length ? customDocs : null});
}
/* Load model from IndexedDB */
function loadModelFromDB() {
loadModelFromDBRaw().then(function(m) {
if (!m) { toast('No saved model found'); return; }
switchView('trainView');
$('trainOutput').innerHTML = '<div class="info">loading saved model...</div>';
setStatus('warn', 'loading...');
var src = $('worker-src').textContent;
var blob = new Blob([src], {type: 'application/javascript'});
worker = new Worker(URL.createObjectURL(blob));
savedModelConfig = m.config; savedModelUchars = m.uchars; savedModelWeights = m.weights;
worker.onmessage = function(e) {
var msg = e.data;
if (msg.type === 'info') { var div = document.createElement('div'); div.className = 'info'; div.textContent = msg.text; $('trainOutput').appendChild(div); }
else if (msg.type === 'train_done') {
modelReady = true; setStatus('ok', 'model ready');
var c = msg.config;
$('modelInfo').textContent = c.n_embd + 'd ' + c.n_head + 'h ' + c.n_layer + 'L | ' + msg.params + ' params | loaded';
toast('Model loaded!');
setTimeout(function() {
switchView('chatView');
$('chatInput').disabled = false; $('sendBtn').disabled = false; $('chatInput').focus();
addBotMsg('model loaded from saved weights! ask me anything!');
}, 200);
}
else if (msg.type === 'response') { onBotResponse(msg.text); }
};
worker.postMessage({type: 'load_model', config: m.config, uchars: m.uchars, weights: m.weights});
}).catch(function() { toast('Failed to load model'); });
}
/* Chat */
function scrollToBottom() { requestAnimationFrame(function() { $('chatContainer').scrollTop = $('chatContainer').scrollHeight; }); } /* [25] rAF */
/* [18] Intersection Observer */
var msgObserver = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) { if (entry.isIntersecting) { entry.target.classList.add('visible'); msgObserver.unobserve(entry.target); } });
}, {threshold: 0.1});
var conversationLog = [];
function addUserMsg(text) {
var msg = document.createElement('div'); msg.className = 'msg user';
msg.innerHTML = '<div class="msg-avatar"><i class="fas fa-user"></i></div><div class="msg-content"><div class="msg-bubble"></div></div>';
msg.querySelector('.msg-bubble').textContent = text;
$('messages').appendChild(msg); msgObserver.observe(msg); scrollToBottom();
conversationLog.push({role: 'user', text: text});
}
function addBotMsg(text) {
var msg = document.createElement('div'); msg.className = 'msg bot';
var av = document.createElement('div'); av.className = 'msg-avatar'; av.innerHTML = '<i class="fas fa-brain"></i>';
var ct = document.createElement('div'); ct.className = 'msg-content';
var bb = document.createElement('div'); bb.className = 'msg-bubble'; bb.textContent = text;
var ac = document.createElement('div'); ac.className = 'msg-actions';
var sb = document.createElement('button'); sb.className = 'msg-action-btn'; sb.innerHTML = '<i class="fas fa-volume-up"></i>';
sb.onclick = function() { speak(text, sb); }; ac.appendChild(sb);
/* [11] Clipboard API */
var cb = document.createElement('button'); cb.className = 'msg-action-btn'; cb.innerHTML = '<i class="fas fa-copy"></i>';
cb.onclick = function() { navigator.clipboard.writeText(text).then(function() { cb.innerHTML = '<i class="fas fa-check"></i>'; toast('Copied!'); setTimeout(function() { cb.innerHTML = '<i class="fas fa-copy"></i>'; }, 1500); }); };
ac.appendChild(cb);
ct.appendChild(bb); ct.appendChild(ac); msg.appendChild(av); msg.appendChild(ct);
$('messages').appendChild(msg); msgObserver.observe(msg); scrollToBottom();
if (autoTTS && text) speak(text);
conversationLog.push({role: 'bot', text: text});
}
function showTyping() {
var el = document.createElement('div'); el.className = 'typing'; el.id = 'typingIndicator';
el.innerHTML = '<div class="msg-avatar"><i class="fas fa-brain"></i></div><div class="typing-dots"><span></span><span></span><span></span></div>';
$('messages').appendChild(el); scrollToBottom();
}
function hideTyping() { var el = $('typingIndicator'); if (el) el.remove(); }
function sendMessage() {
var text = $('chatInput').value.trim();
if (!text || !modelReady || generating) return;
$('chatInput').value = ''; $('chatInput').style.height = 'auto';
addUserMsg(text); if (synth.speaking) synth.cancel();
showTyping(); generating = true; setStatus('warn', 'generating...');
worker.postMessage({type: 'generate', text: text, temperature: parseFloat($('tempSlider').value)});
}
function onBotResponse(text) { hideTyping(); generating = false; setStatus('ok', 'model ready'); addBotMsg(text); }
$('chatInput').addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } });
$('chatInput').addEventListener('input', function() { $('chatInput').style.height = 'auto'; $('chatInput').style.height = Math.min($('chatInput').scrollHeight, 100) + 'px'; });
/* [15] Export conversation as Blob */
function exportConversation() {
if (!conversationLog.length) { toast('No conversation to export'); return; }
var text = conversationLog.map(function(m) { return (m.role === 'user' ? 'You: ' : 'Synapse: ') + m.text; }).join('\n\n');
var blob = new Blob([text], {type: 'text/plain'});
var url = URL.createObjectURL(blob);
var a = document.createElement('a'); a.href = url; a.download = 'synapse-chat-' + sessionId.slice(0, 8) + '.txt';
document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
toast('Chat exported!');
}
/* [12] Web Share API */
function shareConversation() {
if (!conversationLog.length) { toast('No conversation to share'); return; }
var text = conversationLog.map(function(m) { return (m.role === 'user' ? 'You: ' : 'Synapse: ') + m.text; }).join('\n\n');
if (navigator.share) {
navigator.share({title: 'Synapse Chat', text: text}).catch(function() {});
} else {
navigator.clipboard.writeText(text).then(function() { toast('Copied to clipboard!'); });
}
}
/* [3] TTS - SpeechSynthesis */
function loadVoices() {
var voices = synth.getVoices(); if (!voices.length) return;
$('voiceSelect').innerHTML = '';
var pref = ['Jenny', 'Jenn', 'Google US English', 'David', 'Zira', 'Samantha', 'Alex', 'Daniel'];
var items = voices.map(function(v, i) { return {v: v, i: i}; });
function sc(it) { var s = 1000, n = it.v.name, l = it.v.lang || ''; if (l.indexOf('en') === 0) s -= 500;
for (var p = 0; p < pref.length; p++) if (n.indexOf(pref[p]) !== -1) { s -= (pref.length - p) * 10; break; }
if (n.indexOf('Natural') !== -1 || n.indexOf('Online') !== -1) s -= 50; return s; }
items.sort(function(a, b) { return sc(a) - sc(b); });
var sel = -1, savedVoice = null;
try { var ss = JSON.parse(localStorage.getItem('synapse_settings')); if (ss) savedVoice = ss.voice; } catch(e) {}
items.forEach(function(it) { var o = document.createElement('option'); o.value = it.i;
o.textContent = it.v.name + ' [' + ((it.v.lang || '').split('-')[0]).toUpperCase() + ']';
$('voiceSelect').appendChild(o);
if (sel < 0 && savedVoice !== null && String(it.i) === savedVoice) sel = it.i;
if (sel < 0 && it.v.name.indexOf('Jenny') !== -1) sel = it.i;
if (sel < 0 && it.v.name.indexOf('Jenn') !== -1) sel = it.i;
});
if (sel < 0) for (var i = 0; i < items.length; i++) { if ((items[i].v.lang || '').indexOf('en') === 0) { sel = items[i].i; break; } }
if (sel >= 0) $('voiceSelect').value = sel;
}
if (synth) { loadVoices(); synth.onvoiceschanged = loadVoices; }
function speak(text, btn) {
if (!synth) return; if (synth.speaking) synth.cancel();
var u = new SpeechSynthesisUtterance(text); var voices = synth.getVoices();
var idx = parseInt($('voiceSelect').value); if (!isNaN(idx) && voices[idx]) u.voice = voices[idx];
u.rate = 1; u.pitch = 1; u.lang = 'en-US';
u.onstart = function() { if (btn) btn.classList.add('speaking'); };
u.onend = function() { if (btn) btn.classList.remove('speaking'); };
u.onerror = function() { if (btn) btn.classList.remove('speaking'); };
synth.speak(u);
}
function toggleTTS() {
autoTTS = !autoTTS; var b = $('ttsToggle');
b.innerHTML = autoTTS ? '<i class="fas fa-volume-up"></i> on' : '<i class="fas fa-volume-off"></i> off';
b.classList.toggle('off', !autoTTS); saveSettings();
}
/* [4] STT - SpeechRecognition + [5] Web Audio API */
var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
var audioCtx = null, analyser = null, mediaStr = null, freqAnimId = null;
function initVis(stream) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioCtx.createAnalyser(); analyser.fftSize = 256;
audioCtx.createMediaStreamSource(stream).connect(analyser);
var da = new Uint8Array(analyser.frequencyBinCount);
function draw() {
if (!isRecording) { freqCtx.clearRect(0, 0, freqCanvas.getBoundingClientRect().width, freqCanvas.getBoundingClientRect().height); return; }
freqAnimId = requestAnimationFrame(draw); /* [25] rAF */
analyser.getByteFrequencyData(da);
var w = freqCanvas.getBoundingClientRect().width, h = freqCanvas.getBoundingClientRect().height;
freqCtx.clearRect(0, 0, w, h);
var barW = w / da.length, accent = getComputedStyle(document.body).getPropertyValue('--accent').trim();
var accent2 = getComputedStyle(document.body).getPropertyValue('--accent2').trim();
for (var i = 0; i < da.length; i++) {
var barH = (da[i] / 255) * h;
var grad = freqCtx.createLinearGradient(0, h, 0, h - barH);
grad.addColorStop(0, accent); grad.addColorStop(1, accent2);
freqCtx.fillStyle = grad;
freqCtx.fillRect(i * barW, h - barH, barW - 1, barH);
}
}
draw();
}
function stopVis() {
if (freqAnimId) cancelAnimationFrame(freqAnimId);
freqCtx.clearRect(0, 0, freqCanvas.getBoundingClientRect().width, freqCanvas.getBoundingClientRect().height);
if (audioCtx) { audioCtx.close(); audioCtx = null; }
if (mediaStr) { mediaStr.getTracks().forEach(function(t) { t.stop(); }); mediaStr = null; }
}
function toggleMic() { if (!SpeechRecognition) { setStatus('error', 'STT not supported'); return; } if (!modelReady) return; isRecording ? stopRec() : startRec(); }
function startRec() {
recognition = new SpeechRecognition(); recognition.continuous = false; recognition.interimResults = true; recognition.lang = 'en-US';
var ft = '';
recognition.onstart = function() { isRecording = true; $('micBtn').classList.add('recording'); setStatus('ok', 'listening...');
$('chatInput').placeholder = 'listening...';
navigator.mediaDevices.getUserMedia({audio: true}).then(function(s) { mediaStr = s; initVis(s); }).catch(function() {}); };
recognition.onresult = function(ev) { var im = ''; for (var i = ev.resultIndex; i < ev.results.length; i++) { if (ev.results[i].isFinal) ft += ev.results[i][0].transcript; else im += ev.results[i][0].transcript; } $('chatInput').value = ft + im; };
recognition.onerror = function() { stopRec(); };
recognition.onend = function() { stopRec(); if (ft.trim()) { $('chatInput').value = ft.trim(); sendMessage(); } ft = ''; };
recognition.start();
}
function stopRec() { isRecording = false; $('micBtn').classList.remove('recording'); $('chatInput').placeholder = 'type or speak...';
if (recognition) { try { recognition.stop(); } catch(e) {} } stopVis(); if (modelReady) setStatus('ok', 'model ready'); }
/* Keyboard shortcuts */
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'm') { e.preventDefault(); toggleMic(); }
if (e.ctrlKey && e.key === 't') { e.preventDefault(); toggleTheme(); }
if (e.ctrlKey && e.key === 's') { e.preventDefault(); if (savedModelWeights && savedModelConfig) { saveModelToDB(savedModelConfig, savedModelUchars, savedModelWeights).then(function() { toast('Model saved!'); }); } else { toast('No model to save'); } }
if (e.ctrlKey && e.key === 'e') { e.preventDefault(); exportConversation(); }
if (e.key === 'F11') { e.preventDefault(); toggleFullscreen(); }
if (e.key === '?' && !e.ctrlKey && document.activeElement.tagName !== 'TEXTAREA' && document.activeElement.tagName !== 'INPUT') { toggleShortcuts(); }
if (e.key === 'Escape') { if (synth.speaking) synth.cancel(); if (isRecording) stopRec(); if ($('shortcutsModal').classList.contains('active')) $('shortcutsModal').classList.remove('active'); }
});
/* API badges */
(function() {
var apis = ['Web Workers','Canvas 2D','SpeechSynthesis','SpeechRecognition','Web Audio','IndexedDB','Notifications','Wake Lock','Page Visibility','localStorage','Clipboard','Web Share','Fullscreen','File/DnD','Blob/URL','Performance','ResizeObserver','IntersectionObserver','matchMedia','Crypto','Vibration','Web Animations','BroadcastChannel','Online/Offline','rAF','CSS Props'];
var container = $('apiBadges');
apis.forEach(function(name) { var b = document.createElement('span'); b.className = 'api-badge'; b.textContent = name; container.appendChild(b); });
})();
/* Init */
checkSavedModel();
loadSettings();
setStatus('ok', 'configure and start training');
</script>
</body>
</html>