Spaces:
Running
Running
| <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 · chat · 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 · 4 heads · 2 layers · ~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 · 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> · <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 · 4 heads · 1 layer · ~5K params'}, | |
| small: {n_embd: 24, n_head: 4, n_layer: 2, label: '24 dim · 4 heads · 2 layers · ~17K params'}, | |
| medium: {n_embd: 32, n_head: 4, n_layer: 2, label: '32 dim · 4 heads · 2 layers · ~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> | |