Spaces:
Running
Running
Update index.html
Browse files- index.html +1130 -18
index.html
CHANGED
|
@@ -1,19 +1,1131 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 · chat · 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 · 4 heads · 2 layers · ~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 · 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> · <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 · 4 heads · 1 layer · ~5K params'},
|
| 720 |
+
small: {n_embd: 24, n_head: 4, n_layer: 2, label: '24 dim · 4 heads · 2 layers · ~17K params'},
|
| 721 |
+
medium: {n_embd: 32, n_head: 4, n_layer: 2, label: '32 dim · 4 heads · 2 layers · ~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>
|