Lyon28 commited on
Commit
83e2658
·
verified ·
1 Parent(s): cc603f7

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +34 -6
  2. avatar.png +2 -2
  3. background.png +0 -0
  4. index.html +539 -495
app.py CHANGED
@@ -2,7 +2,7 @@ import os
2
  import uvicorn
3
  from fastapi import FastAPI, HTTPException
4
  from fastapi.middleware.cors import CORSMiddleware
5
- from fastapi.responses import HTMLResponse, FileResponse
6
  from fastapi.staticfiles import StaticFiles
7
  from pydantic import BaseModel
8
  from transformers import pipeline, AutoTokenizer, AutoModel, set_seed
@@ -26,11 +26,14 @@ app.add_middleware(
26
  allow_headers=["*"],
27
  )
28
 
29
- @app.get("/", response_class=HTMLResponse)
30
- async def serve_frontend():
31
- return FileResponse("index.html")
32
-
33
 
 
 
 
34
 
35
  # Set seed untuk konsistensi
36
  set_seed(42)
@@ -498,6 +501,32 @@ async def inference(request: dict):
498
  "status": "error"
499
  }
500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  # Run dengan CPU optimizations
502
  if __name__ == "__main__":
503
  port = int(os.environ.get("PORT", 7860))
@@ -505,7 +534,6 @@ if __name__ == "__main__":
505
  app,
506
  host="0.0.0.0",
507
  port=port,
508
- log_level="info",
509
  workers=1,
510
  timeout_keep_alive=30,
511
  access_log=False
 
2
  import uvicorn
3
  from fastapi import FastAPI, HTTPException
4
  from fastapi.middleware.cors import CORSMiddleware
5
+ from fastapi.responses import HTMLResponse, FileResponse
6
  from fastapi.staticfiles import StaticFiles
7
  from pydantic import BaseModel
8
  from transformers import pipeline, AutoTokenizer, AutoModel, set_seed
 
26
  allow_headers=["*"],
27
  )
28
 
29
+ # Serve static files
30
+ @app.get("/avatar.png")
31
+ async def get_avatar():
32
+ return FileResponse("avatar.png")
33
 
34
+ @app.get("/background.png")
35
+ async def get_background():
36
+ return FileResponse("background.png")
37
 
38
  # Set seed untuk konsistensi
39
  set_seed(42)
 
501
  "status": "error"
502
  }
503
 
504
+ # Serve HTML frontend
505
+ @app.get("/", response_class=HTMLResponse)
506
+ async def serve_frontend():
507
+ try:
508
+ with open("index.html", "r", encoding="utf-8") as file:
509
+ return HTMLResponse(content=file.read(), status_code=200)
510
+ except FileNotFoundError:
511
+ return HTMLResponse(content="<h1>Frontend not found</h1>", status_code=404)
512
+
513
+ # API info endpoint
514
+ @app.get("/api")
515
+ async def api_info():
516
+ return {
517
+ "message": "Character AI Backend Ready",
518
+ "version": "1.0.0",
519
+ "platform": "CPU Optimized",
520
+ "endpoints": {
521
+ "chat": "/chat",
522
+ "models": "/models",
523
+ "health": "/health",
524
+ "config": "/config",
525
+ "inference": "/inference"
526
+ },
527
+ "frontend_url": "/"
528
+ }
529
+
530
  # Run dengan CPU optimizations
531
  if __name__ == "__main__":
532
  port = int(os.environ.get("PORT", 7860))
 
534
  app,
535
  host="0.0.0.0",
536
  port=port,
 
537
  workers=1,
538
  timeout_keep_alive=30,
539
  access_log=False
avatar.png CHANGED

Git LFS Details

  • SHA256: 4884f5fa8d5d877370006096a0a8f25145067fdafad1c1184c1d2c7f616c26dc
  • Pointer size: 132 Bytes
  • Size of remote file: 2.08 MB

Git LFS Details

  • SHA256: bb434ca30f4ff9e93a3e493ea31e86a727cc2ce35614f1f5f4f997573ca8cf8f
  • Pointer size: 131 Bytes
  • Size of remote file: 349 kB
background.png ADDED
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Character AI Chat - WhatsApp Style</title>
7
  <style>
