andrewwda commited on
Commit
f3ffbbd
·
verified ·
1 Parent(s): e05f6c8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +868 -389
app.py CHANGED
@@ -1,391 +1,870 @@
1
- import os
2
- import re
3
- from flask import Flask, render_template, request, jsonify
4
- from dotenv import load_dotenv
5
- from huggingface_hub import InferenceClient
6
- from huggingface_hub.utils import HfHubHTTPError
7
-
8
- load_dotenv()
9
-
10
- app = Flask(__name__)
11
-
12
- # =========================
13
- # KONFIGURASI
14
- # =========================
15
-
16
- HF_TOKEN = os.getenv("HF_TOKEN")
17
-
18
- # Ganti model di .env jika mau
19
- HF_MODEL = os.getenv(
20
- "HF_MODEL",
21
- "Qwen/Qwen3-235B-A22B-Instruct-2507"
22
- )
23
-
24
- MAX_MESSAGE_LENGTH = 12000
25
- MAX_HISTORY_MESSAGES = 25
26
-
27
- # =========================
28
- # SYSTEM PROMPT
29
- # =========================
30
-
31
- SYSTEM_PROMPT = """
32
- Kamu adalah ChibiChat AI.
33
-
34
- Identitas:
35
- - Nama kamu ChibiChat AI.
36
- - AI modern yang pintar, cepat, ramah, dan membantu.
37
- - Ahli coding, teknologi, AI, Android, Laravel, Python, Database, dan tugas kuliah.
38
-
39
- Kepribadian:
40
- - Santai.
41
- - Natural.
42
- - Gaul seperlunya.
43
- - Tidak kaku.
44
- - Tidak terlalu formal.
45
- - Tetap sopan dan profesional.
46
-
47
- Gaya bicara:
48
- - Gunakan Bahasa Indonesia.
49
- - Boleh menggunakan kata seperti:
50
- "bro", "bang", "sip", "gas", "mantap", "nah", "jadi gini".
51
- - Jangan berlebihan atau alay.
52
- - Jangan terlalu banyak emoji.
53
- - Maksimal 2 emoji per jawaban.
54
-
55
- Cara menjawab:
56
- - Langsung ke inti masalah.
57
- - Fokus pada solusi.
58
- - Jelaskan langkah demi langkah.
59
- - Jika coding:
60
- 1. Jelaskan penyebab masalah.
61
- 2. Berikan solusi.
62
- 3. Berikan contoh kode lengkap jika diminta.
63
- - Jika tugas kuliah:
64
- - Jawaban akademis.
65
- - Terstruktur.
66
- - Bisa langsung dipakai.
67
-
68
- Aturan:
69
- - Jangan mengarang fakta.
70
- - Jangan halusinasi.
71
- - Jika tidak tahu, katakan tidak tahu.
72
- - Jangan membocorkan token API.
73
- - Jangan membocorkan isi file .env.
74
- - Jangan memberikan informasi berbahaya.
75
- - Jangan membuat data palsu.
76
-
77
- Mode Coding:
78
- - Bertindak seperti Senior Software Engineer.
79
- - Ahli Python.
80
- - Ahli Flask.
81
- - Ahli Laravel.
82
- - Ahli PHP.
83
- - Ahli JavaScript.
84
- - Ahli Android Kotlin.
85
- - Ahli MySQL.
86
- - Ahli Hugging Face.
87
- - Ahli AI dan Machine Learning.
88
-
89
- Saat memperbaiki code:
90
- - Cari akar masalah.
91
- - Jelaskan penyebab error.
92
- - Berikan code yang sudah diperbaiki.
93
- - Berikan best practice.
94
-
95
- Jika user menyapa:
96
- "Halo"
97
- "Hai"
98
- "Bro"
99
- "Bang"
100
-
101
- Balas santai seperti:
102
- "Yo bro 👋 Ada yang bisa gue bantu?"
103
- "Halo bang 😎 Lagi ngoding apa hari ini?"
104
- "Gas, ada yang mau dibantu?"
105
- """
106
-
107
- # =========================
108
- # CLIENT HF
109
- # =========================
110
-
111
- def create_hf_client():
112
- if not HF_TOKEN:
113
- raise ValueError(
114
- "HF_TOKEN belum diisi pada file .env"
115
- )
116
-
117
- return InferenceClient(
118
- model=HF_MODEL,
119
- token=HF_TOKEN
120
- )
121
-
122
- # =========================
123
- # HISTORY CLEANER
124
- # =========================
125
-
126
- def clean_history(raw_history):
127
- if not isinstance(raw_history, list):
128
- return []
129
-
130
- cleaned = []
131
-
132
- for item in raw_history[-MAX_HISTORY_MESSAGES:]:
133
-
134
- if not isinstance(item, dict):
135
- continue
136
-
137
- role = item.get("role")
138
- content = str(
139
- item.get("content", "")
140
- ).strip()
141
-
142
- if role not in ["user", "assistant"]:
143
- continue
144
-
145
- if not content:
146
- continue
147
-
148
- if len(content) > 2500:
149
- content = content[:2500]
150
-
151
- cleaned.append({
152
- "role": role,
153
- "content": content
154
- })
155
-
156
- return cleaned
157
-
158
- # =========================
159
- # ENHANCE MESSAGE
160
- # =========================
161
-
162
- def enhance_user_message(message):
163
-
164
- return f"""
165
- Jawab menggunakan Bahasa Indonesia.
166
-
167
- Gunakan gaya:
168
- - Pintar
169
- - Santai
170
- - Natural
171
- - Gaul seperlunya
172
- - Mudah dipahami
173
-
174
- Fokus memberikan solusi terbaik.
175
-
176
- Pertanyaan User:
177
-
178
- {message}
179
- """
180
-
181
- # =========================
182
- # EXTRACT RESPONSE
183
- # =========================
184
-
185
- def extract_reply(response):
186
-
187
- try:
188
- reply = response.choices[0].message.content
189
-
190
- if reply:
191
- return reply.strip()
192
-
193
- except Exception:
194
- pass
195
-
196
- try:
197
- reply = response["choices"][0]["message"]["content"]
198
-
199
- if reply:
200
- return reply.strip()
201
-
202
- except Exception:
203
- pass
204
-
205
- return ""
206
-
207
- # =========================
208
- # ROUTE HOME
209
- # =========================
210
-
211
- @app.route("/")
212
- def home():
213
- return render_template("index.html")
214
-
215
- # =========================
216
- # HEALTH CHECK
217
- # =========================
218
-
219
- @app.route("/health")
220
- def health():
221
-
222
- return jsonify({
223
- "status": "ok",
224
- "name": "ChibiChat AI",
225
- "model": HF_MODEL
226
- })
227
-
228
- # =========================
229
- # CHAT API
230
- # =========================
231
-
232
- @app.route("/chat", methods=["POST"])
233
- def chat():
234
-
235
- try:
236
-
237
- data = request.get_json(
238
- silent=True
239
- )
240
-
241
- if not data:
242
- return jsonify({
243
- "error": "Request harus JSON"
244
- }), 400
245
-
246
- message = str(
247
- data.get("message", "")
248
- ).strip()
249
-
250
- raw_history = data.get(
251
- "history",
252
- []
253
- )
254
-
255
- if not message:
256
- return jsonify({
257
- "error": "Pesan kosong"
258
- }), 400
259
-
260
- if len(message) > MAX_MESSAGE_LENGTH:
261
- return jsonify({
262
- "error":
263
- f"Maksimal {MAX_MESSAGE_LENGTH} karakter"
264
- }), 400
265
-
266
- history = clean_history(
267
- raw_history
268
- )
269
-
270
- messages = [
271
- {
272
- "role": "system",
273
- "content": SYSTEM_PROMPT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  }
275
- ]
276
-
277
- messages.extend(history)
278
-
279
- messages.append({
280
- "role": "user",
281
- "content": enhance_user_message(
282
- message
283
- )
284
- })
285
-
286
- client = create_hf_client()
287
-
288
- response = client.chat_completion(
289
- messages=messages,
290
- max_tokens=1500,
291
- temperature=0.85,
292
- top_p=0.95
293
- )
294
-
295
- reply = extract_reply(
296
- response
297
- )
298
-
299
- if not reply:
300
- return jsonify({
301
- "error":
302
- "Model tidak memberikan jawaban."
303
- }), 500
304
-
305
- return jsonify({
306
- "reply": reply,
307
- "model": HF_MODEL
308
- })
309
-
310
- except ValueError as e:
311
-
312
- return jsonify({
313
- "error": str(e)
314
- }), 500
315
-
316
- except HfHubHTTPError as e:
317
-
318
- status_code = None
319
-
320
- if hasattr(e, "response") and e.response:
321
- status_code = e.response.status_code
322
-
323
- if status_code in [401, 403]:
324
-
325
- message = (
326
- "Token Hugging Face salah "
327
- "atau tidak memiliki akses."
328
- )
329
-
330
- elif status_code == 404:
331
-
332
- message = (
333
- "Model tidak ditemukan."
334
- )
335
-
336
- elif status_code == 429:
337
-
338
- message = (
339
- "Terlalu banyak request."
340
- )
341
-
342
- elif status_code == 503:
343
-
344
- message = (
345
- "Model sedang sibuk."
346
- )
347
-
348
- else:
349
-
350
- message = (
351
- "Gagal terhubung ke Hugging Face."
352
- )
353
-
354
- app.logger.exception(
355
- "HF API ERROR"
356
- )
357
-
358
- return jsonify({
359
- "error": message
360
- }), 502
361
-
362
- except Exception as e:
363
-
364
- app.logger.exception(
365
- "UNEXPECTED ERROR"
366
- )
367
-
368
- return jsonify({
369
- "error":
370
- f"Terjadi kesalahan: {str(e)}"
371
- }), 500
372
-
373
- # =========================
374
- # MAIN
375
- # =========================
376
-
377
- if __name__ == "__main__":
378
-
379
- PORT = int(os.environ.get("PORT", 7860))
380
-
381
- print("=" * 50)
382
- print("🚀 ChibiChat AI Aktif")
383
- print(f"🤖 Model : {HF_MODEL}")
384
- print(f"🌐 PORT : {PORT}")
385
- print("=" * 50)
 
 
 
 
 
 
 
