Semnykcz commited on
Commit
646a901
·
verified ·
1 Parent(s): df5b56f

Delete app

Browse files
Files changed (3) hide show
  1. app/index.html +0 -156
  2. app/script.js +0 -345
  3. app/styles.css +0 -461
app/index.html DELETED
@@ -1,156 +0,0 @@
1
- <!doctype html>
2
- <html lang="en" class="h-full scroll-smooth">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
- <title>Minimal AI Chat — Tailwind</title>
7
-
8
- <!-- Tailwind Play CDN (compatible with Tailwind v3/v4 classes) -->
9
- <script src="https://cdn.tailwindcss.com"></script>
10
- <link rel="stylesheet" href="styles.css">
11
-
12
- <!-- Prism.js for syntax highlighting -->
13
- <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
14
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
15
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
16
- </head>
17
-
18
- <body class="min-h-dvh bg-neutral-50 text-neutral-900 antialiased dark:bg-neutral-950 dark:text-neutral-100">
19
- <!-- App Shell -->
20
- <div class="grid grid-rows-[auto,1fr,auto] min-h-dvh">
21
- <!-- Header -->
22
- <header class="sticky top-0 z-40 backdrop-blur supports-[backdrop-filter]:bg-white/60 dark:supports-[backdrop-filter]:bg-neutral-900/60 bg-white/90 dark:bg-neutral-90/90 border-b border-neutral-200/70 dark:border-neutral-800/70">
23
- <div class="mx-auto max-w-3xl px-4 sm:px-6 pt-safe">
24
- <div class="flex items-center justify-between pb-3 gap-3">
25
- <div class="flex items-center gap-3 min-w-0">
26
- <div class="size-7 rounded-xl grid place-items-center bg-neutral-900 text-white dark:bg-white dark:text-neutral-900 shadow-soft">
27
- <!-- Dot logo -->
28
- <svg viewBox="0 0 24 24" class="size-4" fill="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="7"/></svg>
29
- </div>
30
- <div class="truncate">
31
- <h1 class="text-sm font-medium tracking-tight text-neutral-900 dark:text-neutral-100">Minimal AI Chat</h1>
32
- <p class="text-xs text-neutral-500 dark:text-neutral-400 truncate">OriginUI‑inspired · Light / Dark</p>
33
- </div>
34
- </div>
35
-
36
- <div class="flex items-center gap-2">
37
- <!-- New chat -->
38
- <button id="newChatBtn" class="hidden sm:inline-flex items-center gap-2 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-3 py-2 text-xs font-medium shadow-soft hover:bg-neutral-50 dark:hover:bg-neutral-800 transition" aria-label="New chat">
39
- <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
40
- New
41
- </button>
42
-
43
- <!-- Theme toggle -->
44
- <button id="themeToggle" class="inline-flex items-center justify-center rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 size-9 shadow-soft hover:bg-neutral-50 dark:hover:bg-neutral-800 transition" aria-label="Toggle theme">
45
- <svg id="iconSun" class="size-4 hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
46
- <svg id="iconMoon" class="size-4 hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
47
- </button>
48
- </div>
49
- </div>
50
- </div>
51
- </header>
52
-
53
- <!-- Messages -->
54
- <main id="scrollArea" class="no-scrollbar overflow-y-auto">
55
- <div class="mx-auto max-w-3xl w-full px-4 sm:px-6 py-4 sm:py-6 pb-40">
56
- <!-- Tip / examples card -->
57
- <section class="mb-4 sm:mb-6">
58
- <div class="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 backdrop-blur p-4 sm:p-5 shadow-soft">
59
- <div class="flex items-start gap-3">
60
- <div class="shrink-0 size-7 rounded-lg grid place-items-center bg-neutral-100 dark:bg-neutral-800">
61
- <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>
62
- </div>
63
- <div class="text-sm leading-6 text-neutral-700 dark:text-neutral-300">
64
- <p class="font-medium text-neutral-900 dark:text-neutral-100">Try prompts</p>
65
- <ul class="mt-1 list-disc pl-5 space-y-1">
66
- <li>"Summarize this text in 3 bullet points."</li>
67
- <li>"Explain quantum tunneling like I'm 12."</li>
68
- <li>"Draft a polite email declining a meeting."</li>
69
- </ul>
70
- </div>
71
- </div>
72
- </div>
73
- </section>
74
-
75
- <div id="messages" class="space-y-3 sm:space-y-4">
76
- <!-- Assistant message (sample) -->
77
- <article class="group">
78
- <div class="flex items-start gap-3">
79
- <div class="shrink-0 size-7 rounded-lg grid place-items-center bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300">
80
- <!-- Bot icon -->
81
- <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="8" width="18" height="10" rx="2"/><path d="M8 8V6a4 4 0 1 1 8 0v2"/><circle cx="8" cy="13" r="1"/><circle cx="16" cy="13" r="1"/></svg>
82
- </div>
83
- <div class="max-w-[80ch] w-full">
84
- <div class="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4 shadow-soft">
85
- <p class="text-sm leading-7">Hi! I’m your AI assistant. Ask me anything — your messages will appear here, and I’ll reply below. This UI is built with Tailwind and inspired by OriginUI’s clean, minimal aesthetic.</p>
86
- </div>
87
- </div>
88
- </div>
89
- </article>
90
-
91
- <!-- User message (sample) -->
92
- <article class="group">
93
- <div class="flex items-start gap-3 justify-end">
94
- <div class="max-w-[80ch] w-full">
95
- <div class="rounded-2xl border border-neutral-200 bg-neutral-900 text-neutral-50 dark:bg-white dark:text-neutral-900 p-4 shadow-soft">
96
- <p class="text-sm leading-7">Give me a short productivity tip.</p>
97
- </div>
98
- </div>
99
- </div>
100
- </article>
101
-
102
- <!-- Assistant message (sample) -->
103
- <article class="group">
104
- <div class="flex items-start gap-3">
105
- <div class="shrink-0 size-7 rounded-lg grid place-items-center bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300">
106
- <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="8" width="18" height="10" rx="2"/><path d="M8 8V6a4 4 0 1 1 8 0v2"/><circle cx="8" cy="13" r="1"/><circle cx="16" cy="13" r="1"/></svg>
107
- </div>
108
- <div class="max-w-[80ch] w-full">
109
- <div class="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4 shadow-soft">
110
- <ul class="text-sm leading-7 list-disc pl-5 space-y-1">
111
- <li>Time‑box tasks to 25 minutes.</li>
112
- <li>Mute non‑urgent notifications.</li>
113
- <li>Write the next step before you stop.</li>
114
- </ul>
115
- </div>
116
- </div>
117
- </div>
118
- </article>
119
- </div>
120
- </div>
121
- </main>
122
-
123
- <!-- Composer -->
124
- <footer class="fixed bottom-0 inset-x-0 z-40">
125
- <div class="pointer-events-none bg-gradient-to-t from-white/95 to-white/0 dark:from-neutral-950/95 dark:to-neutral-950/0">
126
- <div class="mx-auto max-w-3xl px-4 sm:px-6 pb-safe">
127
- <form id="composer" class="pointer-events-auto" autocomplete="off">
128
- <div class="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 shadow-soft p-2 sm:p-2.5">
129
- <div class="flex items-end gap-2">
130
- <button type="button" id="attachBtn" class="shrink-0 grid place-items-center size-9 rounded-xl border border-neutral-200 dark:border-neutral-800 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition" aria-label="Attach">
131
- <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.44 11.05l-8.49 8.49a5 5 0 1 1-7.07-7.07l9.19-9.19a3.5 3.5 0 0 1 4.95 4.95l-9.19 9.19a2 2 0 1 1-2.83-2.83l8.49-8.49"/></svg>
132
- </button>
133
-
134
- <textarea id="input" rows="1" placeholder="Message…" class="flex-1 resize-none bg-transparent focus:outline-none placeholder:text-neutral-400 text-sm leading-6 max-h-40 p-2"></textarea>
135
-
136
- <div class="flex items-center gap-2">
137
- <button type="button" id="queueBtn" class="hidden sm:grid place-items-center size-9 rounded-xl border border-neutral-200 dark:border-neutral-800 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition" aria-label="Add to queue">
138
- <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M3 12h12M3 18h6"/></svg>
139
- </button>
140
-
141
- <button type="submit" class="grid place-items-center size-9 rounded-xl bg-neutral-900 text-white hover:bg-neutral-800 dark:bg-white dark:text-neutral-900 dark:hover:bg-neutral-200 transition" aria-label="Send">
142
- <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/></svg>
143
- </button>
144
- </div>
145
- </div>
146
- </div>
147
- </form>
148
- <p class="pointer-events-auto mt-2 text-[11px] text-neutral-500 dark:text-neutral-400 text-center">Tip: Odeslat Cmd/Ctrl + Enter</p>
149
- </div>
150
- </div>
151
- </footer>
152
- </div>
153
-
154
- <script src="script.js"></script>
155
- </body>
156
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/script.js DELETED
@@ -1,345 +0,0 @@
1
- // ---- Theme: auto + toggle ----
2
- const root = document.documentElement;
3
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
4
- const savedTheme = localStorage.getItem('theme');
5
- const isDark = () => root.classList.contains('dark');
6
-
7
- function applyTheme(theme) {
8
- if (theme === 'dark' || (theme === 'auto' && prefersDark.matches)) {
9
- root.classList.add('dark');
10
- } else {
11
- root.classList.remove('dark');
12
- }
13
- updateThemeIcon();
14
- }
15
-
16
- function updateThemeIcon () {
17
- const sun = document.getElementById('iconSun');
18
- const moon = document.getElementById('iconMoon');
19
- if (isDark()) { moon.classList.add('hidden'); sun.classList.remove('hidden'); }
20
- else { sun.classList.add('hidden'); moon.classList.remove('hidden'); }
21
- }
22
-
23
- applyTheme(savedTheme || 'auto');
24
- prefersDark.addEventListener('change', () => applyTheme(localStorage.getItem('theme') || 'auto'));
25
- document.getElementById('themeToggle').addEventListener('click', () => {
26
- const next = isDark() ? 'light' : 'dark';
27
- localStorage.setItem('theme', next);
28
- applyTheme(next);
29
- });
30
-
31
- // ---- Chat wiring ----
32
- const form = document.getElementById('composer');
33
- const input = document.getElementById('input');
34
- const messages = document.getElementById('messages');
35
- const scrollArea = document.getElementById('scrollArea');
36
-
37
- const history = []; // {role, content}
38
-
39
- function autoResizeTextarea(el) {
40
- el.style.height = 'auto';
41
- const h = Math.min(el.scrollHeight, 320);
42
- el.style.height = h + 'px';
43
- }
44
- input.addEventListener('input', () => autoResizeTextarea(input));
45
- input.addEventListener('keydown', (e) => {
46
- if (e.key === 'Enter' && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
47
- setTimeout(() => autoResizeTextarea(input), 0);
48
- }
49
- });
50
- autoResizeTextarea(input);
51
-
52
- function scrollToBottom() {
53
- requestAnimationFrame(() => scrollArea.scrollTo({ top: scrollArea.scrollHeight, behavior: 'smooth' }));
54
- }
55
-
56
- function processCodeBlocks(content) {
57
- // Replace markdown code blocks with Prism-compatible HTML
58
- return content.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, language, code) => {
59
- const lang = language || 'javascript';
60
- const escapedCode = code.replace(/</g, '&lt;').replace(/>/g, '&gt;');
61
- return `<pre><code class="language-${lang}">${escapedCode}</code></pre>`;
62
- }).replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
63
- }
64
-
65
- function bubble({ role, html }) {
66
- const wrapper = document.createElement('article');
67
- wrapper.className = 'group';
68
-
69
- // Process code blocks for syntax highlighting in assistant messages
70
- if (role === 'assistant') {
71
- html = processCodeBlocks(html);
72
- }
73
-
74
- if (role === 'user') {
75
- wrapper.innerHTML = `
76
- <div class="flex items-start gap-3 justify-end">
77
- <div class="max-w-[80ch] w-full">
78
- <div class="rounded-2xl border border-neutral-200 bg-neutral-900 text-neutral-50 dark:bg-white dark:text-neutral-900 p-4 shadow-soft">
79
- <div class="text-sm leading-7">${html}</div>
80
- </div>
81
- </div>
82
- </div>
83
- `;
84
- } else {
85
- wrapper.innerHTML = `
86
- <div class="flex items-start gap-3">
87
- <div class="shrink-0 size-7 rounded-lg grid place-items-center bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300">
88
- <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="8" width="18" height="10" rx="2"/><path d="M8 8V6a4 4 0 1 1 8 0v2"/><circle cx="8" cy="13" r="1"/><circle cx="16" cy="13" r="1"/></svg>
89
- </div>
90
- <div class="max-w-[80ch] w-full">
91
- <div class="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4 shadow-soft">
92
- <div class="text-sm leading-7">${html}</div>
93
- </div>
94
- </div>
95
- </div>
96
- `;
97
- }
98
-
99
- // Apply syntax highlighting to new code blocks
100
- if (role === 'assistant' && typeof Prism !== 'undefined') {
101
- setTimeout(() => Prism.highlightAllUnder(wrapper), 0);
102
- }
103
-
104
- return wrapper;
105
- }
106
-
107
- function assistantLiveBubble() {
108
- const wrapper = document.createElement('article');
109
- wrapper.className = 'group';
110
- wrapper.innerHTML = `
111
- <div class="flex items-start gap-3">
112
- <div class="shrink-0 size-7 rounded-lg grid place-items-center bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300">
113
- <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="8" width="18" height="10" rx="2"/><path d="M8 8V6a4 4 0 1 1 8 0v2"/><circle cx="8" cy="13" r="1"/><circle cx="16" cy="13" r="1"/></svg>
114
- </div>
115
- <div class="max-w-[80ch] w-full">
116
- <div class="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4 shadow-soft">
117
- <div class="text-[11px] leading-6 text-neutral-500 dark:text-neutral-400 mb-1" id="status">Thinking…</div>
118
- <div class="text-sm leading-7" id="content"></div>
119
- </div>
120
- </div>
121
- </div>
122
- `;
123
- const statusEl = wrapper.querySelector('#status');
124
- const contentEl = wrapper.querySelector('#content');
125
- return {
126
- node: wrapper,
127
- setStatus: (text) => { statusEl.textContent = text; },
128
- appendText: (t) => { contentEl.textContent += t; },
129
- setHTML: (h) => { contentEl.innerHTML = h; },
130
- };
131
- }
132
-
133
- async function sendToBackend(history) {
134
- // Try streaming first
135
- try {
136
- const res = await fetch('/v1/chat/completions', {
137
- method: 'POST',
138
- headers: { 'Content-Type': 'application/json' },
139
- body: JSON.stringify({
140
- model: undefined, // server default
141
- messages: history,
142
- temperature: 0.2,
143
- top_p: 0.95,
144
- max_tokens: 512,
145
- stream: true,
146
- })
147
- });
148
-
149
- const ct = res.headers.get('content-type') || '';
150
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
151
-
152
- if (ct.includes('text/event-stream')) {
153
- return streamSSE(res.body);
154
- }
155
-
156
- // Fallback: non-stream JSON
157
- const data = await res.json();
158
- async function* single() { yield { type: 'final', text: data.choices?.[0]?.message?.content || '' }; }
159
- return single();
160
- } catch (err) {
161
- console.error(err);
162
- async function* errGen() { throw err; }
163
- return errGen();
164
- }
165
- }
166
-
167
- async function* streamSSE(readable) {
168
- const reader = readable.getReader();
169
- const decoder = new TextDecoder();
170
- let buffer = '';
171
- while (true) {
172
- const { value, done } = await reader.read();
173
- if (done) break;
174
- buffer += decoder.decode(value, { stream: true });
175
- const parts = buffer.split('\n\n');
176
- buffer = parts.pop() || '';
177
- for (const part of parts) {
178
- const lines = part.split('\n');
179
- for (const line of lines) {
180
- if (!line.startsWith('data:')) continue;
181
- const data = line.slice(5).trim();
182
- if (data === '[DONE]') { yield { type: 'done' }; return; }
183
- try {
184
- const json = JSON.parse(data);
185
- const choice = json.choices?.[0];
186
- const piece = choice?.delta?.content;
187
- const finish = choice?.finish_reason;
188
- if (piece) yield { type: 'token', text: piece };
189
- if (finish === 'stop') yield { type: 'final' };
190
- } catch (e) {
191
- // ignore parse errors for keep-alives
192
- }
193
- }
194
- }
195
- }
196
- }
197
-
198
- let typingIndicatorElement = null;
199
- let currentAbortController = null;
200
- let stopButton = null;
201
-
202
- function showTypingIndicator() {
203
- if (typingIndicatorElement) return;
204
-
205
- typingIndicatorElement = document.createElement('div');
206
- typingIndicatorElement.className = 'typing-indicator';
207
- typingIndicatorElement.innerHTML = `
208
- <span>AI píše</span>
209
- <div class="typing-dots">
210
- <div class="typing-dot"></div>
211
- <div class="typing-dot"></div>
212
- <div class="typing-dot"></div>
213
- </div>
214
- `;
215
-
216
- messages.appendChild(typingIndicatorElement);
217
- scrollToBottom();
218
- }
219
-
220
- function hideTypingIndicator() {
221
- if (typingIndicatorElement) {
222
- typingIndicatorElement.remove();
223
- typingIndicatorElement = null;
224
- }
225
- }
226
-
227
- function showStopButton() {
228
- if (stopButton) return;
229
-
230
- stopButton = document.createElement('button');
231
- stopButton.className = 'stop-generation-btn';
232
- stopButton.innerHTML = `
233
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
234
- <rect x="6" y="6" width="12" height="12" rx="2"/>
235
- </svg>
236
- Zastavit
237
- `;
238
-
239
- stopButton.onclick = () => {
240
- if (currentAbortController) {
241
- currentAbortController.abort();
242
- hideStopButton();
243
- hideTypingIndicator();
244
- }
245
- };
246
-
247
- // Insert stop button after the input container
248
- const inputContainer = document.querySelector('.input-container') || document.querySelector('form');
249
- if (inputContainer && inputContainer.parentNode) {
250
- inputContainer.parentNode.insertBefore(stopButton, inputContainer.nextSibling);
251
- }
252
- }
253
-
254
- function hideStopButton() {
255
- if (stopButton) {
256
- stopButton.remove();
257
- stopButton = null;
258
- }
259
- }
260
-
261
- form.addEventListener('submit', async (e) => {
262
- e.preventDefault();
263
- const text = input.value.trim();
264
- if (!text) return;
265
-
266
- // Remove example messages on first user interaction
267
- if (history.length === 0) {
268
- const items = [...messages.querySelectorAll('article')];
269
- items.slice(0, 3).forEach(n => n.remove());
270
- }
271
-
272
- // User bubble
273
- messages.appendChild(bubble({ role: 'user', html: text.replace(/</g,'<') }));
274
- scrollToBottom();
275
-
276
- // Push to history and clear input
277
- history.push({ role: 'user', content: text });
278
- input.value = '';
279
- autoResizeTextarea(input);
280
-
281
- // Show typing indicator and stop button
282
- showTypingIndicator();
283
- showStopButton();
284
-
285
- // Create abort controller for this request
286
- currentAbortController = new AbortController();
287
-
288
- // Assistant live bubble
289
- const asst = assistantLiveBubble();
290
- messages.appendChild(asst.node);
291
- scrollToBottom();
292
-
293
- try {
294
- const stream = await sendToBackend(history);
295
- let gotFirst = false;
296
- let collected = '';
297
- for await (const evt of stream) {
298
- if (evt.type === 'token') {
299
- if (!gotFirst) {
300
- hideTypingIndicator();
301
- asst.setStatus('Writing…');
302
- gotFirst = true;
303
- }
304
- asst.appendText(evt.text);
305
- collected += evt.text;
306
- scrollToBottom();
307
- } else if (evt.type === 'final') {
308
- // finalize
309
- } else if (evt.type === 'done') {
310
- break;
311
- }
312
- }
313
- if (collected) history.push({ role: 'assistant', content: collected });
314
- } catch (err) {
315
- console.error('Chat error', err);
316
- hideTypingIndicator();
317
-
318
- if (err.name === 'AbortError') {
319
- asst.setStatus('Stopped');
320
- asst.setHTML('<span class="text-yellow-600 dark:text-yellow-400">Generation stopped by user.</span>');
321
- } else {
322
- asst.setStatus('Error');
323
- asst.setHTML('<span class="text-red-600 dark:text-red-400">Unable to get response.</span>');
324
- }
325
- } finally {
326
- // Clean up
327
- hideStopButton();
328
- currentAbortController = null;
329
- }
330
- });
331
-
332
- // Keyboard shortcut: Cmd/Ctrl+Enter to send
333
- input.addEventListener('keydown', (e) => {
334
- if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
335
- form.requestSubmit();
336
- }
337
- });
338
-
339
- // New chat clears messages (keeps tips)
340
- document.getElementById('newChatBtn')?.addEventListener('click', () => {
341
- const items = [...messages.querySelectorAll('article')];
342
- items.slice(3).forEach(n => n.remove());
343
- history.length = 0;
344
- scrollArea.scrollTo({ top: 0, behavior: 'smooth' });
345
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/styles.css DELETED
@@ -1,461 +0,0 @@
1
- /* Respect iOS notch / safe-area on mobile */
2
- .pb-safe { padding-bottom: max(1rem, env(safe-area-inset-bottom)); }
3
- .pt-safe { padding-top: max(0.75rem, env(safe-area-inset-top)); }
4
-
5
- /* Hide scrollbar on WebKit (messages list looks cleaner) */
6
- .no-scrollbar::-webkit-scrollbar { display: none; }
7
- .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
8
-
9
- /* Smooth textarea autoresize transitions */
10
- textarea { transition: height 120ms ease; }
11
-
12
- /* Mobile Optimizations */
13
- @media (max-width: 768px) {
14
- /* Touch-friendly button sizing */
15
- button, .btn {
16
- min-height: 44px;
17
- min-width: 44px;
18
- padding: 12px 16px;
19
- }
20
-
21
- /* Improved text readability on mobile */
22
- body {
23
- font-size: 16px;
24
- line-height: 1.5;
25
- }
26
-
27
- /* Better input field sizing */
28
- input, textarea {
29
- font-size: 16px; /* Prevents zoom on iOS */
30
- padding: 12px;
31
- border-radius: 8px;
32
- }
33
-
34
- /* Responsive container spacing */
35
- .container {
36
- padding-left: 16px;
37
- padding-right: 16px;
38
- }
39
-
40
- /* Chat message optimizations */
41
- .message {
42
- margin-bottom: 12px;
43
- padding: 12px;
44
- max-width: 85%;
45
- }
46
-
47
- /* Improved scrolling on mobile */
48
- .chat-container {
49
- -webkit-overflow-scrolling: touch;
50
- scroll-behavior: smooth;
51
- }
52
-
53
- pre {
54
- font-size: 0.75rem;
55
- padding: 0.75rem;
56
- margin: 0.5rem 0;
57
- }
58
-
59
- .inline-code {
60
- font-size: 0.8em;
61
- padding: 0.1rem 0.25rem;
62
- }
63
- }
64
-
65
- @media (max-width: 480px) {
66
- /* Extra small screens */
67
- .container {
68
- padding-left: 12px;
69
- padding-right: 12px;
70
- }
71
-
72
- .message {
73
- max-width: 90%;
74
- font-size: 14px;
75
- }
76
-
77
- /* Compact header on small screens */
78
- .header {
79
- padding: 8px 12px;
80
- }
81
-
82
- pre {
83
- font-size: 0.7rem;
84
- padding: 0.5rem;
85
- margin: 0.375rem 0;
86
- overflow-x: auto;
87
- }
88
-
89
- .inline-code {
90
- font-size: 0.75em;
91
- padding: 0.075rem 0.2rem;
92
- }
93
- }
94
-
95
- /* Touch gesture improvements */
96
- .touch-action-manipulation {
97
- touch-action: manipulation;
98
- }
99
-
100
- /* Prevent text selection on UI elements */
101
- .no-select {
102
- -webkit-user-select: none;
103
- -moz-user-select: none;
104
- -ms-user-select: none;
105
- user-select: none;
106
- }
107
-
108
- /* Smooth animations for mobile */
109
- @media (prefers-reduced-motion: no-preference) {
110
- .animate-smooth {
111
- transition: all 0.2s ease-in-out;
112
- }
113
- }
114
-
115
- /* Code blocks and syntax highlighting */
116
- pre {
117
- background: var(--code-bg) !important;
118
- border: 1px solid var(--border-color);
119
- border-radius: 0.5rem;
120
- padding: 1rem;
121
- margin: 0.75rem 0;
122
- overflow-x: auto;
123
- font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
124
- font-size: 0.875rem;
125
- line-height: 1.5;
126
- }
127
-
128
- pre code {
129
- background: none !important;
130
- padding: 0 !important;
131
- border: none !important;
132
- border-radius: 0 !important;
133
- font-size: inherit;
134
- }
135
-
136
- .inline-code {
137
- background: var(--code-bg);
138
- color: var(--code-text);
139
- padding: 0.125rem 0.375rem;
140
- border-radius: 0.25rem;
141
- font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
142
- font-size: 0.875em;
143
- border: 1px solid var(--border-color);
144
- }
145
-
146
- /* Override Prism theme colors for dark mode */
147
- [data-theme="dark"] .token.comment,
148
- [data-theme="dark"] .token.prolog,
149
- [data-theme="dark"] .token.doctype,
150
- [data-theme="dark"] .token.cdata {
151
- color: #6a737d;
152
- }
153
-
154
- [data-theme="dark"] .token.punctuation {
155
- color: #f8f8f2;
156
- }
157
-
158
- [data-theme="dark"] .token.property,
159
- [data-theme="dark"] .token.tag,
160
- [data-theme="dark"] .token.boolean,
161
- [data-theme="dark"] .token.number,
162
- [data-theme="dark"] .token.constant,
163
- [data-theme="dark"] .token.symbol,
164
- [data-theme="dark"] .token.deleted {
165
- color: #ff79c6;
166
- }
167
-
168
- [data-theme="dark"] .token.selector,
169
- [data-theme="dark"] .token.attr-name,
170
- [data-theme="dark"] .token.string,
171
- [data-theme="dark"] .token.char,
172
- [data-theme="dark"] .token.builtin,
173
- [data-theme="dark"] .token.inserted {
174
- color: #50fa7b;
175
- }
176
-
177
- [data-theme="dark"] .token.operator,
178
- [data-theme="dark"] .token.entity,
179
- [data-theme="dark"] .token.url,
180
- [data-theme="dark"] .language-css .token.string,
181
- [data-theme="dark"] .style .token.string {
182
- color: #f8f8f2;
183
- }
184
-
185
- [data-theme="dark"] .token.atrule,
186
- [data-theme="dark"] .token.attr-value,
187
- [data-theme="dark"] .token.keyword {
188
- color: #8be9fd;
189
- }
190
-
191
- [data-theme="dark"] .token.function,
192
- [data-theme="dark"] .token.class-name {
193
- color: #ffb86c;
194
- }
195
-
196
- [data-theme="dark"] .token.regex,
197
- [data-theme="dark"] .token.important,
198
- [data-theme="dark"] .token.variable {
199
- color: #bd93f9;
200
- }
201
-
202
- /* Typing indicator animation */
203
- .typing-indicator {
204
- display: flex;
205
- align-items: center;
206
- padding: 1rem;
207
- margin: 0.5rem 0;
208
- background: var(--input-bg);
209
- border-radius: 1rem;
210
- border: 1px solid var(--border-color);
211
- max-width: fit-content;
212
- }
213
-
214
- .typing-indicator span {
215
- color: var(--text-color);
216
- margin-right: 0.5rem;
217
- font-size: 0.9rem;
218
- }
219
-
220
- .typing-dots {
221
- display: flex;
222
- gap: 0.25rem;
223
- }
224
-
225
- .typing-dot {
226
- width: 0.5rem;
227
- height: 0.5rem;
228
- background: var(--accent);
229
- border-radius: 50%;
230
- animation: typing-bounce 1.4s infinite ease-in-out;
231
- }
232
-
233
- .typing-dot:nth-child(1) {
234
- animation-delay: -0.32s;
235
- }
236
-
237
- .typing-dot:nth-child(2) {
238
- animation-delay: -0.16s;
239
- }
240
-
241
- @keyframes typing-bounce {
242
- 0%, 80%, 100% {
243
- transform: scale(0.8);
244
- opacity: 0.5;
245
- }
246
- 40% {
247
- transform: scale(1);
248
- opacity: 1;
249
- }
250
- }
251
-
252
- /* Stop generation button */
253
- .stop-generation-btn {
254
- display: flex;
255
- align-items: center;
256
- gap: 0.5rem;
257
- padding: 0.75rem 1rem;
258
- margin: 0.5rem auto;
259
- background: #dc3545;
260
- color: white;
261
- border: none;
262
- border-radius: 0.5rem;
263
- font-size: 0.9rem;
264
- font-weight: 500;
265
- cursor: pointer;
266
- transition: all 0.2s ease;
267
- box-shadow: var(--shadow);
268
- max-width: fit-content;
269
- }
270
-
271
- .stop-generation-btn:hover {
272
- background: #c82333;
273
- transform: translateY(-1px);
274
- box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
275
- }
276
-
277
- .stop-generation-btn:active {
278
- transform: translateY(0);
279
- }
280
-
281
- .stop-generation-btn svg {
282
- flex-shrink: 0;
283
- }
284
-
285
- /* Dark/Light Theme Variables */
286
- :root {
287
- --bg-primary: #ffffff;
288
- --bg-secondary: #f8f9fa;
289
- --text-primary: #212529;
290
- --text-secondary: #6c757d;
291
- --border-color: #dee2e6;
292
- --accent-color: #007bff;
293
- --accent-hover: #0056b3;
294
- --shadow: rgba(0, 0, 0, 0.1);
295
- --code-bg: #f8f9fa;
296
- --code-text: #212529;
297
- }
298
-
299
- [data-theme="dark"] {
300
- --bg-primary: #1a1a1a;
301
- --bg-secondary: #2d2d2d;
302
- --text-primary: #ffffff;
303
- --text-secondary: #b0b0b0;
304
- --border-color: #404040;
305
- --accent-color: #4dabf7;
306
- --accent-hover: #339af0;
307
- --shadow: rgba(0, 0, 0, 0.3);
308
- --code-bg: #282a36;
309
- --code-text: #f8f8f2;
310
- }
311
-
312
- /* Apply theme variables */
313
- body {
314
- background-color: var(--bg-primary);
315
- color: var(--text-primary);
316
- transition: background-color 0.3s ease, color 0.3s ease;
317
- }
318
-
319
- .container {
320
- background-color: var(--bg-secondary);
321
- border: 1px solid var(--border-color);
322
- }
323
-
324
- .message {
325
- background-color: var(--bg-secondary);
326
- color: var(--text-primary);
327
- border: 1px solid var(--border-color);
328
- box-shadow: 0 2px 4px var(--shadow);
329
- }
330
-
331
- button {
332
- background-color: var(--accent-color);
333
- color: white;
334
- border: none;
335
- transition: background-color 0.2s ease;
336
- }
337
-
338
- button:hover {
339
- background-color: var(--accent-hover);
340
- }
341
-
342
- input, textarea {
343
- background-color: var(--bg-primary);
344
- color: var(--text-primary);
345
- border: 1px solid var(--border-color);
346
- }
347
-
348
- /* Theme Toggle Button */
349
- .theme-toggle {
350
- position: fixed;
351
- top: 20px;
352
- right: 20px;
353
- width: 50px;
354
- height: 50px;
355
- border-radius: 50%;
356
- background-color: var(--bg-secondary);
357
- border: 2px solid var(--border-color);
358
- cursor: pointer;
359
- display: flex;
360
- align-items: center;
361
- justify-content: center;
362
- font-size: 20px;
363
- transition: all 0.3s ease;
364
- z-index: 1000;
365
- box-shadow: 0 4px 8px var(--shadow);
366
- }
367
-
368
- .theme-toggle:hover {
369
- transform: scale(1.1);
370
- box-shadow: 0 6px 12px var(--shadow);
371
- }
372
-
373
- @media (max-width: 768px) {
374
- .theme-toggle {
375
- top: 15px;
376
- right: 15px;
377
- width: 45px;
378
- height: 45px;
379
- font-size: 18px;
380
- }
381
- }
382
-
383
- /* Mobile Optimizations */
384
- @media (max-width: 768px) {
385
- /* Touch-friendly button sizing */
386
- button, .btn {
387
- min-height: 44px;
388
- min-width: 44px;
389
- padding: 12px 16px;
390
- }
391
-
392
- /* Improved text readability on mobile */
393
- body {
394
- font-size: 16px;
395
- line-height: 1.5;
396
- }
397
-
398
- /* Better input field sizing */
399
- input, textarea {
400
- font-size: 16px; /* Prevents zoom on iOS */
401
- padding: 12px;
402
- border-radius: 8px;
403
- }
404
-
405
- /* Responsive container spacing */
406
- .container {
407
- padding-left: 16px;
408
- padding-right: 16px;
409
- }
410
-
411
- /* Chat message optimizations */
412
- .message {
413
- margin-bottom: 12px;
414
- padding: 12px;
415
- max-width: 85%;
416
- }
417
-
418
- /* Improved scrolling on mobile */
419
- .chat-container {
420
- -webkit-overflow-scrolling: touch;
421
- scroll-behavior: smooth;
422
- }
423
- }
424
-
425
- @media (max-width: 480px) {
426
- /* Extra small screens */
427
- .container {
428
- padding-left: 12px;
429
- padding-right: 12px;
430
- }
431
-
432
- .message {
433
- max-width: 90%;
434
- font-size: 14px;
435
- }
436
-
437
- /* Compact header on small screens */
438
- .header {
439
- padding: 8px 12px;
440
- }
441
- }
442
-
443
- /* Touch gesture improvements */
444
- .touch-action-manipulation {
445
- touch-action: manipulation;
446
- }
447
-
448
- /* Prevent text selection on UI elements */
449
- .no-select {
450
- -webkit-user-select: none;
451
- -moz-user-select: none;
452
- -ms-user-select: none;
453
- user-select: none;
454
- }
455
-
456
- /* Smooth animations for mobile */
457
- @media (prefers-reduced-motion: no-preference) {
458
- .animate-smooth {
459
- transition: all 0.2s ease-in-out;
460
- }
461
- }