8
  * {
9
  margin: 0;
@@ -13,446 +13,444 @@
13
 
14
  body {
15
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16
- background: #000;
 
 
17
  height: 100vh;
18
  overflow: hidden;
19
  }
20
 
21
  .chat-container {
22
- width: 100%;
23
- max-width: 414px;
24
  height: 100vh;
25
- margin: 0 auto;
26
- background: #000;
27
- position: relative;
28
  display: flex;
29
  flex-direction: column;
 
 
 
 
30
  }
31
 
32
  /* Header */
33
  .chat-header {
34
- background: rgba(28, 28, 30, 0.95);
35
- backdrop-filter: blur(20px);
36
- color: #fff;
37
- padding: 20px 16px 12px 16px;
38
  display: flex;
39
  align-items: center;
40
- gap: 12px;
41
- border-bottom: 0.5px solid rgba(255, 255, 255, 0.1);
42
- position: relative;
43
- z-index: 100;
44
- }
45
-
46
- .status-bar {
47
- position: absolute;
48
  top: 0;
49
- left: 0;
50
- right: 0;
51
- height: 44px;
52
- background: rgba(0, 0, 0, 0.1);
53
- display: flex;
54
- justify-content: space-between;
55
- align-items: center;
56
- padding: 0 20px;
57
- font-size: 14px;
58
- font-weight: 600;
59
- color: #fff;
60
- }
61
-
62
- .back-button {
63
- background: none;
64
- border: none;
65
- color: #007AFF;
66
- font-size: 17px;
67
- cursor: pointer;
68
- padding: 8px;
69
- margin-left: -8px;
70
  }
71
 
72
  .avatar {
73
  width: 40px;
74
  height: 40px;
75
- border-radius: 20px;
76
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
77
- display: flex;
78
- align-items: center;
79
- justify-content: center;
80
- font-size: 18px;
81
- color: #fff;
82
- flex-shrink: 0;
83
- position: relative;
84
- }
85
-
86
- .avatar img {
87
- width: 100%;
88
- height: 100%;
89
- border-radius: 20px;
90
- object-fit: cover;
91
  }
92
 
93
- .contact-info {
94
  flex: 1;
95
- display: flex;
96
- flex-direction: column;
97
  }
98
 
99
- .contact-name {
100
- font-size: 17px;
101
  font-weight: 600;
102
- color: #fff;
103
  margin-bottom: 2px;
104
  }
105
 
106
- .contact-status {
107
  font-size: 13px;
108
- color: rgba(255, 255, 255, 0.6);
 
 
 
 
109
  display: flex;
110
  align-items: center;
111
- gap: 4px;
112
  }
113
 
114
- .online-dot {
115
- width: 8px;
116
- height: 8px;
117
- background: #34C759;
118
  border-radius: 50%;
119
- animation: pulse 2s infinite;
120
  }
121
 
122
- @keyframes pulse {
123
- 0% { opacity: 1; }
124
- 50% { opacity: 0.5; }
125
- 100% { opacity: 1; }
126
  }
127
 
128
- .header-actions {
129
- display: flex;
130
- gap: 12px;
 
 
 
 
 
 
 
 
 
131
  }
132
 
133
- .header-btn {
134
- background: none;
135
- border: none;
136
- color: #007AFF;
137
- font-size: 20px;
138
- cursor: pointer;
139
- padding: 8px;
140
  }
141
 
142
- /* Chat Messages */
143
- .chat-messages {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  flex: 1;
145
- background: #000;
146
- background-image:
147
- radial-gradient(circle at 25% 25%, rgba(255, 255, 255, 0.02) 0%, transparent 50%),
148
- radial-gradient(circle at 75% 75%, rgba(255, 255, 255, 0.02) 0%, transparent 50%);
149
  overflow-y: auto;
150
- padding: 16px;
151
- display: flex;
152
- flex-direction: column;
153
- gap: 8px;
 
 
 
154
  }
155
 
156
- .message-wrapper {
 
157
  display: flex;
158
- align-items: flex-end;
159
- gap: 6px;
160
- margin-bottom: 8px;
 
 
 
 
 
 
 
 
161
  }
162
 
163
- .message-wrapper.user {
164
- flex-direction: row-reverse;
165
  }
166
 
167
  .message-avatar {
168
- width: 26px;
169
- height: 26px;
170
- border-radius: 13px;
171
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
172
- display: flex;
173
- align-items: center;
174
- justify-content: center;
175
- font-size: 12px;
176
- color: #fff;
177
- flex-shrink: 0;
 
 
178
  }
179
 
180
- .message-bubble {
181
- max-width: 75%;
182
  padding: 12px 16px;
183
  border-radius: 18px;
184
- font-size: 16px;
185
- line-height: 1.4;
186
  position: relative;
187
  word-wrap: break-word;
 
 
 
 
 
 
 
188
  }
189
 
190
- .message-wrapper.user .message-bubble {
191
- background: #007AFF;
192
- color: #fff;
193
- border-bottom-right-radius: 4px;
194
- margin-right: 8px;
195
  }
196
 
197
- .message-wrapper.bot .message-bubble {
198
- background: rgba(44, 44, 46, 0.9);
199
- color: #fff;
200
- border-bottom-left-radius: 4px;
201
- margin-left: 8px;
202
  }
203
 
204
- .message-time {
205
  display: flex;
206
  align-items: center;
207
- gap: 4px;
208
- font-size: 11px;
209
- color: rgba(255, 255, 255, 0.5);
210
- margin-top: 4px;
211
  justify-content: flex-end;
 
 
 
212
  }
213
 
214
- .message-wrapper.bot .message-time {
215
- justify-content: flex-start;
216
  }
217
 
218
- .message-status {
219
- font-size: 12px;
220
- color: rgba(255, 255, 255, 0.6);
221
  }
222
 
223
- .speed-indicator {
224
- width: 6px;
225
- height: 6px;
226
- border-radius: 50%;
227
- display: inline-block;
228
  }
229
 
230
- .fast { background: #34C759; }
231
- .medium { background: #FF9500; }
232
- .slow { background: #FF3B30; }
233
-
234
  /* Typing Indicator */
235
  .typing-indicator {
236
  display: none;
237
- align-items: flex-end;
238
- gap: 6px;
239
- margin-bottom: 8px;
240
  }
241
 
242
- .typing-bubble {
243
- background: rgba(44, 44, 46, 0.9);
244
- padding: 12px 16px;
245
- border-radius: 18px;
246
- border-bottom-left-radius: 4px;
247
- margin-left: 8px;
248
  display: flex;
249
- align-items: center;
250
- gap: 4px;
251
  }
252
 
253
- .typing-dot {
 
 
 
 
 
 
 
 
 
254
  width: 8px;
255
  height: 8px;
256
- background: rgba(255, 255, 255, 0.4);
257
  border-radius: 50%;
258
- animation: typing 1.4s infinite;
 
 
259
  }
260
 
261
- .typing-dot:nth-child(2) { animation-delay: 0.2s; }
262
- .typing-dot:nth-child(3) { animation-delay: 0.4s; }
263
 
264
  @keyframes typing {
265
- 0%, 60%, 100% { opacity: 0.3; transform: scale(0.8); }
266
- 30% { opacity: 1; transform: scale(1); }
267
  }
268
 
269
- /* Input Area */
270
- .chat-input-container {
271
- background: rgba(28, 28, 30, 0.95);
272
- backdrop-filter: blur(20px);
273
- padding: 12px 16px 32px 16px;
274
- border-top: 0.5px solid rgba(255, 255, 255, 0.1);
 
 
275
  }
276
 
277
- .input-wrapper {
278
  display: flex;
279
  align-items: flex-end;
280
  gap: 8px;
281
- background: rgba(44, 44, 46, 0.8);
282
- border-radius: 20px;
283
- padding: 8px;
284
  }
285
 
286
- .chat-input {
287
- flex: 1;
288
  background: none;
289
  border: none;
290
- color: #fff;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  font-size: 16px;
292
- padding: 8px 12px;
293
- outline: none;
294
- resize: none;
295
- max-height: 100px;
296
- min-height: 20px;
297
  font-family: inherit;
 
 
 
 
298
  }
299
 
300
- .chat-input::placeholder {
301
- color: rgba(255, 255, 255, 0.4);
302
  }
303
 
304
- .send-button {
305
- background: #007AFF;
306
- color: #fff;
307
  border: none;
308
- border-radius: 16px;
309
- width: 32px;
310
- height: 32px;
 
311
  display: flex;
312
  align-items: center;
313
  justify-content: center;
314
- cursor: pointer;
315
- font-size: 14px;
316
- flex-shrink: 0;
317
  transition: all 0.2s;
 
318
  }
319
 
320
- .send-button:hover {
321
- background: #0056b3;
 
322
  }
323
 
324
- .send-button:disabled {
325
- background: rgba(255, 255, 255, 0.2);
326
  cursor: not-allowed;
 
327
  }
328
 
329
- /* Settings Panel */
330
- .settings-panel {
 
331
  position: absolute;
332
- top: 100%;
333
- left: 0;
334
- right: 0;
335
- background: rgba(28, 28, 30, 0.95);
336
- backdrop-filter: blur(20px);
337
- border-top: 0.5px solid rgba(255, 255, 255, 0.1);
338
- padding: 20px;
339
- transform: translateY(0%);
340
- transition: transform 0.3s ease;
341
- z-index: 200;
342
- }
343
-
344
- .settings-panel.hidden {
345
- transform: translateY(100%);
346
- }
347
-
348
- .settings-group {
349
- margin-bottom: 20px;
350
- }
351
-
352
- .settings-label {
353
- color: rgba(255, 255, 255, 0.6);
354
- font-size: 13px;
355
- margin-bottom: 8px;
356
- text-transform: uppercase;
357
- letter-spacing: 0.5px;
358
  }
359
 
360
- .settings-select {
361
- width: 100%;
362
- background: rgba(44, 44, 46, 0.8);
363
- border: 1px solid rgba(255, 255, 255, 0.1);
364
- border-radius: 10px;
365
- color: #fff;
366
- padding: 12px;
367
- font-size: 16px;
368
- outline: none;
369
  }
370
 
371
- .settings-input {
372
- width: 100%;
373
- background: rgba(44, 44, 46, 0.8);
374
- border: 1px solid rgba(255, 255, 255, 0.1);
375
- border-radius: 10px;
376
- color: #fff;
377
- padding: 12px;
378
- font-size: 16px;
379
- outline: none;
380
  }
381
 
382
- .settings-input::placeholder {
383
- color: rgba(255, 255, 255, 0.4);
 
 
 
 
 
 
384
  }
385
 
386
- /* Welcome Message */
387
- .welcome-message {
388
- text-align: center;
389
- color: rgba(255, 255, 255, 0.5);
390
- font-size: 14px;
391
- padding: 20px;
392
- background: rgba(44, 44, 46, 0.3);
393
- border-radius: 12px;
394
- margin: 20px 0;
395
  }
396
 
397
  /* Responsive */
398
- @media (min-width: 415px) {
399
  .chat-container {
400
- border-radius: 20px;
401
- margin: 20px auto;
402
- height: calc(100vh - 40px);
403
- box-shadow: 0 10px 50px rgba(0, 0, 0, 0.5);
 
 
 
 
 
 
404
  }
405
  }
406
 
407
- /* Custom Scrollbar */
408
- .chat-messages::-webkit-scrollbar {
409
- width: 2px;
410
  }
411
 
412
- .chat-messages::-webkit-scrollbar-track {
413
  background: transparent;
414
  }
415
 
416
- .chat-messages::-webkit-scrollbar-thumb {
417
- background: rgba(255, 255, 255, 0.2);
418
- border-radius: 1px;
 
 
 
 
419
  }
420
  </style>
421
  </head>
422
  <body>
423
  <div class="chat-container">
424
- <!-- Status Bar -->
425
- <div class="status-bar">
426
- <span>9:41</span>
427
- <span>●●●● WiFi 📶 100%</span>
428
- </div>
429
-
430
  <!-- Header -->
431
  <div class="chat-header">
432
- <button class="back-button">‹</button>
433
- <div class="avatar">
434
- <img src="avatar.png" alt="Character Avatar" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
435
- <div style="display: none;">🤖</div>
436
- </div>
437
- <div class="contact-info">
438
- <div class="contact-name" id="characterName">Sayang</div>
439
- <div class="contact-status">
440
- <div class="online-dot"></div>
441
- <span id="statusText">online</span>
442
- </div>
443
  </div>
444
  <div class="header-actions">
445
- <button class="header-btn" onclick="toggleSettings()">⚙️</button>
446
- <button class="header-btn">📞</button>
 
 
 
 
 
447
  </div>
448
  </div>
449
 
450
- <!-- Settings Panel -->
451
- <div class="settings-panel hidden" id="settingsPanel">
452
- <div class="settings-group">
453
  <div class="settings-label">Model AI</div>
454
- <select class="settings-select" id="modelSelect">
455
- <option value="distil-gpt-2">DistilGPT-2 ⚡ (Tercepat)</option>
456
  <option value="gpt-2-tinny">GPT-2 Tinny ⚡</option>
457
  <option value="bert-tinny">BERT Tinny 🎭</option>
458
  <option value="distilbert-base-uncased">DistilBERT 🎭</option>
@@ -465,64 +463,62 @@
465
  <option value="gpt-neo">GPT-Neo</option>
466
  </select>
467
  </div>
468
- <div class="settings-group">
469
- <div class="settings-label">Nama Karakter</div>
470
- <input type="text" class="settings-input" id="charNameInput" value="Sayang" placeholder="Masukkan nama karakter">
 
471
  </div>
472
- <div class="settings-group">
473
- <div class="settings-label">Nama Anda</div>
474
- <input type="text" class="settings-input" id="userNameInput" value="Kamu" placeholder="Masukkan nama Anda">
 
475
  </div>
476
- <div class="settings-group">
477
- <div class="settings-label">Situasi</div>
478
- <select class="settings-select" id="situationSelect">
479
- <option value="Santai">Santai</option>
480
- <option value="Romantis">Romantis</option>
481
- <option value="Serius">Serius</option>
482
- <option value="Bermain">Bermain</option>
483
- <option value="Perhatian">Perhatian</option>
484
- </select>
485
- </div>
486
- <div class="settings-group">
487
- <div class="settings-label">Lokasi</div>
488
- <select class="settings-select" id="locationSelect">
489
- <option value="Ruang tamu">Ruang tamu</option>
490
- <option value="Kamar tidur">Kamar tidur</option>
491
- <option value="Kafe">Kafe</option>
492
- <option value="Taman">Taman</option>
493
- <option value="Pantai">Pantai</option>
494
- <option value="Rumah">Rumah</option>
495
- </select>
496
  </div>
497
  </div>
498
 
499
- <!-- Chat Messages -->
500
- <div class="chat-messages" id="chatMessages">
501
- <div class="welcome-message">
502
- <div style="font-size: 20px; margin-bottom: 8px;">🤖✨</div>
503
- <strong>Character AI Chat Ready!</strong><br>
504
- Berbicara dengan AI yang disesuaikan dengan karakter dan situasi pilihan Anda.<br><br>
505
- <small>Tekan ⚙️ untuk mengatur karakter dan situasi</small>
 
 
 
 
506
  </div>
507
- </div>
508
 
509
- <!-- Typing Indicator -->
510
- <div class="typing-indicator" id="typingIndicator">
511
- <div class="message-avatar">🤖</div>
512
- <div class="typing-bubble">
513
- <div class="typing-dot"></div>
514
- <div class="typing-dot"></div>
515
- <div class="typing-dot"></div>
 
516
  </div>
517
  </div>
518
 
519
- <!-- Input Area -->
520
- <div class="chat-input-container">
521
- <div class="input-wrapper">
522
- <textarea class="chat-input" id="chatInput" placeholder="Ketik pesan..." rows="1"></textarea>
523
- <button class="send-button" id="sendButton">
524
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
525
- <path d="M2 21l21-9L2 3v7l15 2-15 2v7z"/>
 
 
 
 
 
 
526
  </svg>
527
  </button>
528
  </div>
@@ -530,220 +526,268 @@
530
  </div>
531
 
532
  <script>
533
- // Global variables
534
- const chatMessages = document.getElementById('chatMessages');
535
- const chatInput = document.getElementById('chatInput');
536
- const sendButton = document.getElementById('sendButton');
537
- const typingIndicator = document.getElementById('typingIndicator');
538
- const settingsPanel = document.getElementById('settingsPanel');
539
- const characterName = document.getElementById('characterName');
540
- const statusText = document.getElementById('statusText');
541
-
542
- // Settings elements
543
- const modelSelect = document.getElementById('modelSelect');
544
- const charNameInput = document.getElementById('charNameInput');
545
- const userNameInput = document.getElementById('userNameInput');
546
- const situationSelect = document.getElementById('situationSelect');
547
- const locationSelect = document.getElementById('locationSelect');
548
-
549
- // API Configuration
550
  const API_BASE = window.location.origin;
551
- let isSettingsOpen = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
 
553
- // Utility functions
554
- function scrollToBottom() {
555
- chatMessages.scrollTop = chatMessages.scrollHeight;
556
- }
 
 
 
557
 
558
- function getSpeedClass(time) {
559
- if (time < 2000) return 'fast';
560
- if (time < 5000) return 'medium';
561
- return 'slow';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
562
  }
563
 
564
- function formatTime() {
565
- return new Date().toLocaleTimeString('id-ID', {
 
566
  hour: '2-digit',
567
  minute: '2-digit'
568
  });
 
569
  }
570
 
571
- function toggleSettings() {
572
- isSettingsOpen = !isSettingsOpen;
573
- if (isSettingsOpen) {
574
- settingsPanel.classList.remove('hidden');
575
- statusText.textContent = 'mengatur...';
576
- } else {
577
- settingsPanel.classList.add('hidden');
578
- statusText.textContent = 'online';
579
- }
580
  }
581
 
582
- function updateCharacterName() {
583
- const name = charNameInput.value.trim() || 'Sayang';
584
- characterName.textContent = name;
 
585
  }
586
 
587
- function addMessage(content, isUser = false, responseTime = null, model = null) {
588
- const messageWrapper = document.createElement('div');
589
- messageWrapper.className = `message-wrapper ${isUser ? 'user' : 'bot'}`;
 
590
 
591
- const avatar = document.createElement('div');
592
- avatar.className = 'message-avatar';
593
- avatar.textContent = isUser ? '👤' : '🤖';
 
594
 
595
- const bubble = document.createElement('div');
596
- bubble.className = 'message-bubble';
597
- bubble.textContent = content;
 
 
 
 
 
 
 
598
 
599
- const timeElement = document.createElement('div');
600
- timeElement.className = 'message-time';
601
- timeElement.innerHTML = formatTime();
 
 
 
 
 
 
 
 
 
602
 
603
- if (responseTime && !isUser) {
604
- const speedClass = getSpeedClass(responseTime);
605
- timeElement.innerHTML += ` <span class="speed-indicator ${speedClass}"></span> ${responseTime}ms`;
606
- if (model) {
607
- timeElement.innerHTML += ` • ${model}`;
608
- }
609
- }
610
 
611
- bubble.appendChild(timeElement);
 
 
 
612
 
613
- if (!isUser) {
614
- messageWrapper.appendChild(avatar);
615
- }
616
- messageWrapper.appendChild(bubble);
617
-
618
- chatMessages.appendChild(messageWrapper);
619
- scrollToBottom();
 
 
 
 
 
 
 
 
 
620
  }
621
 
622
  function showTyping() {
623
- typingIndicator.style.display = 'flex';
624
- statusText.textContent = 'mengetik...';
625
- scrollToBottom();
 
 
 
 
 
626
  }
627
 
628
  function hideTyping() {
629
- typingIndicator.style.display = 'none';
630
- statusText.textContent = 'online';
 
 
 
631
  }
632
 
633
  async function sendMessage() {
634
- const message = chatInput.value.trim();
 
 
635
  if (!message) return;
636
-
637
- // Disable input
638
- chatInput.disabled = true;
639
- sendButton.disabled = true;
640
-
 
 
641
  // Add user message
642
  addMessage(message, true);
643
- chatInput.value = '';
 
 
 
644
  showTyping();
645
-
646
- const startTime = Date.now();
647
-
 
 
648
  try {
649
- const requestData = {
650
- message: message,
651
- model: modelSelect.value,
652
- situation: situationSelect.value,
653
- location: locationSelect.value,
654
- char_name: charNameInput.value.trim() || 'Sayang',
655
- user_name: userNameInput.value.trim() || 'Kamu',
656
- max_length: 200
657
- };
658
-
659
  const response = await fetch(`${API_BASE}/chat`, {
660
  method: 'POST',
661
  headers: {
662
- 'Content-Type': 'application/json'
663
  },
664
- body: JSON.stringify(requestData)
 
 
 
 
 
 
 
 
665
  });
666
-
667
  const data = await response.json();
668
- const responseTime = Date.now() - startTime;
669
-
 
 
670
  hideTyping();
671
-
672
  if (data.status === 'success') {
673
- addMessage(data.response, false, responseTime, data.model);
674
  } else {
675
- addMessage('Maaf, sedang ada masalah. Coba lagi ya 🙏', false, responseTime);
676
  }
677
-
678
  } catch (error) {
679
- const responseTime = Date.now() - startTime;
680
  hideTyping();
681
- addMessage('Koneksi bermasalah, coba lagi nanti 😅', false, responseTime);
682
  console.error('Error:', error);
 
683
  }
684
-
685
- // Re-enable input
686
- chatInput.disabled = false;
687
- sendButton.disabled = false;
688
- chatInput.focus();
689
- autoResize();
690
- }
691
-
692
- function autoResize() {
693
- chatInput.style.height = 'auto';
694
- chatInput.style.height = chatInput.scrollHeight + 'px';
695
  }
696
 
697
- // Event listeners
698
- sendButton.addEventListener('click', sendMessage);
699
-
700
- chatInput.addEventListener('keydown', (e) => {
701
- if (e.key === 'Enter' && !e.shiftKey) {
702
- e.preventDefault();
703
- sendMessage();
704
- }
705
- });
706
-
707
- chatInput.addEventListener('input', autoResize);
708
-
709
- // Settings event listeners
710
- charNameInput.addEventListener('input', updateCharacterName);
711
-
712
- modelSelect.addEventListener('change', () => {
713
- const selectedOption = modelSelect.options[modelSelect.selectedIndex];
714
- statusText.textContent = `Model: ${selectedOption.text}`;
715
- setTimeout(() => {
716
- if (!isSettingsOpen) statusText.textContent = 'online';
717
- }, 2000);
718
  });
719
 
720
- // Close settings when clicking outside
721
- document.addEventListener('click', (e) => {
722
- if (isSettingsOpen && !settingsPanel.contains(e.target) && !e.target.closest('.header-btn')) {
723
- toggleSettings();
724
  }
725
  });
726
-
727
- // Initialize
728
- window.addEventListener('load', () => {
729
- chatInput.focus();
730
- updateCharacterName();
731
-
732
- // Add initial greeting
733
- setTimeout(() => {
734
- addMessage(`Halo ${userNameInput.value}! Aku ${charNameInput.value} 😊 Apa kabar hari ini?`, false, 1200, 'Character AI');
735
- }, 1000);
736
- });
737
-
738
- // Prevent zoom on double tap (iOS)
739
- let lastTouchEnd = 0;
740
- document.addEventListener('touchend', (event) => {
741
- const now = (new Date()).getTime();
742
- if (now - lastTouchEnd <= 300) {
743
- event.preventDefault();
744
- }
745
- lastTouchEnd = now;
746
- }, false);
747
  </script>
748
  </body>
749
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Character AI Chat</title>
7
  <style>
8
  * {
9
  margin: 0;
 
13
 
14
  body {
15
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16
+ background: url('background.png'), linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ background-size: cover;
18
+ background-attachment: fixed;
19
  height: 100vh;
20
  overflow: hidden;
21
  }
22
 
23
  .chat-container {
 
 
24
  height: 100vh;
 
 
 
25
  display: flex;
26
  flex-direction: column;
27
+ max-width: 100%;
28
+ margin: 0 auto;
29
+ background: rgba(255, 255, 255, 0.95);
30
+ backdrop-filter: blur(10px);
31
  }
32
 
33
  /* Header */
34
  .chat-header {
35
+ background: #075e54;
36
+ color: white;
37
+ padding: 10px 16px;
 
38
  display: flex;
39
  align-items: center;
40
+ position: sticky;
 
 
 
 
 
 
 
41
  top: 0;
42
+ z-index: 100;
43
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
  .avatar {
47
  width: 40px;
48
  height: 40px;
49
+ border-radius: 50%;
50
+ margin-right: 12px;
51
+ background: url('avatar.png'), linear-gradient(45deg, #25d366, #128c7e);
52
+ background-size: cover;
53
+ background-position: center;
 
 
 
 
 
 
 
 
 
 
 
54
  }
55
 
56
+ .header-info {
57
  flex: 1;
 
 
58
  }
59
 
60
+ .char-name {
61
+ font-size: 16px;
62
  font-weight: 600;
 
63
  margin-bottom: 2px;
64
  }
65
 
66
+ .status {
67
  font-size: 13px;
68
+ opacity: 0.8;
69
+ color: #dcf8c6;
70
+ }
71
+
72
+ .header-actions {
73
  display: flex;
74
  align-items: center;
75
+ gap: 20px;
76
  }
77
 
78
+ .three-dots {
79
+ cursor: pointer;
80
+ padding: 8px;
 
81
  border-radius: 50%;
82
+ transition: background 0.2s;
83
  }
84
 
85
+ .three-dots:hover {
86
+ background: rgba(255,255,255,0.1);
 
 
87
  }
88
 
89
+ /* Settings Popup */
90
+ .settings-popup {
91
+ display: none;
92
+ position: absolute;
93
+ top: 60px;
94
+ right: 16px;
95
+ background: white;
96
+ border-radius: 12px;
97
+ box-shadow: 0 8px 32px rgba(0,0,0,0.2);
98
+ min-width: 280px;
99
+ z-index: 1000;
100
+ padding: 8px 0;
101
  }
102
 
103
+ .settings-popup.show {
104
+ display: block;
105
+ animation: popupIn 0.2s ease-out;
 
 
 
 
106
  }
107
 
108
+ @keyframes popupIn {
109
+ from { opacity: 0; transform: translateY(-10px) scale(0.95); }
110
+ to { opacity: 1; transform: translateY(0) scale(1); }
111
+ }
112
+
113
+ .settings-section {
114
+ padding: 12px 20px;
115
+ border-bottom: 1px solid #eee;
116
+ }
117
+
118
+ .settings-section:last-child {
119
+ border-bottom: none;
120
+ }
121
+
122
+ .settings-label {
123
+ font-size: 14px;
124
+ font-weight: 600;
125
+ color: #333;
126
+ margin-bottom: 8px;
127
+ }
128
+
129
+ .model-select, .input-field {
130
+ width: 100%;
131
+ padding: 8px 12px;
132
+ border: 1px solid #ddd;
133
+ border-radius: 8px;
134
+ font-size: 14px;
135
+ background: #f8f9fa;
136
+ }
137
+
138
+ .input-field {
139
+ margin-top: 4px;
140
+ }
141
+
142
+ /* Chat Body */
143
+ .chat-body {
144
  flex: 1;
 
 
 
 
145
  overflow-y: auto;
146
+ padding: 20px 16px;
147
+ background: url('background.png'),
148
+ radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3), transparent),
149
+ radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3), transparent),
150
+ linear-gradient(135deg, #667eea 0%, #764ba2 100%);
151
+ background-size: cover, 400px 400px, 400px 400px, cover;
152
+ background-attachment: fixed;
153
  }
154
 
155
+ /* Message Bubbles */
156
+ .message {
157
  display: flex;
158
+ margin-bottom: 12px;
159
+ animation: messageSlide 0.3s ease-out;
160
+ }
161
+
162
+ @keyframes messageSlide {
163
+ from { opacity: 0; transform: translateY(20px); }
164
+ to { opacity: 1; transform: translateY(0); }
165
+ }
166
+
167
+ .message.user {
168
+ justify-content: flex-end;
169
  }
170
 
171
+ .message.char {
172
+ justify-content: flex-start;
173
  }
174
 
175
  .message-avatar {
176
+ width: 32px;
177
+ height: 32px;
178
+ border-radius: 50%;
179
+ margin: 0 8px;
180
+ background: url('avatar.png'), linear-gradient(45deg, #25d366, #128c7e);
181
+ background-size: cover;
182
+ background-position: center;
183
+ align-self: flex-end;
184
+ }
185
+
186
+ .message.user .message-avatar {
187
+ background: linear-gradient(45deg, #0084ff, #00a0ff);
188
  }
189
 
190
+ .bubble {
191
+ max-width: 70%;
192
  padding: 12px 16px;
193
  border-radius: 18px;
 
 
194
  position: relative;
195
  word-wrap: break-word;
196
+ backdrop-filter: blur(10px);
197
+ }
198
+
199
+ .message.user .bubble {
200
+ background: linear-gradient(135deg, #0084ff, #00a0ff);
201
+ color: white;
202
+ border-bottom-right-radius: 6px;
203
  }
204
 
205
+ .message.char .bubble {
206
+ background: rgba(255, 255, 255, 0.95);
207
+ color: #333;
208
+ border-bottom-left-radius: 6px;
209
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
210
  }
211
 
212
+ .message-text {
213
+ font-size: 16px;
214
+ line-height: 1.4;
215
+ margin-bottom: 6px;
 
216
  }
217
 
218
+ .message-meta {
219
  display: flex;
220
  align-items: center;
 
 
 
 
221
  justify-content: flex-end;
222
+ gap: 4px;
223
+ font-size: 12px;
224
+ opacity: 0.7;
225
  }
226
 
227
+ .timestamp {
228
+ color: inherit;
229
  }
230
 
231
+ .checkmarks {
232
+ color: #4fc3f7;
233
+ font-weight: bold;
234
  }
235
 
236
+ .message.char .checkmarks {
237
+ display: none;
 
 
 
238
  }
239
 
 
 
 
 
240
  /* Typing Indicator */
241
  .typing-indicator {
242
  display: none;
243
+ align-items: center;
244
+ margin-bottom: 12px;
 
245
  }
246
 
247
+ .typing-indicator.show {
 
 
 
 
 
248
  display: flex;
 
 
249
  }
250
 
251
+ .typing-dots {
252
+ background: rgba(255, 255, 255, 0.95);
253
+ border-radius: 18px;
254
+ padding: 12px 16px;
255
+ margin-left: 48px;
256
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
257
+ }
258
+
259
+ .typing-dots span {
260
+ display: inline-block;
261
  width: 8px;
262
  height: 8px;
 
263
  border-radius: 50%;
264
+ background: #999;
265
+ margin-right: 4px;
266
+ animation: typing 2s infinite;
267
  }
268
 
269
+ .typing-dots span:nth-child(2) { animation-delay: 0.2s; }
270
+ .typing-dots span:nth-child(3) { animation-delay: 0.4s; }
271
 
272
  @keyframes typing {
273
+ 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
274
+ 30% { transform: translateY(-10px); opacity: 1; }
275
  }
276
 
277
+ /* Footer */
278
+ .chat-footer {
279
+ background: rgba(255, 255, 255, 0.95);
280
+ backdrop-filter: blur(10px);
281
+ padding: 12px 16px;
282
+ border-top: 1px solid rgba(0,0,0,0.1);
283
+ position: sticky;
284
+ bottom: 0;
285
  }
286
 
287
+ .input-container {
288
  display: flex;
289
  align-items: flex-end;
290
  gap: 8px;
 
 
 
291
  }
292
 
293
+ .emoji-btn {
 
294
  background: none;
295
  border: none;
296
+ font-size: 24px;
297
+ cursor: pointer;
298
+ padding: 8px;
299
+ border-radius: 50%;
300
+ transition: background 0.2s;
301
+ }
302
+
303
+ .emoji-btn:hover {
304
+ background: rgba(0,0,0,0.05);
305
+ }
306
+
307
+ .message-input {
308
+ flex: 1;
309
+ min-height: 40px;
310
+ max-height: 120px;
311
+ padding: 10px 16px;
312
+ border: 1px solid #ddd;
313
+ border-radius: 20px;
314
  font-size: 16px;
 
 
 
 
 
315
  font-family: inherit;
316
+ resize: none;
317
+ outline: none;
318
+ background: white;
319
+ transition: border-color 0.2s;
320
  }
321
 
322
+ .message-input:focus {
323
+ border-color: #075e54;
324
  }
325
 
326
+ .send-btn {
327
+ background: #075e54;
328
+ color: white;
329
  border: none;
330
+ width: 40px;
331
+ height: 40px;
332
+ border-radius: 50%;
333
+ cursor: pointer;
334
  display: flex;
335
  align-items: center;
336
  justify-content: center;
 
 
 
337
  transition: all 0.2s;
338
+ font-size: 18px;
339
  }
340
 
341
+ .send-btn:hover {
342
+ background: #128c7e;
343
+ transform: scale(1.05);
344
  }
345
 
346
+ .send-btn:disabled {
347
+ background: #ccc;
348
  cursor: not-allowed;
349
+ transform: none;
350
  }
351
 
352
+ /* Emoji Picker */
353
+ .emoji-picker {
354
+ display: none;
355
  position: absolute;
356
+ bottom: 70px;
357
+ left: 16px;
358
+ background: white;
359
+ border-radius: 12px;
360
+ box-shadow: 0 8px 32px rgba(0,0,0,0.2);
361
+ padding: 16px;
362
+ max-width: 300px;
363
+ z-index: 1000;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  }
365
 
366
+ .emoji-picker.show {
367
+ display: block;
368
+ animation: popupIn 0.2s ease-out;
 
 
 
 
 
 
369
  }
370
 
371
+ .emoji-grid {
372
+ display: grid;
373
+ grid-template-columns: repeat(8, 1fr);
374
+ gap: 8px;
375
+ max-height: 200px;
376
+ overflow-y: auto;
 
 
 
377
  }
378
 
379
+ .emoji-item {
380
+ background: none;
381
+ border: none;
382
+ font-size: 24px;
383
+ cursor: pointer;
384
+ padding: 8px;
385
+ border-radius: 8px;
386
+ transition: background 0.2s;
387
  }
388
 
389
+ .emoji-item:hover {
390
+ background: #f0f0f0;
 
 
 
 
 
 
 
391
  }
392
 
393
  /* Responsive */
394
+ @media (max-width: 768px) {
395
  .chat-container {
396
+ height: 100vh;
397
+ }
398
+
399
+ .bubble {
400
+ max-width: 85%;
401
+ }
402
+
403
+ .settings-popup {
404
+ right: 8px;
405
+ min-width: 260px;
406
  }
407
  }
408
 
409
+ /* Scrollbar Styling */
410
+ .chat-body::-webkit-scrollbar {
411
+ width: 6px;
412
  }
413
 
414
+ .chat-body::-webkit-scrollbar-track {
415
  background: transparent;
416
  }
417
 
418
+ .chat-body::-webkit-scrollbar-thumb {
419
+ background: rgba(0,0,0,0.2);
420
+ border-radius: 3px;
421
+ }
422
+
423
+ .chat-body::-webkit-scrollbar-thumb:hover {
424
+ background: rgba(0,0,0,0.3);
425
  }
426
  </style>
427
  </head>
428
  <body>
429
  <div class="chat-container">
 
 
 
 
 
 
430
  <!-- Header -->
431
  <div class="chat-header">
432
+ <div class="avatar"></div>
433
+ <div class="header-info">
434
+ <div class="char-name" id="charName">Sayang</div>
435
+ <div class="status" id="status">online</div>
 
 
 
 
 
 
 
436
  </div>
437
  <div class="header-actions">
438
+ <div class="three-dots" onclick="toggleSettings()">
439
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
440
+ <circle cx="12" cy="5" r="2"/>
441
+ <circle cx="12" cy="12" r="2"/>
442
+ <circle cx="12" cy="19" r="2"/>
443
+ </svg>
444
+ </div>
445
  </div>
446
  </div>
447
 
448
+ <!-- Settings Popup -->
449
+ <div class="settings-popup" id="settingsPopup">
450
+ <div class="settings-section">
451
  <div class="settings-label">Model AI</div>
452
+ <select class="model-select" id="modelSelect">
453
+ <option value="distil-gpt-2">DistilGPT-2 ⚡</option>
454
  <option value="gpt-2-tinny">GPT-2 Tinny ⚡</option>
455
  <option value="bert-tinny">BERT Tinny 🎭</option>
456
  <option value="distilbert-base-uncased">DistilBERT 🎭</option>
 
463
  <option value="gpt-neo">GPT-Neo</option>
464
  </select>
465
  </div>
466
+ <div class="settings-section">
467
+ <div class="settings-label">Karakter</div>
468
+ <input type="text" class="input-field" id="charNameInput" placeholder="Nama karakter" value="Sayang">
469
+ <input type="text" class="input-field" id="userNameInput" placeholder="Nama kamu" value="Kamu">
470
  </div>
471
+ <div class="settings-section">
472
+ <div class="settings-label">Situasi & Lokasi</div>
473
+ <input type="text" class="input-field" id="situationInput" placeholder="Situasi" value="Santai">
474
+ <input type="text" class="input-field" id="locationInput" placeholder="Lokasi" value="Ruang tamu">
475
  </div>
476
+ <div class="settings-section">
477
+ <div class="settings-label">Panjang Pesan</div>
478
+ <input type="range" class="input-field" id="maxLengthRange" min="50" max="300" value="150">
479
+ <div style="font-size: 12px; color: #666; margin-top: 4px;">
480
+ <span id="maxLengthValue">150</span> karakter maksimal
481
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  </div>
483
  </div>
484
 
485
+ <!-- Chat Body -->
486
+ <div class="chat-body" id="chatBody">
487
+ <!-- Welcome Message -->
488
+ <div class="message char">
489
+ <div class="message-avatar"></div>
490
+ <div class="bubble">
491
+ <div class="message-text">Hai! Aku siap ngobrol sama kamu nih. Mau bahas apa hari ini? 😊</div>
492
+ <div class="message-meta">
493
+ <span class="timestamp" id="welcome-time"></span>
494
+ </div>
495
+ </div>
496
  </div>
 
497
 
498
+ <!-- Typing Indicator -->
499
+ <div class="typing-indicator" id="typingIndicator">
500
+ <div class="message-avatar"></div>
501
+ <div class="typing-dots">
502
+ <span></span>
503
+ <span></span>
504
+ <span></span>
505
+ </div>
506
  </div>
507
  </div>
508
 
509
+ <!-- Footer -->
510
+ <div class="chat-footer">
511
+ <!-- Emoji Picker -->
512
+ <div class="emoji-picker" id="emojiPicker">
513
+ <div class="emoji-grid" id="emojiGrid"></div>
514
+ </div>
515
+
516
+ <div class="input-container">
517
+ <button class="emoji-btn" onclick="toggleEmojiPicker()">😊</button>
518
+ <textarea class="message-input" id="messageInput" placeholder="Ketik pesan..." rows="1"></textarea>
519
+ <button class="send-btn" id="sendBtn" onclick="sendMessage()">
520
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
521
+ <path d="M2,21L23,12L2,3V10L17,12L2,14V21Z"/>
522
  </svg>
523
  </button>
524
  </div>
 
526
  </div>
527
 
528
  <script>
529
+ // Configuration
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  const API_BASE = window.location.origin;
531
+ let isTyping = false;
532
+ let currentSettings = {
533
+ model: 'distil-gpt-2',
534
+ charName: 'Sayang',
535
+ userName: 'Kamu',
536
+ situation: 'Santai',
537
+ location: 'Ruang tamu',
538
+ maxLength: 150
539
+ };
540
+
541
+ // Emoji list
542
+ const emojis = [
543
+ '😊', '😃', '😄', '😁', '😆', '😅', '😂', '🤣',
544
+ '😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😗',
545
+ '😚', '😙', '😋', '😛', '😜', '🤪', '😝', '🤑',
546
+ '🤗', '🤭', '🤫', '🤔', '🤐', '🤨', '😐', '😑',
547
+ '😶', '😏', '😒', '🙄', '😬', '🤥', '😔', '😪',
548
+ '🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮', '🤧',
549
+ '🥵', '🥶', '🥴', '😵', '🤯', '🤠', '🥳', '😎',
550
+ '🤓', '🧐', '😕', '😟', '🙁', '☹️', '😮', '😯',
551
+ '😲', '😳', '🥺', '😦', '😧', '😨', '😰', '😥',
552
+ '😢', '😭', '😱', '😖', '😣', '😞', '😓', '😩',
553
+ '😫', '🥱', '😤', '😡', '😠', '🤬', '😈', '👿',
554
+ '💀', '☠️', '💩', '🤡', '👹', '👺', '👻', '👽'
555
+ ];
556
 
557
+ // Initialize
558
+ document.addEventListener('DOMContentLoaded', function() {
559
+ initializeApp();
560
+ loadEmojiPicker();
561
+ setupEventListeners();
562
+ setWelcomeTime();
563
+ });
564
 
565
+ function initializeApp() {
566
+ // Load settings from elements
567
+ document.getElementById('charName').textContent = currentSettings.charName;
568
+ document.getElementById('charNameInput').value = currentSettings.charName;
569
+ document.getElementById('userNameInput').value = currentSettings.userName;
570
+ document.getElementById('situationInput').value = currentSettings.situation;
571
+ document.getElementById('locationInput').value = currentSettings.location;
572
+ document.getElementById('maxLengthRange').value = currentSettings.maxLength;
573
+ document.getElementById('maxLengthValue').textContent = currentSettings.maxLength;
574
+ document.getElementById('modelSelect').value = currentSettings.model;
575
+ }
576
+
577
+ function setupEventListeners() {
578
+ // Message input
579
+ const messageInput = document.getElementById('messageInput');
580
+ messageInput.addEventListener('keydown', function(e) {
581
+ if (e.key === 'Enter' && !e.shiftKey) {
582
+ e.preventDefault();
583
+ sendMessage();
584
+ }
585
+ });
586
+
587
+ messageInput.addEventListener('input', function() {
588
+ this.style.height = 'auto';
589
+ this.style.height = Math.min(this.scrollHeight, 120) + 'px';
590
+ });
591
+
592
+ // Settings inputs
593
+ document.getElementById('charNameInput').addEventListener('input', updateCharName);
594
+ document.getElementById('maxLengthRange').addEventListener('input', updateMaxLength);
595
+
596
+ // Model select
597
+ document.getElementById('modelSelect').addEventListener('change', function() {
598
+ currentSettings.model = this.value;
599
+ });
600
+
601
+ // Click outside to close popups
602
+ document.addEventListener('click', function(e) {
603
+ if (!e.target.closest('.settings-popup') && !e.target.closest('.three-dots')) {
604
+ document.getElementById('settingsPopup').classList.remove('show');
605
+ }
606
+ if (!e.target.closest('.emoji-picker') && !e.target.closest('.emoji-btn')) {
607
+ document.getElementById('emojiPicker').classList.remove('show');
608
+ }
609
+ });
610
  }
611
 
612
+ function setWelcomeTime() {
613
+ const now = new Date();
614
+ const timeString = now.toLocaleTimeString('id-ID', {
615
  hour: '2-digit',
616
  minute: '2-digit'
617
  });
618
+ document.getElementById('welcome-time').textContent = timeString;
619
  }
620
 
621
+ function updateCharName() {
622
+ const newName = document.getElementById('charNameInput').value || 'Sayang';
623
+ currentSettings.charName = newName;
624
+ document.getElementById('charName').textContent = newName;
 
 
 
 
 
625
  }
626
 
627
+ function updateMaxLength() {
628
+ const value = document.getElementById('maxLengthRange').value;
629
+ currentSettings.maxLength = parseInt(value);
630
+ document.getElementById('maxLengthValue').textContent = value;
631
  }
632
 
633
+ function toggleSettings() {
634
+ const popup = document.getElementById('settingsPopup');
635
+ popup.classList.toggle('show');
636
+ }
637
 
638
+ function toggleEmojiPicker() {
639
+ const picker = document.getElementById('emojiPicker');
640
+ picker.classList.toggle('show');
641
+ }
642
 
643
+ function loadEmojiPicker() {
644
+ const grid = document.getElementById('emojiGrid');
645
+ emojis.forEach(emoji => {
646
+ const button = document.createElement('button');
647
+ button.className = 'emoji-item';
648
+ button.textContent = emoji;
649
+ button.onclick = () => insertEmoji(emoji);
650
+ grid.appendChild(button);
651
+ });
652
+ }
653
 
654
+ function insertEmoji(emoji) {
655
+ const input = document.getElementById('messageInput');
656
+ const start = input.selectionStart;
657
+ const end = input.selectionEnd;
658
+ const text = input.value;
659
+
660
+ input.value = text.substring(0, start) + emoji + text.substring(end);
661
+ input.selectionStart = input.selectionEnd = start + emoji.length;
662
+ input.focus();
663
+
664
+ document.getElementById('emojiPicker').classList.remove('show');
665
+ }
666
 
667
+ function getCurrentTime() {
668
+ const now = new Date();
669
+ return now.toLocaleTimeString('id-ID', {
670
+ hour: '2-digit',
671
+ minute: '2-digit'
672
+ });
673
+ }
674
 
675
+ function addMessage(text, isUser = false, showTime = true) {
676
+ const chatBody = document.getElementById('chatBody');
677
+ const messageDiv = document.createElement('div');
678
+ messageDiv.className = `message ${isUser ? 'user' : 'char'}`;
679
 
680
+ const time = showTime ? getCurrentTime() : '';
681
+ const checkmarks = isUser ? '<span class="checkmarks">✓✓</span>' : '';
682
+
683
+ messageDiv.innerHTML = `
684
+ <div class="message-avatar"></div>
685
+ <div class="bubble">
686
+ <div class="message-text">${text}</div>
687
+ <div class="message-meta">
688
+ <span class="timestamp">${time}</span>
689
+ ${checkmarks}
690
+ </div>
691
+ </div>
692
+ `;
693
+
694
+ chatBody.appendChild(messageDiv);
695
+ chatBody.scrollTop = chatBody.scrollHeight;
696
  }
697
 
698
  function showTyping() {
699
+ if (isTyping) return;
700
+ isTyping = true;
701
+
702
+ document.getElementById('status').textContent = 'mengetik...';
703
+ document.getElementById('typingIndicator').classList.add('show');
704
+
705
+ const chatBody = document.getElementById('chatBody');
706
+ chatBody.scrollTop = chatBody.scrollHeight;
707
  }
708
 
709
  function hideTyping() {
710
+ if (!isTyping) return;
711
+ isTyping = false;
712
+
713
+ document.getElementById('status').textContent = 'online';
714
+ document.getElementById('typingIndicator').classList.remove('show');
715
  }
716
 
717
  async function sendMessage() {
718
+ const input = document.getElementById('messageInput');
719
+ const message = input.value.trim();
720
+
721
  if (!message) return;
722
+
723
+ // Update settings from inputs
724
+ currentSettings.charName = document.getElementById('charNameInput').value || 'Sayang';
725
+ currentSettings.userName = document.getElementById('userNameInput').value || 'Kamu';
726
+ currentSettings.situation = document.getElementById('situationInput').value || 'Santai';
727
+ currentSettings.location = document.getElementById('locationInput').value || 'Ruang tamu';
728
+
729
  // Add user message
730
  addMessage(message, true);
731
+ input.value = '';
732
+ input.style.height = 'auto';
733
+
734
+ // Show typing
735
  showTyping();
736
+
737
+ // Disable send button
738
+ const sendBtn = document.getElementById('sendBtn');
739
+ sendBtn.disabled = true;
740
+
741
  try {
 
 
 
 
 
 
 
 
 
 
742
  const response = await fetch(`${API_BASE}/chat`, {
743
  method: 'POST',
744
  headers: {
745
+ 'Content-Type': 'application/json',
746
  },
747
+ body: JSON.stringify({
748
+ message: message,
749
+ model: currentSettings.model,
750
+ situation: currentSettings.situation,
751
+ location: currentSettings.location,
752
+ char_name: currentSettings.charName,
753
+ user_name: currentSettings.userName,
754
+ max_length: currentSettings.maxLength
755
+ })
756
  });
757
+
758
  const data = await response.json();
759
+
760
+ // Simulate typing delay
761
+ await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000));
762
+
763
  hideTyping();
764
+
765
  if (data.status === 'success') {
766
+ addMessage(data.response);
767
  } else {
768
+ addMessage('Maaf, ada masalah dengan sistem. Coba lagi ya! 😅');
769
  }
770
+
771
  } catch (error) {
 
772
  hideTyping();
 
773
  console.error('Error:', error);
774
+ addMessage('Ups, koneksi bermasalah. Coba lagi nanti ya! 🔄');
775
  }
776
+
777
+ // Re-enable send button
778
+ sendBtn.disabled = false;
 
 
 
 
 
 
 
 
779
  }
780
 
781
+ // Handle online/offline status
782
+ window.addEventListener('online', function() {
783
+ document.getElementById('status').textContent = 'online';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
784
  });
785
 
786
+ window.addEventListener('offline', function() {
787
+ if (!isTyping) {
788
+ document.getElementById('status').textContent = 'offline';
 
789
  }
790
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
791
  </script>
792
  </body>
793
  </html>