LogitCode commited on
Commit
f42e1dc
Β·
verified Β·
1 Parent(s): 5eee74f

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1052 -19
index.html CHANGED
@@ -1,19 +1,1052 @@
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
+ <title>FlexChat β€” AI Chat</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,700;1,9..144,300&display=swap" rel="stylesheet">
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/11.2.0/marked.min.js"></script>
10
+ <script>
11
+ tailwind.config = {
12
+ theme: {
13
+ extend: {
14
+ fontFamily: {
15
+ mono: ['DM Mono', 'monospace'],
16
+ display: ['Fraunces', 'serif'],
17
+ },
18
+ colors: {
19
+ ink: '#0d0d0f',
20
+ paper: '#f5f0e8',
21
+ smoke: '#1a1a1f',
22
+ ash: '#2a2a32',
23
+ mist: '#3d3d48',
24
+ fog: '#6b6b7a',
25
+ ghost: '#9898a8',
26
+ pearl: '#c8c8d8',
27
+ cream: '#e8e4dc',
28
+ amber: '#d4a853',
29
+ ember: '#c47c3a',
30
+ sage: '#6b8f71',
31
+ rose: '#b85c5c',
32
+ },
33
+ animation: {
34
+ 'fade-up': 'fadeUp 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards',
35
+ 'blink': 'blink 1s step-end infinite',
36
+ 'slide-in': 'slideIn 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards',
37
+ 'spin-slow': 'spin 2s linear infinite',
38
+ },
39
+ keyframes: {
40
+ fadeUp: {
41
+ '0%': { opacity: '0', transform: 'translateY(12px)' },
42
+ '100%': { opacity: '1', transform: 'translateY(0)' },
43
+ },
44
+ blink: {
45
+ '0%, 100%': { opacity: '1' },
46
+ '50%': { opacity: '0' },
47
+ },
48
+ slideIn: {
49
+ '0%': { opacity: '0', transform: 'translateX(-8px)' },
50
+ '100%': { opacity: '1', transform: 'translateX(0)' },
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+ </script>
57
+ <style>
58
+ * { box-sizing: border-box; }
59
+ body { background: #0d0d0f; font-family: 'DM Mono', monospace; }
60
+ /* ── Theme variables ─────────────────────────────── */
61
+ body {
62
+ --c-bg: #0d0d0f;
63
+ --c-surface: #1a1a1f;
64
+ --c-border: #2a2a32;
65
+ --c-mist: #3d3d48;
66
+ --c-fog: #6b6b7a;
67
+ --c-ghost: #9898a8;
68
+ --c-pearl: #c8c8d8;
69
+ --c-cream: #e8e4dc;
70
+ --c-text: #c8c8d8;
71
+ }
72
+ body.light {
73
+ --c-bg: #f5f0e8;
74
+ --c-surface: #ede8df;
75
+ --c-border: #d8d0c4;
76
+ --c-mist: #c8bfb0;
77
+ --c-fog: #8a8070;
78
+ --c-ghost: #6a6058;
79
+ --c-pearl: #3a3028;
80
+ --c-cream: #1a1208;
81
+ --c-text: #3a3028;
82
+ }
83
+
84
+
85
+ /* Scrollbar */
86
+ ::-webkit-scrollbar { width: 4px; }
87
+ ::-webkit-scrollbar-track { background: transparent; }
88
+ ::-webkit-scrollbar-thumb { background: #3d3d48; border-radius: 2px; }
89
+ ::-webkit-scrollbar-thumb:hover { background: #6b6b7a; }
90
+
91
+ /* Prose styles for markdown */
92
+ .prose-msg { line-height: 1.65; color: #c8c8d8; }
93
+ .prose-msg p { margin: 0 0 0.6em; }
94
+ .prose-msg p:last-child { margin-bottom: 0; }
95
+ .prose-msg code {
96
+ font-family: 'DM Mono', monospace;
97
+ background: rgba(255,255,255,0.08);
98
+ padding: 0.1em 0.35em;
99
+ border-radius: 3px;
100
+ font-size: 0.875em;
101
+ color: #d4a853;
102
+ }
103
+ .prose-msg pre {
104
+ background: #111116;
105
+ border: 1px solid #2a2a32;
106
+ border-radius: 6px;
107
+ padding: 12px 14px;
108
+ overflow-x: auto;
109
+ margin: 0.6em 0;
110
+ }
111
+ .prose-msg pre code {
112
+ background: transparent;
113
+ padding: 0;
114
+ color: #c8c8d8;
115
+ font-size: 0.82em;
116
+ }
117
+ .prose-msg h1,.prose-msg h2,.prose-msg h3 {
118
+ font-family: 'Fraunces', serif;
119
+ color: #e8e4dc;
120
+ margin: 0.8em 0 0.3em;
121
+ font-weight: 700;
122
+ }
123
+ .prose-msg h1 { font-size: 1.25em; }
124
+ .prose-msg h2 { font-size: 1.1em; }
125
+ .prose-msg h3 { font-size: 1em; }
126
+ .prose-msg ul, .prose-msg ol { padding-left: 1.25em; margin: 0.4em 0 0.6em; }
127
+ .prose-msg li { margin: 0.2em 0; }
128
+ .prose-msg blockquote {
129
+ border-left: 2px solid #d4a853;
130
+ padding-left: 0.75em;
131
+ color: #9898a8;
132
+ margin: 0.5em 0;
133
+ font-style: italic;
134
+ }
135
+ .prose-msg a { color: #d4a853; text-decoration: underline; }
136
+ .prose-msg strong { color: #e8e4dc; font-weight: 500; }
137
+ .prose-msg table { border-collapse: collapse; width: 100%; margin: 0.5em 0; font-size: 0.85em; }
138
+ .prose-msg th { background: #2a2a32; color: #e8e4dc; padding: 5px 8px; text-align: left; }
139
+ .prose-msg td { border-top: 1px solid #2a2a32; padding: 5px 8px; }
140
+
141
+ /* User bubble prose */
142
+ .prose-user { line-height: 1.65; color: #1a1a1f; }
143
+ .prose-user p { margin: 0 0 0.5em; }
144
+ .prose-user p:last-child { margin-bottom: 0; }
145
+ .prose-user code { background: rgba(0,0,0,0.12); padding: 0.1em 0.3em; border-radius: 3px; font-size: 0.875em; }
146
+ .prose-user pre { background: rgba(0,0,0,0.15); border-radius: 5px; padding: 10px 12px; overflow-x: auto; margin: 0.5em 0; }
147
+ .prose-user strong { font-weight: 500; }
148
+
149
+ /* Textarea auto-resize trick */
150
+ textarea { resize: none; overflow: hidden; }
151
+
152
+ /* Sidebar transition */
153
+ #sidebar { transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
154
+ #overlay { transition: opacity 0.3s ease; }
155
+
156
+ /* Thinking dots */
157
+ .dot-1 { animation: blink 1.2s 0s infinite; }
158
+ .dot-2 { animation: blink 1.2s 0.2s infinite; }
159
+ .dot-3 { animation: blink 1.2s 0.4s infinite; }
160
+
161
+ .msg-anim { opacity: 0; animation: fadeUp 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
162
+
163
+ /* Model select */
164
+ select { -webkit-appearance: none; appearance: none; }
165
+
166
+ /* Focus ring override */
167
+ textarea:focus, input:focus, select:focus { outline: none; box-shadow: 0 0 0 1.5px #d4a853; }
168
+
169
+ /* Apply variables to all the fixed-color elements */
170
+ body { background: var(--c-bg); }
171
+ #sidebar { background: var(--c-surface); border-color: var(--c-border); }
172
+ header { background: var(--c-surface); border-color: var(--c-border); }
173
+ .border-ash { border-color: var(--c-border) !important; }
174
+ .bg-smoke { background: var(--c-surface) !important; }
175
+ .bg-ash { background: var(--c-mist) !important; }
176
+ .bg-mist { background: var(--c-mist) !important; }
177
+ .text-fog { color: var(--c-fog) !important; }
178
+ .text-ghost { color: var(--c-ghost) !important; }
179
+ .text-pearl { color: var(--c-pearl) !important; }
180
+ .text-cream { color: var(--c-cream) !important; }
181
+ .border-mist { border-color: var(--c-mist) !important; }
182
+
183
+ /* Prose text in light mode */
184
+ body.light .prose-msg { color: var(--c-text); }
185
+ body.light .prose-msg code { background: rgba(0,0,0,0.08); color: #b06000; }
186
+ body.light .prose-msg pre { background: #e8e0d4; border-color: var(--c-border); }
187
+ body.light .prose-msg pre code { color: var(--c-text); }
188
+ body.light .prose-msg strong { color: var(--c-cream); }
189
+ body.light .prose-msg blockquote { color: var(--c-ghost); }
190
+
191
+ /* Input/select backgrounds */
192
+ body.light textarea,
193
+ body.light input[type="text"],
194
+ body.light input[type="password"],
195
+ body.light input[type="number"],
196
+ body.light select { background: var(--c-surface); color: var(--c-cream); }
197
+
198
+ /* Conversation list hover */
199
+ body.light #conv-list [class*="hover:bg-ash"]:hover { background: var(--c-border) !important; }
200
+ body.light #conv-list [class*="bg-mist"] { background: var(--c-mist) !important; }
201
+
202
+ /* Message container */
203
+ body.light #messages { background: var(--c-bg); }
204
+
205
+ /* Scrollbar in light mode */
206
+ body.light ::-webkit-scrollbar-thumb { background: var(--c-mist); }
207
+
208
+
209
+
210
+
211
+ </style>
212
+ </head>
213
+ <body class="h-screen flex overflow-hidden text-pearl">
214
+
215
+ <!-- SIDEBAR OVERLAY (mobile) -->
216
+ <div id="overlay" class="fixed inset-0 bg-black/60 z-20 hidden opacity-0" onclick="closeSidebar()"></div>
217
+
218
+ <!-- SIDEBAR -->
219
+ <aside id="sidebar" class="fixed md:relative z-30 md:z-auto flex flex-col w-72 h-full bg-smoke border-r border-ash -translate-x-full md:translate-x-0 flex-shrink-0">
220
+
221
+ <!-- Logo -->
222
+ <div class="p-5 border-b border-ash flex items-center justify-between">
223
+ <div>
224
+ <div class="font-display text-2xl font-bold text-cream tracking-tight italic">FlexChat</div>
225
+ <div class="text-fog text-xs mt-0.5 font-mono">AI Chat Interface</div>
226
+ </div>
227
+ <!-- Theme toggle -->
228
+ <button onclick="toggleTheme()" id="theme-btn" title="Toggle light/dark" class="text-fog hover:text-pearl p-1.5 rounded transition-colors">
229
+ <svg id="theme-icon-dark" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707M17.657 17.657l-.707-.707M6.343 6.343l-.707-.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
230
+ <svg id="theme-icon-light" class="w-4 h-4 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/></svg>
231
+ </button>
232
+
233
+ <button onclick="closeSidebar()" class="md:hidden text-fog hover:text-pearl p-1 rounded">
234
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
235
+ </button>
236
+ </div>
237
+
238
+ <!-- New Chat Button -->
239
+ <div class="p-3">
240
+ <button onclick="newChat()" class="w-full flex items-center gap-2 px-3 py-2.5 rounded-md bg-amber/10 hover:bg-amber/20 border border-amber/25 text-amber text-sm transition-all duration-200 group">
241
+ <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
242
+ <span class="font-mono">New conversation</span>
243
+ </button>
244
+ </div>
245
+
246
+ <!-- Conversations List -->
247
+ <div class="flex-1 overflow-y-auto px-2 pb-2">
248
+ <div class="text-fog text-xs px-2 py-1.5 uppercase tracking-widest font-mono">History</div>
249
+ <div id="conv-list" class="space-y-0.5"></div>
250
+ </div>
251
+
252
+ <!-- Settings Section -->
253
+ <div class="border-t border-ash p-4 space-y-3">
254
+ <div class="text-fog text-xs uppercase tracking-widest font-mono mb-2">Settings</div>
255
+
256
+ <!-- API Key -->
257
+ <div>
258
+ <label class="text-ghost text-xs font-mono block mb-1.5">OpenRouter API Key</label>
259
+ <div class="relative">
260
+ <input id="api-key" type="password" placeholder="sk-or-v1-..."
261
+ class="w-full bg-ash border border-mist rounded-md px-3 py-2 text-xs font-mono text-cream placeholder-fog focus:border-amber transition-colors"
262
+ oninput="saveSettings()" />
263
+ <button onclick="toggleKeyVisibility()" class="absolute right-2 top-1/2 -translate-y-1/2 text-fog hover:text-pearl transition-colors">
264
+ <svg id="eye-icon" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
265
+ </button>
266
+ </div>
267
+ </div>
268
+
269
+ <!-- Model Select -->
270
+ <div>
271
+ <label class="text-ghost text-xs font-mono block mb-1.5">Model</label>
272
+ <div class="relative">
273
+ <select id="model-select" class="w-full bg-ash border border-mist rounded-md px-3 py-2 pr-7 text-xs font-mono text-cream focus:border-amber transition-colors cursor-pointer" onchange="saveSettings()">
274
+ <optgroup label='Openrouter'>
275
+ <option value='openrouter/hunter-alpha'>Hunter Alpha </option>
276
+ <option value='openrouter/healer-alpha'>Healer Alpha </option>
277
+ <option value='openrouter/free'>Free Models Router </option>
278
+ </optgroup>
279
+ <optgroup label='Nvidia'>
280
+ <option value='nvidia/nemotron-3-super-120b-a12b:free'>NVIDIA: Nemotron 3 Super (free) </option>
281
+ <option value='nvidia/nemotron-3-nano-30b-a3b:free'>NVIDIA: Nemotron 3 Nano 30B A3B (free) </option>
282
+ <option value='nvidia/nemotron-nano-12b-v2-vl:free'>NVIDIA: Nemotron Nano 12B 2 VL (free) </option>
283
+ <option value='nvidia/nemotron-nano-9b-v2:free'>NVIDIA: Nemotron Nano 9B V2 (free) </option>
284
+ <option value='nvidia/nemotron-3-nano-30b-a3b'>NVIDIA: Nemotron 3 Nano 30B A3B $</option>
285
+ <option value='nvidia/llama-3.3-nemotron-super-49b-v1.5'>NVIDIA: Llama 3.3 Nemotron Super 49B V1.5 $</option>
286
+ <option value='nvidia/nemotron-nano-9b-v2'>NVIDIA: Nemotron Nano 9B V2 $</option>
287
+ <option value='nvidia/nemotron-nano-12b-v2-vl'>NVIDIA: Nemotron Nano 12B 2 VL $$</option>
288
+ <option value='nvidia/llama-3.1-nemotron-70b-instruct'>NVIDIA: Llama 3.1 Nemotron 70B Instruct $$$</option>
289
+ </optgroup>
290
+ <optgroup label='Minimax'>
291
+ <option value='minimax/minimax-m2.5:free'>MiniMax: MiniMax M2.5 (free) </option>
292
+ <option value='minimax/minimax-m2.1'>MiniMax: MiniMax M2.1 $$</option>
293
+ <option value='minimax/minimax-m2'>MiniMax: MiniMax M2 $$</option>
294
+ <option value='minimax/minimax-m2.5'>MiniMax: MiniMax M2.5 $$$</option>
295
+ <option value='minimax/minimax-m2-her'>MiniMax: MiniMax M2-her $$$</option>
296
+ <option value='minimax/minimax-m1'>MiniMax: MiniMax M1 $$$</option>
297
+ <option value='minimax/minimax-01'>MiniMax: MiniMax-01 $$$</option>
298
+ </optgroup>
299
+ <optgroup label='Qwen'>
300
+ <option value='qwen/qwen3-next-80b-a3b-instruct:free'>Qwen: Qwen3 Next 80B A3B Instruct (free) </option>
301
+ <option value='qwen/qwen3-coder:free'>Qwen: Qwen3 Coder 480B A35B (free) </option>
302
+ <option value='qwen/qwen3-4b:free'>Qwen: Qwen3 4B (free) </option>
303
+ <option value='qwen/qwen3.5-9b'>Qwen: Qwen3.5-9B $</option>
304
+ <option value='qwen/qwen3.5-flash-02-23'>Qwen: Qwen3.5-Flash $</option>
305
+ <option value='qwen/qwen3-30b-a3b-thinking-2507'>Qwen: Qwen3 30B A3B Thinking 2507 $</option>
306
+ <option value='qwen/qwen3-coder-30b-a3b-instruct'>Qwen: Qwen3 Coder 30B A3B Instruct $</option>
307
+ <option value='qwen/qwen3-30b-a3b-instruct-2507'>Qwen: Qwen3 30B A3B Instruct 2507 $</option>
308
+ <option value='qwen/qwen3-235b-a22b-2507'>Qwen: Qwen3 235B A22B Instruct 2507 $</option>
309
+ <option value='qwen/qwen3-30b-a3b'>Qwen: Qwen3 30B A3B $</option>
310
+ <option value='qwen/qwen3-32b'>Qwen: Qwen3 32B $</option>
311
+ <option value='qwen/qwq-32b'>Qwen: QwQ 32B $</option>
312
+ <option value='qwen/qwen3-coder-next'>Qwen: Qwen3 Coder Next $$</option>
313
+ <option value='qwen/qwen3-vl-30b-a3b-instruct'>Qwen: Qwen3 VL 30B A3B Instruct $$</option>
314
+ <option value='qwen/qwen3-vl-235b-a22b-instruct'>Qwen: Qwen3 VL 235B A22B Instruct $$</option>
315
+ <option value='qwen/qwen3-coder-flash'>Qwen: Qwen3 Coder Flash $$</option>
316
+ <option value='qwen/qwen3-next-80b-a3b-thinking'>Qwen: Qwen3 Next 80B A3B Thinking $$</option>
317
+ <option value='qwen/qwen-plus-2025-07-28:thinking'>Qwen: Qwen Plus 0728 (thinking) $$</option>
318
+ <option value='qwen/qwen-plus-2025-07-28'>Qwen: Qwen Plus 0728 $$</option>
319
+ <option value='qwen/qwen3-235b-a22b-thinking-2507'>Qwen: Qwen3 235B A22B Thinking 2507 $$</option>
320
+ <option value='qwen/qwen3-coder'>Qwen: Qwen3 Coder 480B A35B $$</option>
321
+ <option value='qwen/qwen3.5-35b-a3b'>Qwen: Qwen3.5-35B-A3B $$$</option>
322
+ <option value='qwen/qwen3.5-27b'>Qwen: Qwen3.5-27B $$$</option>
323
+ <option value='qwen/qwen3.5-122b-a10b'>Qwen: Qwen3.5-122B-A10B $$$</option>
324
+ <option value='qwen/qwen3.5-plus-02-15'>Qwen: Qwen3.5 Plus 2026-02-15 $$$</option>
325
+ <option value='qwen/qwen3.5-397b-a17b'>Qwen: Qwen3.5 397B A17B $$$</option>
326
+ <option value='qwen/qwen3-max-thinking'>Qwen: Qwen3 Max Thinking $$$</option>
327
+ <option value='qwen/qwen3-max'>Qwen: Qwen3 Max $$$</option>
328
+ <option value='qwen/qwen3-coder-plus'>Qwen: Qwen3 Coder Plus $$$</option>
329
+ <option value='qwen/qwen3-next-80b-a3b-instruct'>Qwen: Qwen3 Next 80B A3B Instruct $$$</option>
330
+ <option value='qwen/qwen3-235b-a22b'>Qwen: Qwen3 235B A22B $$$</option>
331
+ </optgroup>
332
+ <optgroup label='Openai'>
333
+ <option value='openai/gpt-oss-120b:free'>OpenAI: gpt-oss-120b (free) </option>
334
+ <option value='openai/gpt-oss-20b:free'>OpenAI: gpt-oss-20b (free) </option>
335
+ <option value='openai/gpt-oss-120b'>OpenAI: gpt-oss-120b $</option>
336
+ <option value='openai/gpt-oss-20b'>OpenAI: gpt-oss-20b $</option>
337
+ <option value='openai/gpt-4o-mini-search-preview'>OpenAI: GPT-4o-mini Search Preview $$</option>
338
+ <option value='openai/gpt-4o-mini-2024-07-18'>OpenAI: GPT-4o-mini (2024-07-18) $$</option>
339
+ <option value='openai/gpt-4o-mini'>OpenAI: GPT-4o-mini $$</option>
340
+ <option value='openai/gpt-5.4-nano'>OpenAI: GPT-5.4 Nano $$$</option>
341
+ <option value='openai/gpt-5.4-mini'>OpenAI: GPT-5.4 Mini $$$</option>
342
+ <option value='openai/o4-mini-high'>OpenAI: o4 Mini High $$$</option>
343
+ <option value='openai/o4-mini'>OpenAI: o4 Mini $$$</option>
344
+ <option value='openai/o3-mini-high'>OpenAI: o3 Mini High $$$</option>
345
+ <option value='openai/o3-mini'>OpenAI: o3 Mini $$$</option>
346
+ <option value='openai/o4-mini-deep-research'>OpenAI: o4 Mini Deep Research $$$$</option>
347
+ <option value='openai/o3'>OpenAI: o3 $$$$</option>
348
+ <option value='openai/gpt-5.4'>OpenAI: GPT-5.4 $$$$$</option>
349
+ <option value='openai/gpt-5.3-codex'>OpenAI: GPT-5.3-Codex $$$$$</option>
350
+ <option value='openai/gpt-5.2-codex'>OpenAI: GPT-5.2-Codex $$$$$</option>
351
+ <option value='openai/gpt-5.2-chat'>OpenAI: GPT-5.2 Chat $$$$$</option>
352
+ <option value='openai/gpt-5.2'>OpenAI: GPT-5.2 $$$$$</option>
353
+ <option value='openai/o3-deep-research'>OpenAI: o3 Deep Research $$$$$</option>
354
+ <option value='openai/gpt-5.4-pro'>OpenAI: GPT-5.4 Pro $$$$$$</option>
355
+ <option value='openai/gpt-5.2-pro'>OpenAI: GPT-5.2 Pro $$$$$$</option>
356
+ <option value='openai/o3-pro'>OpenAI: o3 Pro $$$$$$</option>
357
+ </optgroup>
358
+ <optgroup label='Z-ai'>
359
+ <option value='z-ai/glm-4.5-air:free'>Z.ai: GLM 4.5 Air (free) </option>
360
+ <option value='z-ai/glm-4.7-flash'>Z.ai: GLM 4.7 Flash $</option>
361
+ <option value='z-ai/glm-4-32b'>Z.ai: GLM 4 32B $</option>
362
+ <option value='z-ai/glm-4.6v'>Z.ai: GLM 4.6V $$</option>
363
+ <option value='z-ai/glm-4.5-air'>Z.ai: GLM 4.5 Air $$</option>
364
+ <option value='z-ai/glm-5-turbo'>Z.ai: GLM 5 Turbo $$$</option>
365
+ <option value='z-ai/glm-5'>Z.ai: GLM 5 $$$</option>
366
+ <option value='z-ai/glm-4.7'>Z.ai: GLM 4.7 $$$</option>
367
+ <option value='z-ai/glm-4.6'>Z.ai: GLM 4.6 $$$</option>
368
+ <option value='z-ai/glm-4.5v'>Z.ai: GLM 4.5V $$$</option>
369
+ <option value='z-ai/glm-4.5'>Z.ai: GLM 4.5 $$$</option>
370
+ </optgroup>
371
+ <optgroup label='Google'>
372
+ <option value='google/gemma-3n-e2b-it:free'>Google: Gemma 3n 2B (free) </option>
373
+ <option value='google/gemma-3n-e4b-it:free'>Google: Gemma 3n 4B (free) </option>
374
+ <option value='google/gemma-3-4b-it:free'>Google: Gemma 3 4B (free) </option>
375
+ <option value='google/gemma-3-12b-it:free'>Google: Gemma 3 12B (free) </option>
376
+ <option value='google/gemma-3-27b-it:free'>Google: Gemma 3 27B (free) </option>
377
+ <option value='google/gemini-2.5-flash-lite-preview-09-2025'>Google: Gemini 2.5 Flash Lite Preview 09-2025 $</option>
378
+ <option value='google/gemini-2.5-flash-lite'>Google: Gemini 2.5 Flash Lite $</option>
379
+ <option value='google/gemma-3-12b-it'>Google: Gemma 3 12B $</option>
380
+ <option value='google/gemma-3-27b-it'>Google: Gemma 3 27B $</option>
381
+ <option value='google/gemini-2.0-flash-lite-001'>Google: Gemini 2.0 Flash Lite $</option>
382
+ <option value='google/gemini-2.0-flash-001'>Google: Gemini 2.0 Flash $</option>
383
+ <option value='google/gemma-2-27b-it'>Google: Gemma 2 27B $$</option>
384
+ <option value='google/gemini-3.1-flash-lite-preview'>Google: Gemini 3.1 Flash Lite Preview $$$</option>
385
+ <option value='google/gemini-3.1-flash-image-preview'>Google: Nano Banana 2 (Gemini 3.1 Flash Image Preview) $$$</option>
386
+ <option value='google/gemini-3-flash-preview'>Google: Gemini 3 Flash Preview $$$</option>
387
+ <option value='google/gemini-2.5-flash-image'>Google: Nano Banana (Gemini 2.5 Flash Image) $$$</option>
388
+ <option value='google/gemini-2.5-flash'>Google: Gemini 2.5 Flash $$$</option>
389
+ <option value='google/gemini-2.5-pro'>Google: Gemini 2.5 Pro $$$$</option>
390
+ <option value='google/gemini-2.5-pro-preview'>Google: Gemini 2.5 Pro Preview 06-05 $$$$</option>
391
+ <option value='google/gemini-2.5-pro-preview-05-06'>Google: Gemini 2.5 Pro Preview 05-06 $$$$</option>
392
+ <option value='google/gemini-3.1-pro-preview-customtools'>Google: Gemini 3.1 Pro Preview Custom Tools $$$$$</option>
393
+ <option value='google/gemini-3.1-pro-preview'>Google: Gemini 3.1 Pro Preview $$$$$</option>
394
+ <option value='google/gemini-3-pro-image-preview'>Google: Nano Banana Pro (Gemini 3 Pro Image Preview) $$$$$</option>
395
+ <option value='google/gemini-3-pro-preview'>Google: Gemini 3 Pro Preview $$$$$</option>
396
+ </optgroup>
397
+ <optgroup label='Meta-llama'>
398
+ <option value='meta-llama/llama-3.3-70b-instruct:free'>Meta: Llama 3.3 70B Instruct (free) </option>
399
+ <option value='meta-llama/llama-3.2-3b-instruct:free'>Meta: Llama 3.2 3B Instruct (free) </option>
400
+ <option value='meta-llama/llama-guard-4-12b'>Meta: Llama Guard 4 12B $</option>
401
+ <option value='meta-llama/llama-4-scout'>Meta: Llama 4 Scout $</option>
402
+ <option value='meta-llama/llama-3.3-70b-instruct'>Meta: Llama 3.3 70B Instruct $</option>
403
+ <option value='meta-llama/llama-3.2-3b-instruct'>Meta: Llama 3.2 3B Instruct $</option>
404
+ <option value='meta-llama/llama-3.2-1b-instruct'>Meta: Llama 3.2 1B Instruct $</option>
405
+ <option value='meta-llama/llama-3.1-70b-instruct'>Meta: Llama 3.1 70B Instruct $</option>
406
+ <option value='meta-llama/llama-4-maverick'>Meta: Llama 4 Maverick $$</option>
407
+ <option value='meta-llama/llama-3-70b-instruct'>Meta: Llama 3 70B Instruct $$</option>
408
+ <option value='meta-llama/llama-3.1-405b'>Meta: Llama 3.1 405B (base) $$$</option>
409
+ </optgroup>
410
+ <optgroup label='Nousresearch'>
411
+ <option value='nousresearch/hermes-3-llama-3.1-405b:free'>Nous: Hermes 3 405B Instruct (free) </option>
412
+ <option value='nousresearch/hermes-4-70b'>Nous: Hermes 4 70B $</option>
413
+ <option value='nousresearch/hermes-3-llama-3.1-70b'>Nous: Hermes 3 70B Instruct $</option>
414
+ <option value='nousresearch/hermes-2-pro-llama-3-8b'>NousResearch: Hermes 2 Pro - Llama-3 8B $</option>
415
+ <option value='nousresearch/hermes-3-llama-3.1-405b'>Nous: Hermes 3 405B Instruct $$</option>
416
+ <option value='nousresearch/hermes-4-405b'>Nous: Hermes 4 405B $$$</option>
417
+ </optgroup>
418
+ <optgroup label='Deepseek'>
419
+ <option value='deepseek/deepseek-v3.2'>DeepSeek: DeepSeek V3.2 $</option>
420
+ <option value='deepseek/deepseek-v3.2-exp'>DeepSeek: DeepSeek V3.2 Exp $</option>
421
+ <option value='deepseek/deepseek-r1-distill-qwen-32b'>DeepSeek: R1 Distill Qwen 32B $</option>
422
+ <option value='deepseek/deepseek-v3.1-terminus'>DeepSeek: DeepSeek V3.1 Terminus $$</option>
423
+ <option value='deepseek/deepseek-chat-v3.1'>DeepSeek: DeepSeek V3.1 $$</option>
424
+ <option value='deepseek/deepseek-chat-v3-0324'>DeepSeek: DeepSeek V3 0324 $$</option>
425
+ <option value='deepseek/deepseek-r1-distill-llama-70b'>DeepSeek: R1 Distill Llama 70B $$</option>
426
+ <option value='deepseek/deepseek-chat'>DeepSeek: DeepSeek V3 $$</option>
427
+ <option value='deepseek/deepseek-v3.2-speciale'>DeepSeek: DeepSeek V3.2 Speciale $$$</option>
428
+ <option value='deepseek/deepseek-r1-0528'>DeepSeek: R1 0528 $$$</option>
429
+ <option value='deepseek/deepseek-r1'>DeepSeek: R1 $$$</option>
430
+ </optgroup>
431
+ <optgroup label='X-ai'>
432
+ <option value='x-ai/grok-4.1-fast'>xAI: Grok 4.1 Fast $</option>
433
+ <option value='x-ai/grok-4-fast'>xAI: Grok 4 Fast $</option>
434
+ <option value='x-ai/grok-3-mini'>xAI: Grok 3 Mini $</option>
435
+ <option value='x-ai/grok-3-mini-beta'>xAI: Grok 3 Mini Beta $</option>
436
+ <option value='x-ai/grok-code-fast-1'>xAI: Grok Code Fast 1 $$$</option>
437
+ <option value='x-ai/grok-4.20-multi-agent-beta'>xAI: Grok 4.20 Multi-Agent Beta $$$$</option>
438
+ <option value='x-ai/grok-4.20-beta'>xAI: Grok 4.20 Beta $$$$</option>
439
+ <option value='x-ai/grok-4'>xAI: Grok 4 $$$$$</option>
440
+ <option value='x-ai/grok-3'>xAI: Grok 3 $$$$$</option>
441
+ <option value='x-ai/grok-3-beta'>xAI: Grok 3 Beta $$$$$</option>
442
+ </optgroup>
443
+ <optgroup label='Moonshotai'>
444
+ <option value='moonshotai/kimi-k2.5'>MoonshotAI: Kimi K2.5 $$$</option>
445
+ <option value='moonshotai/kimi-k2-thinking'>MoonshotAI: Kimi K2 Thinking $$$</option>
446
+ <option value='moonshotai/kimi-k2-0905'>MoonshotAI: Kimi K2 0905 $$$</option>
447
+ <option value='moonshotai/kimi-k2'>MoonshotAI: Kimi K2 0711 $$$</option>
448
+ </optgroup>
449
+ <optgroup label='Anthropic'>
450
+ <option value='anthropic/claude-haiku-4.5'>Anthropic: Claude Haiku 4.5 $$$</option>
451
+ <option value='anthropic/claude-3.5-haiku'>Anthropic: Claude 3.5 Haiku $$$</option>
452
+ <option value='anthropic/claude-3-haiku'>Anthropic: Claude 3 Haiku $$$</option>
453
+ <option value='anthropic/claude-sonnet-4.6'>Anthropic: Claude Sonnet 4.6 $$$$$</option>
454
+ <option value='anthropic/claude-opus-4.6'>Anthropic: Claude Opus 4.6 $$$$$</option>
455
+ <option value='anthropic/claude-opus-4.5'>Anthropic: Claude Opus 4.5 $$$$$</option>
456
+ <option value='anthropic/claude-sonnet-4.5'>Anthropic: Claude Sonnet 4.5 $$$$$</option>
457
+ <option value='anthropic/claude-sonnet-4'>Anthropic: Claude Sonnet 4 $$$$$</option>
458
+ <option value='anthropic/claude-3.7-sonnet'>Anthropic: Claude 3.7 Sonnet $$$$$</option>
459
+ <option value='anthropic/claude-3.7-sonnet:thinking'>Anthropic: Claude 3.7 Sonnet (thinking) $$$$$</option>
460
+ <option value='anthropic/claude-3.5-sonnet'>Anthropic: Claude 3.5 Sonnet $$$$$</option>
461
+ <option value='anthropic/claude-opus-4.1'>Anthropic: Claude Opus 4.1 $$$$$$</option>
462
+ <option value='anthropic/claude-opus-4'>Anthropic: Claude Opus 4 $$$$$$</option>
463
+ </optgroup>
464
+
465
+ </select>
466
+ <div class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-fog">
467
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
468
+ </div>
469
+ </div>
470
+ </div>
471
+
472
+ <!-- System Prompt -->
473
+ <div>
474
+ <label class="text-ghost text-xs font-mono block mb-1.5">System Prompt <span class="text-fog">(optional)</span></label>
475
+ <textarea id="system-prompt" placeholder="You are a helpful assistant..." rows="2"
476
+ class="w-full bg-ash border border-mist rounded-md px-3 py-2 text-xs font-mono text-cream placeholder-fog focus:border-amber transition-colors"
477
+ oninput="saveSettings(); autoResize(this)"></textarea>
478
+ </div>
479
+
480
+ <!-- Temperature -->
481
+ <div>
482
+ <div class="flex justify-between items-center mb-1.5">
483
+ <label class="text-ghost text-xs font-mono">Temperature</label>
484
+ <span id="temp-val" class="text-amber text-xs font-mono">0.7</span>
485
+ </div>
486
+ <input id="temperature" type="range" min="0" max="2" step="0.1" value="0.7"
487
+ class="w-full accent-amber cursor-pointer"
488
+ oninput="document.getElementById('temp-val').textContent=this.value; saveSettings()" />
489
+ </div>
490
+
491
+ <!-- Max Tokens -->
492
+ <div>
493
+ <label class="text-ghost text-xs font-mono block mb-1.5">Max Tokens</label>
494
+ <input id="max-tokens" type="number" value="2048" min="1" max="32000"
495
+ class="w-full bg-ash border border-mist rounded-md px-3 py-2 text-xs font-mono text-cream focus:border-amber transition-colors"
496
+ oninput="saveSettings()" />
497
+ </div>
498
+ </div>
499
+ </aside>
500
+
501
+ <!-- MAIN CHAT AREA -->
502
+ <main class="flex-1 flex flex-col min-w-0 h-full">
503
+
504
+ <!-- Header -->
505
+ <header class="flex items-center justify-between px-4 py-3 border-b border-ash bg-smoke flex-shrink-0">
506
+ <div class="flex items-center gap-3">
507
+ <!-- Mobile sidebar toggle -->
508
+ <button onclick="openSidebar()" class="md:hidden text-fog hover:text-pearl p-1 rounded transition-colors">
509
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
510
+ </button>
511
+ <div>
512
+ <div id="chat-title" class="text-cream font-display italic text-base font-bold tracking-tight">New conversation</div>
513
+ <div id="model-badge" class="text-fog text-xs font-mono mt-0.5">β€” select a model</div>
514
+ </div>
515
+ </div>
516
+
517
+ <div class="flex items-center gap-2">
518
+ <!-- Token counter -->
519
+ <div id="token-info" class="hidden text-fog text-xs font-mono border border-ash rounded px-2 py-1">
520
+ <span id="token-count">0</span> tokens
521
+ </div>
522
+ <!-- Clear button -->
523
+ <button onclick="clearCurrentChat()" title="Clear messages" class="text-fog hover:text-rose p-1.5 rounded transition-colors">
524
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
525
+ </button>
526
+ <!-- Export button -->
527
+ <button onclick="exportChat()" title="Export chat" class="text-fog hover:text-sage p-1.5 rounded transition-colors">
528
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
529
+ </button>
530
+ </div>
531
+ </header>
532
+
533
+ <!-- Messages -->
534
+ <div id="messages" class="flex-1 overflow-y-auto px-4 py-6 space-y-6">
535
+ <!-- Empty state -->
536
+ <div id="empty-state" class="h-full flex flex-col items-center justify-center text-center py-16 opacity-100 transition-opacity duration-300">
537
+ <div class="font-display text-5xl italic text-cream/20 mb-4 select-none">Nexus</div>
538
+ <div class="text-fog text-sm font-mono max-w-xs">Enter your OpenRouter API key in the sidebar, choose a model, and start a conversation.</div>
539
+ <div class="mt-8 grid grid-cols-2 gap-2 max-w-xs w-full">
540
+ <button onclick="insertPrompt('Explain quantum computing in simple terms')" class="text-left px-3 py-2.5 bg-ash hover:bg-mist border border-mist rounded-md text-xs font-mono text-ghost hover:text-pearl transition-all">
541
+ Explain quantum computing β†’
542
+ </button>
543
+ <button onclick="insertPrompt('Write a haiku about artificial intelligence')" class="text-left px-3 py-2.5 bg-ash hover:bg-mist border border-mist rounded-md text-xs font-mono text-ghost hover:text-pearl transition-all">
544
+ Write a haiku about AI β†’
545
+ </button>
546
+ <button onclick="insertPrompt('What are the best practices for clean code?')" class="text-left px-3 py-2.5 bg-ash hover:bg-mist border border-mist rounded-md text-xs font-mono text-ghost hover:text-pearl transition-all">
547
+ Clean code practices β†’
548
+ </button>
549
+ <button onclick="insertPrompt('Help me brainstorm a startup idea')" class="text-left px-3 py-2.5 bg-ash hover:bg-mist border border-mist rounded-md text-xs font-mono text-ghost hover:text-pearl transition-all">
550
+ Startup brainstorm β†’
551
+ </button>
552
+ </div>
553
+ </div>
554
+ </div>
555
+
556
+ <!-- Error banner -->
557
+ <div id="error-banner" class="hidden mx-4 mb-2 px-4 py-2.5 bg-rose/10 border border-rose/30 rounded-md text-rose text-xs font-mono flex items-center gap-2">
558
+ <svg class="w-3.5 h-3.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
559
+ <span id="error-text"></span>
560
+ </div>
561
+
562
+ <!-- Input -->
563
+ <div class="border-t border-ash bg-smoke px-4 py-3 flex-shrink-0">
564
+ <div class="flex items-end gap-2 bg-ash rounded-lg border border-mist focus-within:border-amber transition-colors px-3 py-2">
565
+ <textarea id="user-input"
566
+ placeholder="Send a message..."
567
+ rows="1"
568
+ class="flex-1 bg-transparent text-sm font-mono text-cream placeholder-fog py-1 max-h-48 leading-relaxed"
569
+ onkeydown="handleKeydown(event)"
570
+ oninput="autoResize(this)"
571
+ ></textarea>
572
+ <button id="send-btn" onclick="sendMessage()"
573
+ class="flex-shrink-0 w-8 h-8 rounded-md bg-amber hover:bg-ember disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center transition-colors mb-0.5"
574
+ title="Send (Enter)">
575
+ <svg class="w-4 h-4 text-ink" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>
576
+ </button>
577
+ </div>
578
+ <div class="flex justify-between items-center mt-1.5 px-1">
579
+ <span class="text-fog/60 text-xs font-mono">Enter to send Β· Shift+Enter for newline</span>
580
+ <span id="char-count" class="text-fog/60 text-xs font-mono"></span>
581
+ </div>
582
+ </div>
583
+ </main>
584
+
585
+ <script>
586
+ // ── State ──────────────────────────────────────────────────────────────────
587
+ let currentChatId = null;
588
+ let isStreaming = false;
589
+ let abortController = null;
590
+ let chats = {}; // { id: { id, title, messages: [], createdAt } }
591
+
592
+ // ── Init ───────────────────────────────────────────────────────────────────
593
+ function init() {
594
+ if (localStorage.getItem('nexus_theme') === 'light') toggleTheme(); // ← add this
595
+
596
+ loadSettings();
597
+ loadChats();
598
+ updateModelBadge();
599
+ newChat();
600
+ }
601
+ function toggleTheme() {
602
+ const isLight = document.body.classList.toggle('light');
603
+ document.getElementById('theme-icon-dark').classList.toggle('hidden', isLight);
604
+ document.getElementById('theme-icon-light').classList.toggle('hidden', !isLight);
605
+ localStorage.setItem('nexus_theme', isLight ? 'light' : 'dark');
606
+ }
607
+ // ── Settings ───────────────────────────────────────────────────────────────
608
+ function saveSettings() {
609
+ const settings = {
610
+ apiKey: document.getElementById('api-key').value,
611
+ model: document.getElementById('model-select').value,
612
+ systemPrompt: document.getElementById('system-prompt').value,
613
+ temperature: document.getElementById('temperature').value,
614
+ maxTokens: document.getElementById('max-tokens').value,
615
+ };
616
+ localStorage.setItem('nexus_settings', JSON.stringify(settings));
617
+ updateModelBadge();
618
+ }
619
+
620
+ function loadSettings() {
621
+ try {
622
+ const s = JSON.parse(localStorage.getItem('nexus_settings') || '{}');
623
+ if (s.apiKey) document.getElementById('api-key').value = s.apiKey;
624
+ if (s.model) document.getElementById('model-select').value = s.model;
625
+ if (s.systemPrompt) document.getElementById('system-prompt').value = s.systemPrompt;
626
+ if (s.temperature) {
627
+ document.getElementById('temperature').value = s.temperature;
628
+ document.getElementById('temp-val').textContent = s.temperature;
629
+ }
630
+ if (s.maxTokens) document.getElementById('max-tokens').value = s.maxTokens;
631
+ } catch(e) {}
632
+ }
633
+
634
+ function updateModelBadge() {
635
+ const model = document.getElementById('model-select').value;
636
+ const short = model.split('/').pop();
637
+ document.getElementById('model-badge').textContent = 'β€” ' + short;
638
+ }
639
+
640
+ function toggleKeyVisibility() {
641
+ const input = document.getElementById('api-key');
642
+ input.type = input.type === 'password' ? 'text' : 'password';
643
+ }
644
+
645
+ // ── Chat Management ────────────────────────────────────────────────────────
646
+ function genId() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 6); }
647
+
648
+ function saveChats() {
649
+ localStorage.setItem('nexus_chats', JSON.stringify(chats));
650
+ }
651
+
652
+ function loadChats() {
653
+ try {
654
+ chats = JSON.parse(localStorage.getItem('nexus_chats') || '{}');
655
+ } catch(e) { chats = {}; }
656
+ renderConvList();
657
+ }
658
+
659
+ function newChat() {
660
+ const id = genId();
661
+ chats[id] = { id, title: 'New conversation', messages: [], createdAt: Date.now() };
662
+ currentChatId = id;
663
+ saveChats();
664
+ renderConvList();
665
+ renderMessages();
666
+ closeSidebar();
667
+ document.getElementById('chat-title').textContent = 'New conversation';
668
+ document.getElementById('user-input').focus();
669
+ }
670
+
671
+ function switchChat(id) {
672
+ if (isStreaming) stopStreaming();
673
+ currentChatId = id;
674
+ renderMessages();
675
+ renderConvList();
676
+ const chat = chats[id];
677
+ document.getElementById('chat-title').textContent = chat.title;
678
+ closeSidebar();
679
+ }
680
+
681
+ function deleteChat(id, e) {
682
+ e.stopPropagation();
683
+ delete chats[id];
684
+ saveChats();
685
+ if (currentChatId === id) newChat();
686
+ else renderConvList();
687
+ }
688
+
689
+ function clearCurrentChat() {
690
+ if (!currentChatId) return;
691
+ chats[currentChatId].messages = [];
692
+ chats[currentChatId].title = 'New conversation';
693
+ document.getElementById('chat-title').textContent = 'New conversation';
694
+ saveChats();
695
+ renderConvList();
696
+ renderMessages();
697
+ }
698
+
699
+ function renderConvList() {
700
+ const list = document.getElementById('conv-list');
701
+ const sorted = Object.values(chats).sort((a,b) => b.createdAt - a.createdAt);
702
+ if (sorted.length === 0) {
703
+ list.innerHTML = '<div class="text-fog/60 text-xs font-mono px-3 py-2">No conversations yet</div>';
704
+ return;
705
+ }
706
+ list.innerHTML = sorted.map(c => `
707
+ <div onclick="switchChat('${c.id}')" class="group relative flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer transition-all duration-150 ${c.id === currentChatId ? 'bg-mist text-cream' : 'hover:bg-ash text-ghost hover:text-pearl'}">
708
+ <svg class="w-3.5 h-3.5 flex-shrink-0 ${c.id === currentChatId ? 'text-amber' : 'text-fog'}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/></svg>
709
+ <span class="text-xs font-mono truncate flex-1">${escHtml(c.title)}</span>
710
+ <button onclick="deleteChat('${c.id}', event)" class="opacity-0 group-hover:opacity-100 text-fog hover:text-rose transition-all p-0.5 rounded flex-shrink-0">
711
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
712
+ </button>
713
+ </div>
714
+ `).join('');
715
+ }
716
+
717
+ // ── Message Rendering ──────────────────────────────────────────────────────
718
+ // Store message content by key so onclick attrs never embed raw content
719
+ const msgContentMap = {};
720
+
721
+ function renderMessages() {
722
+ const container = document.getElementById('messages');
723
+ const msgs = chats[currentChatId]?.messages || [];
724
+
725
+ if (msgs.length === 0) {
726
+ container.innerHTML = `<div id="empty-state" class="h-full flex flex-col items-center justify-center text-center py-16">
727
+ <div class="font-display text-5xl italic text-cream/20 mb-4 select-none">Nexus</div>
728
+ <div class="text-fog text-sm font-mono max-w-xs">Enter your OpenRouter API key in the sidebar, choose a model, and start a conversation.</div>
729
+ <div class="mt-8 grid grid-cols-2 gap-2 max-w-xs w-full">
730
+ <button onclick="insertPrompt('Explain quantum computing in simple terms')" class="text-left px-3 py-2.5 bg-ash hover:bg-mist border border-mist rounded-md text-xs font-mono text-ghost hover:text-pearl transition-all">Explain quantum computing β†’</button>
731
+ <button onclick="insertPrompt('Write a haiku about artificial intelligence')" class="text-left px-3 py-2.5 bg-ash hover:bg-mist border border-mist rounded-md text-xs font-mono text-ghost hover:text-pearl transition-all">Write a haiku about AI β†’</button>
732
+ <button onclick="insertPrompt('What are the best practices for clean code?')" class="text-left px-3 py-2.5 bg-ash hover:bg-mist border border-mist rounded-md text-xs font-mono text-ghost hover:text-pearl transition-all">Clean code practices β†’</button>
733
+ <button onclick="insertPrompt('Help me brainstorm a startup idea')" class="text-left px-3 py-2.5 bg-ash hover:bg-mist border border-mist rounded-md text-xs font-mono text-ghost hover:text-pearl transition-all">Startup brainstorm β†’</button>
734
+ </div>
735
+ </div>`;
736
+ document.getElementById('token-info').classList.add('hidden');
737
+ return;
738
+ }
739
+
740
+ document.getElementById('token-info').classList.remove('hidden');
741
+ container.innerHTML = msgs.map((m, i) => renderMessage(m, i)).join('');
742
+ // Wire up copy buttons via data attributes (avoids embedding content in onclick)
743
+ container.querySelectorAll('[data-copy-key]').forEach(btn => {
744
+ btn.addEventListener('click', () => copyText(msgContentMap[btn.dataset.copyKey] || ''));
745
+ });
746
+ updateTokenCount();
747
+ scrollToBottom(false);
748
+ }
749
+
750
+ function renderMessage(msg, idx) {
751
+ const key = currentChatId + '_' + idx;
752
+ msgContentMap[key] = msg.content;
753
+ if (msg.role === 'user') {
754
+ return `<div class="flex justify-end">
755
+ <div class="max-w-[80%] group">
756
+ <div class="bg-amber text-ink rounded-2xl rounded-tr-sm px-4 py-3 text-sm shadow-md">
757
+ <div class="prose-user">${renderMarkdown(msg.content)}</div>
758
+ </div>
759
+ <div class="flex justify-end items-center gap-2 mt-1 opacity-0 group-hover:opacity-100 transition-opacity">
760
+ <button data-copy-key="${key}" class="text-fog hover:text-pearl text-xs font-mono transition-colors">copy</button>
761
+ <span class="text-fog/40 text-xs font-mono">${formatTime(msg.ts)}</span>
762
+ </div>
763
+ </div>
764
+ </div>`;
765
+ } else {
766
+ return `<div class="flex gap-3 items-start">
767
+ <div class="w-7 h-7 rounded-md bg-gradient-to-br from-amber to-ember flex items-center justify-center flex-shrink-0 mt-0.5 shadow-md">
768
+ <span class="text-ink text-xs font-display font-bold italic">N</span>
769
+ </div>
770
+ <div class="flex-1 min-w-0 group">
771
+ <div class="text-xs font-mono text-ghost mb-1.5">${escHtml(msg.model || 'assistant')}</div>
772
+ <div class="prose-msg text-sm leading-relaxed">${renderMarkdown(msg.content)}</div>
773
+ <div class="flex items-center gap-3 mt-2 opacity-0 group-hover:opacity-100 transition-opacity">
774
+ <button data-copy-key="${key}" class="text-fog hover:text-pearl text-xs font-mono transition-colors">copy</button>
775
+ <span class="text-fog/40 text-xs font-mono">${formatTime(msg.ts)}</span>
776
+ ${msg.usage ? `<span class="text-fog/40 text-xs font-mono">${msg.usage.completion_tokens || '?'} tokens</span>` : ''}
777
+ </div>
778
+ </div>
779
+ </div>`;
780
+ }
781
+ }
782
+
783
+ function renderMarkdown(text) {
784
+ if (!text) return '';
785
+ try {
786
+ marked.setOptions({ breaks: true, gfm: true });
787
+ return marked.parse(text);
788
+ } catch(e) {
789
+ return escHtml(text).replace(/\n/g, '<br>');
790
+ }
791
+ }
792
+
793
+ function escHtml(str) {
794
+ return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
795
+ }
796
+
797
+ function formatTime(ts) {
798
+ if (!ts) return '';
799
+ return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
800
+ }
801
+
802
+ function scrollToBottom(smooth = true) {
803
+ const c = document.getElementById('messages');
804
+ c.scrollTo({ top: c.scrollHeight, behavior: smooth ? 'smooth' : 'instant' });
805
+ }
806
+
807
+ // ── Send Message ───────────────────────────────────────────────────────────
808
+ async function sendMessage() {
809
+ const input = document.getElementById('user-input');
810
+ const text = input.value.trim();
811
+ if (!text || isStreaming) return;
812
+
813
+ const apiKey = document.getElementById('api-key').value.trim();
814
+ if (!apiKey) { showError('Please enter your OpenRouter API key in the sidebar.'); return; }
815
+
816
+ hideError();
817
+ input.value = '';
818
+ autoResize(input);
819
+
820
+ const chat = chats[currentChatId];
821
+ const model = document.getElementById('model-select').value;
822
+
823
+ // Add user message
824
+ chat.messages.push({ role: 'user', content: text, ts: Date.now() });
825
+
826
+ // Auto-title after first message
827
+ if (chat.messages.length === 1) {
828
+ chat.title = text.slice(0, 45) + (text.length > 45 ? '…' : '');
829
+ document.getElementById('chat-title').textContent = chat.title;
830
+ renderConvList();
831
+ }
832
+
833
+ saveChats();
834
+
835
+ // Directly append user bubble β€” never wipe the container mid-stream
836
+ const container = document.getElementById('messages');
837
+ document.getElementById('empty-state')?.remove();
838
+ document.getElementById('token-info').classList.remove('hidden');
839
+ const userBubble = document.createElement('div');
840
+ userBubble.className = 'flex justify-end';
841
+ userBubble.innerHTML = `
842
+ <div class="max-w-[80%]">
843
+ <div class="bg-amber text-ink rounded-2xl rounded-tr-sm px-4 py-3 text-sm shadow-md">
844
+ <div class="prose-user">${renderMarkdown(text)}</div>
845
+ </div>
846
+ </div>`;
847
+ container.appendChild(userBubble);
848
+ scrollToBottom(false);
849
+
850
+ // Add assistant placeholder
851
+ const placeholderId = 'streaming-' + genId();
852
+ const placeholder = document.createElement('div');
853
+ placeholder.id = placeholderId;
854
+ placeholder.className = 'flex gap-3 items-start';
855
+ placeholder.innerHTML = `
856
+ <div class="w-7 h-7 rounded-md bg-gradient-to-br from-amber to-ember flex items-center justify-center flex-shrink-0 mt-0.5 shadow-md">
857
+ <span class="text-ink text-xs font-display font-bold italic">N</span>
858
+ </div>
859
+ <div class="flex-1 min-w-0">
860
+ <div class="text-xs font-mono text-ghost mb-1.5">${escHtml(model.split('/').pop())}</div>
861
+ <div id="${placeholderId}-content" class="prose-msg text-sm leading-relaxed">
862
+ <span class="dot-1 inline-block">β–ͺ</span><span class="dot-2 inline-block">β–ͺ</span><span class="dot-3 inline-block">β–ͺ</span>
863
+ </div>
864
+ </div>`;
865
+ container.appendChild(placeholder);
866
+ scrollToBottom(false);
867
+
868
+ // Disable send
869
+ isStreaming = true;
870
+ setSendState(true);
871
+ abortController = new AbortController();
872
+
873
+ // Build messages array for API
874
+ const systemPrompt = document.getElementById('system-prompt').value.trim();
875
+ const apiMessages = chat.messages
876
+ .filter(m => m.role === 'user' || m.role === 'assistant')
877
+ .map(m => ({ role: m.role, content: m.content }));
878
+
879
+ const body = {
880
+ model,
881
+ messages: apiMessages,
882
+ stream: true,
883
+ temperature: parseFloat(document.getElementById('temperature').value),
884
+ max_tokens: parseInt(document.getElementById('max-tokens').value),
885
+ };
886
+ if (systemPrompt) body.messages = [{ role: 'system', content: systemPrompt }, ...body.messages];
887
+
888
+ let fullResponse = '';
889
+ let usage = null;
890
+
891
+ try {
892
+ const resp = await fetch('https://openrouter.ai/api/v1/chat/completions', {
893
+ method: 'POST',
894
+ headers: {
895
+ 'Authorization': 'Bearer ' + apiKey,
896
+ 'Content-Type': 'application/json',
897
+ 'HTTP-Referer': window.location.href,
898
+ 'X-Title': 'Nexus Chat',
899
+ },
900
+ body: JSON.stringify(body),
901
+ signal: abortController.signal,
902
+ });
903
+
904
+ if (!resp.ok) {
905
+ const err = await resp.json().catch(() => ({}));
906
+ throw new Error(err?.error?.message || `HTTP ${resp.status}`);
907
+ }
908
+
909
+ const reader = resp.body.getReader();
910
+ const decoder = new TextDecoder();
911
+ const contentEl = document.getElementById(placeholderId + '-content');
912
+ let buffer = '';
913
+
914
+ while (true) {
915
+ const { done, value } = await reader.read();
916
+ if (done) break;
917
+ buffer += decoder.decode(value, { stream: true });
918
+ const lines = buffer.split('\n');
919
+ buffer = lines.pop();
920
+
921
+ for (const line of lines) {
922
+ if (!line.startsWith('data: ')) continue;
923
+ const data = line.slice(6).trim();
924
+ if (data === '[DONE]') continue;
925
+ try {
926
+ const json = JSON.parse(data);
927
+ const delta = json.choices?.[0]?.delta?.content || '';
928
+ fullResponse += delta;
929
+ if (contentEl) contentEl.innerHTML = renderMarkdown(fullResponse) || '<span class="text-fog">…</span>';
930
+ if (json.usage) usage = json.usage;
931
+ scrollToBottom();
932
+ } catch(e) {}
933
+ }
934
+ }
935
+ } catch(err) {
936
+ if (err.name === 'AbortError') {
937
+ fullResponse = fullResponse || '[stopped]';
938
+ } else {
939
+ showError(err.message);
940
+ placeholder.remove();
941
+ isStreaming = false;
942
+ setSendState(false);
943
+ return;
944
+ }
945
+ }
946
+
947
+ // Finalize
948
+ placeholder.remove();
949
+ chat.messages.push({ role: 'assistant', content: fullResponse, model: model.split('/').pop(), ts: Date.now(), usage });
950
+ saveChats();
951
+ renderMessages();
952
+ isStreaming = false;
953
+ setSendState(false);
954
+ updateTokenCount();
955
+ }
956
+
957
+ function stopStreaming() {
958
+ if (abortController) abortController.abort();
959
+ }
960
+
961
+ function setSendState(streaming) {
962
+ const btn = document.getElementById('send-btn');
963
+ if (streaming) {
964
+ btn.innerHTML = `<svg class="w-4 h-4 text-ink" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="1" stroke-width="2"/></svg>`;
965
+ btn.title = 'Stop';
966
+ btn.onclick = stopStreaming;
967
+ btn.classList.add('bg-rose');
968
+ btn.classList.remove('bg-amber');
969
+ } else {
970
+ btn.innerHTML = `<svg class="w-4 h-4 text-ink" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>`;
971
+ btn.title = 'Send (Enter)';
972
+ btn.onclick = sendMessage;
973
+ btn.classList.remove('bg-rose');
974
+ btn.classList.add('bg-amber');
975
+ }
976
+ }
977
+
978
+ // ── Helpers ────────────────────────────────────────────────────────────────
979
+ function handleKeydown(e) {
980
+ const charCount = document.getElementById('char-count');
981
+ charCount.textContent = e.target.value.length > 0 ? e.target.value.length + ' chars' : '';
982
+ if (e.key === 'Enter' && !e.shiftKey) {
983
+ e.preventDefault();
984
+ sendMessage();
985
+ }
986
+ }
987
+
988
+ function autoResize(el) {
989
+ el.style.height = 'auto';
990
+ el.style.height = Math.min(el.scrollHeight, 192) + 'px';
991
+ }
992
+
993
+ function insertPrompt(text) {
994
+ const input = document.getElementById('user-input');
995
+ input.value = text;
996
+ autoResize(input);
997
+ input.focus();
998
+ }
999
+
1000
+ function showError(msg) {
1001
+ document.getElementById('error-text').textContent = msg;
1002
+ document.getElementById('error-banner').classList.remove('hidden');
1003
+ setTimeout(hideError, 6000);
1004
+ }
1005
+ function hideError() { document.getElementById('error-banner').classList.add('hidden'); }
1006
+
1007
+ async function copyText(text) {
1008
+ try { await navigator.clipboard.writeText(text); } catch(e) {}
1009
+ }
1010
+
1011
+ function updateTokenCount() {
1012
+ const msgs = chats[currentChatId]?.messages || [];
1013
+ const approx = msgs.reduce((sum, m) => sum + Math.ceil((m.content || '').length / 4), 0);
1014
+ document.getElementById('token-count').textContent = approx.toLocaleString();
1015
+ }
1016
+
1017
+ function exportChat() {
1018
+ const chat = chats[currentChatId];
1019
+ if (!chat || !chat.messages.length) return;
1020
+ const md = chat.messages.map(m =>
1021
+ `## ${m.role === 'user' ? 'You' : (m.model || 'Assistant')}\n\n${m.content}`
1022
+ ).join('\n\n---\n\n');
1023
+ const blob = new Blob([`# ${chat.title}\n\n${md}`], { type: 'text/markdown' });
1024
+ const a = Object.assign(document.createElement('a'), {
1025
+ href: URL.createObjectURL(blob),
1026
+ download: chat.title.replace(/[^a-z0-9]/gi, '_') + '.md',
1027
+ });
1028
+ a.click();
1029
+ URL.revokeObjectURL(a.href);
1030
+ }
1031
+
1032
+ function openSidebar() {
1033
+ const s = document.getElementById('sidebar');
1034
+ const o = document.getElementById('overlay');
1035
+ s.classList.remove('-translate-x-full');
1036
+ o.classList.remove('hidden');
1037
+ requestAnimationFrame(() => o.classList.remove('opacity-0'));
1038
+ }
1039
+
1040
+ function closeSidebar() {
1041
+ const s = document.getElementById('sidebar');
1042
+ const o = document.getElementById('overlay');
1043
+ s.classList.add('-translate-x-full');
1044
+ o.classList.add('opacity-0');
1045
+ setTimeout(() => o.classList.add('hidden'), 300);
1046
+ }
1047
+
1048
+ // ── Boot ───────────────────────────────────────────────────────────────────
1049
+ init();
1050
+ </script>
1051
+ </body>
1052
+ </html>