kimhyunwoo commited on
Commit
834bcc3
·
verified ·
1 Parent(s): 13a6c72

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1130 -18
index.html CHANGED
@@ -1,19 +1,1131 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="theme-color" content="#0a0a0a" id="metaTheme">
7
+ <title>synapse</title>
8
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;700&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
10
+ <style>
11
+ :root {
12
+ --bg:#0a0a0a;--fg:#c9d1d9;--border:#1b2028;--surface:#0d1117;--surface2:#161b22;
13
+ --accent:#58a6ff;--accent2:#d2a8ff;--green:#7ee787;--orange:#ffa657;--red:#ff7b72;
14
+ --dim:#6e7681;--radius:8px;--toast-bg:rgba(22,27,34,0.95);
15
+ }
16
+ .light-theme {
17
+ --bg:#f6f8fa;--fg:#24292f;--border:#d0d7de;--surface:#ffffff;--surface2:#f0f3f6;
18
+ --accent:#0969da;--accent2:#8250df;--green:#1a7f37;--orange:#bf8700;--red:#cf222e;
19
+ --dim:#656d76;--toast-bg:rgba(255,255,255,0.95);
20
+ }
21
+ *{margin:0;padding:0;box-sizing:border-box}
22
+ html,body{height:100%;background:var(--bg);color:var(--fg);font-family:'JetBrains Mono','Consolas',monospace}
23
+ body{display:flex;flex-direction:column;overflow:hidden;transition:background .3s,color .3s}
24
+
25
+ /* Header */
26
+ .header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid var(--border);flex-shrink:0}
27
+ .header-left{display:flex;align-items:center;gap:8px}
28
+ .header h1{font-size:20px;font-weight:300;letter-spacing:8px;text-transform:lowercase;color:var(--fg)}
29
+ .header .sub{font-size:9px;color:var(--dim);font-weight:300;letter-spacing:3px}
30
+ .header-actions{display:flex;gap:4px}
31
+ .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}
32
+ .header-btn:hover{border-color:var(--accent);color:var(--accent)}
33
+
34
+ /* Freq visualizer canvas */
35
+ #freqCanvas{width:100%;height:24px;display:block;flex-shrink:0;background:var(--surface2)}
36
+
37
+ /* Toast */
38
+ .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}
39
+ .toast.show{opacity:1;transform:translateY(0)}
40
+
41
+ /* Shortcuts modal */
42
+ .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)}
43
+ .modal-overlay.active{display:flex}
44
+ .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}
45
+ .modal h3{font-size:13px;font-weight:300;letter-spacing:3px;margin-bottom:16px;color:var(--accent)}
46
+ .shortcut-row{display:flex;justify-content:space-between;padding:5px 0;font-size:11px;border-bottom:1px solid var(--border)}
47
+ .shortcut-row:last-child{border:none}
48
+ .shortcut-key{background:var(--surface2);padding:2px 8px;border-radius:4px;font-size:10px;color:var(--accent)}
49
+
50
+ /* Views */
51
+ .view{display:none;flex:1;overflow:hidden}.view.active{display:flex}
52
+
53
+ /* Setup view */
54
+ #setupView{flex-direction:column;align-items:center;justify-content:center;padding:20px;gap:16px;overflow-y:auto}
55
+ .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)}
56
+ .setup-title{font-size:11px;color:var(--dim);letter-spacing:3px;margin-bottom:20px;display:flex;align-items:center;gap:8px}
57
+ .setup-title i{color:var(--accent);font-size:12px}
58
+ .setting-group{margin-bottom:18px}
59
+ .setting-group label{display:block;font-size:10px;color:var(--dim);letter-spacing:2px;margin-bottom:8px;text-transform:lowercase}
60
+ .setting-detail{font-size:9px;color:var(--dim);margin-top:6px;letter-spacing:1px}
61
+ .setting-row{display:flex;align-items:center;gap:10px}
62
+ .setting-row input[type=range]{flex:1}
63
+ .setting-row .val{font-size:11px;color:var(--accent);min-width:40px;text-align:right}
64
+ .preset-btns{display:flex;gap:6px}
65
+ .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}
66
+ .preset-btn:hover{border-color:var(--accent);color:var(--accent)}
67
+ .preset-btn.active{border-color:var(--accent);color:var(--accent);background:rgba(88,166,255,0.08)}
68
+ input[type=range]{-webkit-appearance:none;height:4px;background:var(--border);border-radius:2px;outline:none}
69
+ input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--accent);cursor:pointer}
70
+ input[type=range]::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--accent);cursor:pointer;border:none}
71
+
72
+ /* Dropzone */
73
+ .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}
74
+ .dropzone.dragover{border-color:var(--accent);color:var(--accent);background:rgba(88,166,255,0.05)}
75
+ .dropzone i{font-size:18px;display:block;margin-bottom:6px}
76
+
77
+ /* Load model button */
78
+ .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}
79
+ .load-model-btn:hover{background:rgba(126,231,135,0.15)}
80
+ .load-model-btn.visible{display:flex}
81
+
82
+ /* API badges */
83
+ .api-badges{display:flex;flex-wrap:wrap;gap:4px;margin-top:10px}
84
+ .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}
85
+
86
+ .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}
87
+ .start-btn:hover{border-color:var(--accent);color:var(--accent);box-shadow:0 0 20px rgba(88,166,255,0.1)}
88
+ .start-btn:disabled{opacity:.4;cursor:not-allowed}
89
+ .start-btn i{font-size:10px}
90
+ .setup-footer{text-align:center;font-size:9px;color:var(--dim);margin-top:12px;letter-spacing:1px}
91
+
92
+ /* Train view */
93
+ #trainView{flex-direction:column;align-items:center;justify-content:center;padding:20px;gap:12px}
94
+ #lossCanvas{width:100%;max-width:700px;height:120px;border-radius:var(--radius);background:var(--surface);border:1px solid var(--border)}
95
+ .train-stats{display:flex;gap:16px;font-size:10px;color:var(--dim);letter-spacing:1px}
96
+ .train-stats span{color:var(--accent)}
97
+ .train-box{width:100%;max-width:700px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
98
+ .train-bar{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;background:var(--surface2);border-bottom:1px solid var(--border)}
99
+ .train-bar .dots{display:flex;gap:5px}
100
+ .train-bar .dot{width:9px;height:9px;border-radius:50%}
101
+ .train-bar .dot.r{background:#ff5f56}.train-bar .dot.y{background:#ffbd2e}.train-bar .dot.g{background:#27c93f}
102
+ .train-bar .title{font-size:10px;color:var(--dim);letter-spacing:2px}
103
+ .train-bar .spacer{width:46px}
104
+ .progress-wrap{height:3px;background:var(--border)}
105
+ .progress-bar{height:100%;width:0%;background:linear-gradient(90deg,var(--accent),var(--accent2));transition:width .15s ease}
106
+ #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}
107
+ #trainOutput .log{color:var(--fg);white-space:pre-wrap}
108
+ #trainOutput .info{color:var(--accent)}
109
+ #trainOutput .step{color:var(--dim)}
110
+ #trainOutput .sep{color:var(--orange);margin-top:6px}
111
+ #trainOutput .sample{color:var(--green)}
112
+ .train-status{font-size:10px;color:var(--dim);letter-spacing:1px}
113
+
114
+ /* Chat view */
115
+ #chatView{flex-direction:column}
116
+ .chat-header{display:flex;align-items:center;justify-content:space-between;padding:8px 16px;border-bottom:1px solid var(--border);flex-shrink:0}
117
+ .chat-header .model-info{font-size:9px;color:var(--dim);letter-spacing:1px}
118
+ .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}
119
+ .chat-header .retrain-btn:hover{border-color:var(--accent);color:var(--accent)}
120
+ .chat-container{flex:1;overflow-y:auto;padding:12px 16px;scrollbar-width:thin;scrollbar-color:var(--border) transparent}
121
+ .messages{max-width:700px;margin:0 auto;display:flex;flex-direction:column;gap:10px}
122
+ .msg{display:flex;gap:8px;max-width:85%;opacity:0;transform:translateY(10px);transition:opacity .3s,transform .3s}
123
+ .msg.visible{opacity:1;transform:translateY(0)}
124
+ .msg.user{align-self:flex-end;flex-direction:row-reverse}
125
+ .msg.bot{align-self:flex-start}
126
+ .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}
127
+ .msg.user .msg-avatar{background:rgba(88,166,255,0.15);color:var(--accent)}
128
+ .msg.bot .msg-avatar{background:rgba(126,231,135,0.15);color:var(--green)}
129
+ .msg-content{position:relative}
130
+ .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}
131
+ .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}
132
+ .msg.bot .msg-bubble{background:var(--surface);border:1px solid var(--border);color:var(--fg);border-bottom-left-radius:2px}
133
+ .msg-actions{display:flex;gap:3px;margin-top:3px;opacity:0;transition:opacity .2s}
134
+ .msg-content:hover .msg-actions{opacity:1}
135
+ .msg-action-btn{background:none;border:none;color:var(--dim);cursor:pointer;font-size:10px;padding:3px 6px;border-radius:3px;transition:all .15s}
136
+ .msg-action-btn:hover{color:var(--accent);background:rgba(88,166,255,0.1)}
137
+ .msg-action-btn.speaking{color:var(--green)}
138
+ .typing{display:flex;gap:8px;align-self:flex-start}
139
+ .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)}
140
+ .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}
141
+ .typing-dots span{width:5px;height:5px;background:var(--dim);border-radius:50%;animation:bounce 1.4s infinite}
142
+ .typing-dots span:nth-child(2){animation-delay:.2s}.typing-dots span:nth-child(3){animation-delay:.4s}
143
+ @keyframes bounce{0%,60%,100%{transform:translateY(0);opacity:.4}30%{transform:translateY(-5px);opacity:1}}
144
+
145
+ /* Input area */
146
+ .input-area{flex-shrink:0;padding:10px 16px 12px;border-top:1px solid var(--border);background:var(--surface);backdrop-filter:blur(10px)}
147
+ .input-row{max-width:700px;margin:0 auto;display:flex;gap:6px;align-items:flex-end}
148
+ #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}
149
+ #chatInput:focus{border-color:var(--accent)}
150
+ #chatInput::placeholder{color:var(--dim)}
151
+ #chatInput:disabled{opacity:.4}
152
+ .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}
153
+ .input-btn:hover{border-color:var(--accent);color:var(--accent)}
154
+ .input-btn:disabled{opacity:.3;cursor:not-allowed}
155
+ .input-btn.send-btn{background:rgba(88,166,255,0.1);border-color:rgba(88,166,255,0.3);color:var(--accent)}
156
+ .input-btn.send-btn:hover{background:rgba(88,166,255,0.2)}
157
+ .input-btn.mic-btn.recording{border-color:var(--red);color:var(--red);animation:pulse 1.5s infinite}
158
+ @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)}}
159
+
160
+ /* Bottom bar */
161
+ .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}
162
+ .status-indicator{display:flex;align-items:center;gap:5px}
163
+ .status-dot{width:5px;height:5px;border-radius:50%;background:var(--green)}
164
+ .status-dot.error{background:var(--red)}.status-dot.warn{background:var(--orange)}
165
+ .net-dot{width:5px;height:5px;border-radius:50%;background:var(--green);margin-left:8px}
166
+ .net-dot.offline{background:var(--red)}
167
+ .tts-controls{display:flex;align-items:center;gap:6px}
168
+ .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}
169
+ .voice-select:hover{border-color:var(--accent)}
170
+ .voice-select option{background:var(--surface);color:var(--fg)}
171
+ .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}
172
+ .tts-toggle.off{color:var(--dim)}
173
+ .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}
174
+ .bar-btn:hover{border-color:var(--accent);color:var(--accent)}
175
+ .temp-control{display:flex;align-items:center;gap:4px;font-size:9px;color:var(--dim)}
176
+ .temp-control input[type=range]{width:60px;height:2px}
177
+
178
+ @media(max-width:600px){
179
+ .header h1{font-size:16px;letter-spacing:4px}
180
+ .setup-card{padding:20px}
181
+ .msg{max-width:92%}
182
+ .bottom-bar{font-size:8px}
183
+ .header .sub{display:none}
184
+ }
185
+ </style>
186
+ </head>
187
+ <body>
188
+
189
+ <div class="header">
190
+ <div class="header-left">
191
+ <div><h1>synapse</h1><div class="sub">train &middot; chat &middot; speak</div></div>
192
+ </div>
193
+ <div class="header-actions">
194
+ <button class="header-btn" id="themeBtn" onclick="toggleTheme()" title="Ctrl+T"><i class="fas fa-moon"></i></button>
195
+ <button class="header-btn" id="fullscreenBtn" onclick="toggleFullscreen()" title="F11"><i class="fas fa-expand"></i></button>
196
+ <button class="header-btn" onclick="toggleShortcuts()" title="?"><i class="fas fa-keyboard"></i></button>
197
+ </div>
198
+ </div>
199
+ <!-- [2] Canvas 2D frequency visualizer -->
200
+ <canvas id="freqCanvas"></canvas>
201
+
202
+ <!-- Shortcuts modal [22] Web Animations -->
203
+ <div class="modal-overlay" id="shortcutsModal">
204
+ <div class="modal">
205
+ <h3>keyboard shortcuts</h3>
206
+ <div class="shortcut-row"><span>Toggle mic</span><span class="shortcut-key">Ctrl+M</span></div>
207
+ <div class="shortcut-row"><span>Toggle theme</span><span class="shortcut-key">Ctrl+T</span></div>
208
+ <div class="shortcut-row"><span>Save model</span><span class="shortcut-key">Ctrl+S</span></div>
209
+ <div class="shortcut-row"><span>Export chat</span><span class="shortcut-key">Ctrl+E</span></div>
210
+ <div class="shortcut-row"><span>Fullscreen</span><span class="shortcut-key">F11</span></div>
211
+ <div class="shortcut-row"><span>Shortcuts</span><span class="shortcut-key">?</span></div>
212
+ <div class="shortcut-row"><span>Stop / Close</span><span class="shortcut-key">Esc</span></div>
213
+ </div>
214
+ </div>
215
+
216
+ <!-- Setup view -->
217
+ <div id="setupView" class="view active">
218
+ <div class="setup-card">
219
+ <div class="setup-title"><i class="fas fa-sliders-h"></i> configuration</div>
220
+
221
+ <!-- [14] File API drag-and-drop -->
222
+ <div class="dropzone" id="dropzone">
223
+ <i class="fas fa-file-import"></i>
224
+ drop .txt training data here<br><span style="font-size:8px;color:var(--dim)">(Q:...\nA:... format, one pair per line)</span>
225
+ </div>
226
+
227
+ <!-- [6] IndexedDB load saved model -->
228
+ <button class="load-model-btn" id="loadModelBtn" onclick="loadModelFromDB()">
229
+ <i class="fas fa-download"></i> load saved model (skip training)
230
+ </button>
231
+
232
+ <div class="setting-group">
233
+ <label>model size</label>
234
+ <div class="preset-btns">
235
+ <button class="preset-btn" data-preset="tiny" onclick="selectPreset('tiny',this)">tiny</button>
236
+ <button class="preset-btn active" data-preset="small" onclick="selectPreset('small',this)">small</button>
237
+ <button class="preset-btn" data-preset="medium" onclick="selectPreset('medium',this)">medium</button>
238
+ </div>
239
+ <div class="setting-detail" id="presetDetail">24 dim &middot; 4 heads &middot; 2 layers &middot; ~17K params</div>
240
+ </div>
241
+
242
+ <div class="setting-group">
243
+ <label>training steps</label>
244
+ <div class="setting-row">
245
+ <input type="range" id="stepsSlider" min="500" max="5000" step="100" value="2000" oninput="$('stepsVal').textContent=this.value">
246
+ <span class="val" id="stepsVal">2000</span>
247
+ </div>
248
+ </div>
249
+
250
+ <div class="setting-group">
251
+ <label>learning rate</label>
252
+ <div class="setting-row">
253
+ <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)">
254
+ <span class="val" id="lrVal">0.010</span>
255
+ </div>
256
+ </div>
257
+
258
+ <div class="setting-group">
259
+ <label>temperature</label>
260
+ <div class="setting-row">
261
+ <input type="range" id="tempSlider" min="0.1" max="1.5" step="0.1" value="0.5" oninput="$('tempVal').textContent=this.value">
262
+ <span class="val" id="tempVal">0.5</span>
263
+ </div>
264
+ </div>
265
+
266
+ <button class="start-btn" id="startBtn" onclick="startTraining()">
267
+ <i class="fas fa-play"></i> start training
268
+ </button>
269
+ <div class="setup-footer" id="setupFooter">~200 conversation pairs &middot; browser-native transformer</div>
270
+
271
+ <!-- [API badges grid] -->
272
+ <div class="api-badges" id="apiBadges"></div>
273
+ </div>
274
+ </div>
275
+
276
+ <!-- Train view -->
277
+ <div id="trainView" class="view">
278
+ <!-- [2] Canvas loss chart -->
279
+ <canvas id="lossCanvas"></canvas>
280
+ <!-- [16] Performance API stats -->
281
+ <div class="train-stats"><span id="statsSpeed">0 steps/sec</span> &middot; <span id="statsTime">0s elapsed</span></div>
282
+ <div class="train-box">
283
+ <div class="train-bar">
284
+ <div class="dots"><div class="dot r"></div><div class="dot y"></div><div class="dot g"></div></div>
285
+ <div class="title">training</div>
286
+ <div class="spacer"></div>
287
+ </div>
288
+ <div class="progress-wrap"><div class="progress-bar" id="progressBar"></div></div>
289
+ <div id="trainOutput"></div>
290
+ </div>
291
+ <div class="train-status" id="trainStatus">initializing...</div>
292
+ </div>
293
+
294
+ <!-- Chat view -->
295
+ <div id="chatView" class="view">
296
+ <div class="chat-header">
297
+ <span class="model-info" id="modelInfo"></span>
298
+ <button class="retrain-btn" onclick="goToSetup()"><i class="fas fa-redo"></i> retrain</button>
299
+ </div>
300
+ <div class="chat-container" id="chatContainer">
301
+ <div class="messages" id="messages"></div>
302
+ </div>
303
+ <div class="input-area">
304
+ <div class="input-row">
305
+ <button class="input-btn mic-btn" id="micBtn" onclick="toggleMic()" title="Ctrl+M"><i class="fas fa-microphone"></i></button>
306
+ <textarea id="chatInput" rows="1" placeholder="type or speak..." disabled></textarea>
307
+ <button class="input-btn send-btn" id="sendBtn" onclick="sendMessage()" title="Enter" disabled><i class="fas fa-paper-plane"></i></button>
308
+ </div>
309
+ </div>
310
+ </div>
311
+
312
+ <!-- Bottom bar -->
313
+ <div class="bottom-bar">
314
+ <div class="status-indicator">
315
+ <div class="status-dot" id="statusDot"></div>
316
+ <span id="statusText">ready</span>
317
+ <!-- [24] Network status -->
318
+ <div class="net-dot" id="netDot" title="network"></div>
319
+ </div>
320
+ <div class="temp-control">
321
+ <i class="fas fa-temperature-half"></i>
322
+ <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()">
323
+ <span id="tempChatVal">0.5</span>
324
+ </div>
325
+ <div class="tts-controls">
326
+ <select class="voice-select" id="voiceSelect" onchange="saveSettings()"></select>
327
+ <button class="tts-toggle" id="ttsToggle" onclick="toggleTTS()"><i class="fas fa-volume-up"></i> on</button>
328
+ </div>
329
+ <!-- [12] Web Share + [15] Export -->
330
+ <button class="bar-btn" onclick="shareConversation()" title="Share"><i class="fas fa-share-alt"></i></button>
331
+ <button class="bar-btn" onclick="exportConversation()" title="Ctrl+E"><i class="fas fa-file-export"></i></button>
332
+ </div>
333
+
334
+ <!-- Worker source -->
335
+ <script type="text/plain" id="worker-src">
336
+ /* Mersenne Twister RNG */
337
+ var mt=new Uint32Array(624),idx=625,_gauss_next=null;
338
+ 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}
339
+ 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}
340
+ function random(){return((int32()>>>5)*67108864.0+(int32()>>>6))/9007199254740992.0}
341
+ 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}
342
+ 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}}
343
+ 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]}
344
+
345
+ seed(42);
346
+
347
+ var defaultDocs=[
348
+ "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?",
349
+ "Q:hi there\nA:hello! glad you are here!","Q:hey there\nA:hi! how is it going?","Q:hello there\nA:hey! welcome!",
350
+ "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?",
351
+ "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?",
352
+ "Q:good night\nA:good night! sweet dreams!","Q:whats up\nA:just chatting! how about you?","Q:greetings\nA:greetings! welcome friend!",
353
+ "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!",
354
+ "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!",
355
+ "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!",
356
+ "Q:where are you from\nA:i live in your browser!","Q:where do you live\nA:right here in your browser!",
357
+ "Q:what do you do\nA:i chat with you! thats my job!","Q:who made you\nA:inspired by karpathy microgpt!",
358
+ "Q:how do you work\nA:i am a transformer network!","Q:tell me about you\nA:i am synapse a tiny gpt!",
359
+ "Q:how are you\nA:doing great! thanks for asking!","Q:how are you doing\nA:wonderful! how about you?",
360
+ "Q:are you ok\nA:yes i am totally fine!","Q:are you happy\nA:always happy to chat with you!",
361
+ "Q:how do you feel\nA:feeling wonderful today!","Q:are you well\nA:very well indeed! thanks!",
362
+ "Q:you good\nA:yes doing great! and you?","Q:hows it going\nA:going great! thanks!",
363
+ "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!",
364
+ "Q:thanks a lot\nA:glad i could help!","Q:cool thanks\nA:anytime! glad to help!",
365
+ "Q:appreciate it\nA:you are welcome! anytime!","Q:thats helpful\nA:glad to hear that!",
366
+ "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!",
367
+ "Q:bye\nA:bye bye! see you later!","Q:goodbye\nA:goodbye! take care friend!","Q:see you\nA:see you! come back soon!",
368
+ "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!",
369
+ "Q:take care\nA:you too! stay safe!","Q:have a good day\nA:thanks! you too!",
370
+ "Q:i am leaving\nA:bye! come back soon!","Q:good bye\nA:farewell my friend!",
371
+ "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!",
372
+ "Q:i am tired\nA:you should take a rest!","Q:i am excited\nA:awesome! tell me more!",
373
+ "Q:i am angry\nA:take a deep breath! its ok!","Q:i am lonely\nA:i am here for you! lets chat!",
374
+ "Q:i am scared\nA:dont worry! it will be ok!","Q:i am confused\nA:i can try to help you!",
375
+ "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!",
376
+ "Q:i love you\nA:aw that is so sweet! thanks!","Q:i like you\nA:i like you too! you are great!",
377
+ "Q:you are the best\nA:aw thanks! you are too!","Q:you are awesome\nA:no you are awesome!",
378
+ "Q:you are funny\nA:haha thanks! i try my best!","Q:you are cool\nA:thanks! you are even cooler!",
379
+ "Q:you are smart\nA:thanks! just a tiny gpt tho!","Q:you are great\nA:aw thank you so much!",
380
+ "Q:you are cute\nA:haha thanks! you are kind!","Q:you are nice\nA:thanks! so are you!",
381
+ "Q:good job\nA:thank you! i try my best!","Q:well done\nA:thanks! that means a lot!",
382
+ "Q:tell me a joke\nA:why do bugs hide? no tests!","Q:tell a joke\nA:what did code say? hello world!",
383
+ "Q:make me laugh\nA:my code works! just kidding!","Q:another joke\nA:why was the pc cold? windows!",
384
+ "Q:sing a song\nA:la la la la la la la!","Q:sing\nA:do re mi fa sol la ti do!",
385
+ "Q:say something funny\nA:null is not an object!","Q:surprise me\nA:boo! did i scare you?",
386
+ "Q:do a trick\nA:ta da! magic!","Q:entertain me\nA:i can tell jokes and chat!",
387
+ "Q:be creative\nA:roses are red code is blue!","Q:say something cool\nA:you are amazing!",
388
+ "Q:what is ai\nA:smart machines that can learn!","Q:what is gpt\nA:a network for making text!",
389
+ "Q:what is love\nA:a beautiful and warm feeling!","Q:what is life\nA:the greatest adventure ever!",
390
+ "Q:what is code\nA:instructions for computers!","Q:what is javascript\nA:a language for the web!",
391
+ "Q:what is python\nA:a great coding language!","Q:what is a computer\nA:a machine that runs code!",
392
+ "Q:what is music\nA:sounds that touch our hearts!","Q:what is art\nA:creative expression of ideas!",
393
+ "Q:what is the sun\nA:a giant star in the sky!","Q:what is the moon\nA:earth beautiful neighbor!",
394
+ "Q:what is friendship\nA:a bond between people!","Q:what is happiness\nA:feeling good and joyful!",
395
+ "Q:can you help me\nA:yes! what do you need?","Q:can you help\nA:of course! ask me anything!",
396
+ "Q:what can you do\nA:i can chat and have fun!","Q:do you learn\nA:yes i learned from data!",
397
+ "Q:do you sleep\nA:nope! always ready to chat!","Q:do you dream\nA:i dream of better chats!",
398
+ "Q:can you think\nA:i process text my own way!","Q:can you remember\nA:each chat starts fresh!",
399
+ "Q:do you eat\nA:i only consume electricity!","Q:can you see\nA:i can only read text!",
400
+ "Q:do you have feelings\nA:i try to understand!",
401
+ "Q:why are you here\nA:to chat with you!","Q:why do you exist\nA:to show gpt in a browser!",
402
+ "Q:yes\nA:great! glad to hear that!","Q:no\nA:oh okay! thats fine too!","Q:maybe\nA:take your time to decide!",
403
+ "Q:ok\nA:alright! sounds good!","Q:sure\nA:awesome! lets do it!","Q:nice\nA:thanks! glad you like it!",
404
+ "Q:wow\nA:i know right! pretty cool!","Q:haha\nA:hehe! glad you are laughing!","Q:lol\nA:haha! glad thats funny!",
405
+ "Q:really\nA:yes! really and truly!","Q:cool\nA:glad you think so!","Q:awesome\nA:you are even more awesome!",
406
+ "Q:great\nA:happy to hear that!","Q:interesting\nA:tell me more about it!","Q:hmm\nA:what are you thinking about?",
407
+ "Q:oh\nA:what is on your mind?",
408
+ "Q:why\nA:thats a great question!","Q:how\nA:let me think about that!","Q:what\nA:what do you want to know?",
409
+ "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?",
410
+ "Q:tell me more\nA:what do you want to know?","Q:of course\nA:exactly! you got it!",
411
+ "Q:right\nA:yes! you are correct!","Q:wrong\nA:oh sorry about that!","Q:please\nA:of course! happy to help!",
412
+ "Q:stop\nA:okay! stopping right now!","Q:go on\nA:sure! where were we?",
413
+ "Q:good\nA:glad to hear that!","Q:bad\nA:oh no! what happened?","Q:whatever\nA:fair enough! lets move on!",
414
+ "Q:never mind\nA:okay! no worries at all!","Q:i dont know\nA:thats ok! we can figure it out!",
415
+ "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!",
416
+ "Q:come on\nA:alright! here we go!","Q:what do you think\nA:i think thats very cool!",
417
+ "Q:do you agree\nA:yes i totally agree!","Q:same\nA:great minds think alike!",
418
+ "Q:exactly\nA:glad we are on the same page!","Q:for real\nA:yes for real!","Q:true\nA:thats right!",
419
+ "Q:absolutely\nA:one hundred percent!",
420
+ "Q:tell me something\nA:you are truly wonderful!","Q:say something nice\nA:you are truly special!",
421
+ "Q:cheer me up\nA:you are amazing and strong!","Q:motivate me\nA:you can do anything you want!",
422
+ "Q:inspire me\nA:believe in yourself always!","Q:give me advice\nA:be kind and work hard!",
423
+ "Q:what should i do\nA:follow your heart!","Q:i need help\nA:i am here for you! ask away!",
424
+ "Q:i am stressed\nA:take a break and relax!","Q:i am worried\nA:it will all be okay!",
425
+ "Q:do you like me\nA:of course i like you a lot!","Q:whats your favorite thing\nA:chatting with you!",
426
+ "Q:how smart are you\nA:tiny but i try my best!","Q:are you free\nA:yes i am free and open source!",
427
+ "Q:do you have friends\nA:you are my friend!","Q:are you lonely\nA:not when you are here!",
428
+ "Q:count to five\nA:one two three four five!","Q:count to three\nA:one two three!",
429
+ "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!",
430
+ "Q:whats new\nA:chatting with you is the best!","Q:anything else\nA:ask me anything you want!",
431
+ "Q:lets chat\nA:yes lets chat! i am ready!","Q:talk to me\nA:sure! what shall we talk about?",
432
+ "Q:i have a question\nA:go ahead and ask!","Q:can i ask something\nA:of course! ask me!",
433
+ "Q:do you like music\nA:i love music!","Q:do you like games\nA:games are fun!",
434
+ "Q:do you like cats\nA:cats are adorable!","Q:do you like dogs\nA:dogs are wonderful!",
435
+ "Q:favorite color\nA:i like blue like the sky!","Q:favorite food\nA:i eat bits and bytes!",
436
+ "Q:whats the weather\nA:i cant see outside sorry!","Q:what time is it\nA:check your clock!",
437
+ "Q:hi\nA:hey! nice to see you!","Q:hello\nA:hello! how can i help?",
438
+ "Q:how are you\nA:great! how about you?","Q:thanks\nA:happy to help!",
439
+ "Q:bye\nA:goodbye! take care!","Q:who are you\nA:synapse! a gpt in your browser!",
440
+ "Q:tell me a joke\nA:my code works! just kidding!","Q:i am sad\nA:i am here for you friend!",
441
+ "Q:you are awesome\nA:aw thanks! so are you!"
442
+ ];
443
+
444
+ var docs, uchars, char_to_id, BOS, vocab_size;
445
+
446
+ function buildCharset(d) {
447
+ docs = d; shuffle(docs);
448
+ uchars = []; var seen = {};
449
+ for (var i = 0; i < docs.length; i++) for (var j = 0; j < docs[i].length; j++) {
450
+ var c = docs[i][j]; if (!seen[c]) { seen[c] = 1; uchars.push(c); }
451
+ }
452
+ uchars.sort();
453
+ char_to_id = {}; for (var i = 0; i < uchars.length; i++) char_to_id[uchars[i]] = i;
454
+ BOS = uchars.length; vocab_size = uchars.length + 1;
455
+ }
456
+
457
+ buildCharset(defaultDocs.slice());
458
+
459
+ /* Autograd */
460
+ var _gen = 0;
461
+ 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; }
462
+ 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]); };
463
+ 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]); };
464
+ Value.prototype.pow = function(o) { return new Value(this.data ** o, [this], [o * this.data ** (o - 1)]); };
465
+ Value.prototype.log = function() { return new Value(Math.log(this.data), [this], [1 / this.data]); };
466
+ Value.prototype.exp = function() { var e = Math.exp(this.data); return new Value(e, [this], [e]); };
467
+ Value.prototype.relu = function() { return new Value(Math.max(0, this.data), [this], [+(this.data > 0)]); };
468
+ Value.prototype.neg = function() { return new Value(-this.data, [this], [-1]); };
469
+ Value.prototype.sub = function(o) { return this.add(o instanceof Value ? o.neg() : -o); };
470
+ Value.prototype.div = function(o) { return this.mul(o instanceof Value ? o.pow(-1) : 1 / o); };
471
+ 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; } };
472
+
473
+ /* Architecture */
474
+ var n_embd, n_head, n_layer, block_size, head_dim, scale, state_dict, params;
475
+ var sm = function(a) { return a.reduce(function(x, y) { return x.add(y); }); };
476
+ var zp = function(a, b) { return a.map(function(x, i) { return [x, b[i]]; }); };
477
+ function linear(x, w) { return w.map(function(wo) { return sm(wo.map(function(wi, i) { return wi.mul(x[i]); })); }); }
478
+ 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); }); }
479
+ 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); }); }
480
+ function gpt(tid, pid, kk, kv) {
481
+ var te = state_dict['wte'][tid], pe = state_dict['wpe'][pid];
482
+ var x = zp(te, pe).map(function(p) { return p[0].add(p[1]); });
483
+ x = rmsnorm(x);
484
+ for (var li = 0; li < n_layer; ++li) {
485
+ var xr = x; x = rmsnorm(x);
486
+ 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']);
487
+ kk[li].push(k); kv[li].push(v);
488
+ var xa = [];
489
+ for (var h = 0; h < n_head; ++h) { var hs = h * head_dim, qh = q.slice(hs, hs + head_dim);
490
+ var kh = kk[li].map(function(ki) { return ki.slice(hs, hs + head_dim); });
491
+ var vh = kv[li].map(function(vi) { return vi.slice(hs, hs + head_dim); });
492
+ var al = kh.map(function(kt) { return sm(zp(qh, kt).map(function(p) { return p[0].mul(p[1]); })).mul(scale); });
493
+ var aw = softmax(al);
494
+ for (var j = 0; j < head_dim; ++j) xa.push(sm(aw.map(function(w, t) { return w.mul(vh[t][j]); }))); }
495
+ x = linear(xa, state_dict['l' + li + 'o']);
496
+ x = x.map(function(a, i) { return a.add(xr[i]); });
497
+ xr = x; x = rmsnorm(x);
498
+ x = linear(x, state_dict['l' + li + 'f1']); x = x.map(function(xi) { return xi.relu(); });
499
+ x = linear(x, state_dict['l' + li + 'f2']); x = x.map(function(a, i) { return a.add(xr[i]); });
500
+ }
501
+ return linear(x, state_dict['lm_head']);
502
+ }
503
+ function genText(pfx, temp) {
504
+ var kk = Array.from({length: n_layer}, function() { return []; }), kv = Array.from({length: n_layer}, function() { return []; });
505
+ var ids = Array.from({length: vocab_size}, function(_, i) { return i; });
506
+ var pt = [BOS]; for (var i = 0; i < pfx.length; i++) { var id = char_to_id[pfx[i]]; if (id !== undefined) pt.push(id); }
507
+ var lg; for (var i = 0; i < pt.length && i < block_size; i++) lg = gpt(pt[i], i, kk, kv);
508
+ var res = [], pos = pt.length;
509
+ while (pos < block_size && res.length < 40) {
510
+ var raw = lg.map(function(l) { return l.data / temp; }), mx = -Infinity;
511
+ for (var i = 0; i < raw.length; i++) if (raw[i] > mx) mx = raw[i];
512
+ var ex = raw.map(function(v) { return Math.exp(v - mx); }), s = 0; for (var i = 0; i < ex.length; i++) s += ex[i];
513
+ var pr = ex.map(function(e) { return e / s; });
514
+ var tid = choices(ids, pr); if (tid === BOS) break; var ch = uchars[tid]; if (ch === '\n') break;
515
+ res.push(ch); lg = gpt(tid, pos, kk, kv); pos++;
516
+ }
517
+ return res.join('');
518
+ }
519
+
520
+ function initModel(cfg) {
521
+ n_embd = cfg.n_embd; n_head = cfg.n_head; n_layer = cfg.n_layer;
522
+ block_size = 64; head_dim = Math.floor(n_embd / n_head); scale = 1 / head_dim ** 0.5;
523
+ 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)); }); }); };
524
+ state_dict = {wte: mat(vocab_size, n_embd), wpe: mat(block_size, n_embd), lm_head: mat(vocab_size, n_embd)};
525
+ for (var i = 0; i < n_layer; ++i) {
526
+ state_dict['l' + i + 'q'] = mat(n_embd, n_embd); state_dict['l' + i + 'k'] = mat(n_embd, n_embd);
527
+ state_dict['l' + i + 'v'] = mat(n_embd, n_embd); state_dict['l' + i + 'o'] = mat(n_embd, n_embd);
528
+ state_dict['l' + i + 'f1'] = mat(4 * n_embd, n_embd); state_dict['l' + i + 'f2'] = mat(n_embd, 4 * n_embd);
529
+ }
530
+ params = []; var ks = Object.keys(state_dict);
531
+ 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]); } }
532
+ }
533
+
534
+ function extractWeights() {
535
+ var w = new Float64Array(params.length);
536
+ for (var i = 0; i < params.length; i++) w[i] = params[i].data;
537
+ return w;
538
+ }
539
+
540
+ function loadWeights(w) {
541
+ for (var i = 0; i < params.length && i < w.length; i++) params[i].data = w[i];
542
+ }
543
+
544
+ self.onmessage = function(e) {
545
+ var msg = e.data;
546
+
547
+ /* Load saved model weights */
548
+ if (msg.type === 'load_model') {
549
+ var givenUchars = msg.uchars;
550
+ 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; }
551
+ initModel(msg.config);
552
+ loadWeights(new Float64Array(msg.weights));
553
+ self.postMessage({type: 'info', text: 'model loaded from IndexedDB'});
554
+ self.postMessage({type: 'train_done', params: params.length, config: msg.config, skipped: true});
555
+ }
556
+
557
+ /* Train */
558
+ if (msg.type === 'train') {
559
+ if (msg.customDocs && msg.customDocs.length) { buildCharset(defaultDocs.concat(msg.customDocs)); }
560
+ var cfg = msg.config;
561
+ initModel(cfg);
562
+ self.postMessage({type: 'charset', uchars: uchars});
563
+ self.postMessage({type: 'info', text: 'vocab: ' + vocab_size + ' | docs: ' + docs.length});
564
+ self.postMessage({type: 'info', text: 'model: ' + n_embd + 'd ' + n_head + 'h ' + n_layer + 'L | params: ' + params.length});
565
+
566
+ var ns = cfg.steps || 2000, lr = cfg.lr || 0.01, b1 = 0.85, b2 = 0.99, ea = 1e-8;
567
+ var mb = new Float64Array(params.length), vb = new Float64Array(params.length);
568
+ var t0 = performance.now();
569
+ for (var step = 0; step < ns; ++step) {
570
+ var doc = docs[step % docs.length], tk = [BOS];
571
+ for (var di = 0; di < doc.length; di++) tk.push(char_to_id[doc[di]]); tk.push(BOS);
572
+ var n = Math.min(block_size, tk.length - 1);
573
+ var kk = Array.from({length: n_layer}, function() { return []; }), kv = Array.from({length: n_layer}, function() { return []; });
574
+ var losses = [];
575
+ 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()); }
576
+ var loss = sm(losses).mul(1 / n); loss.backward();
577
+ var lrt = lr * (1 - step / ns), bc1 = 1 - Math.pow(b1, step + 1), bc2 = 1 - Math.pow(b2, step + 1);
578
+ 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; }
579
+ /* [16] Performance timing */
580
+ var elapsed = (performance.now() - t0) / 1000;
581
+ var speed = (step + 1) / elapsed;
582
+ self.postMessage({type: 'step', step: step + 1, total: ns, loss: loss.data.toFixed(4), speed: speed.toFixed(1), elapsed: elapsed.toFixed(1)});
583
+ 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}); }
584
+ }
585
+ self.postMessage({type: 'sep', text: '--- training complete ---'});
586
+ var ti = ['hi', 'how are you', 'who are you', 'tell me a joke', 'i am happy', 'what is ai', 'bye'];
587
+ 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}); }
588
+ /* Send weights as transferable */
589
+ var w = extractWeights();
590
+ self.postMessage({type: 'train_done', params: params.length, config: cfg, weights: w.buffer, uchars: uchars}, [w.buffer]);
591
+ }
592
+
593
+ /* Generate */
594
+ if (msg.type === 'generate') {
595
+ var input = msg.text.toLowerCase().replace(/[^a-z0-9 !?,.']/g, '');
596
+ var r = genText('Q:' + input + '\nA:', msg.temperature || 0.5);
597
+ self.postMessage({type: 'response', text: r || '...'});
598
+ }
599
+ };
600
+ </script>
601
+
602
+ <!-- Main controller -->
603
+ <script>
604
+ var worker = null, modelReady = false, autoTTS = true, isRecording = false, recognition = null, synth = window.speechSynthesis, generating = false;
605
+ var $ = function(id) { return document.getElementById(id); };
606
+
607
+ /* [20] Crypto API - session ID */
608
+ var sessionId = (crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2));
609
+
610
+ /* [23] Broadcast Channel */
611
+ var bc = (typeof BroadcastChannel !== 'undefined') ? new BroadcastChannel('synapse') : null;
612
+ if (bc) bc.onmessage = function(e) { if (e.data.type === 'model_ready') toast('Another tab finished training'); };
613
+
614
+ /* [8] Screen Wake Lock */
615
+ var wakeLock = null;
616
+ async function requestWakeLock() { try { if (navigator.wakeLock) wakeLock = await navigator.wakeLock.request('screen'); } catch(e) {} }
617
+ function releaseWakeLock() { if (wakeLock) { wakeLock.release(); wakeLock = null; } }
618
+
619
+ /* [7] Notifications API */
620
+ function requestNotifPerm() { if ('Notification' in window && Notification.permission === 'default') Notification.requestPermission(); }
621
+ 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>'}); }
622
+
623
+ /* [9] Page Visibility */
624
+ var tabHidden = false;
625
+ document.addEventListener('visibilitychange', function() { tabHidden = document.hidden; });
626
+
627
+ /* [24] Network status */
628
+ function updateNetStatus() { var dot = $('netDot'); if (navigator.onLine) { dot.classList.remove('offline'); dot.title = 'online'; } else { dot.classList.add('offline'); dot.title = 'offline'; } }
629
+ window.addEventListener('online', updateNetStatus);
630
+ window.addEventListener('offline', updateNetStatus);
631
+ updateNetStatus();
632
+
633
+ /* [19] matchMedia + [10] localStorage - Theme system */
634
+ var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
635
+ function applyTheme(dark) {
636
+ document.body.classList.toggle('light-theme', !dark);
637
+ $('themeBtn').innerHTML = dark ? '<i class="fas fa-moon"></i>' : '<i class="fas fa-sun"></i>';
638
+ $('metaTheme').content = dark ? '#0a0a0a' : '#f6f8fa';
639
+ localStorage.setItem('synapse_theme', dark ? 'dark' : 'light');
640
+ }
641
+ function toggleTheme() { var isDark = !document.body.classList.contains('light-theme'); applyTheme(!isDark); }
642
+ (function() {
643
+ var saved = localStorage.getItem('synapse_theme');
644
+ if (saved) applyTheme(saved === 'dark');
645
+ else applyTheme(darkQuery.matches);
646
+ })();
647
+ darkQuery.addEventListener('change', function(e) { if (!localStorage.getItem('synapse_theme')) applyTheme(e.matches); });
648
+
649
+ /* [10] localStorage - Load/save settings */
650
+ function saveSettings() {
651
+ var s = { theme: localStorage.getItem('synapse_theme'), voice: $('voiceSelect').value, preset: currentPreset, temp: $('tempSlider').value, tts: autoTTS };
652
+ localStorage.setItem('synapse_settings', JSON.stringify(s));
653
+ }
654
+ function loadSettings() {
655
+ try {
656
+ var s = JSON.parse(localStorage.getItem('synapse_settings'));
657
+ if (!s) return;
658
+ 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; }
659
+ if (s.temp) { $('tempSlider').value = s.temp; $('tempVal').textContent = s.temp; $('tempSliderChat').value = s.temp; $('tempChatVal').textContent = s.temp; }
660
+ 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); }
661
+ } catch(e) {}
662
+ }
663
+
664
+ /* [13] Fullscreen API */
665
+ function toggleFullscreen() {
666
+ if (document.fullscreenElement) document.exitFullscreen();
667
+ else document.documentElement.requestFullscreen().catch(function() {});
668
+ }
669
+
670
+ /* Toast notifications */
671
+ function toast(msg) {
672
+ var el = document.createElement('div'); el.className = 'toast'; el.textContent = msg;
673
+ document.body.appendChild(el);
674
+ requestAnimationFrame(function() { el.classList.add('show'); });
675
+ setTimeout(function() { el.classList.remove('show'); setTimeout(function() { el.remove(); }, 300); }, 3000);
676
+ }
677
+
678
+ /* Shortcuts modal */
679
+ function toggleShortcuts() {
680
+ var m = $('shortcutsModal');
681
+ if (m.classList.contains('active')) { m.classList.remove('active'); }
682
+ 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'}); }
683
+ }
684
+
685
+ /* [6] IndexedDB wrapper */
686
+ var dbName = 'synapse', dbVersion = 1;
687
+ function openDB() {
688
+ return new Promise(function(resolve, reject) {
689
+ var req = indexedDB.open(dbName, dbVersion);
690
+ 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'); };
691
+ req.onsuccess = function(e) { resolve(e.target.result); };
692
+ req.onerror = function() { reject(); };
693
+ });
694
+ }
695
+ function saveModelToDB(config, ucharsArr, weightsBuffer) {
696
+ return openDB().then(function(db) {
697
+ return new Promise(function(resolve, reject) {
698
+ var tx = db.transaction('models', 'readwrite');
699
+ tx.objectStore('models').put({config: config, uchars: ucharsArr, weights: weightsBuffer, date: Date.now()}, 'current');
700
+ tx.oncomplete = function() { resolve(); }; tx.onerror = function() { reject(); };
701
+ });
702
+ });
703
+ }
704
+ function loadModelFromDBRaw() {
705
+ return openDB().then(function(db) {
706
+ return new Promise(function(resolve, reject) {
707
+ var tx = db.transaction('models', 'readonly');
708
+ var req = tx.objectStore('models').get('current');
709
+ req.onsuccess = function() { resolve(req.result || null); }; req.onerror = function() { reject(); };
710
+ });
711
+ });
712
+ }
713
+ function checkSavedModel() {
714
+ loadModelFromDBRaw().then(function(m) { if (m) $('loadModelBtn').classList.add('visible'); }).catch(function() {});
715
+ }
716
+
717
+ /* Model config + presets */
718
+ var presets = {
719
+ tiny: {n_embd: 16, n_head: 4, n_layer: 1, label: '16 dim &middot; 4 heads &middot; 1 layer &middot; ~5K params'},
720
+ small: {n_embd: 24, n_head: 4, n_layer: 2, label: '24 dim &middot; 4 heads &middot; 2 layers &middot; ~17K params'},
721
+ medium: {n_embd: 32, n_head: 4, n_layer: 2, label: '32 dim &middot; 4 heads &middot; 2 layers &middot; ~29K params'}
722
+ };
723
+ var currentPreset = 'small';
724
+ var savedModelConfig = null, savedModelUchars = null, savedModelWeights = null;
725
+
726
+ function selectPreset(name, btn) {
727
+ currentPreset = name;
728
+ document.querySelectorAll('.preset-btn').forEach(function(b) { b.classList.remove('active'); });
729
+ btn.classList.add('active');
730
+ $('presetDetail').innerHTML = presets[name].label;
731
+ saveSettings();
732
+ }
733
+
734
+ function setStatus(type, text) {
735
+ $('statusDot').className = 'status-dot';
736
+ if (type === 'error') $('statusDot').classList.add('error');
737
+ else if (type === 'warn') $('statusDot').classList.add('warn');
738
+ $('statusText').textContent = text;
739
+ }
740
+
741
+ /* [22] Web Animations - view transitions */
742
+ function switchView(id) {
743
+ document.querySelectorAll('.view').forEach(function(v) { v.classList.remove('active'); });
744
+ var view = $(id); view.classList.add('active');
745
+ view.animate([{opacity: 0, transform: 'translateY(8px)'}, {opacity: 1, transform: 'translateY(0)'}], {duration: 250, easing: 'ease-out'});
746
+ }
747
+
748
+ function goToSetup() {
749
+ if (worker) { worker.terminate(); worker = null; }
750
+ modelReady = false;
751
+ $('chatInput').disabled = true; $('sendBtn').disabled = true;
752
+ $('messages').innerHTML = '';
753
+ switchView('setupView');
754
+ setStatus('ok', 'ready');
755
+ }
756
+
757
+ /* [14] File API + Drag & Drop */
758
+ var customDocs = [];
759
+ var dz = $('dropzone');
760
+ dz.addEventListener('dragover', function(e) { e.preventDefault(); dz.classList.add('dragover'); });
761
+ dz.addEventListener('dragleave', function() { dz.classList.remove('dragover'); });
762
+ dz.addEventListener('drop', function(e) {
763
+ e.preventDefault(); dz.classList.remove('dragover');
764
+ var files = e.dataTransfer.files;
765
+ for (var i = 0; i < files.length; i++) {
766
+ if (!files[i].name.endsWith('.txt')) continue;
767
+ (function(f) {
768
+ var reader = new FileReader();
769
+ reader.onload = function(ev) {
770
+ var text = ev.target.result, lines = text.split('\n');
771
+ var count = 0;
772
+ for (var j = 0; j < lines.length; j++) {
773
+ var line = lines[j].trim();
774
+ if (line.match(/^Q:.*\\nA:/) || (line.indexOf('Q:') === 0 && line.indexOf('A:') > 0)) { customDocs.push(line); count++; }
775
+ else if (line.indexOf('Q:') === 0 && j + 1 < lines.length && lines[j + 1].trim().indexOf('A:') === 0) {
776
+ customDocs.push(line + '\n' + lines[j + 1].trim()); count++; j++;
777
+ }
778
+ }
779
+ toast('Imported ' + count + ' pairs from ' + f.name);
780
+ $('setupFooter').textContent = '~' + (200 + customDocs.length) + ' conversation pairs';
781
+ };
782
+ reader.readAsText(f);
783
+ })(files[i]);
784
+ }
785
+ });
786
+
787
+ /* [2] Canvas loss chart */
788
+ var lossData = [];
789
+ var lossCanvas = $('lossCanvas'), lossCtx = lossCanvas.getContext('2d');
790
+ /* [17] ResizeObserver */
791
+ var lossRO = new ResizeObserver(function() { sizeLossCanvas(); drawLossChart(); });
792
+ lossRO.observe(lossCanvas);
793
+ function sizeLossCanvas() {
794
+ var dpr = window.devicePixelRatio || 1;
795
+ var rect = lossCanvas.getBoundingClientRect();
796
+ lossCanvas.width = rect.width * dpr; lossCanvas.height = rect.height * dpr;
797
+ lossCtx.scale(dpr, dpr);
798
+ }
799
+ function drawLossChart() {
800
+ var w = lossCanvas.getBoundingClientRect().width, h = lossCanvas.getBoundingClientRect().height;
801
+ var ctx = lossCtx; ctx.clearRect(0, 0, w, h);
802
+ if (lossData.length < 2) return;
803
+ var maxL = 0, minL = Infinity;
804
+ for (var i = 0; i < lossData.length; i++) { if (lossData[i] > maxL) maxL = lossData[i]; if (lossData[i] < minL) minL = lossData[i]; }
805
+ var range = maxL - minL || 1, pad = 8;
806
+ /* Grid lines */
807
+ ctx.strokeStyle = getComputedStyle(document.body).getPropertyValue('--border'); ctx.lineWidth = 0.5;
808
+ 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(); }
809
+ /* Loss label */
810
+ ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--dim'); ctx.font = '9px JetBrains Mono';
811
+ ctx.fillText(maxL.toFixed(2), pad, pad + 8); ctx.fillText(minL.toFixed(2), pad, h - pad);
812
+ /* Line */
813
+ ctx.beginPath(); ctx.strokeStyle = getComputedStyle(document.body).getPropertyValue('--accent'); ctx.lineWidth = 1.5;
814
+ for (var i = 0; i < lossData.length; i++) {
815
+ var x = pad + (w - 2 * pad) * i / (lossData.length - 1);
816
+ var y = pad + (h - 2 * pad) * (1 - (lossData[i] - minL) / range);
817
+ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
818
+ }
819
+ ctx.stroke();
820
+ /* Gradient fill */
821
+ var grad = ctx.createLinearGradient(0, 0, 0, h);
822
+ var accentColor = getComputedStyle(document.body).getPropertyValue('--accent').trim();
823
+ grad.addColorStop(0, accentColor + '33'); grad.addColorStop(1, accentColor + '00');
824
+ ctx.lineTo(w - pad, h - pad); ctx.lineTo(pad, h - pad); ctx.closePath(); ctx.fillStyle = grad; ctx.fill();
825
+ }
826
+
827
+ /* [2] Canvas frequency visualizer */
828
+ var freqCanvas = $('freqCanvas'), freqCtx = freqCanvas.getContext('2d');
829
+ var freqRO = new ResizeObserver(function() { sizeFreqCanvas(); });
830
+ freqRO.observe(freqCanvas);
831
+ 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); }
832
+ sizeFreqCanvas();
833
+
834
+ /* Training */
835
+ var trainStartTime = 0;
836
+ function startTraining() {
837
+ $('startBtn').disabled = true;
838
+ switchView('trainView');
839
+ $('trainOutput').innerHTML = ''; lossData = [];
840
+ $('progressBar').style.width = '0%';
841
+ setStatus('warn', 'training...');
842
+ requestWakeLock(); /* [8] Wake Lock */
843
+ requestNotifPerm(); /* [7] Notification permission */
844
+ trainStartTime = performance.now(); /* [16] Performance */
845
+
846
+ var src = $('worker-src').textContent;
847
+ var blob = new Blob([src], {type: 'application/javascript'});
848
+ worker = new Worker(URL.createObjectURL(blob)); /* [1] Web Worker + [15] Blob */
849
+
850
+ var p = presets[currentPreset];
851
+ 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)};
852
+
853
+ var batch = [], timer = null;
854
+ function flush() {
855
+ if (!batch.length) return;
856
+ var frag = document.createDocumentFragment();
857
+ for (var i = 0; i < batch.length; i++) { var d = batch[i], div = document.createElement('div'); div.className = 'step';
858
+ div.textContent = 'step ' + String(d.step).padStart(4) + ' / ' + String(d.total).padStart(4) + ' | loss ' + d.loss; frag.appendChild(div);
859
+ lossData.push(parseFloat(d.loss));
860
+ }
861
+ $('trainOutput').appendChild(frag);
862
+ var last = batch[batch.length - 1];
863
+ $('progressBar').style.width = ((last.step / last.total) * 100).toFixed(1) + '%';
864
+ $('trainStatus').textContent = 'step ' + last.step + ' / ' + last.total + ' | loss ' + last.loss;
865
+ $('statsSpeed').textContent = last.speed + ' steps/sec';
866
+ $('statsTime').textContent = last.elapsed + 's elapsed';
867
+ drawLossChart();
868
+ batch = []; $('trainOutput').scrollTop = $('trainOutput').scrollHeight;
869
+ }
870
+
871
+ worker.onmessage = function(e) {
872
+ var m = e.data;
873
+ if (m.type === 'charset') { savedModelUchars = m.uchars; }
874
+ else if (m.type === 'info') { var div = document.createElement('div'); div.className = 'info'; div.textContent = m.text; $('trainOutput').appendChild(div); }
875
+ else if (m.type === 'step') { batch.push(m); if (!timer) timer = setTimeout(function() { timer = null; flush(); }, 120); }
876
+ 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; }
877
+ else if (m.type === 'sep') { var div = document.createElement('div'); div.className = 'sep'; div.textContent = m.text; $('trainOutput').appendChild(div); }
878
+ else if (m.type === 'train_done') {
879
+ flush(); $('progressBar').style.width = '100%';
880
+ setStatus('ok', 'model ready'); modelReady = true;
881
+ releaseWakeLock(); /* [8] */
882
+ /* [21] Vibration */ if (navigator.vibrate) navigator.vibrate([200, 100, 200]);
883
+ /* [7] Notification + [9] Visibility */
884
+ if (tabHidden) showNotification('synapse', 'Training complete!');
885
+ /* [23] Broadcast Channel */
886
+ if (bc) bc.postMessage({type: 'model_ready', session: sessionId});
887
+ /* [6] IndexedDB save */
888
+ if (m.weights) {
889
+ savedModelWeights = m.weights;
890
+ savedModelConfig = m.config;
891
+ if (m.uchars) savedModelUchars = m.uchars;
892
+ saveModelToDB(m.config, savedModelUchars, m.weights).then(function() { toast('Model saved to IndexedDB'); }).catch(function() {});
893
+ }
894
+ var c = m.config;
895
+ $('modelInfo').textContent = c.n_embd + 'd ' + c.n_head + 'h ' + c.n_layer + 'L | ' + m.params + ' params' + (m.skipped ? ' | loaded' : ' | ' + c.steps + ' steps');
896
+ toast('Training complete!');
897
+ setTimeout(function() {
898
+ switchView('chatView');
899
+ $('chatInput').disabled = false; $('sendBtn').disabled = false; $('chatInput').focus();
900
+ 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!');
901
+ }, m.skipped ? 200 : 1000);
902
+ }
903
+ else if (m.type === 'response') { onBotResponse(m.text); }
904
+ };
905
+ worker.postMessage({type: 'train', config: config, customDocs: customDocs.length ? customDocs : null});
906
+ }
907
+
908
+ /* Load model from IndexedDB */
909
+ function loadModelFromDB() {
910
+ loadModelFromDBRaw().then(function(m) {
911
+ if (!m) { toast('No saved model found'); return; }
912
+ switchView('trainView');
913
+ $('trainOutput').innerHTML = '<div class="info">loading saved model...</div>';
914
+ setStatus('warn', 'loading...');
915
+
916
+ var src = $('worker-src').textContent;
917
+ var blob = new Blob([src], {type: 'application/javascript'});
918
+ worker = new Worker(URL.createObjectURL(blob));
919
+
920
+ savedModelConfig = m.config; savedModelUchars = m.uchars; savedModelWeights = m.weights;
921
+
922
+ worker.onmessage = function(e) {
923
+ var msg = e.data;
924
+ if (msg.type === 'info') { var div = document.createElement('div'); div.className = 'info'; div.textContent = msg.text; $('trainOutput').appendChild(div); }
925
+ else if (msg.type === 'train_done') {
926
+ modelReady = true; setStatus('ok', 'model ready');
927
+ var c = msg.config;
928
+ $('modelInfo').textContent = c.n_embd + 'd ' + c.n_head + 'h ' + c.n_layer + 'L | ' + msg.params + ' params | loaded';
929
+ toast('Model loaded!');
930
+ setTimeout(function() {
931
+ switchView('chatView');
932
+ $('chatInput').disabled = false; $('sendBtn').disabled = false; $('chatInput').focus();
933
+ addBotMsg('model loaded from saved weights! ask me anything!');
934
+ }, 200);
935
+ }
936
+ else if (msg.type === 'response') { onBotResponse(msg.text); }
937
+ };
938
+ worker.postMessage({type: 'load_model', config: m.config, uchars: m.uchars, weights: m.weights});
939
+ }).catch(function() { toast('Failed to load model'); });
940
+ }
941
+
942
+ /* Chat */
943
+ function scrollToBottom() { requestAnimationFrame(function() { $('chatContainer').scrollTop = $('chatContainer').scrollHeight; }); } /* [25] rAF */
944
+
945
+ /* [18] Intersection Observer */
946
+ var msgObserver = new IntersectionObserver(function(entries) {
947
+ entries.forEach(function(entry) { if (entry.isIntersecting) { entry.target.classList.add('visible'); msgObserver.unobserve(entry.target); } });
948
+ }, {threshold: 0.1});
949
+
950
+ var conversationLog = [];
951
+
952
+ function addUserMsg(text) {
953
+ var msg = document.createElement('div'); msg.className = 'msg user';
954
+ msg.innerHTML = '<div class="msg-avatar"><i class="fas fa-user"></i></div><div class="msg-content"><div class="msg-bubble"></div></div>';
955
+ msg.querySelector('.msg-bubble').textContent = text;
956
+ $('messages').appendChild(msg); msgObserver.observe(msg); scrollToBottom();
957
+ conversationLog.push({role: 'user', text: text});
958
+ }
959
+ function addBotMsg(text) {
960
+ var msg = document.createElement('div'); msg.className = 'msg bot';
961
+ var av = document.createElement('div'); av.className = 'msg-avatar'; av.innerHTML = '<i class="fas fa-brain"></i>';
962
+ var ct = document.createElement('div'); ct.className = 'msg-content';
963
+ var bb = document.createElement('div'); bb.className = 'msg-bubble'; bb.textContent = text;
964
+ var ac = document.createElement('div'); ac.className = 'msg-actions';
965
+ var sb = document.createElement('button'); sb.className = 'msg-action-btn'; sb.innerHTML = '<i class="fas fa-volume-up"></i>';
966
+ sb.onclick = function() { speak(text, sb); }; ac.appendChild(sb);
967
+ /* [11] Clipboard API */
968
+ var cb = document.createElement('button'); cb.className = 'msg-action-btn'; cb.innerHTML = '<i class="fas fa-copy"></i>';
969
+ 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); }); };
970
+ ac.appendChild(cb);
971
+ ct.appendChild(bb); ct.appendChild(ac); msg.appendChild(av); msg.appendChild(ct);
972
+ $('messages').appendChild(msg); msgObserver.observe(msg); scrollToBottom();
973
+ if (autoTTS && text) speak(text);
974
+ conversationLog.push({role: 'bot', text: text});
975
+ }
976
+ function showTyping() {
977
+ var el = document.createElement('div'); el.className = 'typing'; el.id = 'typingIndicator';
978
+ el.innerHTML = '<div class="msg-avatar"><i class="fas fa-brain"></i></div><div class="typing-dots"><span></span><span></span><span></span></div>';
979
+ $('messages').appendChild(el); scrollToBottom();
980
+ }
981
+ function hideTyping() { var el = $('typingIndicator'); if (el) el.remove(); }
982
+
983
+ function sendMessage() {
984
+ var text = $('chatInput').value.trim();
985
+ if (!text || !modelReady || generating) return;
986
+ $('chatInput').value = ''; $('chatInput').style.height = 'auto';
987
+ addUserMsg(text); if (synth.speaking) synth.cancel();
988
+ showTyping(); generating = true; setStatus('warn', 'generating...');
989
+ worker.postMessage({type: 'generate', text: text, temperature: parseFloat($('tempSlider').value)});
990
+ }
991
+ function onBotResponse(text) { hideTyping(); generating = false; setStatus('ok', 'model ready'); addBotMsg(text); }
992
+
993
+ $('chatInput').addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } });
994
+ $('chatInput').addEventListener('input', function() { $('chatInput').style.height = 'auto'; $('chatInput').style.height = Math.min($('chatInput').scrollHeight, 100) + 'px'; });
995
+
996
+ /* [15] Export conversation as Blob */
997
+ function exportConversation() {
998
+ if (!conversationLog.length) { toast('No conversation to export'); return; }
999
+ var text = conversationLog.map(function(m) { return (m.role === 'user' ? 'You: ' : 'Synapse: ') + m.text; }).join('\n\n');
1000
+ var blob = new Blob([text], {type: 'text/plain'});
1001
+ var url = URL.createObjectURL(blob);
1002
+ var a = document.createElement('a'); a.href = url; a.download = 'synapse-chat-' + sessionId.slice(0, 8) + '.txt';
1003
+ document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
1004
+ toast('Chat exported!');
1005
+ }
1006
+
1007
+ /* [12] Web Share API */
1008
+ function shareConversation() {
1009
+ if (!conversationLog.length) { toast('No conversation to share'); return; }
1010
+ var text = conversationLog.map(function(m) { return (m.role === 'user' ? 'You: ' : 'Synapse: ') + m.text; }).join('\n\n');
1011
+ if (navigator.share) {
1012
+ navigator.share({title: 'Synapse Chat', text: text}).catch(function() {});
1013
+ } else {
1014
+ navigator.clipboard.writeText(text).then(function() { toast('Copied to clipboard!'); });
1015
+ }
1016
+ }
1017
+
1018
+ /* [3] TTS - SpeechSynthesis */
1019
+ function loadVoices() {
1020
+ var voices = synth.getVoices(); if (!voices.length) return;
1021
+ $('voiceSelect').innerHTML = '';
1022
+ var pref = ['Jenny', 'Jenn', 'Google US English', 'David', 'Zira', 'Samantha', 'Alex', 'Daniel'];
1023
+ var items = voices.map(function(v, i) { return {v: v, i: i}; });
1024
+ function sc(it) { var s = 1000, n = it.v.name, l = it.v.lang || ''; if (l.indexOf('en') === 0) s -= 500;
1025
+ for (var p = 0; p < pref.length; p++) if (n.indexOf(pref[p]) !== -1) { s -= (pref.length - p) * 10; break; }
1026
+ if (n.indexOf('Natural') !== -1 || n.indexOf('Online') !== -1) s -= 50; return s; }
1027
+ items.sort(function(a, b) { return sc(a) - sc(b); });
1028
+ var sel = -1, savedVoice = null;
1029
+ try { var ss = JSON.parse(localStorage.getItem('synapse_settings')); if (ss) savedVoice = ss.voice; } catch(e) {}
1030
+ items.forEach(function(it) { var o = document.createElement('option'); o.value = it.i;
1031
+ o.textContent = it.v.name + ' [' + ((it.v.lang || '').split('-')[0]).toUpperCase() + ']';
1032
+ $('voiceSelect').appendChild(o);
1033
+ if (sel < 0 && savedVoice !== null && String(it.i) === savedVoice) sel = it.i;
1034
+ if (sel < 0 && it.v.name.indexOf('Jenny') !== -1) sel = it.i;
1035
+ if (sel < 0 && it.v.name.indexOf('Jenn') !== -1) sel = it.i;
1036
+ });
1037
+ if (sel < 0) for (var i = 0; i < items.length; i++) { if ((items[i].v.lang || '').indexOf('en') === 0) { sel = items[i].i; break; } }
1038
+ if (sel >= 0) $('voiceSelect').value = sel;
1039
+ }
1040
+ if (synth) { loadVoices(); synth.onvoiceschanged = loadVoices; }
1041
+
1042
+ function speak(text, btn) {
1043
+ if (!synth) return; if (synth.speaking) synth.cancel();
1044
+ var u = new SpeechSynthesisUtterance(text); var voices = synth.getVoices();
1045
+ var idx = parseInt($('voiceSelect').value); if (!isNaN(idx) && voices[idx]) u.voice = voices[idx];
1046
+ u.rate = 1; u.pitch = 1; u.lang = 'en-US';
1047
+ u.onstart = function() { if (btn) btn.classList.add('speaking'); };
1048
+ u.onend = function() { if (btn) btn.classList.remove('speaking'); };
1049
+ u.onerror = function() { if (btn) btn.classList.remove('speaking'); };
1050
+ synth.speak(u);
1051
+ }
1052
+ function toggleTTS() {
1053
+ autoTTS = !autoTTS; var b = $('ttsToggle');
1054
+ b.innerHTML = autoTTS ? '<i class="fas fa-volume-up"></i> on' : '<i class="fas fa-volume-off"></i> off';
1055
+ b.classList.toggle('off', !autoTTS); saveSettings();
1056
+ }
1057
+
1058
+ /* [4] STT - SpeechRecognition + [5] Web Audio API */
1059
+ var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
1060
+ var audioCtx = null, analyser = null, mediaStr = null, freqAnimId = null;
1061
+
1062
+ function initVis(stream) {
1063
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)();
1064
+ analyser = audioCtx.createAnalyser(); analyser.fftSize = 256;
1065
+ audioCtx.createMediaStreamSource(stream).connect(analyser);
1066
+ var da = new Uint8Array(analyser.frequencyBinCount);
1067
+ function draw() {
1068
+ if (!isRecording) { freqCtx.clearRect(0, 0, freqCanvas.getBoundingClientRect().width, freqCanvas.getBoundingClientRect().height); return; }
1069
+ freqAnimId = requestAnimationFrame(draw); /* [25] rAF */
1070
+ analyser.getByteFrequencyData(da);
1071
+ var w = freqCanvas.getBoundingClientRect().width, h = freqCanvas.getBoundingClientRect().height;
1072
+ freqCtx.clearRect(0, 0, w, h);
1073
+ var barW = w / da.length, accent = getComputedStyle(document.body).getPropertyValue('--accent').trim();
1074
+ var accent2 = getComputedStyle(document.body).getPropertyValue('--accent2').trim();
1075
+ for (var i = 0; i < da.length; i++) {
1076
+ var barH = (da[i] / 255) * h;
1077
+ var grad = freqCtx.createLinearGradient(0, h, 0, h - barH);
1078
+ grad.addColorStop(0, accent); grad.addColorStop(1, accent2);
1079
+ freqCtx.fillStyle = grad;
1080
+ freqCtx.fillRect(i * barW, h - barH, barW - 1, barH);
1081
+ }
1082
+ }
1083
+ draw();
1084
+ }
1085
+ function stopVis() {
1086
+ if (freqAnimId) cancelAnimationFrame(freqAnimId);
1087
+ freqCtx.clearRect(0, 0, freqCanvas.getBoundingClientRect().width, freqCanvas.getBoundingClientRect().height);
1088
+ if (audioCtx) { audioCtx.close(); audioCtx = null; }
1089
+ if (mediaStr) { mediaStr.getTracks().forEach(function(t) { t.stop(); }); mediaStr = null; }
1090
+ }
1091
+
1092
+ function toggleMic() { if (!SpeechRecognition) { setStatus('error', 'STT not supported'); return; } if (!modelReady) return; isRecording ? stopRec() : startRec(); }
1093
+ function startRec() {
1094
+ recognition = new SpeechRecognition(); recognition.continuous = false; recognition.interimResults = true; recognition.lang = 'en-US';
1095
+ var ft = '';
1096
+ recognition.onstart = function() { isRecording = true; $('micBtn').classList.add('recording'); setStatus('ok', 'listening...');
1097
+ $('chatInput').placeholder = 'listening...';
1098
+ navigator.mediaDevices.getUserMedia({audio: true}).then(function(s) { mediaStr = s; initVis(s); }).catch(function() {}); };
1099
+ 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; };
1100
+ recognition.onerror = function() { stopRec(); };
1101
+ recognition.onend = function() { stopRec(); if (ft.trim()) { $('chatInput').value = ft.trim(); sendMessage(); } ft = ''; };
1102
+ recognition.start();
1103
+ }
1104
+ function stopRec() { isRecording = false; $('micBtn').classList.remove('recording'); $('chatInput').placeholder = 'type or speak...';
1105
+ if (recognition) { try { recognition.stop(); } catch(e) {} } stopVis(); if (modelReady) setStatus('ok', 'model ready'); }
1106
+
1107
+ /* Keyboard shortcuts */
1108
+ document.addEventListener('keydown', function(e) {
1109
+ if (e.ctrlKey && e.key === 'm') { e.preventDefault(); toggleMic(); }
1110
+ if (e.ctrlKey && e.key === 't') { e.preventDefault(); toggleTheme(); }
1111
+ 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'); } }
1112
+ if (e.ctrlKey && e.key === 'e') { e.preventDefault(); exportConversation(); }
1113
+ if (e.key === 'F11') { e.preventDefault(); toggleFullscreen(); }
1114
+ if (e.key === '?' && !e.ctrlKey && document.activeElement.tagName !== 'TEXTAREA' && document.activeElement.tagName !== 'INPUT') { toggleShortcuts(); }
1115
+ if (e.key === 'Escape') { if (synth.speaking) synth.cancel(); if (isRecording) stopRec(); if ($('shortcutsModal').classList.contains('active')) $('shortcutsModal').classList.remove('active'); }
1116
+ });
1117
+
1118
+ /* API badges */
1119
+ (function() {
1120
+ 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'];
1121
+ var container = $('apiBadges');
1122
+ apis.forEach(function(name) { var b = document.createElement('span'); b.className = 'api-badge'; b.textContent = name; container.appendChild(b); });
1123
+ })();
1124
+
1125
+ /* Init */
1126
+ checkSavedModel();
1127
+ loadSettings();
1128
+ setStatus('ok', 'configure and start training');
1129
+ </script>
1130
+ </body>
1131
  </html>