diamond-in commited on
Commit
64867cc
·
verified ·
1 Parent(s): 7648509

Update app/page.tsx

Browse files
Files changed (1) hide show
  1. app/page.tsx +584 -479
app/page.tsx CHANGED
@@ -1,651 +1,756 @@
1
  "use client";
2
 
3
- import { useEffect, useMemo, useRef, useState } from "react";
4
 
5
- type Role = "user" | "ai";
6
- type ChatMsg = { id: string; role: Role; text: string; ts: number };
7
- type HistoryMsg = { role: "user" | "assistant"; content: string };
 
 
 
 
 
 
8
 
9
  function uid() {
10
- return Math.random().toString(36).slice(2) + Date.now().toString(36);
11
- }
12
- function formatTime(ts: number) {
13
- const d = new Date(ts);
14
- return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
15
  }
16
 
17
- const SUGGESTIONS = [
18
- "Explain black holes like I'm 10 explaining to a friend.",
19
- "Make a 7-day study plan for Java + DSA.",
20
- "Write a scary 6-line story with a twist ending.",
21
- "Give 5 business ideas for students in India.",
22
- ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- const LS_KEY = "hf_chat_state_v4";
 
 
 
 
25
 
26
- function NewYearSplash({
27
- seconds = 5,
28
- onDone,
29
  }: {
30
- seconds?: number;
31
- onDone: () => void;
32
  }) {
33
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
34
  const rafRef = useRef<number | null>(null);
35
- const doneRef = useRef(false);
36
- const [left, setLeft] = useState(seconds);
37
-
38
- useEffect(() => {
39
- const t = setInterval(() => {
40
- setLeft((v) => {
41
- const nv = v - 1;
42
- if (nv <= 0) {
43
- clearInterval(t);
44
- if (!doneRef.current) {
45
- doneRef.current = true;
46
- onDone();
47
- }
48
- }
49
- return nv;
50
- });
51
- }, 1000);
52
- return () => clearInterval(t);
53
- }, [onDone]);
54
 
55
  useEffect(() => {
56
- const c = canvasRef.current!;
57
- const ctx = c.getContext("2d")!;
58
- let w = 0, h = 0;
59
 
60
- const DPR = Math.min(2, window.devicePixelRatio || 1);
 
 
61
 
62
  function resize() {
63
- w = window.innerWidth;
64
- h = window.innerHeight;
65
- c.width = Math.floor(w * DPR);
66
- c.height = Math.floor(h * DPR);
67
- c.style.width = `${w}px`;
68
- c.style.height = `${h}px`;
69
  ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
70
  }
71
 
72
  resize();
73
  window.addEventListener("resize", resize);
74
 
75
- type P = { x:number; y:number; vx:number; vy:number; life:number; col:string; s:number };
76
- const parts: P[] = [];
77
-
78
- function randCol() {
79
- const cols = ["#ffcc00", "#ffe985", "#4cff79", "#66a3ff", "#ff4d4d", "#d47cff"];
80
- return cols[Math.floor(Math.random() * cols.length)];
81
- }
82
-
83
- function burst(x:number, y:number) {
84
- const col = randCol();
85
- const n = 40 + Math.floor(Math.random() * 18);
86
- for (let i=0;i<n;i++){
87
- const a = (Math.PI * 2 * i) / n + (Math.random() * 0.25 - 0.125);
88
- const sp = 2.0 + Math.random() * 3.6;
89
- parts.push({
90
- x, y,
91
- vx: Math.cos(a) * sp,
92
- vy: Math.sin(a) * sp - (1.4 + Math.random()),
93
- life: 44 + Math.random() * 34,
94
- col,
95
- s: 3
96
  });
97
  }
98
  }
99
 
100
- let tickN = 0;
101
-
102
- function tick() {
103
- rafRef.current = requestAnimationFrame(tick);
104
- tickN++;
105
 
106
- // fade (keeps trails). This is intentional for the splash.
107
- ctx.fillStyle = "rgba(0,0,0,0.18)";
108
  ctx.fillRect(0, 0, w, h);
109
 
110
- // sparkles drizzle
111
- if (tickN % 2 === 0) {
112
- parts.push({
113
- x: Math.random() * w,
114
- y: Math.random() * h,
115
- vx: (Math.random() - 0.5) * 0.5,
116
- vy: (Math.random() - 0.5) * 0.5,
117
- life: 16 + Math.random() * 10,
118
- col: Math.random() < 0.65 ? "#4cff79" : "#ffe985",
119
- s: 2
120
- });
121
- }
122
-
123
- // fireworks bursts
124
- if (tickN % 28 === 0) {
125
- burst(80 + Math.random() * (w - 160), 80 + Math.random() * (h * 0.35));
126
- }
127
-
128
- // update + draw pixels
129
- for (let i = parts.length - 1; i >= 0; i--) {
130
- const p = parts[i];
131
- p.vy += 0.07;
132
- p.x += p.vx;
133
- p.y += p.vy;
134
- p.life -= 1;
135
-
136
- if (p.life <= 0 || p.x < -80 || p.x > w + 80 || p.y > h + 120) {
137
- parts.splice(i, 1);
138
  continue;
139
  }
140
 
141
- ctx.fillStyle = p.col;
142
- ctx.fillRect(Math.round(p.x), Math.round(p.y), p.s, p.s);
 
143
  }
144
 
145
- if (parts.length > 1600) parts.splice(0, parts.length - 1600);
146
  }
147
 
148
- ctx.clearRect(0, 0, w, h); // clears to transparent black :contentReference[oaicite:2]{index=2}
149
- rafRef.current = requestAnimationFrame(tick);
 
 
150
 
151
  return () => {
152
- if (rafRef.current) cancelAnimationFrame(rafRef.current);
153
  window.removeEventListener("resize", resize);
 
 
 
154
  };
155
- }, []);
156
-
157
- function skip() {
158
- if (doneRef.current) return;
159
- doneRef.current = true;
160
- onDone();
161
- }
162
 
163
- const year = new Date().getFullYear();
164
 
165
  return (
166
- <div className="splash" onClick={skip} role="button" aria-label="Skip splash">
167
- <canvas className="splashCanvas" ref={canvasRef} />
168
- <div className="splashPanelWrap">
169
- <div className="splashPanel">
170
- <div className="splashTitle">
171
- 🎆 HAPPY NEW YEAR 🎆
172
- <span className="splashYear">{year}</span>
173
- </div>
174
-
175
- <div className="splashSub">
176
- Pixel fireworks • Sparkles • Loading AI Chat…
177
- </div>
178
-
179
- <div className="splashHint">
180
- Tap anywhere to skip • {Math.max(0, left)}s
181
- </div>
182
- </div>
183
  </div>
184
  </div>
185
  );
186
  }
187
 
188
- export default function Home() {
189
- const [showSplash, setShowSplash] = useState(true);
190
-
191
  const [input, setInput] = useState("");
192
- const [loading, setLoading] = useState(false);
 
193
 
194
- const [drawerOpen, setDrawerOpen] = useState(false);
195
- const [toast, setToast] = useState<string | null>(null);
196
- const [showTimestamps, setShowTimestamps] = useState(true);
197
- const [autoSave, setAutoSave] = useState(true);
198
-
199
- // Settings (model dropdown is inside drawer)
200
- const [model, setModel] = useState("Qwen/Qwen2.5-7B-Instruct:together");
201
  const [temperature, setTemperature] = useState(0.7);
202
- const [maxTokens, setMaxTokens] = useState(256);
203
- const [system, setSystem] = useState("You are a helpful AI assistant.");
 
 
 
204
 
205
- const [messages, setMessages] = useState<ChatMsg[]>([
206
- { id: uid(), role: "ai", text: "", ts: Date.now() },
207
- ]);
208
 
209
- const chatRef = useRef<HTMLDivElement | null>(null);
210
  const abortRef = useRef<AbortController | null>(null);
211
- const streamingRef = useRef(false);
212
- const [showScrollDown, setShowScrollDown] = useState(false);
213
 
214
- const hasUserMessage = useMemo(() => messages.some((m) => m.role === "user"), [messages]);
215
- const showOverlay = !hasUserMessage;
 
 
 
 
 
 
 
 
 
 
 
216
 
217
- // Load saved
218
  useEffect(() => {
219
  try {
220
- const raw = localStorage.getItem(LS_KEY);
221
  if (!raw) return;
222
- const p = JSON.parse(raw);
223
- if (p?.messages?.length) setMessages(p.messages);
224
- if (p?.model) setModel(p.model);
225
- if (typeof p?.temperature === "number") setTemperature(p.temperature);
226
- if (typeof p?.maxTokens === "number") setMaxTokens(p.maxTokens);
227
- if (typeof p?.system === "string") setSystem(p.system);
228
- if (typeof p?.showTimestamps === "boolean") setShowTimestamps(p.showTimestamps);
229
- if (typeof p?.autoSave === "boolean") setAutoSave(p.autoSave);
230
- } catch {}
 
231
  }, []);
232
 
233
- // Save
234
  useEffect(() => {
235
- if (!autoSave) return;
236
  try {
237
  localStorage.setItem(
238
- LS_KEY,
239
- JSON.stringify({ messages, model, temperature, maxTokens, system, showTimestamps, autoSave })
 
 
 
 
 
 
 
240
  );
241
- } catch {}
242
- }, [messages, model, temperature, maxTokens, system, showTimestamps, autoSave]);
 
 
243
 
244
  // Auto-scroll
245
  useEffect(() => {
246
- const el = chatRef.current;
247
  if (!el) return;
248
  el.scrollTop = el.scrollHeight;
249
- }, [messages, loading]);
250
 
251
- // Scroll-down button
252
  useEffect(() => {
253
- const el = chatRef.current;
254
- if (!el) return;
255
- const onScroll = () => {
256
- const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 140;
257
- setShowScrollDown(!nearBottom);
258
- };
259
- el.addEventListener("scroll", onScroll);
260
- onScroll();
261
- return () => el.removeEventListener("scroll", onScroll);
262
- }, []);
263
 
264
- const canSend = useMemo(() => input.trim().length > 0 && !loading, [input, loading]);
 
 
 
 
 
 
265
 
266
- function showToast(text: string) {
267
- setToast(text);
268
- setTimeout(() => setToast(null), 900);
269
- }
 
 
270
 
271
- function copyText(text: string) {
272
- navigator.clipboard?.writeText(text);
273
- showToast("Copied!");
274
- }
275
 
276
- function stopGeneration() {
277
- try {
278
- abortRef.current?.abort();
279
- abortRef.current = null;
280
- streamingRef.current = false;
281
- setLoading(false);
282
- showToast("Stopped");
283
- } catch {}
284
  }
285
 
286
- function clearChat() {
287
- stopGeneration();
288
- setMessages([{ id: uid(), role: "ai", text: "", ts: Date.now() }]);
289
- showToast("Cleared");
290
- setShowSplash(true);
291
  }
292
 
293
- function scrollToBottom() {
294
- const el = chatRef.current;
295
- if (!el) return;
296
- el.scrollTop = el.scrollHeight;
 
 
 
297
  }
298
 
299
  function exportChat() {
300
- const lines = messages
301
- .filter((m) => m.text.trim().length > 0)
302
- .map((m) => `${m.role === "user" ? "You" : "AI"}: ${m.text}`);
303
- const blob = new Blob([lines.join("\n\n")], { type: "text/plain" });
 
 
 
 
 
 
304
  const url = URL.createObjectURL(blob);
305
  const a = document.createElement("a");
306
  a.href = url;
307
- a.download = "chat.txt";
308
  a.click();
309
  URL.revokeObjectURL(url);
310
  }
311
 
312
- function buildHistoryForAPI(): HistoryMsg[] {
313
- return messages
314
- .slice(-12)
315
- .filter((m) => m.text.trim().length > 0)
316
- .map((m) => ({ role: m.role === "user" ? "user" : "assistant", content: m.text }));
317
- }
318
 
319
- function lastUserMessage(): string | null {
320
- for (let i = messages.length - 1; i >= 0; i--) {
321
- if (messages[i].role === "user") return messages[i].text;
 
 
322
  }
323
- return null;
324
- }
325
-
326
- async function streamChat(userText: string, mode: "normal" | "regen") {
327
- if (streamingRef.current) return;
328
- streamingRef.current = true;
329
-
330
- const controller = new AbortController();
331
- abortRef.current = controller;
332
-
333
- if (mode === "regen") {
334
- setMessages((m) => {
335
- const copy = [...m];
336
- for (let i = copy.length - 1; i >= 0; i--) {
337
- if (copy[i].role === "ai") { copy.splice(i, 1); break; }
338
- }
339
- return copy;
340
- });
341
  }
342
 
343
- const aiId = uid();
344
- setMessages((m) => [...m, { id: aiId, role: "ai", text: "", ts: Date.now() }]);
345
- setLoading(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
 
347
  try {
 
 
348
  const res = await fetch("/api/chat", {
349
  method: "POST",
350
  headers: { "Content-Type": "application/json" },
351
- signal: controller.signal,
352
  body: JSON.stringify({
353
- message: userText,
354
- history: buildHistoryForAPI(),
355
- model,
356
  temperature,
357
  max_tokens: maxTokens,
358
- system,
359
- stream: true,
360
  }),
361
  });
362
 
363
  if (!res.ok) {
364
- const err = await res.json().catch(() => ({}));
365
- const msg = err?.error || "Request failed";
366
- setMessages((m) => m.map((x) => (x.id === aiId ? { ...x, text: `⚠️ ${msg}` } : x)));
367
- return;
368
  }
369
 
370
- const reader = res.body?.getReader();
371
- if (!reader) {
372
- setMessages((m) => m.map((x) => (x.id === aiId ? { ...x, text: "⚠️ No stream body" } : x)));
 
 
 
 
 
 
 
 
373
  return;
374
  }
375
 
376
- const decoder = new TextDecoder();
377
- let buf = "";
378
- let acc = "";
379
-
380
- while (true) {
381
- const { value, done } = await reader.read();
382
- if (done) break;
383
-
384
- buf += decoder.decode(value, { stream: true });
385
- const parts = buf.split("\n\n");
386
- buf = parts.pop() || "";
387
-
388
- for (const part of parts) {
389
- for (const line of part.split("\n")) {
390
- const t = line.trim();
391
- if (!t.startsWith("data:")) continue;
392
-
393
- const payload = t.slice(5).trim();
394
- if (payload === "[DONE]") break;
395
-
396
- try {
397
- const obj = JSON.parse(payload);
398
- const delta = obj?.choices?.[0]?.delta?.content ?? "";
399
- if (delta) {
400
- acc += delta;
401
- setMessages((m) => m.map((x) => (x.id === aiId ? { ...x, text: acc } : x)));
402
- }
403
- } catch {}
404
- }
405
- }
406
- }
407
 
408
- if (!acc) {
409
- setMessages((m) => m.map((x) => (x.id === aiId ? { ...x, text: "⚠️ Empty response" } : x)));
410
- }
411
  } catch (e: any) {
412
- const msg = controller.signal.aborted ? "Stopped." : "Network error.";
413
- setMessages((m) => m.map((x) => (x.id === aiId ? { ...x, text: `⚠️ ${msg}` } : x)));
414
- } finally {
415
- streamingRef.current = false;
 
 
 
416
  abortRef.current = null;
417
- setLoading(false);
 
 
 
 
 
 
 
 
 
 
 
 
418
  }
419
  }
420
 
421
- async function sendText(text: string) {
422
- if (loading) return;
423
- const t = text.trim();
424
- if (!t) return;
425
-
426
- if (t === "/clear") { clearChat(); return; }
427
-
428
- setMessages((m) => [...m, { id: uid(), role: "user", text: t, ts: Date.now() }]);
429
  setInput("");
430
- await streamChat(t, "normal");
431
  }
432
 
433
- async function sendMessage() {
434
- await sendText(input);
 
 
 
 
435
  }
436
 
437
- async function regenerate() {
438
- const last = lastUserMessage();
439
- if (!last || loading) return;
440
- await streamChat(last, "regen");
441
- }
442
-
443
- function onKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
444
- if (e.key === "Enter" && !e.shiftKey) {
445
- e.preventDefault();
446
- if (canSend) sendMessage();
447
- }
448
- if (e.key === "Escape") stopGeneration();
449
- }
450
 
451
  return (
452
- <>
453
- <div className="bgfx" />
454
-
455
- {showSplash && (
456
- <NewYearSplash seconds={5} onDone={() => setShowSplash(false)} />
457
- )}
458
-
459
- <div className="shell">
460
- <div className="card">
461
- <div className="header">
462
- <div className="brand">
463
- <div className="logo">🤖</div>
464
- <div className="brandText">
465
- <b>AI Chat</b>
466
- <span>{loading ? "Generating…" : "Ready"}</span>
 
467
  </div>
468
  </div>
469
 
470
- <div className="headerRight">
471
- <div className="pill">
472
- <span className="dot" />
473
- <span>{loading ? "Thinking…" : "Online"}</span>
474
- </div>
475
- <button className="btnGhost" onClick={() => setDrawerOpen(true)} title="Settings">
 
476
 
477
  </button>
478
  </div>
479
  </div>
480
 
481
- <div className="chat" ref={chatRef}>
482
- {showOverlay && (
483
- <div className="overlayWrap">
484
- <div className="overlayBox">
485
- <div className="overlayTop">
486
- <div className="logo">🧱</div>
487
- <div className="overlayTitle">
488
- <b>AI Chat</b>
489
- <span>Sharp Streaming Qwen</span>
 
490
  </div>
491
- </div>
492
 
493
- <div style={{ color: "rgba(255,255,255,.78)", fontSize: 9, marginTop: 10 }}>
494
- Tap a prompt to start (they disappear after you chat).
 
 
 
 
 
 
 
 
 
 
 
 
495
  </div>
 
 
496
 
497
- <div className="chips">
498
- {SUGGESTIONS.map((s) => (
499
- <button key={s} className="chip" onClick={() => sendText(s)}>
500
- {s}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  </button>
502
- ))}
503
  </div>
504
- </div>
505
- </div>
506
- )}
507
-
508
- {messages
509
- .filter((m) => m.text.trim().length > 0)
510
- .map((m) => (
511
- <div key={m.id} className={`row ${m.role === "user" ? "user" : "ai"}`}>
512
- <div className={`bubble ${m.role === "user" ? "user" : ""}`}>
513
- {m.text}
514
- <div className="meta">
515
- <span>
516
- {m.role === "user" ? "You" : "AI"}
517
- {showTimestamps ? ` • ${formatTime(m.ts)}` : ""}
518
- </span>
519
- <span className="actions">
520
- <button className="iconBtn" onClick={() => copyText(m.text)}>COPY</button>
521
- </span>
522
- </div>
523
- </div>
524
- </div>
525
- ))}
526
 
527
- {loading && (
528
- <div className="row ai">
529
- <div className="bubble">
530
- <span className="dots">
531
- <i /> <i /> <i />
532
- </span>
533
- <div className="meta">
534
- <span>AI {showTimestamps ? ` • ${formatTime(Date.now())}` : ""}</span>
535
- <span />
536
  </div>
537
  </div>
538
- </div>
539
- )}
540
-
541
- {showScrollDown && (
542
- <div className="scrollDown">
543
- <button className="btnGhost" onClick={scrollToBottom}>↓</button>
544
- </div>
545
- )}
546
- </div>
547
 
548
- <div className="composer">
549
- <div className="composerInner">
550
  <textarea
551
- className="textarea"
 
552
  value={input}
 
 
553
  onChange={(e) => setInput(e.target.value)}
554
- onKeyDown={onKeyDown}
555
- placeholder="Type… (Enter=send, Shift+Enter=new line, Esc=stop) • /clear"
 
 
 
 
556
  />
557
-
558
- {!loading ? (
559
- <button className="btn btnGold" onClick={sendMessage} disabled={!canSend}>
560
- SEND
561
  </button>
562
- ) : (
563
- <button className="btn btnDanger" onClick={stopGeneration}>
564
- STOP
565
  </button>
566
- )}
567
-
568
- <button className="btn" onClick={regenerate} disabled={loading}>
569
- REGEN
570
- </button>
571
- </div>
572
-
573
- <div className="hint">
574
- <span>Tip: Esc stops streaming • /clear clears chat</span>
575
- <span>{input.trim().length}/2000</span>
 
 
 
 
 
 
576
  </div>
577
  </div>
578
  </div>
579
  </div>
580
 
581
- {toast && <div className="toast">{toast}</div>}
582
-
583
- {drawerOpen && (
584
- <div className="drawerOverlay" onClick={() => setDrawerOpen(false)}>
585
- <div className="drawer" onClick={(e) => e.stopPropagation()}>
586
- <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 10 }}>
587
- <h3>SETTINGS</h3>
588
- <button className="btnGhost" onClick={() => setDrawerOpen(false)}>CLOSE</button>
 
589
  </div>
590
 
591
- <div className="field">
592
- <div className="labelRow">
593
- <span>MODEL</span>
594
- <span>AI ENGINE</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  </div>
596
- <select className="select" value={model} onChange={(e) => setModel(e.target.value)}>
597
- <option value="Qwen/Qwen2.5-7B-Instruct:together">QWEN 2.5 7B</option>
598
- <option value="Qwen/Qwen2.5-14B-Instruct:together">QWEN 2.5 14B</option>
599
- </select>
600
- </div>
601
 
602
- <div className="field">
603
- <div className="labelRow">
604
- <span>SYSTEM</span>
605
- <span>{system.length}/400</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
  </div>
607
- <textarea className="input" value={system} onChange={(e) => setSystem(e.target.value.slice(0, 400))} />
608
- </div>
609
 
610
- <div className="field">
611
- <div className="labelRow">
612
- <span>TEMP</span>
613
- <span>{temperature.toFixed(2)}</span>
 
 
 
 
 
 
 
 
 
614
  </div>
615
- <input
616
- className="range"
617
- type="range"
618
- min={0}
619
- max={2}
620
- step={0.05}
621
- value={temperature}
622
- onChange={(e) => setTemperature(Number(e.target.value))}
623
- />
624
- </div>
625
 
626
- <div className="field">
627
- <div className="labelRow">
628
- <span>MAX TOKENS</span>
629
- <span>{maxTokens}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
  </div>
631
- <input
632
- className="num"
633
- type="number"
634
- min={64}
635
- max={1024}
636
- step={32}
637
- value={maxTokens}
638
- onChange={(e) => setMaxTokens(Number(e.target.value))}
639
- />
640
  </div>
641
 
642
- <div className="field" style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
643
- <button className="btn" onClick={exportChat}>EXPORT</button>
644
- <button className="btn btnDanger" onClick={clearChat}>CLEAR</button>
645
  </div>
646
  </div>
647
  </div>
648
  )}
649
- </>
650
  );
