AnesKAM commited on
Commit
7fd6923
ยท
verified ยท
1 Parent(s): 021e5aa

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +414 -738
index.html CHANGED
@@ -7,851 +7,527 @@
7
  <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
8
  <style>
9
  :root {
10
- --bg: #0d0f14;
11
- --surface: #161920;
12
- --surface2: #1e2230;
13
- --surface3: #252a3a;
14
- --border: #2a2f42;
15
- --border2: #333a52;
16
- --accent: #4f8ef7;
17
- --accent2: #7c5cf7;
18
- --accent-soft: rgba(79,142,247,0.12);
19
- --text: #e8eaf2;
20
- --text2: #9aa3be;
21
- --text3: #5c6480;
22
- --user-bubble: #1a2340;
23
- --bot-bubble: #161920;
24
- --danger: #f75f5f;
25
- --success: #3dd68c;
26
- --radius: 18px;
27
- --radius-sm: 10px;
28
- --sidebar-w: 280px;
29
- --transition: 0.22s cubic-bezier(.4,0,.2,1);
30
  }
31
  [data-theme="light"] {
32
- --bg: #f0f2f8;
33
- --surface: #ffffff;
34
- --surface2: #f5f6fa;
35
- --surface3: #ebedf5;
36
- --border: #d8dce8;
37
- --border2: #c5cad8;
38
- --text: #1a1d2e;
39
- --text2: #4a5070;
40
- --text3: #8a90a8;
41
- --user-bubble: #dde6ff;
42
- --bot-bubble: #ffffff;
43
- --accent-soft: rgba(79,142,247,0.1);
44
- }
45
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
46
- html, body { height: 100%; font-family: 'Cairo', sans-serif; background: var(--bg); color: var(--text); }
47
-
48
- /* โ”€โ”€ Scrollbar โ”€โ”€ */
49
- ::-webkit-scrollbar { width: 5px; }
50
- ::-webkit-scrollbar-track { background: transparent; }
51
- ::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 99px; }
52
-
53
- /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
54
- LAYOUT
55
- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
56
- .app { display: flex; height: 100vh; overflow: hidden; }
57
-
58
- /* โ”€โ”€ SIDEBAR โ”€โ”€ */
59
- .sidebar {
60
- width: var(--sidebar-w);
61
- min-width: var(--sidebar-w);
62
- background: var(--surface);
63
- border-left: 1px solid var(--border);
64
- display: flex;
65
- flex-direction: column;
66
- transition: width var(--transition), min-width var(--transition);
67
- overflow: hidden;
68
- }
69
- .sidebar.collapsed { width: 0; min-width: 0; border: none; }
70
-
71
- .sidebar-header {
72
- padding: 18px 16px 12px;
73
- display: flex;
74
- align-items: center;
75
- gap: 10px;
76
- border-bottom: 1px solid var(--border);
77
- }
78
- .sidebar-logo { display: flex; align-items: center; gap: 10px; flex: 1; }
79
- .sidebar-logo img { width: 32px; height: 32px; border-radius: 8px; object-fit: cover; }
80
- .sidebar-logo-name { font-size: 1.1rem; font-weight: 700; background: linear-gradient(135deg, var(--accent), var(--accent2)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
81
-
82
- .btn-new-chat {
83
- margin: 12px 12px 4px;
84
- padding: 10px 14px;
85
- background: linear-gradient(135deg, var(--accent), var(--accent2));
86
- color: #fff;
87
- border: none;
88
- border-radius: var(--radius-sm);
89
- cursor: pointer;
90
- font-family: 'Cairo', sans-serif;
91
- font-size: 0.9rem;
92
- font-weight: 600;
93
- display: flex;
94
- align-items: center;
95
- gap: 8px;
96
- transition: opacity var(--transition), transform var(--transition);
97
- }
98
- .btn-new-chat:hover { opacity: 0.88; transform: translateY(-1px); }
99
-
100
- .sidebar-section-label {
101
- padding: 10px 16px 4px;
102
- font-size: 0.72rem;
103
- font-weight: 600;
104
- color: var(--text3);
105
- letter-spacing: .06em;
106
- text-transform: uppercase;
107
- }
108
- .chats-list { flex: 1; overflow-y: auto; padding: 4px 8px; display: flex; flex-direction: column; gap: 2px; }
109
- .chat-item {
110
- padding: 9px 12px;
111
- border-radius: var(--radius-sm);
112
- cursor: pointer;
113
- font-size: 0.87rem;
114
- color: var(--text2);
115
- display: flex;
116
- align-items: center;
117
- gap: 8px;
118
- transition: background var(--transition), color var(--transition);
119
- position: relative;
120
- white-space: nowrap;
121
- overflow: hidden;
122
- text-overflow: ellipsis;
123
- }
124
- .chat-item:hover { background: var(--surface2); color: var(--text); }
125
- .chat-item.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
126
- .chat-item .chat-title { flex: 1; overflow: hidden; text-overflow: ellipsis; }
127
- .chat-item .del-btn {
128
- opacity: 0;
129
- background: none;
130
- border: none;
131
- color: var(--danger);
132
- cursor: pointer;
133
- font-size: 0.85rem;
134
- padding: 2px 5px;
135
- border-radius: 5px;
136
- transition: opacity var(--transition);
137
- }
138
- .chat-item:hover .del-btn { opacity: 1; }
139
-
140
- .sidebar-footer {
141
- padding: 12px;
142
- border-top: 1px solid var(--border);
143
- display: flex;
144
- align-items: center;
145
- gap: 10px;
146
- }
147
- .btn-icon {
148
- width: 36px; height: 36px;
149
- background: var(--surface2);
150
- border: 1px solid var(--border);
151
- border-radius: var(--radius-sm);
152
- color: var(--text2);
153
- cursor: pointer;
154
- display: flex;
155
- align-items: center;
156
- justify-content: center;
157
- font-size: 1rem;
158
- transition: background var(--transition), color var(--transition);
159
- }
160
- .btn-icon:hover { background: var(--surface3); color: var(--text); }
161
-
162
- /* โ”€โ”€ MAIN โ”€โ”€ */
163
- .main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
164
-
165
- /* โ”€โ”€ TOPBAR โ”€โ”€ */
166
- .topbar {
167
- height: 58px;
168
- padding: 0 18px;
169
- display: flex;
170
- align-items: center;
171
- gap: 12px;
172
- border-bottom: 1px solid var(--border);
173
- background: var(--surface);
174
- flex-shrink: 0;
175
- }
176
- .topbar-title { flex: 1; font-size: 0.95rem; font-weight: 600; color: var(--text2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
177
- .topbar .btn-icon { font-size: 1.1rem; }
178
-
179
- /* โ”€โ”€ CHAT AREA โ”€โ”€ */
180
- .chat-area {
181
- flex: 1;
182
- overflow-y: auto;
183
- padding: 28px 0;
184
- display: flex;
185
- flex-direction: column;
186
- gap: 0;
187
- }
188
- .welcome-screen {
189
- flex: 1;
190
- display: flex;
191
- flex-direction: column;
192
- align-items: center;
193
- justify-content: center;
194
- gap: 16px;
195
- padding: 40px 24px;
196
- text-align: center;
197
- }
198
- .welcome-logo { width: 68px; height: 68px; border-radius: 18px; object-fit: cover; box-shadow: 0 8px 32px rgba(79,142,247,0.22); }
199
- .welcome-title { font-size: 2rem; font-weight: 700; background: linear-gradient(135deg, var(--accent), var(--accent2)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
200
- .welcome-sub { color: var(--text3); font-size: 0.95rem; }
201
- .welcome-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 8px; max-width: 520px; }
202
- .chip {
203
- padding: 8px 16px;
204
- background: var(--surface2);
205
- border: 1px solid var(--border);
206
- border-radius: 99px;
207
- font-size: 0.83rem;
208
- color: var(--text2);
209
- cursor: pointer;
210
- transition: background var(--transition), border-color var(--transition), color var(--transition);
211
- }
212
- .chip:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
213
-
214
- /* Messages */
215
- .msg-row {
216
- display: flex;
217
- padding: 6px 28px;
218
- gap: 12px;
219
- animation: fadeUp 0.28s ease;
220
- }
221
- @keyframes fadeUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
222
- .msg-row.user { flex-direction: row-reverse; }
223
- .msg-avatar {
224
- width: 34px; height: 34px;
225
- border-radius: 10px;
226
- flex-shrink: 0;
227
- display: flex; align-items: center; justify-content: center;
228
- font-size: 1rem;
229
- font-weight: 700;
230
- margin-top: 2px;
231
- }
232
- .msg-row.bot .msg-avatar { background: linear-gradient(135deg,var(--accent),var(--accent2)); }
233
- .msg-row.bot .msg-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 10px; }
234
- .msg-row.user .msg-avatar { background: var(--surface3); color: var(--text2); }
235
- .bubble {
236
- max-width: min(620px, 78vw);
237
- padding: 12px 16px;
238
- border-radius: var(--radius);
239
- font-size: 0.93rem;
240
- line-height: 1.72;
241
- word-break: break-word;
242
- }
243
- .msg-row.bot .bubble { background: var(--bot-bubble); border: 1px solid var(--border); border-top-right-radius: 4px; }
244
- .msg-row.user .bubble { background: var(--user-bubble); border: 1px solid var(--border2); border-top-left-radius: 4px; }
245
- .bubble pre { background: var(--surface3); border-radius: 8px; padding: 12px; overflow-x: auto; font-family: 'JetBrains Mono', monospace; font-size: 0.82rem; margin: 8px 0; border: 1px solid var(--border); }
246
- .bubble code { font-family: 'JetBrains Mono', monospace; font-size: 0.83rem; background: var(--surface3); padding: 2px 5px; border-radius: 4px; }
247
- .bubble p { margin-bottom: 6px; }
248
- .bubble p:last-child { margin-bottom: 0; }
249
-
250
- /* File preview in bubble */
251
- .file-preview-bubble { display: flex; align-items: center; gap: 8px; background: var(--surface3); border-radius: 8px; padding: 8px 12px; margin-bottom: 6px; font-size: 0.82rem; color: var(--text2); }
252
- .file-preview-bubble .file-icon { font-size: 1.2rem; }
253
-
254
- /* Typing indicator */
255
- .typing-indicator { display: flex; gap: 4px; padding: 4px 0; align-items: center; }
256
- .typing-indicator span {
257
- width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
258
- animation: bounce 1.2s infinite ease-in-out;
259
- }
260
- .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
261
- .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
262
- @keyframes bounce { 0%,80%,100%{transform:scale(0.7);opacity:.5} 40%{transform:scale(1);opacity:1} }
263
-
264
- /* โ”€โ”€ INPUT AREA โ”€โ”€ */
265
- .input-area {
266
- padding: 14px 20px 18px;
267
- background: var(--surface);
268
- border-top: 1px solid var(--border);
269
- flex-shrink: 0;
270
- }
271
- .file-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
272
- .file-chip {
273
- display: flex; align-items: center; gap: 6px;
274
- background: var(--surface2);
275
- border: 1px solid var(--border);
276
- border-radius: 99px;
277
- padding: 4px 10px 4px 8px;
278
- font-size: 0.8rem;
279
- color: var(--text2);
280
- }
281
- .file-chip button { background: none; border: none; color: var(--text3); cursor: pointer; font-size: 0.85rem; padding: 0; line-height: 1; }
282
- .file-chip button:hover { color: var(--danger); }
283
-
284
- .input-box {
285
- display: flex;
286
- align-items: flex-end;
287
- gap: 10px;
288
- background: var(--surface2);
289
- border: 1.5px solid var(--border);
290
- border-radius: var(--radius);
291
- padding: 10px 12px;
292
- transition: border-color var(--transition);
293
- }
294
- .input-box:focus-within { border-color: var(--accent); }
295
- .input-box textarea {
296
- flex: 1;
297
- background: none;
298
- border: none;
299
- outline: none;
300
- color: var(--text);
301
- font-family: 'Cairo', sans-serif;
302
- font-size: 0.93rem;
303
- resize: none;
304
- max-height: 160px;
305
- line-height: 1.6;
306
- direction: rtl;
307
- }
308
- .input-box textarea::placeholder { color: var(--text3); }
309
- .input-actions { display: flex; align-items: center; gap: 6px; }
310
- .btn-attach {
311
- background: none; border: none; color: var(--text3); cursor: pointer; font-size: 1.15rem;
312
- padding: 4px; border-radius: 6px; transition: color var(--transition);
313
- display: flex; align-items: center;
314
- }
315
- .btn-attach:hover { color: var(--accent); }
316
- .btn-send {
317
- width: 36px; height: 36px;
318
- background: linear-gradient(135deg, var(--accent), var(--accent2));
319
- border: none; border-radius: 10px; cursor: pointer; color: #fff;
320
- display: flex; align-items: center; justify-content: center; font-size: 1rem;
321
- transition: opacity var(--transition), transform var(--transition);
322
- }
323
- .btn-send:hover { opacity: 0.85; transform: scale(1.05); }
324
- .btn-send:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
325
- #file-input { display: none; }
326
-
327
- /* โ”€โ”€ SETTINGS MODAL โ”€โ”€ */
328
- .modal-overlay {
329
- position: fixed; inset: 0; z-index: 999;
330
- background: rgba(0,0,0,0.55);
331
- display: flex; align-items: center; justify-content: center;
332
- backdrop-filter: blur(4px);
333
- opacity: 0; pointer-events: none;
334
- transition: opacity var(--transition);
335
- }
336
- .modal-overlay.open { opacity: 1; pointer-events: all; }
337
- .modal {
338
- background: var(--surface);
339
- border: 1px solid var(--border);
340
- border-radius: 20px;
341
- padding: 28px;
342
- width: min(420px, 92vw);
343
- box-shadow: 0 24px 60px rgba(0,0,0,0.4);
344
- transform: translateY(16px);
345
- transition: transform var(--transition);
346
- }
347
- .modal-overlay.open .modal { transform: translateY(0); }
348
- .modal-header { display: flex; align-items: center; gap: 10px; margin-bottom: 22px; }
349
- .modal-title { font-size: 1.1rem; font-weight: 700; flex: 1; }
350
- .modal-close { background: none; border: none; color: var(--text3); cursor: pointer; font-size: 1.2rem; }
351
- .setting-row { display: flex; align-items: center; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid var(--border); }
352
- .setting-row:last-child { border-bottom: none; }
353
- .setting-label { font-size: 0.9rem; color: var(--text2); }
354
- /* Toggle switch */
355
- .toggle { position: relative; width: 42px; height: 24px; }
356
- .toggle input { opacity: 0; width: 0; height: 0; }
357
- .toggle-slider {
358
- position: absolute; inset: 0; background: var(--surface3); border-radius: 99px; cursor: pointer;
359
- transition: background var(--transition);
360
- }
361
- .toggle-slider::before {
362
- content: ''; position: absolute;
363
- width: 18px; height: 18px; border-radius: 50%;
364
- background: #fff; top: 3px; left: 3px;
365
- transition: transform var(--transition);
366
- }
367
- .toggle input:checked + .toggle-slider { background: var(--accent); }
368
- .toggle input:checked + .toggle-slider::before { transform: translateX(18px); }
369
- .setting-api-input {
370
- width: 100%;
371
- background: var(--surface2);
372
- border: 1px solid var(--border);
373
- border-radius: 8px;
374
- padding: 8px 12px;
375
- color: var(--text);
376
- font-size: 0.85rem;
377
- font-family: 'JetBrains Mono', monospace;
378
- outline: none;
379
- margin-top: 8px;
380
- direction: ltr;
381
- }
382
- .setting-api-input:focus { border-color: var(--accent); }
383
- .btn-save {
384
- width: 100%;
385
- margin-top: 16px;
386
- padding: 11px;
387
- background: linear-gradient(135deg, var(--accent), var(--accent2));
388
- color: #fff;
389
- border: none;
390
- border-radius: 10px;
391
- cursor: pointer;
392
- font-family: 'Cairo', sans-serif;
393
- font-size: 0.95rem;
394
- font-weight: 600;
395
- transition: opacity var(--transition);
396
- }
397
- .btn-save:hover { opacity: 0.85; }
398
-
399
- /* Responsive */
400
- @media (max-width: 640px) {
401
- .sidebar { position: fixed; right: 0; top: 0; bottom: 0; z-index: 100; transform: translateX(0); }
402
- .sidebar.collapsed { transform: translateX(100%); width: var(--sidebar-w); min-width: var(--sidebar-w); border: none; }
403
- .msg-row { padding: 6px 12px; }
404
  }
405
  </style>
406
  </head>
407
  <body>
408
- <div class="app" id="app">
409
 
410
- <!-- SIDEBAR -->
411
  <aside class="sidebar" id="sidebar">
412
  <div class="sidebar-header">
413
- <div class="sidebar-logo">
414
- <img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" alt="Genisi" />
415
- <span class="sidebar-logo-name">Genisi AI</span>
416
  </div>
417
  </div>
418
-
419
- <button class="btn-new-chat" onclick="newChat()">
420
- <span>โœฆ</span> ู…ุญุงุฏุซุฉ ุฌุฏูŠุฏุฉ
421
- </button>
422
-
423
- <div class="sidebar-section-label">ุงู„ู…ุญุงุฏุซุงุช</div>
424
  <div class="chats-list" id="chats-list"></div>
425
-
426
- <div class="sidebar-footer">
427
- <button class="btn-icon" title="ุงู„ุฅุนุฏุงุฏุงุช" onclick="openSettings()">โš™</button>
428
- <button class="btn-icon" title="ุชุจุฏูŠู„ ุงู„ุซูŠู…" id="theme-toggle-btn" onclick="toggleTheme()">๐ŸŒ™</button>
429
  </div>
430
  </aside>
431
 
432
- <!-- MAIN -->
433
  <main class="main">
434
- <!-- TOPBAR -->
435
  <div class="topbar">
436
- <button class="btn-icon" onclick="toggleSidebar()" title="ุงู„ู‚ุงุฆู…ุฉ">โ˜ฐ</button>
437
  <div class="topbar-title" id="topbar-title">Genisi AI</div>
438
  </div>
439
 
440
- <!-- CHAT -->
441
  <div class="chat-area" id="chat-area">
442
- <div class="welcome-screen" id="welcome-screen">
443
- <img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" class="welcome-logo" alt="Genisi"/>
444
- <div class="welcome-title">ู…ุฑุญุจู‹ุง ุจูƒ ููŠ Genisi</div>
445
- <div class="welcome-sub">ู…ุณุงุนุฏูƒ ุงู„ุฐูƒูŠ ู…ู† AnesNT โ€” ูˆู„ุงูŠุฉ ุจุงุชู†ุฉ ๐Ÿ‡ฉ๐Ÿ‡ฟ</div>
446
- <div class="welcome-chips">
447
  <div class="chip" onclick="quickSend('ู…ุง ู‡ูˆ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠุŸ')">ู…ุง ู‡ูˆ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠุŸ</div>
448
- <div class="chip" onclick="quickSend('ุณุงุนุฏู†ูŠ ููŠ ูƒุชุงุจุฉ ูƒูˆุฏ Python')">ุณุงุนุฏู†ูŠ ููŠ ูƒุชุงุจุฉ ูƒูˆุฏ Python</div>
449
- <div class="chip" onclick="quickSend('ู„ุฎู‘ุต ู„ูŠ ู‡ุฐุง ุงู„ู…ูˆุถูˆุน')">ู„ุฎู‘ุต ู…ูˆุถูˆุนู‹ุง</div>
450
  <div class="chip" onclick="quickSend('ู…ุง ุงู„ูุฑู‚ ุจูŠู† ู†ู…ุงุฐุฌ LLMุŸ')">ุงู„ูุฑู‚ ุจูŠู† ู†ู…ุงุฐุฌ LLM</div>
451
  </div>
452
  </div>
453
  </div>
454
 
455
- <!-- INPUT -->
456
  <div class="input-area">
457
  <div class="file-chips" id="file-chips"></div>
458
  <div class="input-box">
459
- <div class="input-actions">
460
- <button class="btn-attach" onclick="document.getElementById('file-input').click()" title="ุฑูุน ู…ู„ู">๐Ÿ“Ž</button>
461
- <input type="file" id="file-input" multiple accept="*/*" onchange="handleFiles(this.files)" />
462
  </div>
463
- <textarea
464
- id="msg-input"
465
- placeholder="ุงูƒุชุจ ุฑุณุงู„ุชูƒ ู‡ู†ุง..."
466
- rows="1"
467
- onkeydown="handleKey(event)"
468
- oninput="autoResize(this)"
469
- ></textarea>
470
  <button class="btn-send" id="send-btn" onclick="sendMessage()">โžค</button>
471
  </div>
472
  </div>
473
  </main>
474
  </div>
475
 
476
- <!-- SETTINGS MODAL -->
477
- <div class="modal-overlay" id="settings-modal">
478
  <div class="modal">
479
- <div class="modal-header">
480
  <span style="font-size:1.3rem">โš™</span>
481
- <div class="modal-title">ุงู„ุฅุนุฏุงุฏุงุช</div>
482
- <button class="modal-close" onclick="closeSettings()">โœ•</button>
483
  </div>
484
-
485
- <div class="setting-row">
486
- <span class="setting-label">ุงู„ูˆุถุน ุงู„ู„ูŠู„ูŠ / ุงู„ู†ู‡ุงุฑูŠ</span>
487
  <label class="toggle">
488
- <input type="checkbox" id="dark-toggle" onchange="applyThemeFromToggle()" />
489
- <span class="toggle-slider"></span>
490
  </label>
491
  </div>
492
-
493
- <div class="setting-row" style="flex-direction:column;align-items:flex-start;gap:4px">
494
- <span class="setting-label">ุนู†ูˆุงู† ุงู„ุฎุงุฏู… (Backend URL)</span>
495
- <input type="text" class="setting-api-input" id="backend-url" placeholder="http://localhost:8000" />
 
 
496
  </div>
497
-
498
- <div class="setting-row">
499
- <span class="setting-label">ุญุฐู ุฌู…ูŠุน ุงู„ู…ุญุงุฏุซุงุช</span>
500
- <button class="btn-icon" style="color:var(--danger)" onclick="clearAllChats()">๐Ÿ—‘</button>
 
 
501
  </div>
502
-
503
- <button class="btn-save" onclick="saveSettings()">ุญูุธ ุงู„ุฅุนุฏุงุฏุงุช</button>
504
  </div>
505
  </div>
506
 
507
  <script>
508
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
509
- // STATE
510
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
511
- let chats = JSON.parse(localStorage.getItem('genisi_chats') || '[]');
512
- let activeChatId = null;
513
- let pendingFiles = [];
514
- let isLoading = false;
515
-
516
- const BACKEND_URL_KEY = 'genisi_backend_url';
517
- const THEME_KEY = 'genisi_theme';
518
-
519
- // โ”€โ”€ Helpers โ”€โ”€
520
- function genId() { return Date.now().toString(36) + Math.random().toString(36).slice(2,7); }
521
- function getBackendUrl() { return localStorage.getItem(BACKEND_URL_KEY) || 'http://localhost:8000'; }
522
- function saveChats() { localStorage.setItem('genisi_chats', JSON.stringify(chats)); }
523
- function getActiveChat() { return chats.find(c => c.id === activeChatId); }
524
-
525
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
526
- // THEME
527
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
528
- function initTheme() {
529
- const t = localStorage.getItem(THEME_KEY) || 'dark';
530
- document.documentElement.setAttribute('data-theme', t);
531
- const isDark = t === 'dark';
532
- document.getElementById('theme-toggle-btn').textContent = isDark ? '๐ŸŒ™' : 'โ˜€๏ธ';
533
- const toggle = document.getElementById('dark-toggle');
534
- if (toggle) toggle.checked = isDark;
535
- }
536
- function toggleTheme() {
537
- const current = document.documentElement.getAttribute('data-theme');
538
- const next = current === 'dark' ? 'light' : 'dark';
539
- document.documentElement.setAttribute('data-theme', next);
540
- localStorage.setItem(THEME_KEY, next);
541
- document.getElementById('theme-toggle-btn').textContent = next === 'dark' ? '๐ŸŒ™' : 'โ˜€๏ธ';
542
- const toggle = document.getElementById('dark-toggle');
543
- if (toggle) toggle.checked = next === 'dark';
544
  }
545
- function applyThemeFromToggle() {
546
- const isDark = document.getElementById('dark-toggle').checked;
547
- const next = isDark ? 'dark' : 'light';
548
- document.documentElement.setAttribute('data-theme', next);
549
- localStorage.setItem(THEME_KEY, next);
550
- document.getElementById('theme-toggle-btn').textContent = isDark ? '๐ŸŒ™' : 'โ˜€๏ธ';
 
 
 
 
551
  }
552
 
553
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
554
- // SIDEBAR
555
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
556
- function toggleSidebar() {
557
- document.getElementById('sidebar').classList.toggle('collapsed');
 
 
 
558
  }
559
- function renderChatList() {
560
- const list = document.getElementById('chats-list');
561
- list.innerHTML = '';
562
- const sorted = [...chats].reverse();
563
- sorted.forEach(chat => {
564
- const div = document.createElement('div');
565
- div.className = 'chat-item' + (chat.id === activeChatId ? ' active' : '');
566
- div.innerHTML = `
567
- <span>๐Ÿ’ฌ</span>
568
- <span class="chat-title">${escapeHtml(chat.title || 'ู…ุญุงุฏุซุฉ')}</span>
569
- <button class="del-btn" onclick="deleteChat('${chat.id}',event)" title="ุญุฐู">โœ•</button>
570
- `;
571
- div.addEventListener('click', () => loadChat(chat.id));
 
 
572
  list.appendChild(div);
573
  });
574
  }
575
 
576
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
577
- // CHAT MANAGEMENT
578
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
579
- function newChat() {
580
- activeChatId = null;
581
- pendingFiles = [];
582
- document.getElementById('file-chips').innerHTML = '';
583
- document.getElementById('msg-input').value = '';
584
- document.getElementById('topbar-title').textContent = 'Genisi AI';
585
- const area = document.getElementById('chat-area');
586
- area.innerHTML = `
587
- <div class="welcome-screen" id="welcome-screen">
588
- <img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" class="welcome-logo" alt="Genisi"/>
589
- <div class="welcome-title">ู…ุฑุญุจู‹ุง ุจูƒ ููŠ Genisi</div>
590
- <div class="welcome-sub">ู…ุณุงุนุฏูƒ ุงู„ุฐูƒูŠ ู…ู† AnesNT โ€” ูˆู„ุงูŠุฉ ุจุงุชู†ุฉ ๐Ÿ‡ฉ๐Ÿ‡ฟ</div>
591
- <div class="welcome-chips">
592
  <div class="chip" onclick="quickSend('ู…ุง ู‡ูˆ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠุŸ')">ู…ุง ู‡ูˆ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠุŸ</div>
593
- <div class="chip" onclick="quickSend('ุณุงุนุฏู†ูŠ ููŠ ูƒุชุงุจุฉ ูƒูˆุฏ Python')">ุณุงุนุฏู†ูŠ ููŠ ูƒุชุงุจุฉ ูƒูˆุฏ Python</div>
594
- <div class="chip" onclick="quickSend('ู„ุฎู‘ุต ู„ูŠ ู‡ุฐุง ุงู„ู…ูˆุถูˆุน')">ู„ุฎู‘ุต ู…ูˆุถูˆุนู‹ุง</div>
595
  <div class="chip" onclick="quickSend('ู…ุง ุงู„ูุฑู‚ ุจูŠู† ู†ู…ุงุฐุฌ LLMุŸ')">ุงู„ูุฑู‚ ุจูŠู† ู†ู…ุงุฐุฌ LLM</div>
596
  </div>
597
  </div>`;
598
  renderChatList();
599
  }
600
 
601
- function loadChat(id) {
602
- activeChatId = id;
603
- const chat = getActiveChat();
604
- if (!chat) return;
605
- document.getElementById('topbar-title').textContent = chat.title || 'ู…ุญุงุฏุซุฉ';
606
- const area = document.getElementById('chat-area');
607
- area.innerHTML = '';
608
- chat.history.forEach(entry => {
609
- appendBubble('user', entry.user, entry.files || []);
610
- appendBubble('bot', entry.bot);
611
  });
612
- area.scrollTop = area.scrollHeight;
613
  renderChatList();
614
  }
615
 
616
- function deleteChat(id, e) {
617
  e.stopPropagation();
618
- chats = chats.filter(c => c.id !== id);
619
- saveChats();
620
- if (activeChatId === id) newChat();
621
- else renderChatList();
622
  }
623
-
624
- function clearAllChats() {
625
- if (!confirm('ู‡ู„ ุฃู†ุช ู…ุชุฃูƒุฏ ู…ู† ุญุฐู ุฌู…ูŠุน ุงู„ู…ุญุงุฏุซุงุชุŸ')) return;
626
- chats = [];
627
- saveChats();
628
- newChat();
629
- closeSettings();
630
  }
631
 
632
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
633
- // FILES
634
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
635
- function handleFiles(files) {
636
- Array.from(files).forEach(f => {
637
- pendingFiles.push(f);
638
- });
639
  renderFileChips();
 
640
  }
641
- function renderFileChips() {
642
- const c = document.getElementById('file-chips');
643
- c.innerHTML = '';
644
- pendingFiles.forEach((f, i) => {
645
- const chip = document.createElement('div');
646
- chip.className = 'file-chip';
647
- chip.innerHTML = `<span>${fileIcon(f.name)}</span><span>${escapeHtml(f.name)}</span><button onclick="removeFile(${i})">โœ•</button>`;
648
  c.appendChild(chip);
649
  });
650
  }
651
- function removeFile(i) {
652
- pendingFiles.splice(i, 1);
653
- renderFileChips();
654
- }
655
- function fileIcon(name) {
656
- if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(name)) return '๐Ÿ–ผ';
657
- if (/\.(pdf)$/i.test(name)) return '๐Ÿ“„';
658
- if (/\.(py|js|ts|html|css|json)$/i.test(name)) return '๐Ÿ’ป';
659
- if (/\.(txt|md)$/i.test(name)) return '๐Ÿ“';
660
- return '๐Ÿ“Ž';
661
- }
662
-
663
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
664
- // MESSAGING
665
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
666
- function quickSend(text) {
667
- document.getElementById('msg-input').value = text;
668
- sendMessage();
669
- }
670
-
671
- function handleKey(e) {
672
- if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
673
- }
674
-
675
- function autoResize(el) {
676
- el.style.height = 'auto';
677
- el.style.height = Math.min(el.scrollHeight, 160) + 'px';
678
- }
679
-
680
- async function sendMessage() {
681
- if (isLoading) return;
682
- const input = document.getElementById('msg-input');
683
- const text = input.value.trim();
684
- if (!text && pendingFiles.length === 0) return;
685
-
686
- // Hide welcome screen
687
- const welcome = document.getElementById('welcome-screen');
688
- if (welcome) welcome.remove();
689
-
690
- // Copy files
691
- const filesForMsg = [...pendingFiles];
692
- pendingFiles = [];
693
- document.getElementById('file-chips').innerHTML = '';
694
- input.value = '';
695
- input.style.height = 'auto';
696
-
697
- // Ensure active chat
698
- if (!activeChatId) {
699
- const id = genId();
700
- const title = text.slice(0, 36) || filesForMsg[0]?.name || 'ู…ุญุงุฏุซุฉ';
701
- activeChatId = id;
702
- chats.push({ id, title, history: [] });
703
- document.getElementById('topbar-title').textContent = title;
704
  }
705
 
706
- // User bubble
707
- appendBubble('user', text, filesForMsg);
708
 
709
- // Show typing
710
- const typingId = 'typing-' + genId();
711
  appendTyping(typingId);
712
-
713
- isLoading = true;
714
- document.getElementById('send-btn').disabled = true;
715
 
716
  try {
717
- const chat = getActiveChat();
718
- const history = chat ? chat.history : [];
719
-
720
- // Read file contents as base64 or text
721
- const filePayloads = await Promise.all(filesForMsg.map(async f => {
722
- return { name: f.name, size: f.size, type: f.type };
 
723
  }));
724
 
725
- const resp = await fetch(getBackendUrl() + '/chat', {
726
- method: 'POST',
727
- headers: { 'Content-Type': 'application/json' },
728
- body: JSON.stringify({
729
- message: text + (filePayloads.length ? `\n[ู…ู„ูุงุช ู…ุฑูู‚ุฉ: ${filePayloads.map(f=>f.name).join(', ')}]` : ''),
730
- history
731
- })
732
  });
733
 
734
- if (!resp.ok) throw new Error(`ุฎุทุฃ ู…ู† ุงู„ุฎุงุฏู…: ${resp.status}`);
735
- const data = await resp.json();
736
- const botText = data.response || 'โš  ู„ุง ูŠูˆุฌุฏ ุฑุฏ';
 
737
 
738
- removeTyping(typingId);
739
- appendBubble('bot', botText);
740
 
741
- // Save to history
742
- if (chat) {
743
- chat.history.push({ user: text, bot: botText, files: filesForMsg.map(f=>f.name) });
744
- saveChats();
745
- }
746
- } catch (err) {
747
  removeTyping(typingId);
748
- appendBubble('bot', `โš  **ุฎุทุฃ:** ${err.message}\n\n*ุชุฃูƒุฏ ู…ู† ุชุดุบูŠู„ ุงู„ุฎุงุฏู… ูˆุถุจุท ุงู„ุนู†ูˆุงู† ููŠ ุงู„ุฅุนุฏุงุฏุงุช.*`);
749
- }
750
-
751
- isLoading = false;
752
- document.getElementById('send-btn').disabled = false;
753
- renderChatList();
754
- }
755
 
756
- function appendBubble(role, text, files) {
757
- const area = document.getElementById('chat-area');
758
- const row = document.createElement('div');
759
- row.className = `msg-row ${role}`;
760
-
761
- const avatar = document.createElement('div');
762
- avatar.className = 'msg-avatar';
763
- if (role === 'bot') {
764
- avatar.innerHTML = `<img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" alt="Genisi"/>`;
765
- } else {
766
- avatar.textContent = '๐Ÿ‘ค';
767
- }
768
 
769
- const bubble = document.createElement('div');
770
- bubble.className = 'bubble';
771
-
772
- // File previews
773
- if (files && files.length) {
774
- const names = Array.isArray(files) && typeof files[0] === 'string' ? files : files.map(f => f.name || f);
775
- names.forEach(name => {
776
- const fp = document.createElement('div');
777
- fp.className = 'file-preview-bubble';
778
- fp.innerHTML = `<span class="file-icon">${fileIcon(name)}</span><span>${escapeHtml(name)}</span>`;
779
- bubble.appendChild(fp);
780
- });
781
- }
782
 
783
- if (text) {
784
- const content = document.createElement('div');
785
- content.innerHTML = formatText(text);
786
- bubble.appendChild(content);
 
 
787
  }
788
 
789
- row.appendChild(avatar);
790
- row.appendChild(bubble);
791
- area.appendChild(row);
792
- area.scrollTop = area.scrollHeight;
793
  }
794
 
795
- function appendTyping(id) {
796
- const area = document.getElementById('chat-area');
797
- const row = document.createElement('div');
798
- row.className = 'msg-row bot';
799
- row.id = id;
800
- row.innerHTML = `
801
- <div class="msg-avatar"><img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" alt="Genisi"/></div>
802
- <div class="bubble"><div class="typing-indicator"><span></span><span></span><span></span></div></div>`;
803
- area.appendChild(row);
804
- area.scrollTop = area.scrollHeight;
805
- }
806
- function removeTyping(id) {
807
- const el = document.getElementById(id);
808
- if (el) el.remove();
 
 
 
809
  }
810
 
811
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
812
- // TEXT FORMATTING
813
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
814
- function formatText(text) {
815
- // Code blocks
816
- text = text.replace(/```([\s\S]*?)```/g, (_, c) => `<pre>${escapeHtml(c.trim())}</pre>`);
817
- // Inline code
818
- text = text.replace(/`([^`]+)`/g, (_, c) => `<code>${escapeHtml(c)}</code>`);
819
- // Bold
820
- text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
821
- // Italic
822
- text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
823
- // Line breaks
824
- text = text.split('\n').map(l => l ? `<p>${l}</p>` : '').join('');
825
- return text;
826
  }
827
- function escapeHtml(t) {
828
- return String(t).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
 
 
 
 
 
 
829
  }
830
 
831
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
832
- // SETTINGS MODAL
833
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
834
- function openSettings() {
835
  document.getElementById('settings-modal').classList.add('open');
836
- document.getElementById('backend-url').value = getBackendUrl();
837
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
838
- document.getElementById('dark-toggle').checked = isDark;
839
- }
840
- function closeSettings() {
841
- document.getElementById('settings-modal').classList.remove('open');
842
  }
843
- function saveSettings() {
844
- const url = document.getElementById('backend-url').value.trim();
845
- if (url) localStorage.setItem(BACKEND_URL_KEY, url);
846
  closeSettings();
847
  }
848
- document.getElementById('settings-modal').addEventListener('click', e => {
849
- if (e.target === e.currentTarget) closeSettings();
850
- });
851
 
852
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
853
- // INIT
854
- // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
855
  initTheme();
856
  renderChatList();
857
  </script>
 
7
  <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
8
  <style>
9
  :root {
10
+ --bg: #0d0f14; --surface: #161920; --surface2: #1e2230; --surface3: #252a3a;
11
+ --border: #2a2f42; --border2: #333a52;
12
+ --accent: #4f8ef7; --accent2: #7c5cf7; --accent-soft: rgba(79,142,247,0.12);
13
+ --text: #e8eaf2; --text2: #9aa3be; --text3: #5c6480;
14
+ --user-bubble: #1a2340; --bot-bubble: #161920;
15
+ --danger: #f75f5f; --radius: 18px; --radius-sm: 10px;
16
+ --sidebar-w: 280px; --tr: 0.22s cubic-bezier(.4,0,.2,1);
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  }
18
  [data-theme="light"] {
19
+ --bg: #f0f2f8; --surface: #fff; --surface2: #f5f6fa; --surface3: #ebedf5;
20
+ --border: #d8dce8; --border2: #c5cad8;
21
+ --text: #1a1d2e; --text2: #4a5070; --text3: #8a90a8;
22
+ --user-bubble: #dde6ff; --bot-bubble: #fff; --accent-soft: rgba(79,142,247,0.1);
23
+ }
24
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
25
+ html,body{height:100%;font-family:'Cairo',sans-serif;background:var(--bg);color:var(--text)}
26
+ ::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:transparent}
27
+ ::-webkit-scrollbar-thumb{background:var(--border2);border-radius:99px}
28
+
29
+ .app{display:flex;height:100vh;overflow:hidden}
30
+
31
+ /* SIDEBAR */
32
+ .sidebar{width:var(--sidebar-w);min-width:var(--sidebar-w);background:var(--surface);
33
+ border-left:1px solid var(--border);display:flex;flex-direction:column;
34
+ transition:width var(--tr),min-width var(--tr);overflow:hidden}
35
+ .sidebar.collapsed{width:0;min-width:0;border:none}
36
+ .sidebar-header{padding:18px 16px 12px;display:flex;align-items:center;gap:10px;border-bottom:1px solid var(--border)}
37
+ .s-logo{display:flex;align-items:center;gap:10px;flex:1}
38
+ .s-logo img{width:32px;height:32px;border-radius:8px;object-fit:cover}
39
+ .s-logo-name{font-size:1.1rem;font-weight:700;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
40
+ .btn-new{margin:12px 12px 4px;padding:10px 14px;background:linear-gradient(135deg,var(--accent),var(--accent2));
41
+ color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-family:'Cairo',sans-serif;
42
+ font-size:.9rem;font-weight:600;display:flex;align-items:center;gap:8px;
43
+ transition:opacity var(--tr),transform var(--tr)}
44
+ .btn-new:hover{opacity:.88;transform:translateY(-1px)}
45
+ .s-label{padding:10px 16px 4px;font-size:.72rem;font-weight:600;color:var(--text3);letter-spacing:.06em;text-transform:uppercase}
46
+ .chats-list{flex:1;overflow-y:auto;padding:4px 8px;display:flex;flex-direction:column;gap:2px}
47
+ .chat-item{padding:9px 12px;border-radius:var(--radius-sm);cursor:pointer;font-size:.87rem;color:var(--text2);
48
+ display:flex;align-items:center;gap:8px;transition:background var(--tr),color var(--tr);
49
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
50
+ .chat-item:hover{background:var(--surface2);color:var(--text)}
51
+ .chat-item.active{background:var(--accent-soft);color:var(--accent);font-weight:600}
52
+ .chat-item .ct{flex:1;overflow:hidden;text-overflow:ellipsis}
53
+ .del-btn{opacity:0;background:none;border:none;color:var(--danger);cursor:pointer;font-size:.85rem;
54
+ padding:2px 5px;border-radius:5px;transition:opacity var(--tr)}
55
+ .chat-item:hover .del-btn{opacity:1}
56
+ .s-footer{padding:12px;border-top:1px solid var(--border);display:flex;align-items:center;gap:10px}
57
+ .btn-icon{width:36px;height:36px;background:var(--surface2);border:1px solid var(--border);
58
+ border-radius:var(--radius-sm);color:var(--text2);cursor:pointer;display:flex;
59
+ align-items:center;justify-content:center;font-size:1rem;transition:background var(--tr),color var(--tr)}
60
+ .btn-icon:hover{background:var(--surface3);color:var(--text)}
61
+
62
+ /* MAIN */
63
+ .main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
64
+ .topbar{height:58px;padding:0 18px;display:flex;align-items:center;gap:12px;
65
+ border-bottom:1px solid var(--border);background:var(--surface);flex-shrink:0}
66
+ .topbar-title{flex:1;font-size:.95rem;font-weight:600;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
67
+
68
+ /* CHAT */
69
+ .chat-area{flex:1;overflow-y:auto;padding:28px 0;display:flex;flex-direction:column}
70
+ .welcome{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
71
+ gap:16px;padding:40px 24px;text-align:center}
72
+ .w-logo{width:72px;height:72px;border-radius:18px;object-fit:cover;box-shadow:0 8px 32px rgba(79,142,247,.25)}
73
+ .w-title{font-size:2rem;font-weight:700;background:linear-gradient(135deg,var(--accent),var(--accent2));
74
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent}
75
+ .w-sub{color:var(--text3);font-size:.95rem}
76
+ .chips{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:8px;max-width:520px}
77
+ .chip{padding:8px 16px;background:var(--surface2);border:1px solid var(--border);border-radius:99px;
78
+ font-size:.83rem;color:var(--text2);cursor:pointer;transition:background var(--tr),border-color var(--tr),color var(--tr)}
79
+ .chip:hover{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}
80
+
81
+ /* MESSAGES */
82
+ .msg-row{display:flex;padding:5px 28px;gap:12px;animation:fadeUp .28s ease}
83
+ @keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
84
+ .msg-row.user{flex-direction:row-reverse}
85
+ .msg-av{width:34px;height:34px;border-radius:10px;flex-shrink:0;display:flex;align-items:center;
86
+ justify-content:center;font-size:1rem;font-weight:700;margin-top:2px;overflow:hidden}
87
+ .msg-row.bot .msg-av{background:linear-gradient(135deg,var(--accent),var(--accent2))}
88
+ .msg-row.bot .msg-av img{width:100%;height:100%;object-fit:cover}
89
+ .msg-row.user .msg-av{background:var(--surface3);color:var(--text2)}
90
+ .bubble{max-width:min(640px,78vw);padding:12px 16px;border-radius:var(--radius);
91
+ font-size:.93rem;line-height:1.72;word-break:break-word}
92
+ .msg-row.bot .bubble{background:var(--bot-bubble);border:1px solid var(--border);border-top-right-radius:4px}
93
+ .msg-row.user .bubble{background:var(--user-bubble);border:1px solid var(--border2);border-top-left-radius:4px}
94
+ .bubble pre{background:var(--surface3);border-radius:8px;padding:12px;overflow-x:auto;
95
+ font-family:'JetBrains Mono',monospace;font-size:.82rem;margin:8px 0;border:1px solid var(--border)}
96
+ .bubble code{font-family:'JetBrains Mono',monospace;font-size:.83rem;background:var(--surface3);padding:2px 5px;border-radius:4px}
97
+ .bubble p{margin-bottom:6px}.bubble p:last-child{margin-bottom:0}
98
+ .bubble ul,.bubble ol{padding-right:20px;margin:6px 0}.bubble li{margin-bottom:4px}
99
+ .file-prev{display:flex;align-items:center;gap:8px;background:var(--surface3);border-radius:8px;
100
+ padding:8px 12px;margin-bottom:6px;font-size:.82rem;color:var(--text2)}
101
+
102
+ /* memory badge */
103
+ .mem-badge{font-size:.71rem;color:var(--text3);padding:0 28px 2px;display:flex;align-items:center;gap:4px}
104
+
105
+ /* typing */
106
+ .typing{display:flex;gap:4px;padding:4px 0;align-items:center}
107
+ .typing span{width:7px;height:7px;border-radius:50%;background:var(--accent);animation:bounce 1.2s infinite ease-in-out}
108
+ .typing span:nth-child(2){animation-delay:.2s}.typing span:nth-child(3){animation-delay:.4s}
109
+ @keyframes bounce{0%,80%,100%{transform:scale(.7);opacity:.5}40%{transform:scale(1);opacity:1}}
110
+
111
+ /* INPUT */
112
+ .input-area{padding:14px 20px 18px;background:var(--surface);border-top:1px solid var(--border);flex-shrink:0}
113
+ .file-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
114
+ .file-chip{display:flex;align-items:center;gap:6px;background:var(--surface2);border:1px solid var(--border);
115
+ border-radius:99px;padding:4px 10px 4px 8px;font-size:.8rem;color:var(--text2)}
116
+ .file-chip button{background:none;border:none;color:var(--text3);cursor:pointer;font-size:.85rem;padding:0;line-height:1}
117
+ .file-chip button:hover{color:var(--danger)}
118
+ .input-box{display:flex;align-items:flex-end;gap:10px;background:var(--surface2);
119
+ border:1.5px solid var(--border);border-radius:var(--radius);padding:10px 12px;
120
+ transition:border-color var(--tr)}
121
+ .input-box:focus-within{border-color:var(--accent)}
122
+ .input-box textarea{flex:1;background:none;border:none;outline:none;color:var(--text);
123
+ font-family:'Cairo',sans-serif;font-size:.93rem;resize:none;max-height:160px;line-height:1.6;direction:rtl}
124
+ .input-box textarea::placeholder{color:var(--text3)}
125
+ .in-actions{display:flex;align-items:center;gap:6px}
126
+ .btn-attach{background:none;border:none;color:var(--text3);cursor:pointer;font-size:1.15rem;
127
+ padding:4px;border-radius:6px;transition:color var(--tr);display:flex;align-items:center}
128
+ .btn-attach:hover{color:var(--accent)}
129
+ .btn-send{width:36px;height:36px;background:linear-gradient(135deg,var(--accent),var(--accent2));
130
+ border:none;border-radius:10px;cursor:pointer;color:#fff;display:flex;align-items:center;
131
+ justify-content:center;font-size:1rem;transition:opacity var(--tr),transform var(--tr)}
132
+ .btn-send:hover{opacity:.85;transform:scale(1.05)}
133
+ .btn-send:disabled{opacity:.4;cursor:not-allowed;transform:none}
134
+ #file-input{display:none}
135
+
136
+ /* MODAL */
137
+ .overlay{position:fixed;inset:0;z-index:999;background:rgba(0,0,0,.55);display:flex;
138
+ align-items:center;justify-content:center;backdrop-filter:blur(4px);
139
+ opacity:0;pointer-events:none;transition:opacity var(--tr)}
140
+ .overlay.open{opacity:1;pointer-events:all}
141
+ .modal{background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:28px;
142
+ width:min(440px,92vw);box-shadow:0 24px 60px rgba(0,0,0,.4);
143
+ transform:translateY(16px);transition:transform var(--tr)}
144
+ .overlay.open .modal{transform:translateY(0)}
145
+ .m-header{display:flex;align-items:center;gap:10px;margin-bottom:22px}
146
+ .m-title{font-size:1.1rem;font-weight:700;flex:1}
147
+ .m-close{background:none;border:none;color:var(--text3);cursor:pointer;font-size:1.2rem}
148
+ .s-row{display:flex;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:1px solid var(--border)}
149
+ .s-row:last-of-type{border-bottom:none}
150
+ .s-lbl{font-size:.9rem;color:var(--text2)}
151
+ .s-desc{font-size:.78rem;color:var(--text3);margin-top:2px}
152
+ .toggle{position:relative;width:42px;height:24px}
153
+ .toggle input{opacity:0;width:0;height:0}
154
+ .tslider{position:absolute;inset:0;background:var(--surface3);border-radius:99px;cursor:pointer;transition:background var(--tr)}
155
+ .tslider::before{content:'';position:absolute;width:18px;height:18px;border-radius:50%;
156
+ background:#fff;top:3px;left:3px;transition:transform var(--tr)}
157
+ .toggle input:checked+.tslider{background:var(--accent)}
158
+ .toggle input:checked+.tslider::before{transform:translateX(18px)}
159
+ .s-input{width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:8px;
160
+ padding:8px 12px;color:var(--text);font-size:.85rem;font-family:'JetBrains Mono',monospace;
161
+ outline:none;margin-top:8px;direction:ltr}
162
+ .s-input:focus{border-color:var(--accent)}
163
+ .s-col{display:flex;flex-direction:column;width:100%}
164
+ .btn-save{width:100%;margin-top:16px;padding:11px;background:linear-gradient(135deg,var(--accent),var(--accent2));
165
+ color:#fff;border:none;border-radius:10px;cursor:pointer;font-family:'Cairo',sans-serif;
166
+ font-size:.95rem;font-weight:600;transition:opacity var(--tr)}
167
+ .btn-save:hover{opacity:.85}
168
+ .btn-danger{padding:6px 14px;background:rgba(247,95,95,.12);border:1px solid var(--danger);
169
+ color:var(--danger);border-radius:8px;cursor:pointer;font-size:.83rem;font-family:'Cairo',sans-serif;
170
+ transition:background var(--tr)}
171
+ .btn-danger:hover{background:rgba(247,95,95,.22)}
172
+
173
+ @media(max-width:640px){
174
+ .sidebar{position:fixed;right:0;top:0;bottom:0;z-index:100}
175
+ .sidebar.collapsed{transform:translateX(100%);width:var(--sidebar-w);min-width:var(--sidebar-w)}
176
+ .msg-row{padding:5px 12px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  }
178
  </style>
179
  </head>
180
  <body>
181
+ <div class="app">
182
 
 
183
  <aside class="sidebar" id="sidebar">
184
  <div class="sidebar-header">
185
+ <div class="s-logo">
186
+ <img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" alt="Genisi"/>
187
+ <span class="s-logo-name">Genisi AI</span>
188
  </div>
189
  </div>
190
+ <button class="btn-new" onclick="newChat()">โœฆ ู…ุญุงุฏุซุฉ ุฌุฏูŠุฏุฉ</button>
191
+ <div class="s-label">ุงู„ู…ุญุงุฏุซุงุช</div>
 
 
 
 
192
  <div class="chats-list" id="chats-list"></div>
193
+ <div class="s-footer">
194
+ <button class="btn-icon" onclick="openSettings()">โš™</button>
195
+ <button class="btn-icon" id="theme-btn" onclick="toggleTheme()">๐ŸŒ™</button>
 
196
  </div>
197
  </aside>
198
 
 
199
  <main class="main">
 
200
  <div class="topbar">
201
+ <button class="btn-icon" onclick="toggleSidebar()">โ˜ฐ</button>
202
  <div class="topbar-title" id="topbar-title">Genisi AI</div>
203
  </div>
204
 
 
205
  <div class="chat-area" id="chat-area">
206
+ <div class="welcome" id="welcome">
207
+ <img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" class="w-logo" alt="Genisi"/>
208
+ <div class="w-title">ู…ุฑุญุจู‹ุง ููŠ Genisi</div>
209
+ <div class="w-sub">ู…ุณุงุนุฏูƒ ุงู„ุฐูƒูŠ ู…ู† AnesNT โ€” ูˆู„ุงูŠุฉ ุจุงุชู†ุฉ ๐Ÿ‡ฉ๐Ÿ‡ฟ</div>
210
+ <div class="chips">
211
  <div class="chip" onclick="quickSend('ู…ุง ู‡ูˆ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠุŸ')">ู…ุง ู‡ูˆ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠุŸ</div>
212
+ <div class="chip" onclick="quickSend('ุณุงุนุฏู†ูŠ ููŠ ูƒุชุงุจุฉ ูƒูˆุฏ Python')">ุณุงุนุฏู†ูŠ ููŠ ูƒูˆุฏ Python</div>
213
+ <div class="chip" onclick="quickSend('ู„ุฎู‘ุต ู„ูŠ ู…ูˆุถูˆุนู‹ุง')">ู„ุฎู‘ุต ู…ูˆุถูˆุนู‹ุง</div>
214
  <div class="chip" onclick="quickSend('ู…ุง ุงู„ูุฑู‚ ุจูŠู† ู†ู…ุงุฐุฌ LLMุŸ')">ุงู„ูุฑู‚ ุจูŠู† ู†ู…ุงุฐุฌ LLM</div>
215
  </div>
216
  </div>
217
  </div>
218
 
 
219
  <div class="input-area">
220
  <div class="file-chips" id="file-chips"></div>
221
  <div class="input-box">
222
+ <div class="in-actions">
223
+ <button class="btn-attach" onclick="document.getElementById('file-input').click()">๐Ÿ“Ž</button>
224
+ <input type="file" id="file-input" multiple onchange="handleFiles(this.files)"/>
225
  </div>
226
+ <textarea id="msg-input"
227
+ placeholder="ุงูƒุชุจ ุฑุณุงู„ุชูƒ... (Enter ู„ู„ุฅุฑุณุงู„ุŒ Shift+Enter ู„ุณุทุฑ ุฌุฏูŠุฏ)"
228
+ rows="1" onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
 
 
 
 
229
  <button class="btn-send" id="send-btn" onclick="sendMessage()">โžค</button>
230
  </div>
231
  </div>
232
  </main>
233
  </div>
234
 
235
+ <!-- SETTINGS -->
236
+ <div class="overlay" id="settings-modal">
237
  <div class="modal">
238
+ <div class="m-header">
239
  <span style="font-size:1.3rem">โš™</span>
240
+ <div class="m-title">ุงู„ุฅุนุฏุงุฏุงุช</div>
241
+ <button class="m-close" onclick="closeSettings()">โœ•</button>
242
  </div>
243
+ <div class="s-row">
244
+ <div class="s-lbl">ุงู„ูˆุถุน ุงู„ู„ูŠู„ูŠ</div>
 
245
  <label class="toggle">
246
+ <input type="checkbox" id="dark-toggle" onchange="applyThemeToggle()"/>
247
+ <span class="tslider"></span>
248
  </label>
249
  </div>
250
+ <div class="s-row" style="flex-direction:column;align-items:flex-start">
251
+ <div class="s-col">
252
+ <div class="s-lbl">ุนู†ูˆุงู† ุงู„ุฎุงุฏู… (Backend URL)</div>
253
+ <div class="s-desc">ุงุชุฑูƒู‡ ูุงุฑุบู‹ุง ุฅุฐุง ูƒุงู†ุช ุงู„ูˆุงุฌู‡ุฉ ูˆุงู„ู€ API ููŠ ู†ูุณ ุงู„ู€ Space</div>
254
+ <input type="text" class="s-input" id="backend-url" placeholder="https://username-space-name.hf.space"/>
255
+ </div>
256
  </div>
257
+ <div class="s-row" style="flex-direction:column;align-items:flex-start;gap:8px">
258
+ <div>
259
+ <div class="s-lbl">ุญุฐู ุฌู…ูŠุน ุงู„ู…ุญุงุฏุซุงุช</div>
260
+ <div class="s-desc">ู„ุง ูŠู…ูƒู† ุงู„ุชุฑุงุฌุน ุนู† ู‡ุฐุง ุงู„ุฅุฌุฑุงุก</div>
261
+ </div>
262
+ <button class="btn-danger" onclick="clearAllChats()">๐Ÿ—‘ ุญุฐู ุงู„ูƒู„</button>
263
  </div>
264
+ <button class="btn-save" onclick="saveSettings()">๐Ÿ’พ ุญูุธ</button>
 
265
  </div>
266
  </div>
267
 
268
  <script>
269
+ // โ•โ•โ•โ•โ•โ•โ•โ• KEYS โ•โ•โ•โ•โ•โ•โ•โ•
270
+ const KC = 'genisi_chats', KT = 'genisi_theme', KB = 'genisi_backend';
271
+
272
+ // โ•โ•โ•โ•โ•โ•โ•โ• STATE โ•โ•โ•โ•โ•โ•โ•โ•
273
+ let chats = JSON.parse(localStorage.getItem(KC) || '[]');
274
+ let activeChatId = null, pendingFiles = [], isLoading = false;
275
+
276
+ // โ•โ•โ•โ•โ•โ•โ•โ• UTILS โ•โ•โ•โ•โ•โ•โ•โ•
277
+ const genId = () => Date.now().toString(36) + Math.random().toString(36).slice(2,6);
278
+ const saveChats = () => localStorage.setItem(KC, JSON.stringify(chats));
279
+ const getChat = (id) => chats.find(c => c.id === (id ?? activeChatId));
280
+ const getBackend= () => (localStorage.getItem(KB) || '').replace(/\/$/, '');
281
+
282
+ function esc(t){ return String(t).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
283
+ function fileIcon(n){
284
+ if(/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(n)) return '๐Ÿ–ผ';
285
+ if(/\.pdf$/i.test(n)) return '๐Ÿ“„';
286
+ if(/\.(py|js|ts|html|css|json)$/i.test(n)) return '๐Ÿ’ป';
287
+ if(/\.(txt|md)$/i.test(n)) return '๐Ÿ“';
288
+ return '๐Ÿ“Ž';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  }
290
+ function fmt(raw){
291
+ let t = raw;
292
+ t = t.replace(/```(\w*)\n?([\s\S]*?)```/g,(_,l,c)=>`<pre><code>${esc(c.trim())}</code></pre>`);
293
+ t = t.replace(/`([^`]+)`/g,(_,c)=>`<code>${esc(c)}</code>`);
294
+ t = t.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>');
295
+ t = t.replace(/\*(.+?)\*/g,'<em>$1</em>');
296
+ t = t.replace(/^[-โ€ข]\s+(.+)$/gm,'<li>$1</li>');
297
+ t = t.replace(/(<li>[\s\S]*?<\/li>)+/g,m=>`<ul>${m}</ul>`);
298
+ t = t.split('\n').map(l=>l.startsWith('<')?l:(l.trim()?`<p>${l}</p>`:'')).join('');
299
+ return t;
300
  }
301
 
302
+ // โ•โ•โ•โ•โ•โ•โ•โ• THEME โ•โ•โ•โ•โ•โ•โ•โ•
303
+ function initTheme(){ applyTheme(localStorage.getItem(KT)||'dark'); }
304
+ function applyTheme(t){
305
+ document.documentElement.setAttribute('data-theme',t);
306
+ localStorage.setItem(KT,t);
307
+ document.getElementById('theme-btn').textContent = t==='dark'?'๐ŸŒ™':'โ˜€๏ธ';
308
+ const tog=document.getElementById('dark-toggle');
309
+ if(tog) tog.checked = t==='dark';
310
  }
311
+ function toggleTheme(){ applyTheme(document.documentElement.getAttribute('data-theme')==='dark'?'light':'dark'); }
312
+ function applyThemeToggle(){ applyTheme(document.getElementById('dark-toggle').checked?'dark':'light'); }
313
+
314
+ // โ•โ•โ•โ•โ•โ•โ•โ• SIDEBAR โ•โ•โ•โ•โ•โ•โ•โ•
315
+ function toggleSidebar(){ document.getElementById('sidebar').classList.toggle('collapsed'); }
316
+
317
+ function renderChatList(){
318
+ const list=document.getElementById('chats-list');
319
+ list.innerHTML='';
320
+ [...chats].reverse().forEach(chat=>{
321
+ const div=document.createElement('div');
322
+ div.className='chat-item'+(chat.id===activeChatId?' active':'');
323
+ div.innerHTML=`<span>๐Ÿ’ฌ</span><span class="ct">${esc(chat.title||'ู…ุญุงุฏุซุฉ')}</span>
324
+ <button class="del-btn" onclick="deleteChat('${chat.id}',event)">โœ•</button>`;
325
+ div.addEventListener('click',()=>loadChat(chat.id));
326
  list.appendChild(div);
327
  });
328
  }
329
 
330
+ // โ•โ•โ•โ•โ•โ•โ•โ• CHAT MANAGEMENT โ•โ•โ•โ•โ•โ•โ•โ•
331
+ function newChat(){
332
+ activeChatId=null; pendingFiles=[];
333
+ document.getElementById('file-chips').innerHTML='';
334
+ document.getElementById('msg-input').value='';
335
+ document.getElementById('msg-input').style.height='auto';
336
+ document.getElementById('topbar-title').textContent='Genisi AI';
337
+ document.getElementById('chat-area').innerHTML=`
338
+ <div class="welcome" id="welcome">
339
+ <img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" class="w-logo" alt="Genisi"/>
340
+ <div class="w-title">ู…ุฑุญุจู‹ุง ููŠ Genisi</div>
341
+ <div class="w-sub">ู…ุณุงุนุฏูƒ ุงู„ุฐูƒูŠ ู…ู† AnesNT โ€” ูˆู„ุงูŠุฉ ุจุงุชู†ุฉ ๐Ÿ‡ฉ๐Ÿ‡ฟ</div>
342
+ <div class="chips">
 
 
 
343
  <div class="chip" onclick="quickSend('ู…ุง ู‡ูˆ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠุŸ')">ู…ุง ู‡ูˆ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠุŸ</div>
344
+ <div class="chip" onclick="quickSend('ุณุงุนุฏู†ูŠ ููŠ ูƒุชุงุจุฉ ูƒูˆุฏ Python')">ุณุงุนุฏู†ูŠ ููŠ ูƒูˆุฏ Python</div>
345
+ <div class="chip" onclick="quickSend('ู„ุฎู‘ุต ู„ูŠ ู…ูˆุถูˆุนู‹ุง')">ู„ุฎู‘ุต ู…ูˆุถูˆุนู‹ุง</div>
346
  <div class="chip" onclick="quickSend('ู…ุง ุงู„ูุฑู‚ ุจูŠู† ู†ู…ุงุฐุฌ LLMุŸ')">ุงู„ูุฑู‚ ุจูŠู† ู†ู…ุงุฐุฌ LLM</div>
347
  </div>
348
  </div>`;
349
  renderChatList();
350
  }
351
 
352
+ function loadChat(id){
353
+ activeChatId=id;
354
+ const chat=getChat(id); if(!chat) return;
355
+ document.getElementById('topbar-title').textContent=chat.title||'ู…ุญุงุฏุซุฉ';
356
+ const area=document.getElementById('chat-area');
357
+ area.innerHTML='';
358
+ chat.history.forEach((entry,idx)=>{
359
+ appendBubble('user',entry.user,entry.files||[]);
360
+ appendBubble('bot',entry.bot);
361
+ appendMemBadge(idx+1, chat.history.length);
362
  });
363
+ area.scrollTop=area.scrollHeight;
364
  renderChatList();
365
  }
366
 
367
+ function deleteChat(id,e){
368
  e.stopPropagation();
369
+ chats=chats.filter(c=>c.id!==id); saveChats();
370
+ if(activeChatId===id) newChat(); else renderChatList();
 
 
371
  }
372
+ function clearAllChats(){
373
+ if(!confirm('ู‡ู„ ุฃู†ุช ู…ุชุฃูƒุฏุŸ ุณูŠุชู… ุญุฐู ุฌู…ูŠุน ุงู„ู…ุญุงุฏุซุงุช.')) return;
374
+ chats=[]; saveChats(); newChat(); closeSettings();
 
 
 
 
375
  }
376
 
377
+ // โ•โ•โ•โ•โ•โ•โ•โ• FILES โ•โ•โ•โ•โ•โ•โ•โ•
378
+ function handleFiles(files){
379
+ Array.from(files).forEach(f=>pendingFiles.push(f));
 
 
 
 
380
  renderFileChips();
381
+ document.getElementById('file-input').value='';
382
  }
383
+ function renderFileChips(){
384
+ const c=document.getElementById('file-chips'); c.innerHTML='';
385
+ pendingFiles.forEach((f,i)=>{
386
+ const chip=document.createElement('div'); chip.className='file-chip';
387
+ chip.innerHTML=`<span>${fileIcon(f.name)}</span><span>${esc(f.name)}</span><button onclick="removeFile(${i})">โœ•</button>`;
 
 
388
  c.appendChild(chip);
389
  });
390
  }
391
+ function removeFile(i){ pendingFiles.splice(i,1); renderFileChips(); }
392
+
393
+ // โ•โ•โ•โ•โ•โ•โ•โ• MESSAGING โ•โ•โ•โ•โ•โ•โ•โ•
394
+ function quickSend(text){ document.getElementById('msg-input').value=text; sendMessage(); }
395
+ function handleKey(e){ if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();} }
396
+ function autoResize(el){ el.style.height='auto'; el.style.height=Math.min(el.scrollHeight,160)+'px'; }
397
+
398
+ async function sendMessage(){
399
+ if(isLoading) return;
400
+ const input=document.getElementById('msg-input');
401
+ const text=input.value.trim();
402
+ if(!text&&pendingFiles.length===0) return;
403
+
404
+ const welcome=document.getElementById('welcome');
405
+ if(welcome) welcome.remove();
406
+
407
+ const filesForMsg=[...pendingFiles];
408
+ pendingFiles=[];
409
+ document.getElementById('file-chips').innerHTML='';
410
+ input.value=''; input.style.height='auto';
411
+
412
+ // ุฅู†ุดุงุก ู…ุญุงุฏุซุฉ ุฅู† ู„ู… ุชูƒู† ู…ูˆุฌูˆุฏุฉ
413
+ if(!activeChatId){
414
+ const id=genId();
415
+ const title=text.slice(0,38)||filesForMsg[0]?.name||'ู…ุญุงุฏุซุฉ';
416
+ activeChatId=id;
417
+ chats.push({id, title, history:[]});
418
+ document.getElementById('topbar-title').textContent=title;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  }
420
 
421
+ const chat=getChat();
422
+ appendBubble('user',text,filesForMsg);
423
 
424
+ const typingId='t-'+genId();
 
425
  appendTyping(typingId);
426
+ isLoading=true;
427
+ document.getElementById('send-btn').disabled=true;
 
428
 
429
  try {
430
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
431
+ // ู†ูุฑุณู„ ุงู„ุชุงุฑูŠุฎ ุงู„ูƒุงู…ู„ ู„ู„ู…ุญุงุฏุซุฉ ู…ุน ูƒู„ ุทู„ุจ
432
+ // ู‡ุฐุง ู‡ูˆ ุณุฑ "ุงู„ุฐุงูƒุฑุฉ" โ€” ุงู„ู†ู…ูˆุฐุฌ ูŠุฑู‰ ูƒู„ ู…ุง ู‚ูŠู„
433
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
434
+ const historyPayload = chat.history.map(e => ({
435
+ user: e.user,
436
+ bot: e.bot
437
  }));
438
 
439
+ const msgText = text + (filesForMsg.length
440
+ ? `\n[ู…ู„ูุงุช ู…ุฑูู‚ุฉ: ${filesForMsg.map(f=>f.name).join(', ')}]` : '');
441
+
442
+ const resp = await fetch(getBackend()+'/chat', {
443
+ method: 'POST',
444
+ headers: {'Content-Type':'application/json'},
445
+ body: JSON.stringify({ message: msgText, history: historyPayload })
446
  });
447
 
448
+ if(!resp.ok){
449
+ const err=await resp.text().catch(()=>resp.statusText);
450
+ throw new Error(`ุฎุทุฃ ${resp.status}: ${err}`);
451
+ }
452
 
453
+ const data=await resp.json();
454
+ const botText=data.response||'โš  ู„ุง ูŠูˆุฌุฏ ุฑุฏ ู…ู† ุงู„ุฎุงุฏู….';
455
 
 
 
 
 
 
 
456
  removeTyping(typingId);
457
+ appendBubble('bot',botText);
 
 
 
 
 
 
458
 
459
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
460
+ // ุญูุธ ููŠ localStorage = ุฐุงูƒุฑุฉ ุฏุงุฆู…ุฉ ุชุจู‚ู‰ ุญุชู‰ ุจุนุฏ
461
+ // ุฅุบู„ุงู‚ ุงู„ู…ุชุตูุญ ูˆุฅุนุงุฏุฉ ูุชุญ ุงู„ุตูุญุฉ
462
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
463
+ chat.history.push({ user:text, bot:botText, files:filesForMsg.map(f=>f.name) });
464
+ saveChats();
 
 
 
 
 
 
465
 
466
+ appendMemBadge(chat.history.length, chat.history.length);
 
 
 
 
 
 
 
 
 
 
 
 
467
 
468
+ } catch(err){
469
+ removeTyping(typingId);
470
+ appendBubble('bot',
471
+ `โš  **ุฎุทุฃ ููŠ ุงู„ุงุชุตุงู„ ุจุงู„ุฎุงุฏู…**\n\n${err.message}\n\n` +
472
+ `ุชุฃูƒุฏ ู…ู† ุฃู† ุงู„ู€ Space ูŠุนู…ู„ ูˆุฃู† ุงู„ู€ Dockerfile ุตุญูŠุญ.`
473
+ );
474
  }
475
 
476
+ isLoading=false;
477
+ document.getElementById('send-btn').disabled=false;
478
+ renderChatList();
 
479
  }
480
 
481
+ // โ•โ•โ•โ•โ•โ•โ•โ• DOM HELPERS โ•โ•โ•โ•โ•โ•โ•โ•
482
+ function appendBubble(role,text,files){
483
+ const area=document.getElementById('chat-area');
484
+ const row=document.createElement('div'); row.className=`msg-row ${role}`;
485
+ const av=document.createElement('div'); av.className='msg-av';
486
+ if(role==='bot'){
487
+ av.innerHTML=`<img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" alt="G"/>`;
488
+ } else { av.textContent='๐Ÿ‘ค'; }
489
+ const bub=document.createElement('div'); bub.className='bubble';
490
+ if(files&&files.length){
491
+ const names=files.map(f=>typeof f==='string'?f:f.name);
492
+ names.forEach(n=>{ const fp=document.createElement('div'); fp.className='file-prev';
493
+ fp.innerHTML=`<span>${fileIcon(n)}</span><span>${esc(n)}</span>`; bub.appendChild(fp); });
494
+ }
495
+ if(text){ const d=document.createElement('div'); d.innerHTML=fmt(text); bub.appendChild(d); }
496
+ row.appendChild(av); row.appendChild(bub);
497
+ area.appendChild(row); area.scrollTop=area.scrollHeight;
498
  }
499
 
500
+ function appendTyping(id){
501
+ const area=document.getElementById('chat-area');
502
+ const row=document.createElement('div'); row.className='msg-row bot'; row.id=id;
503
+ row.innerHTML=`<div class="msg-av"><img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" alt="G"/></div>
504
+ <div class="bubble"><div class="typing"><span></span><span></span><span></span></div></div>`;
505
+ area.appendChild(row); area.scrollTop=area.scrollHeight;
 
 
 
 
 
 
 
 
 
506
  }
507
+ function removeTyping(id){ const el=document.getElementById(id); if(el) el.remove(); }
508
+
509
+ // ุดุงุฑุฉ ุงู„ุฐุงูƒุฑุฉ โ€” ุชูุธู‡ุฑ ุนุฏุฏ ุฑุณุงุฆู„ ุงู„ุชุงุฑูŠุฎ ุงู„ู…ูุฑุณูŽู„ ู„ู„ู†ู…ูˆุฐุฌ
510
+ function appendMemBadge(count, total){
511
+ const area=document.getElementById('chat-area');
512
+ const b=document.createElement('div'); b.className='mem-badge';
513
+ b.innerHTML=`๐Ÿง  ุงู„ุฐุงูƒุฑุฉ: ${count}/${total} ${total===1?'ุฑุณุงู„ุฉ':'ุฑุณุงุฆู„'}`;
514
+ area.appendChild(b); area.scrollTop=area.scrollHeight;
515
  }
516
 
517
+ // โ•โ•โ•โ•โ•โ•โ•โ• SETTINGS โ•โ•โ•โ•โ•โ•โ•โ•
518
+ function openSettings(){
 
 
519
  document.getElementById('settings-modal').classList.add('open');
520
+ document.getElementById('backend-url').value=localStorage.getItem(KB)||'';
521
+ document.getElementById('dark-toggle').checked=document.documentElement.getAttribute('data-theme')==='dark';
 
 
 
 
522
  }
523
+ function closeSettings(){ document.getElementById('settings-modal').classList.remove('open'); }
524
+ function saveSettings(){
525
+ localStorage.setItem(KB,document.getElementById('backend-url').value.trim());
526
  closeSettings();
527
  }
528
+ document.getElementById('settings-modal').addEventListener('click',e=>{ if(e.target===e.currentTarget) closeSettings(); });
 
 
529
 
530
+ // โ•โ•โ•โ•โ•โ•โ•โ• INIT โ•โ•โ•โ•โ•โ•โ•โ•
 
 
531
  initTheme();
532
  renderChatList();
533
  </script>