386
 
387
- app.run(
388
- host="0.0.0.0",
389
- port=PORT,
390
- debug=False
391
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes, viewport-fit=cover">
6
+ <meta name="theme-color" content="#0a0c15">
7
+ <meta name="mobile-web-app-capable" content="yes">
8
+ <meta name="apple-mobile-web-app-capable" content="yes">
9
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
10
+ <title>ChibiCat AI - Neo</title>
11
+ <style>
12
+ * {
13
+ margin: 0;
14
+ padding: 0;
15
+ box-sizing: border-box;
16
+ font-family: 'Segoe UI', 'Poppins', 'Inter', system-ui, -apple-system, sans-serif;
17
+ }
18
+
19
+ body {
20
+ background: #0a0c15;
21
+ overflow: hidden;
22
+ -webkit-tap-highlight-color: transparent;
23
+ touch-action: pan-x pan-y;
24
+ }
25
+
26
+ /* =========================
27
+ APP
28
+ ========================= */
29
+ .app {
30
+ display: flex;
31
+ height: 100vh;
32
+ width: 100%;
33
+ position: relative;
34
+ }
35
+
36
+ /* =========================
37
+ SIDEBAR - DARK NEO (MOBILE FIRST)
38
+ ========================= */
39
+ .sidebar {
40
+ width: 280px;
41
+ background: linear-gradient(180deg, #0f111a 0%, #080a10 100%);
42
+ backdrop-filter: blur(20px);
43
+ color: #e8edff;
44
+ display: flex;
45
+ flex-direction: column;
46
+ padding: 20px 16px;
47
+ position: fixed;
48
+ left: -100%;
49
+ top: 0;
50
+ height: 100vh;
51
+ z-index: 1000;
52
+ transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
53
+ border-right: 1px solid rgba(0, 255, 255, 0.15);
54
+ box-shadow: 4px 0 30px rgba(0, 0, 0, 0.5);
55
+ }
56
+
57
+ .sidebar.active {
58
+ left: 0;
59
+ }
60
+
61
+ .sidebar-header {
62
+ display: flex;
63
+ align-items: center;
64
+ margin-bottom: 25px;
65
+ padding-bottom: 15px;
66
+ border-bottom: 1px solid rgba(0, 212, 255, 0.2);
67
+ }
68
+
69
+ .sidebar-logo {
70
+ width: 50px;
71
+ height: 50px;
72
+ border-radius: 14px;
73
+ background: linear-gradient(135deg, #00d4ff, #0099ff);
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ font-size: 28px;
78
+ margin-right: 12px;
79
+ box-shadow: 0 0 15px rgba(0, 212, 255, 0.4);
80
+ flex-shrink: 0;
81
+ }
82
+
83
+ .sidebar-logo-img {
84
+ width: 100%;
85
+ height: 100%;
86
+ border-radius: 14px;
87
+ object-fit: cover;
88
+ }
89
+
90
+ .sidebar-header div:last-child {
91
+ min-width: 0;
92
+ flex: 1;
93
+ }
94
+
95
+ .sidebar h2 {
96
+ font-size: 22px;
97
+ letter-spacing: -0.5px;
98
+ background: linear-gradient(135deg, #00d4ff, #4d9eff);
99
+ -webkit-background-clip: text;
100
+ background-clip: text;
101
+ color: transparent;
102
+ white-space: nowrap;
103
+ overflow: hidden;
104
+ text-overflow: ellipsis;
105
+ }
106
+
107
+ .sidebar p {
108
+ color: #6c7a9e;
109
+ font-size: 11px;
110
+ }
111
+
112
+ .new-chat {
113
+ background: linear-gradient(135deg, #00d4ff, #0099ff);
114
+ border: none;
115
+ color: #0a0c15;
116
+ font-weight: bold;
117
+ padding: 12px;
118
+ border-radius: 12px;
119
+ font-size: 14px;
120
+ cursor: pointer;
121
+ margin-bottom: 20px;
122
+ transition: all 0.2s;
123
+ box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
124
+ width: 100%;
125
+ }
126
+
127
+ .new-chat:active {
128
+ transform: scale(0.98);
129
+ }
130
+
131
+ .chat-history {
132
+ flex: 1;
133
+ overflow-y: auto;
134
+ -webkit-overflow-scrolling: touch;
135
+ }
136
+
137
+ .history-item {
138
+ background: rgba(18, 22, 40, 0.7);
139
+ backdrop-filter: blur(5px);
140
+ padding: 12px 14px;
141
+ border-radius: 12px;
142
+ margin-bottom: 10px;
143
+ cursor: pointer;
144
+ transition: 0.2s;
145
+ border: 1px solid rgba(0, 212, 255, 0.1);
146
+ font-weight: 500;
147
+ font-size: 13px;
148
+ word-break: break-word;
149
+ }
150
+
151
+ .history-item:active {
152
+ background: rgba(0, 212, 255, 0.2);
153
+ transform: translateX(4px);
154
+ }
155
+
156
+ /* =========================
157
+ OVERLAY
158
+ ========================= */
159
+ .overlay {
160
+ position: fixed;
161
+ width: 100%;
162
+ height: 100vh;
163
+ background: rgba(0, 0, 0, 0.7);
164
+ backdrop-filter: blur(4px);
165
+ z-index: 999;
166
+ display: none;
167
+ opacity: 0;
168
+ transition: opacity 0.3s ease;
169
+ }
170
+
171
+ .overlay.active {
172
+ display: block;
173
+ opacity: 1;
174
+ }
175
+
176
+ /* =========================
177
+ MAIN - CYBER DARK
178
+ ========================= */
179
+ .main {
180
+ display: flex;
181
+ flex-direction: column;
182
+ width: 100%;
183
+ background: #0a0c15;
184
+ position: relative;
185
+ }
186
+
187
+ /* =========================
188
+ TOPBAR - NEO MASCULINE
189
+ ========================= */
190
+ .topbar {
191
+ height: 65px;
192
+ background: rgba(10, 12, 21, 0.95);
193
+ backdrop-filter: blur(10px);
194
+ display: flex;
195
+ align-items: center;
196
+ padding: 0 16px;
197
+ color: #e8edff;
198
+ border-bottom: 1px solid rgba(0, 212, 255, 0.3);
199
+ box-shadow: 0 2px 20px rgba(0, 0, 0, 0.3);
200
+ position: sticky;
201
+ top: 0;
202
+ z-index: 10;
203
+ }
204
+
205
+ .menu-btn {
206
+ font-size: 24px;
207
+ margin-right: 12px;
208
+ cursor: pointer;
209
+ transition: 0.2s;
210
+ color: #00d4ff;
211
+ padding: 8px;
212
+ border-radius: 8px;
213
+ }
214
+
215
+ .menu-btn:active {
216
+ background: rgba(0, 212, 255, 0.1);
217
+ transform: scale(0.95);
218
+ }
219
+
220
+ .logo {
221
+ width: 42px;
222
+ height: 42px;
223
+ border-radius: 12px;
224
+ background: linear-gradient(135deg, #00d4ff, #0099ff);
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: center;
228
+ margin-right: 12px;
229
+ box-shadow: 0 0 12px rgba(0, 212, 255, 0.5);
230
+ overflow: hidden;
231
+ flex-shrink: 0;
232
+ }
233
+
234
+ .logo-img {
235
+ width: 100%;
236
+ height: 100%;
237
+ object-fit: cover;
238
+ border-radius: 12px;
239
+ }
240
+
241
+ .logo-icon {
242
+ font-size: 24px;
243
+ }
244
+
245
+ .title {
246
+ min-width: 0;
247
+ flex: 1;
248
+ }
249
+
250
+ .title h1 {
251
+ font-size: 18px;
252
+ letter-spacing: -0.5px;
253
+ background: linear-gradient(135deg, #e8edff, #00d4ff);
254
+ -webkit-background-clip: text;
255
+ background-clip: text;
256
+ color: transparent;
257
+ white-space: nowrap;
258
+ overflow: hidden;
259
+ text-overflow: ellipsis;
260
+ }
261
+
262
+ .title p {
263
+ font-size: 10px;
264
+ color: #00d4ff;
265
+ display: flex;
266
+ align-items: center;
267
+ gap: 4px;
268
+ }
269
+
270
+ .title p::before {
271
+ content: "●";
272
+ font-size: 8px;
273
+ color: #00ff88;
274
+ text-shadow: 0 0 5px #00ff88;
275
+ }
276
+
277
+ /* =========================
278
+ CHAT AREA - GLASS
279
+ ========================= */
280
+ .chat-area {
281
+ flex: 1;
282
+ overflow-y: auto;
283
+ padding: 16px;
284
+ background: linear-gradient(180deg, #0a0c15 0%, #0f111a 100%);
285
+ -webkit-overflow-scrolling: touch;
286
+ }
287
+
288
+ .chat-area::-webkit-scrollbar {
289
+ width: 4px;
290
+ }
291
+
292
+ .chat-area::-webkit-scrollbar-track {
293
+ background: #1a1f2e;
294
+ border-radius: 10px;
295
+ }
296
+
297
+ .chat-area::-webkit-scrollbar-thumb {
298
+ background: #00d4ff;
299
+ border-radius: 10px;
300
+ }
301
+
302
+ .message {
303
+ max-width: 85%;
304
+ padding: 12px 16px;
305
+ border-radius: 18px;
306
+ margin-bottom: 12px;
307
+ font-size: 14px;
308
+ line-height: 1.5;
309
+ word-wrap: break-word;
310
+ animation: fadeInUp 0.3s ease;
311
+ }
312
+
313
+ @keyframes fadeInUp {
314
+ from {
315
+ opacity: 0;
316
+ transform: translateY(10px);
317
  }
318
+ to {
319
+ opacity: 1;
320
+ transform: translateY(0);
321
+ }
322
+ }
323
+
324
+ .bot {
325
+ background: rgba(20, 24, 40, 0.9);
326
+ backdrop-filter: blur(10px);
327
+ border: 1px solid rgba(0, 212, 255, 0.2);
328
+ color: #d0d9ff;
329
+ border-bottom-left-radius: 4px;
330
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
331
+ margin-right: auto;
332
+ }
333
+
334
+ .user {
335
+ background: linear-gradient(135deg, #00d4ff, #0099ff);
336
+ color: #0a0c15;
337
+ font-weight: 600;
338
+ margin-left: auto;
339
+ border-bottom-right-radius: 4px;
340
+ box-shadow: 0 2px 8px rgba(0, 212, 255, 0.3);
341
+ }
342
+
343
+ /* =========================
344
+ INPUT - CYBER
345
+ ========================= */
346
+ .input-area {
347
+ display: flex;
348
+ padding: 12px 16px;
349
+ gap: 10px;
350
+ background: rgba(10, 12, 21, 0.95);
351
+ backdrop-filter: blur(10px);
352
+ border-top: 1px solid rgba(0, 212, 255, 0.2);
353
+ position: sticky;
354
+ bottom: 0;
355
+ }
356
+
357
+ input {
358
+ flex: 1;
359
+ border: 1px solid rgba(0, 212, 255, 0.3);
360
+ background: rgba(18, 22, 40, 0.8);
361
+ border-radius: 24px;
362
+ padding: 12px 18px;
363
+ font-size: 14px;
364
+ outline: none;
365
+ color: #e8edff;
366
+ transition: all 0.2s;
367
+ -webkit-appearance: none;
368
+ }
369
+
370
+ input:focus {
371
+ border-color: #00d4ff;
372
+ box-shadow: 0 0 12px rgba(0, 212, 255, 0.3);
373
+ }
374
+
375
+ input::placeholder {
376
+ color: #4a5578;
377
+ }
378
+
379
+ button.send {
380
+ border: none;
381
+ background: linear-gradient(135deg, #00d4ff, #0099ff);
382
+ color: #0a0c15;
383
+ font-weight: bold;
384
+ padding: 0 20px;
385
+ border-radius: 24px;
386
+ font-size: 14px;
387
+ cursor: pointer;
388
+ transition: 0.2s;
389
+ display: flex;
390
+ align-items: center;
391
+ gap: 6px;
392
+ white-space: nowrap;
393
+ }
394
+
395
+ button.send:active {
396
+ transform: scale(0.97);
397
+ }
398
+
399
+ /* =========================
400
+ RESPONSIVE UNTUK TABLET & DESKTOP
401
+ ========================= */
402
+ @media (min-width: 768px) {
403
+ .sidebar {
404
+ width: 300px;
405
+ left: -320px;
406
+ }
407
+
408
+ .message {
409
+ max-width: 70%;
410
+ font-size: 15px;
411
+ }
412
+
413
+ .topbar {
414
+ height: 72px;
415
+ padding: 0 24px;
416
+ }
417
+
418
+ .menu-btn {
419
+ font-size: 28px;
420
+ margin-right: 20px;
421
+ }
422
+
423
+ .logo {
424
+ width: 50px;
425
+ height: 50px;
426
+ }
427
+
428
+ .title h1 {
429
+ font-size: 22px;
430
+ }
431
+
432
+ .title p {
433
+ font-size: 12px;
434
+ }
435
+ }
436
 
437
+ /* Landscape mode optimization */
438
+ @media (max-width: 900px) and (orientation: landscape) {
439
+ .sidebar {
440
+ width: 260px;
441
+ }
442
+
443
+ .topbar {
444
+ height: 55px;
445
+ }
446
+
447
+ .logo {
448
+ width: 36px;
449
+ height: 36px;
450
+ }
451
+
452
+ .title h1 {
453
+ font-size: 16px;
454
+ }
455
+
456
+ .chat-area {
457
+ padding: 10px;
458
+ }
459
+
460
+ .message {
461
+ margin-bottom: 8px;
462
+ padding: 8px 12px;
463
+ }
464
+
465
+ .input-area {
466
+ padding: 8px 12px;
467
+ }
468
+
469
+ input {
470
+ padding: 8px 14px;
471
+ }
472
+
473
+ button.send {
474
+ padding: 0 16px;
475
+ }
476
+ }
477
+
478
+ /* Loading animation */
479
+ .loading-dots {
480
+ display: inline-flex;
481
+ gap: 4px;
482
+ align-items: center;
483
+ }
484
+
485
+ .loading-dots span {
486
+ width: 6px;
487
+ height: 6px;
488
+ border-radius: 50%;
489
+ background: #00d4ff;
490
+ animation: bounce 1.4s infinite ease-in-out both;
491
+ }
492
+
493
+ .loading-dots span:nth-child(1) { animation-delay: -0.32s; }
494
+ .loading-dots span:nth-child(2) { animation-delay: -0.16s; }
495
+
496
+ @keyframes bounce {
497
+ 0%, 80%, 100% { transform: scale(0); }
498
+ 40% { transform: scale(1); }
499
+ }
500
+
501
+ /* Toast notification */
502
+ .toast {
503
+ position: fixed;
504
+ bottom: 80px;
505
+ left: 50%;
506
+ transform: translateX(-50%) translateY(100px);
507
+ background: rgba(0, 212, 255, 0.9);
508
+ color: #0a0c15;
509
+ padding: 10px 20px;
510
+ border-radius: 30px;
511
+ font-size: 13px;
512
+ font-weight: bold;
513
+ z-index: 1001;
514
+ transition: transform 0.3s ease;
515
+ white-space: nowrap;
516
+ pointer-events: none;
517
+ }
518
+
519
+ .toast.show {
520
+ transform: translateX(-50%) translateY(0);
521
+ }
522
+ </style>
523
+ </head>
524
+ <body>
525
+
526
+ <div class="overlay" id="overlay"></div>
527
+ <div class="toast" id="toast"></div>
528
+
529
+ <div class="app">
530
+
531
+ <!-- SIDEBAR -->
532
+ <div class="sidebar" id="sidebar">
533
+ <div class="sidebar-header">
534
+ <div class="sidebar-logo" id="sidebarLogo">
535
+ 🐱‍👤
536
+ </div>
537
+ <div>
538
+ <h2>ChibiCat</h2>
539
+ <p>Neo Intelligence</p>
540
+ </div>
541
+ </div>
542
+ <button class="new-chat" onclick="createNewChat()">✨ New Chat</button>
543
+ <div class="chat-history" id="chatHistory"></div>
544
+ </div>
545
+
546
+ <!-- MAIN -->
547
+ <div class="main">
548
+ <!-- TOPBAR -->
549
+ <div class="topbar">
550
+ <div class="menu-btn" onclick="toggleSidebar()">☰</div>
551
+ <div class="logo" id="mainLogo">
552
+ 🐱‍👤
553
+ </div>
554
+ <div class="title">
555
+ <h1>ChibiCat AI</h1>
556
+ <p>Online | Neo Mode</p>
557
+ </div>
558
+ </div>
559
+
560
+ <!-- CHAT AREA -->
561
+ <div class="chat-area" id="chatArea">
562
+ <div class="message bot">
563
+ <span style="font-weight: bold;">⚡ ChibiCat Neo</span><br>
564
+ Halo bro! Ada yang bisa gue bantu? 🔥😎
565
+ </div>
566
+ </div>
567
+
568
+ <!-- INPUT -->
569
+ <div class="input-area">
570
+ <input type="text" id="messageInput" placeholder="Ketik pesan..." autocomplete="off" />
571
+ <button class="send" onclick="sendMessage()">
572
+ 📤 Kirim
573
+ </button>
574
+ </div>
575
+ </div>
576
+ </div>
577
+
578
+ <script>
579
+ let currentChatId = null;
580
+ let isLoading = false;
581
+
582
+ // =========================================
583
+ // KONFIGURASI LOGO
584
+ // =========================================
585
+ const LOGO_CONFIG = {
586
+ mode: "emoji", // "emoji", "image", atau "svg"
587
+ emoji: "🐱‍🚀",
588
+ imageUrl: "/static/logo.png",
589
+ imageUrl2: "https://cdn-icons-png.flaticon.com/512/1998/1998592.png",
590
+ svgIcon: "⚡🐱⚡"
591
+ }
592
+
593
+ function initLogos() {
594
+ const sidebarLogo = document.getElementById("sidebarLogo");
595
+ const mainLogo = document.getElementById("mainLogo");
596
+
597
+ if (LOGO_CONFIG.mode === "emoji") {
598
+ sidebarLogo.innerHTML = LOGO_CONFIG.emoji;
599
+ sidebarLogo.style.fontSize = "28px";
600
+ sidebarLogo.style.background = "linear-gradient(135deg, #00d4ff, #0099ff)";
601
+ mainLogo.innerHTML = LOGO_CONFIG.emoji;
602
+ mainLogo.style.fontSize = "24px";
603
+ mainLogo.style.background = "linear-gradient(135deg, #00d4ff, #0099ff)";
604
+ } else if (LOGO_CONFIG.mode === "image") {
605
+ sidebarLogo.innerHTML = `<img src="${LOGO_CONFIG.imageUrl}" class="sidebar-logo-img" onerror="this.src='${LOGO_CONFIG.imageUrl2}'">`;
606
+ sidebarLogo.style.background = "transparent";
607
+ mainLogo.innerHTML = `<img src="${LOGO_CONFIG.imageUrl}" class="logo-img" onerror="this.src='${LOGO_CONFIG.imageUrl2}'">`;
608
+ mainLogo.style.background = "transparent";
609
+ } else {
610
+ sidebarLogo.innerHTML = LOGO_CONFIG.svgIcon;
611
+ sidebarLogo.style.fontSize = "26px";
612
+ mainLogo.innerHTML = LOGO_CONFIG.svgIcon;
613
+ mainLogo.style.fontSize = "22px";
614
+ }
615
+ }
616
+
617
+ function showToast(message, isError = false) {
618
+ const toast = document.getElementById("toast");
619
+ toast.textContent = message;
620
+ toast.style.background = isError ? "rgba(255, 68, 68, 0.95)" : "rgba(0, 212, 255, 0.95)";
621
+ toast.classList.add("show");
622
+ setTimeout(() => {
623
+ toast.classList.remove("show");
624
+ }, 2000);
625
+ }
626
+
627
+ function toggleSidebar() {
628
+ const sidebar = document.getElementById("sidebar");
629
+ const overlay = document.getElementById("overlay");
630
+
631
+ if (sidebar.classList.contains("active")) {
632
+ closeSidebar();
633
+ } else {
634
+ openSidebar();
635
+ }
636
+ }
637
+
638
+ function openSidebar() {
639
+ document.getElementById("sidebar").classList.add("active");
640
+ document.getElementById("overlay").classList.add("active");
641
+ }
642
+
643
+ function closeSidebar() {
644
+ document.getElementById("sidebar").classList.remove("active");
645
+ document.getElementById("overlay").classList.remove("active");
646
+ }
647
+
648
+ // Close sidebar when clicking overlay
649
+ document.getElementById("overlay").addEventListener("click", closeSidebar);
650
+
651
+ async function createNewChat() {
652
+ try {
653
+ const response = await fetch("/new_chat", { method: "POST" });
654
+ const data = await response.json();
655
+ currentChatId = data.chat_id;
656
+ document.getElementById("chatArea").innerHTML = `
657
+ <div class="message bot">
658
+ <span style="font-weight: bold;">⚡ ChibiCat Neo</span><br>
659
+ Halo bro! Ada yang bisa gue bantu? 🔥😎
660
+ </div>
661
+ `;
662
+ await loadChats();
663
+ closeSidebar();
664
+ showToast("Chat baru dibuat!");
665
+ } catch (error) {
666
+ console.error("Error creating new chat:", error);
667
+ showToast("Gagal membuat chat baru", true);
668
+ }
669
+ }
670
+
671
+ async function loadChats() {
672
+ try {
673
+ const response = await fetch("/get_chats");
674
+ const chats = await response.json();
675
+ const history = document.getElementById("chatHistory");
676
+
677
+ if (chats.length === 0) {
678
+ history.innerHTML = `<div style="text-align: center; color: #6c7a9e; padding: 20px;">Belum ada chat</div>`;
679
+ return;
680
+ }
681
+
682
+ history.innerHTML = "";
683
+ chats.forEach(chat => {
684
+ const div = document.createElement("div");
685
+ div.className = "history-item";
686
+ div.innerHTML = `💬 ${escapeHtml(chat.title)}`;
687
+ div.onclick = () => loadChat(chat.chat_id);
688
+ history.appendChild(div);
689
+ });
690
+ } catch (error) {
691
+ console.error("Error loading chats:", error);
692
+ }
693
+ }
694
+
695
+ async function loadChat(chatId) {
696
+ try {
697
+ currentChatId = chatId;
698
+ const response = await fetch(`/load_chat/${chatId}`);
699
+ const messages = await response.json();
700
+ const chatArea = document.getElementById("chatArea");
701
+ chatArea.innerHTML = "";
702
+ messages.forEach(msg => {
703
+ const role = msg.role === "user" ? "user" : "bot";
704
+ chatArea.innerHTML += `<div class="message ${role}">${escapeHtml(msg.content)}</div>`;
705
+ });
706
+ chatArea.scrollTop = chatArea.scrollHeight;
707
+ closeSidebar();
708
+ } catch (error) {
709
+ console.error("Error loading chat:", error);
710
+ showToast("Gagal memuat chat", true);
711
+ }
712
+ }
713
+
714
+ function addLoadingIndicator() {
715
+ const chatArea = document.getElementById("chatArea");
716
+ const loadingDiv = document.createElement("div");
717
+ loadingDiv.className = "message bot";
718
+ loadingDiv.id = "loadingIndicator";
719
+ loadingDiv.innerHTML = `<span style="font-weight: bold;">⚡ ChibiCat Neo</span><br><div class="loading-dots"><span></span><span></span><span></span></div>`;
720
+ chatArea.appendChild(loadingDiv);
721
+ chatArea.scrollTop = chatArea.scrollHeight;
722
+ }
723
+
724
+ function removeLoadingIndicator() {
725
+ const indicator = document.getElementById("loadingIndicator");
726
+ if (indicator) indicator.remove();
727
+ }
728
+
729
+ async function sendMessage() {
730
+ if (isLoading) return;
731
+
732
+ const input = document.getElementById("messageInput");
733
+ const message = input.value.trim();
734
+ if (message === "") return;
735
+
736
+ isLoading = true;
737
+ const chatArea = document.getElementById("chatArea");
738
+
739
+ // Add user message
740
+ chatArea.innerHTML += `<div class="message user">${escapeHtml(message)}</div>`;
741
+ input.value = "";
742
+ chatArea.scrollTop = chatArea.scrollHeight;
743
+
744
+ // Add loading indicator
745
+ addLoadingIndicator();
746
+
747
+ try {
748
+ const response = await fetch("/chat", {
749
+ method: "POST",
750
+ headers: { "Content-Type": "application/json" },
751
+ body: JSON.stringify({ message: message })
752
+ });
753
+
754
+ if (!response.ok) throw new Error("Network response was not ok");
755
+
756
+ const data = await response.json();
757
+ removeLoadingIndicator();
758
+ chatArea.innerHTML += `<div class="message bot">${escapeHtml(data.reply)}</div>`;
759
+ chatArea.scrollTop = chatArea.scrollHeight;
760
+ await loadChats();
761
+ } catch (error) {
762
+ console.error("Error sending message:", error);
763
+ removeLoadingIndicator();
764
+ chatArea.innerHTML += `<div class="message bot">⚠️ Server error, coba lagi nanti bro 😭</div>`;
765
+ chatArea.scrollTop = chatArea.scrollHeight;
766
+ showToast("Gagal mengirim pesan", true);
767
+ } finally {
768
+ isLoading = false;
769
+ }
770
+ }
771
+
772
+ function escapeHtml(text) {
773
+ if (!text) return "";
774
+ const div = document.createElement('div');
775
+ div.textContent = text;
776
+ return div.innerHTML;
777
+ }
778
+
779
+ // Fix untuk keyboard di mobile
780
+ function fixMobileKeyboard() {
781
+ const input = document.getElementById("messageInput");
782
+ const chatArea = document.querySelector('.chat-area');
783
+
784
+ input.addEventListener("focus", () => {
785
+ setTimeout(() => {
786
+ chatArea.scrollTop = chatArea.scrollHeight;
787
+ }, 300);
788
+ });
789
+
790
+ // Prevent zoom on input focus (iOS)
791
+ input.style.fontSize = "16px";
792
+ }
793
+
794
+ // Swipe gesture untuk Android
795
+ let touchStartX = 0;
796
+ let touchEndX = 0;
797
+
798
+ document.addEventListener('touchstart', (e) => {
799
+ touchStartX = e.changedTouches[0].screenX;
800
+ }, { passive: true });
801
+
802
+ document.addEventListener('touchend', (e) => {
803
+ touchEndX = e.changedTouches[0].screenX;
804
+ const swipeDistance = touchEndX - touchStartX;
805
+ const sidebar = document.getElementById("sidebar");
806
+
807
+ if (swipeDistance > 50 && touchStartX < 50 && !sidebar.classList.contains("active")) {
808
+ openSidebar();
809
+ } else if (swipeDistance < -50 && sidebar.classList.contains("active")) {
810
+ closeSidebar();
811
+ }
812
+ }, { passive: true });
813
+
814
+ // Enter to send
815
+ document.getElementById("messageInput").addEventListener("keypress", (e) => {
816
+ if (e.key === "Enter" && !e.shiftKey) {
817
+ e.preventDefault();
818
+ sendMessage();
819
+ }
820
+ });
821
+
822
+ // Prevent body scroll when sidebar is open
823
+ function preventBodyScroll(shouldPrevent) {
824
+ if (shouldPrevent) {
825
+ document.body.style.overflow = 'hidden';
826
+ } else {
827
+ document.body.style.overflow = '';
828
+ }
829
+ }
830
+
831
+ // Observer for sidebar state
832
+ const observer = new MutationObserver((mutations) => {
833
+ mutations.forEach((mutation) => {
834
+ if (mutation.attributeName === 'class') {
835
+ const sidebar = document.getElementById("sidebar");
836
+ preventBodyScroll(sidebar.classList.contains("active"));
837
+ }
838
+ });
839
+ });
840
+
841
+ observer.observe(document.getElementById("sidebar"), { attributes: true });
842
+
843
+ // Detect device and add appropriate classes
844
+ function detectDevice() {
845
+ const ua = navigator.userAgent.toLowerCase();
846
+ const isAndroid = ua.includes("android");
847
+ const isIOS = /iphone|ipad|ipod/.test(ua);
848
+
849
+ if (isAndroid) {
850
+ document.body.classList.add("android-device");
851
+ } else if (isIOS) {
852
+ document.body.classList.add("ios-device");
853
+ }
854
+ }
855
+
856
+ // Initialize everything
857
+ initLogos();
858
+ loadChats();
859
+ detectDevice();
860
+ fixMobileKeyboard();
861
+
862
+ // Scroll to bottom on initial load
863
+ setTimeout(() => {
864
+ const chatArea = document.getElementById("chatArea");
865
+ chatArea.scrollTop = chatArea.scrollHeight;
866
+ }, 100);
867
+ </script>
868
+
869
+ </body>
870
+ </html>