651
  }
 
1
  "use client";
2
 
3
+ import React, { useEffect, useMemo, useRef, useState } from "react";
4
 
5
+ type Role = "system" | "user" | "assistant";
6
+ type ChatMsg = { id: string; role: Role; content: string; ts: number };
7
+
8
+ const DEFAULT_MODELS = [
9
+ { id: "Qwen/Qwen2.5-7B-Instruct", label: "Qwen 2.5 7B (Instruct)" },
10
+ { id: "Qwen/Qwen2.5-Coder-32B-Instruct", label: "Qwen 2.5 Coder 32B" },
11
+ { id: "google/gemma-2-2b-it", label: "Gemma 2 2B IT" },
12
+ { id: "meta-llama/Llama-3.1-8B-Instruct", label: "Llama 3.1 8B Instruct" },
13
+ ];
14
 
15
  function uid() {
16
+ return Math.random().toString(16).slice(2) + "-" + Date.now().toString(16);
 
 
 
 
17
  }
18
 
19
+ /** Minimal SSE parser for OpenAI-style streaming */
20
+ async function readSSE(
21
+ res: Response,
22
+ onDelta: (text: string) => void,
23
+ signal?: AbortSignal
24
+ ) {
25
+ if (!res.body) throw new Error("No response body to stream.");
26
+ const reader = res.body.getReader();
27
+ const decoder = new TextDecoder("utf-8");
28
+
29
+ let buf = "";
30
+ while (true) {
31
+ if (signal?.aborted) throw new Error("aborted");
32
+ const { value, done } = await reader.read();
33
+ if (done) break;
34
+ buf += decoder.decode(value, { stream: true });
35
+
36
+ // SSE events are separated by \n\n
37
+ let idx: number;
38
+ while ((idx = buf.indexOf("\n\n")) !== -1) {
39
+ const rawEvent = buf.slice(0, idx);
40
+ buf = buf.slice(idx + 2);
41
+
42
+ const lines = rawEvent.split("\n");
43
+ for (const line of lines) {
44
+ const trimmed = line.trim();
45
+ if (!trimmed.startsWith("data:")) continue;
46
+
47
+ const data = trimmed.slice(5).trim();
48
+ if (!data) continue;
49
+ if (data === "[DONE]") return;
50
+
51
+ let json: any;
52
+ try {
53
+ json = JSON.parse(data);
54
+ } catch {
55
+ continue;
56
+ }
57
+
58
+ // OpenAI-style: choices[0].delta.content
59
+ const delta =
60
+ json?.choices?.[0]?.delta?.content ??
61
+ json?.choices?.[0]?.message?.content ??
62
+ "";
63
 
64
+ if (typeof delta === "string" && delta.length) onDelta(delta);
65
+ }
66
+ }
67
+ }
68
+ }
69
 
70
+ function PixelFireworks({
71
+ show,
72
+ onSkip,
73
  }: {
74
+ show: boolean;
75
+ onSkip: () => void;
76
  }) {
77
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
78
  const rafRef = useRef<number | null>(null);
79
+ const particlesRef = useRef<
80
+ { x: number; y: number; vx: number; vy: number; life: number; c: string }[]
81
+ >([]);
82
+
83
+ const year = useMemo(() => {
84
+ const d = new Date();
85
+ // If it's December, show next year; otherwise current year
86
+ return d.getMonth() === 11 ? d.getFullYear() + 1 : d.getFullYear();
87
+ }, []);
 
 
 
 
 
 
 
 
 
 
88
 
89
  useEffect(() => {
90
+ if (!show) return;
 
 
91
 
92
+ const canvas = canvasRef.current!;
93
+ const ctx = canvas.getContext("2d", { alpha: true })!;
94
+ const DPR = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
95
 
96
  function resize() {
97
+ const w = window.innerWidth;
98
+ const h = window.innerHeight;
99
+ canvas.width = Math.floor(w * DPR);
100
+ canvas.height = Math.floor(h * DPR);
101
+ canvas.style.width = `${w}px`;
102
+ canvas.style.height = `${h}px`;
103
  ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
104
  }
105
 
106
  resize();
107
  window.addEventListener("resize", resize);
108
 
109
+ const colors = ["#ffd300", "#00ff6a", "#57a7ff", "#ff5a5a", "#c35bff"];
110
+
111
+ function burst() {
112
+ const w = window.innerWidth;
113
+ const h = window.innerHeight;
114
+ const x = w * (0.2 + Math.random() * 0.6);
115
+ const y = h * (0.2 + Math.random() * 0.45);
116
+ const c = colors[(Math.random() * colors.length) | 0];
117
+
118
+ const n = 70;
119
+ for (let i = 0; i < n; i++) {
120
+ const a = (Math.PI * 2 * i) / n;
121
+ const s = 1.2 + Math.random() * 2.8;
122
+ particlesRef.current.push({
123
+ x,
124
+ y,
125
+ vx: Math.cos(a) * s,
126
+ vy: Math.sin(a) * s,
127
+ life: 60 + (Math.random() * 35) | 0,
128
+ c,
 
129
  });
130
  }
131
  }
132
 
133
+ let t = 0;
134
+ function frame() {
135
+ const w = window.innerWidth;
136
+ const h = window.innerHeight;
 
137
 
138
+ // fade
139
+ ctx.fillStyle = "rgba(0,0,0,0.22)";
140
  ctx.fillRect(0, 0, w, h);
141
 
142
+ // spawn bursts
143
+ t++;
144
+ if (t % 18 === 0) burst();
145
+
146
+ // draw particles as pixel blocks
147
+ const p = particlesRef.current;
148
+ for (let i = p.length - 1; i >= 0; i--) {
149
+ const q = p[i];
150
+ q.x += q.vx;
151
+ q.y += q.vy;
152
+ q.vy += 0.03; // gravity
153
+ q.life -= 1;
154
+
155
+ if (q.life <= 0 || q.x < -50 || q.y < -50 || q.x > w + 50 || q.y > h + 50) {
156
+ p.splice(i, 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  continue;
158
  }
159
 
160
+ const size = q.life > 40 ? 3 : 2;
161
+ ctx.fillStyle = q.c;
162
+ ctx.fillRect((q.x | 0), (q.y | 0), size, size);
163
  }
164
 
165
+ rafRef.current = requestAnimationFrame(frame);
166
  }
167
 
168
+ // initial clear
169
+ ctx.fillStyle = "black";
170
+ ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
171
+ rafRef.current = requestAnimationFrame(frame);
172
 
173
  return () => {
 
174
  window.removeEventListener("resize", resize);
175
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
176
+ rafRef.current = null;
177
+ particlesRef.current = [];
178
  };
179
+ }, [show]);
 
 
 
 
 
 
180
 
181
+ if (!show) return null;
182
 
183
  return (
184
+ <div className="nyOverlay" role="dialog" aria-modal="true">
185
+ <canvas className="nyCanvas" ref={canvasRef} />
186
+ <div className="nyPanel">
187
+ <div className="nyTitle">HAPPY NEW YEAR</div>
188
+ <div className="nyYear">{year}</div>
189
+ <div className="nySub">Pixel fireworks Loading chat…</div>
190
+ <button className="btn pixelBtn" onClick={onSkip}>
191
+ SKIP
192
+ </button>
 
 
 
 
 
 
 
 
193
  </div>
194
  </div>
195
  );
196
  }
197
 
198
+ export default function Page() {
199
+ const [messages, setMessages] = useState<ChatMsg[]>([]);
 
200
  const [input, setInput] = useState("");
201
+ const [streaming, setStreaming] = useState(false);
202
+ const [error, setError] = useState<string | null>(null);
203
 
204
+ const [settingsOpen, setSettingsOpen] = useState(false);
205
+ const [model, setModel] = useState(DEFAULT_MODELS[0].id);
206
+ const [customModel, setCustomModel] = useState("");
 
 
 
 
207
  const [temperature, setTemperature] = useState(0.7);
208
+ const [maxTokens, setMaxTokens] = useState(512);
209
+ const [systemPrompt, setSystemPrompt] = useState(
210
+ "You are a helpful assistant. Keep answers clear and practical."
211
+ );
212
+ const [useStreaming, setUseStreaming] = useState(true);
213
 
214
+ const [showPrompts, setShowPrompts] = useState(true);
215
+ const [showIntro, setShowIntro] = useState(false);
 
216
 
 
217
  const abortRef = useRef<AbortController | null>(null);
218
+ const listRef = useRef<HTMLDivElement | null>(null);
219
+ const inputRef = useRef<HTMLTextAreaElement | null>(null);
220
 
221
+ // Intro once per session
222
+ useEffect(() => {
223
+ try {
224
+ const seen = sessionStorage.getItem("ny_seen");
225
+ if (seen === "1") return;
226
+ sessionStorage.setItem("ny_seen", "1");
227
+ setShowIntro(true);
228
+ const t = window.setTimeout(() => setShowIntro(false), 5000);
229
+ return () => window.clearTimeout(t);
230
+ } catch {
231
+ // no-op
232
+ }
233
+ }, []);
234
 
235
+ // Load persisted settings
236
  useEffect(() => {
237
  try {
238
+ const raw = localStorage.getItem("mc_chat_settings");
239
  if (!raw) return;
240
+ const s = JSON.parse(raw);
241
+ if (typeof s.model === "string") setModel(s.model);
242
+ if (typeof s.customModel === "string") setCustomModel(s.customModel);
243
+ if (typeof s.temperature === "number") setTemperature(s.temperature);
244
+ if (typeof s.maxTokens === "number") setMaxTokens(s.maxTokens);
245
+ if (typeof s.systemPrompt === "string") setSystemPrompt(s.systemPrompt);
246
+ if (typeof s.useStreaming === "boolean") setUseStreaming(s.useStreaming);
247
+ } catch {
248
+ // ignore
249
+ }
250
  }, []);
251
 
252
+ // Persist settings
253
  useEffect(() => {
 
254
  try {
255
  localStorage.setItem(
256
+ "mc_chat_settings",
257
+ JSON.stringify({
258
+ model,
259
+ customModel,
260
+ temperature,
261
+ maxTokens,
262
+ systemPrompt,
263
+ useStreaming,
264
+ })
265
  );
266
+ } catch {
267
+ // ignore
268
+ }
269
+ }, [model, customModel, temperature, maxTokens, systemPrompt, useStreaming]);
270
 
271
  // Auto-scroll
272
  useEffect(() => {
273
+ const el = listRef.current;
274
  if (!el) return;
275
  el.scrollTop = el.scrollHeight;
276
+ }, [messages, streaming]);
277
 
278
+ // Hide prompt overlay after first user message
279
  useEffect(() => {
280
+ if (messages.some((m) => m.role === "user")) setShowPrompts(false);
281
+ }, [messages]);
 
 
 
 
 
 
 
 
282
 
283
+ // Keyboard shortcuts
284
+ useEffect(() => {
285
+ function onKeyDown(e: KeyboardEvent) {
286
+ if (e.key === "Escape") {
287
+ if (streaming) stopStreaming();
288
+ return;
289
+ }
290
 
291
+ // Ctrl+L -> clear
292
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "l") {
293
+ e.preventDefault();
294
+ doClear();
295
+ }
296
+ }
297
 
298
+ window.addEventListener("keydown", onKeyDown);
299
+ return () => window.removeEventListener("keydown", onKeyDown);
300
+ }, [streaming]);
 
301
 
302
+ function activeModel() {
303
+ return (customModel.trim() || model).trim();
 
 
 
 
 
 
304
  }
305
 
306
+ function stopStreaming() {
307
+ abortRef.current?.abort();
308
+ abortRef.current = null;
309
+ setStreaming(false);
 
310
  }
311
 
312
+ function doClear() {
313
+ stopStreaming();
314
+ setMessages([]);
315
+ setShowPrompts(true);
316
+ setError(null);
317
+ setInput("");
318
+ inputRef.current?.focus();
319
  }
320
 
321
  function exportChat() {
322
+ const payload = {
323
+ model: activeModel(),
324
+ temperature,
325
+ max_tokens: maxTokens,
326
+ created_at: new Date().toISOString(),
327
+ messages,
328
+ };
329
+ const blob = new Blob([JSON.stringify(payload, null, 2)], {
330
+ type: "application/json",
331
+ });
332
  const url = URL.createObjectURL(blob);
333
  const a = document.createElement("a");
334
  a.href = url;
335
+ a.download = `chat-${Date.now()}.json`;
336
  a.click();
337
  URL.revokeObjectURL(url);
338
  }
339
 
340
+ async function runChat(userText: string, replaceLastUser?: boolean) {
341
+ setError(null);
 
 
 
 
342
 
343
+ // Slash commands
344
+ const t = userText.trim();
345
+ if (t === "/clear") {
346
+ doClear();
347
+ return;
348
  }
349
+ if (t === "/export") {
350
+ exportChat();
351
+ return;
352
+ }
353
+ if (t === "/help") {
354
+ setMessages((prev) => [
355
+ ...prev,
356
+ {
357
+ id: uid(),
358
+ role: "assistant",
359
+ ts: Date.now(),
360
+ content:
361
+ "Commands:\n/clear clear chat\n/export — download JSON\n/help — show this\n\nShortcuts:\nEnter = send • Shift+Enter = new line • Esc = stop • Ctrl+L = clear",
362
+ },
363
+ ]);
364
+ return;
 
 
365
  }
366
 
367
+ const now = Date.now();
368
+ const userMsg: ChatMsg = { id: uid(), role: "user", ts: now, content: userText };
369
+
370
+ setMessages((prev) => {
371
+ if (replaceLastUser) {
372
+ const copy = [...prev];
373
+ // remove trailing assistant message if present
374
+ if (copy.length && copy[copy.length - 1].role === "assistant") copy.pop();
375
+ // remove trailing user message if present
376
+ if (copy.length && copy[copy.length - 1].role === "user") copy.pop();
377
+ return [...copy, userMsg, { id: uid(), role: "assistant", ts: now, content: "" }];
378
+ }
379
+ return [...prev, userMsg, { id: uid(), role: "assistant", ts: now, content: "" }];
380
+ });
381
+
382
+ const ac = new AbortController();
383
+ abortRef.current = ac;
384
+
385
+ const packedMessages = [
386
+ { role: "system", content: systemPrompt },
387
+ // include the entire chat so far (excluding the empty assistant placeholder)
388
+ ...(() => {
389
+ const base = replaceLastUser
390
+ ? (() => {
391
+ // after state update, easiest is to build from current messages but remove last assistant/user as needed
392
+ const copy = [...messages];
393
+ if (copy.length && copy[copy.length - 1].role === "assistant") copy.pop();
394
+ if (copy.length && copy[copy.length - 1].role === "user") copy.pop();
395
+ return [...copy, userMsg];
396
+ })()
397
+ : [...messages, userMsg];
398
+
399
+ return base
400
+ .filter((m) => m.role === "user" || m.role === "assistant")
401
+ .map((m) => ({ role: m.role, content: m.content }));
402
+ })(),
403
+ ];
404
 
405
  try {
406
+ setStreaming(true);
407
+
408
  const res = await fetch("/api/chat", {
409
  method: "POST",
410
  headers: { "Content-Type": "application/json" },
411
+ signal: ac.signal,
412
  body: JSON.stringify({
413
+ model: activeModel(),
414
+ messages: packedMessages,
 
415
  temperature,
416
  max_tokens: maxTokens,
417
+ stream: useStreaming,
 
418
  }),
419
  });
420
 
421
  if (!res.ok) {
422
+ const text = await res.text();
423
+ throw new Error(text || `HTTP ${res.status}`);
 
 
424
  }
425
 
426
+ if (!useStreaming) {
427
+ const json = await res.json();
428
+ const out = json?.choices?.[0]?.message?.content ?? "";
429
+ setMessages((prev) => {
430
+ const copy = [...prev];
431
+ const last = copy[copy.length - 1];
432
+ if (last?.role === "assistant") last.content = String(out);
433
+ return copy;
434
+ });
435
+ setStreaming(false);
436
+ abortRef.current = null;
437
  return;
438
  }
439
 
440
+ await readSSE(
441
+ res,
442
+ (delta) => {
443
+ setMessages((prev) => {
444
+ const copy = [...prev];
445
+ const last = copy[copy.length - 1];
446
+ if (last?.role === "assistant") last.content += delta;
447
+ return copy;
448
+ });
449
+ },
450
+ ac.signal
451
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
 
453
+ setStreaming(false);
454
+ abortRef.current = null;
 
455
  } catch (e: any) {
456
+ const msg = String(e?.message ?? e ?? "Unknown error");
457
+ if (msg === "aborted") {
458
+ setStreaming(false);
459
+ abortRef.current = null;
460
+ return;
461
+ }
462
+ setStreaming(false);
463
  abortRef.current = null;
464
+ setError(msg);
465
+
466
+ // put error in assistant bubble
467
+ setMessages((prev) => {
468
+ const copy = [...prev];
469
+ const last = copy[copy.length - 1];
470
+ if (last?.role === "assistant" && !last.content.trim()) {
471
+ last.content = `⚠️ ${msg}`;
472
+ } else {
473
+ copy.push({ id: uid(), role: "assistant", ts: Date.now(), content: `⚠️ ${msg}` });
474
+ }
475
+ return copy;
476
+ });
477
  }
478
  }
479
 
480
+ function onSend() {
481
+ if (streaming) return;
482
+ const text = input.trim();
483
+ if (!text) return;
 
 
 
 
484
  setInput("");
485
+ runChat(text);
486
  }
487
 
488
+ function onRegen() {
489
+ if (streaming) return;
490
+ // find last user message
491
+ const lastUser = [...messages].reverse().find((m) => m.role === "user");
492
+ if (!lastUser) return;
493
+ runChat(lastUser.content, true);
494
  }
495
 
496
+ const promptChips = [
497
+ "Explain black holes like I'm 10 explaining to a friend.",
498
+ "Make a 7-day study plan for Java + DSA.",
499
+ "Write a scary 6-line story with a twist ending.",
500
+ "Give 5 business ideas for students in India.",
501
+ ];
 
 
 
 
 
 
 
502
 
503
  return (
504
+ <div className="mcRoot">
505
+ <PixelFireworks show={showIntro} onSkip={() => setShowIntro(false)} />
506
+
507
+ <div className="mcShell">
508
+ <div className="mcFrame">
509
+ <div className="mcTop">
510
+ <div className="mcBrand">
511
+ <div className="mcIcon" aria-hidden="true">
512
+ 🤖
513
+ </div>
514
+ <div className="mcTitleWrap">
515
+ <div className="mcTitle">AI CHAT</div>
516
+ <div className="mcSub">
517
+ READY • {useStreaming ? "STREAM" : "NO-STREAM"} •{" "}
518
+ {(customModel.trim() || model).split("/").pop()}
519
+ </div>
520
  </div>
521
  </div>
522
 
523
+ <div className="mcTopRight">
524
+ <button
525
+ className="btn pixelBtn"
526
+ onClick={() => setSettingsOpen(true)}
527
+ aria-label="Open settings"
528
+ title="Settings"
529
+ >
530
 
531
  </button>
532
  </div>
533
  </div>
534
 
535
+ <div className="mcBody">
536
+ <div className="mcList" ref={listRef}>
537
+ {messages.length === 0 && showPrompts && (
538
+ <div className="mcOverlay">
539
+ <div className="mcOverlayBox">
540
+ <div className="mcOverlayHead">
541
+ <div className="mcOverlayBadge">TIP</div>
542
+ <div className="mcOverlayText">
543
+ Tap a prompt to start (it disappears after you chat).
544
+ </div>
545
  </div>
 
546
 
547
+ <div className="mcChips">
548
+ {promptChips.map((p) => (
549
+ <button
550
+ key={p}
551
+ className="mcChip"
552
+ onClick={() => {
553
+ setInput(p);
554
+ inputRef.current?.focus();
555
+ }}
556
+ >
557
+ {p}
558
+ </button>
559
+ ))}
560
+ </div>
561
  </div>
562
+ </div>
563
+ )}
564
 
565
+ {messages.map((m) => (
566
+ <div
567
+ key={m.id}
568
+ className={`mcMsg ${m.role === "user" ? "isUser" : m.role === "assistant" ? "isAI" : "isSys"
569
+ }`}
570
+ >
571
+ <div className="mcMsgMeta">
572
+ <span className="mcWho">{m.role === "user" ? "YOU" : m.role === "assistant" ? "AI" : "SYS"}</span>
573
+ <span className="mcDot">•</span>
574
+ <span className="mcTime">
575
+ {new Date(m.ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
576
+ </span>
577
+
578
+ {m.role === "assistant" && m.content.trim() && (
579
+ <button
580
+ className="mcMiniBtn"
581
+ title="Copy"
582
+ onClick={() => navigator.clipboard.writeText(m.content)}
583
+ >
584
+ COPY
585
  </button>
586
+ )}
587
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
 
589
+ <div className="mcBubble">
590
+ <pre className="mcText">{m.content}</pre>
 
 
 
 
 
 
 
591
  </div>
592
  </div>
593
+ ))}
594
+ </div>
 
 
 
 
 
 
 
595
 
596
+ <div className="mcComposer">
 
597
  <textarea
598
+ ref={inputRef}
599
+ className="mcInput"
600
  value={input}
601
+ placeholder="Type… (Enter=send, Shift+Enter=new line, Esc=stop) • /help"
602
+ maxLength={2000}
603
  onChange={(e) => setInput(e.target.value)}
604
+ onKeyDown={(e) => {
605
+ if (e.key === "Enter" && !e.shiftKey) {
606
+ e.preventDefault();
607
+ onSend();
608
+ }
609
+ }}
610
  />
611
+ <div className="mcActions">
612
+ <button className="btn pixelBtn" onClick={onSend} disabled={streaming}>
613
+ {streaming ? "..." : "SEND"}
 
614
  </button>
615
+ <button className="btn pixelBtn" onClick={onRegen} disabled={streaming || messages.length === 0}>
616
+ REGEN
 
617
  </button>
618
+ {streaming ? (
619
+ <button className="btn pixelBtn danger" onClick={stopStreaming}>
620
+ STOP
621
+ </button>
622
+ ) : (
623
+ <button className="btn pixelBtn ghost" onClick={doClear}>
624
+ CLEAR
625
+ </button>
626
+ )}
627
+ </div>
628
+ <div className="mcFooter">
629
+ <div className="mcHint">
630
+ Tip: Esc stops streaming • /export downloads chat
631
+ </div>
632
+ <div className="mcCount">{input.length}/2000</div>
633
+ </div>
634
  </div>
635
  </div>
636
  </div>
637
  </div>
638
 
639
+ {/* SETTINGS MODAL */}
640
+ {settingsOpen && (
641
+ <div className="mcModal" role="dialog" aria-modal="true">
642
+ <div className="mcModalBox">
643
+ <div className="mcModalTop">
644
+ <div className="mcModalTitle">SETTINGS</div>
645
+ <button className="btn pixelBtn" onClick={() => setSettingsOpen(false)}>
646
+
647
+ </button>
648
  </div>
649
 
650
+ <div className="mcGrid">
651
+ <div className="mcField">
652
+ <div className="mcLabel">Model</div>
653
+ <select
654
+ className="mcSelect"
655
+ value={model}
656
+ onChange={(e) => setModel(e.target.value)}
657
+ >
658
+ {DEFAULT_MODELS.map((m) => (
659
+ <option key={m.id} value={m.id}>
660
+ {m.label}
661
+ </option>
662
+ ))}
663
+ </select>
664
+ <div className="mcSmall">
665
+ Optional: paste a custom model id below (overrides dropdown).
666
+ </div>
667
+ <input
668
+ className="mcTextIn"
669
+ value={customModel}
670
+ onChange={(e) => setCustomModel(e.target.value)}
671
+ placeholder="Custom model id (optional)"
672
+ />
673
  </div>
 
 
 
 
 
674
 
675
+ <div className="mcField">
676
+ <div className="mcLabel">Temperature (0 → 2)</div>
677
+ <input
678
+ className="mcRange"
679
+ type="range"
680
+ min={0}
681
+ max={2}
682
+ step={0.05}
683
+ value={temperature}
684
+ onChange={(e) => setTemperature(Number(e.target.value))}
685
+ />
686
+ <div className="mcRow">
687
+ <div className="mcPill">{temperature.toFixed(2)}</div>
688
+ <label className="mcCheck">
689
+ <input
690
+ type="checkbox"
691
+ checked={useStreaming}
692
+ onChange={(e) => setUseStreaming(e.target.checked)}
693
+ />
694
+ <span>Streaming</span>
695
+ </label>
696
+ </div>
697
  </div>
 
 
698
 
699
+ <div className="mcField">
700
+ <div className="mcLabel">Max Tokens</div>
701
+ <input
702
+ className="mcTextIn"
703
+ type="number"
704
+ min={16}
705
+ max={4096}
706
+ value={maxTokens}
707
+ onChange={(e) => setMaxTokens(Number(e.target.value))}
708
+ />
709
+ <div className="mcSmall">
710
+ (2 is a common max temperature; tokens depends on model.)
711
+ </div>
712
  </div>
 
 
 
 
 
 
 
 
 
 
713
 
714
+ <div className="mcField mcWide">
715
+ <div className="mcLabel">System Prompt</div>
716
+ <textarea
717
+ className="mcTextArea"
718
+ value={systemPrompt}
719
+ onChange={(e) => setSystemPrompt(e.target.value)}
720
+ />
721
+ </div>
722
+
723
+ <div className="mcField mcWide">
724
+ <div className="mcRow">
725
+ <button className="btn pixelBtn" onClick={exportChat}>
726
+ EXPORT CHAT
727
+ </button>
728
+ <button
729
+ className="btn pixelBtn ghost"
730
+ onClick={() => {
731
+ setModel(DEFAULT_MODELS[0].id);
732
+ setCustomModel("");
733
+ setTemperature(0.7);
734
+ setMaxTokens(512);
735
+ setSystemPrompt(
736
+ "You are a helpful assistant. Keep answers clear and practical."
737
+ );
738
+ setUseStreaming(true);
739
+ }}
740
+ >
741
+ RESET DEFAULTS
742
+ </button>
743
+ </div>
744
+ {error && <div className="mcError">Last error: {error}</div>}
745
  </div>
 
 
 
 
 
 
 
 
 
746
  </div>
747
 
748
+ <div className="mcModalHint">
749
+ Note: don’t paste any “citation text” into your code (it can break TypeScript).
 
750
  </div>
751
  </div>
752
  </div>
753
  )}
754
+ </div>
755
  );
756
  }