DevouringStars commited on
Commit
85f4d1b
·
1 Parent(s): e376ddc

feat: Add full Python backend with Flask, RAG, Mongo, and Chroma

Browse files
.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Folder library Python (wajib)
2
+ venv/
3
+
4
+ # Database ChromaDB lokal (wajib)
5
+ my_chroma_db/
6
+
7
+ # File rahasia API keys (wajib)
8
+ .env
9
+
10
+ # Cache Python
11
+ __pycache__/
12
+ *.pyc
app.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from flask import Flask, request, jsonify, render_template
3
+ from flask_cors import CORS
4
+ from pymongo import MongoClient, ReturnDocument
5
+ from pymongo.errors import PyMongoError
6
+ from chromadb import HttpClient
7
+ from sentence_transformers import SentenceTransformer
8
+ from huggingface_hub import InferenceClient
9
+ from dotenv import load_dotenv
10
+ from bson.objectid import ObjectId
11
+ from datetime import datetime
12
+
13
+ # --- 1. Muat .env dan Inisialisasi ---
14
+ load_dotenv()
15
+ app = Flask(__name__)
16
+ CORS(app)
17
+
18
+ # --- 2. Inisialisasi Klien (Global) ---
19
+ hf_token = os.getenv("HF_TOKEN")
20
+ hf_client = InferenceClient(
21
+ "meta-llama/Meta-Llama-3-8B-Instruct",
22
+ token=hf_token
23
+ )
24
+
25
+ mongo_url = os.getenv("MONGO_URL")
26
+ mongo_client = MongoClient(mongo_url)
27
+ db = mongo_client.get_database("chatbot_db")
28
+ chat_history_collection = db.get_collection("conversations")
29
+ print("✅ Berhasil terkoneksi ke MongoDB Atlas.")
30
+
31
+ print("Memuat model embedding (ini mungkin butuh waktu)...")
32
+ embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
33
+ print("✅ Model embedding ('all-MiniLM-L6-v2') berhasil dimuat.")
34
+
35
+ try:
36
+ chroma_client = HttpClient(host='localhost', port=8000)
37
+ knowledge_collection = chroma_client.get_collection(
38
+ name="website_knowledge"
39
+ )
40
+ print("✅ Berhasil terkoneksi ke ChromaDB (localhost:8000).")
41
+ except Exception as e:
42
+ print(f"❌ GAGAL terhubung ke ChromaDB. Pastikan server chroma run... berjalan. Error: {e}")
43
+
44
+ # --- 3. Endpoint Frontend ---
45
+ @app.route("/")
46
+ def home():
47
+ return render_template("chat.html")
48
+
49
+ # --- 4. Endpoint API - Ambil SEMUA History (untuk F5) ---
50
+ @app.route("/api/conversations", methods=["GET"])
51
+ def get_conversations():
52
+ user_id = request.args.get("userId")
53
+ if not user_id:
54
+ return jsonify({"error": "userId dibutuhkan"}), 400
55
+
56
+ try:
57
+ convos = list(chat_history_collection.find(
58
+ {"userId": user_id}
59
+ ).sort("updatedAt", -1))
60
+
61
+ conversations_list = []
62
+ for convo in convos:
63
+ conversations_list.append({
64
+ "id": str(convo.get('_id')),
65
+ "title": convo.get("title", "Obrolan Baru"),
66
+ "messages": convo.get("messages", []),
67
+ "createdAt": convo.get("createdAt")
68
+ })
69
+ return jsonify(conversations_list)
70
+ except Exception as e:
71
+ print(f"Error di /api/conversations GET: {e}")
72
+ return jsonify({"error": "Gagal mengambil percakapan"}), 500
73
+
74
+ # --- 5. ENDPOINT API BARU (UNTUK "CLEAR ALL") ---
75
+ @app.route("/api/conversations", methods=["DELETE"])
76
+ def clear_conversations():
77
+ """
78
+ Menghapus SEMUA percakapan untuk satu userId.
79
+ """
80
+ data = request.json
81
+ user_id = data.get("userId")
82
+ if not user_id:
83
+ return jsonify({"error": "userId dibutuhkan"}), 400
84
+
85
+ try:
86
+ # Hapus semua dokumen di MongoDB yang cocok dengan userId
87
+ result = chat_history_collection.delete_many({"userId": user_id})
88
+
89
+ print(f"Berhasil menghapus {result.deleted_count} percakapan untuk userId {user_id}.")
90
+ return jsonify({
91
+ "message": "History berhasil dihapus",
92
+ "deleted_count": result.deleted_count
93
+ })
94
+
95
+ except Exception as e:
96
+ print(f"Error di /api/conversations DELETE: {e}")
97
+ return jsonify({"error": "Gagal menghapus history"}), 500
98
+
99
+ # --- 6. Endpoint API Chat (Utama) ---
100
+ @app.route("/api/chat", methods = ["POST"])
101
+ def handle_chat():
102
+ try:
103
+ data = request.json
104
+ user_message = data.get("message")
105
+ user_id = data.get("userId")
106
+ conversation_id = data.get("conversationId") # ID chat yang aktif, bisa null
107
+
108
+ if not user_message or not user_id:
109
+ return jsonify({"error": "Parameter 'message' dan 'userId' dibutuhkan."}), 400
110
+
111
+ history = []
112
+ user_message_doc = {"role": "user", "content": user_message, "timestamp": datetime.now()}
113
+
114
+ # --- 1. (CRUD) Ambil/Buat Percakapan ---
115
+ if conversation_id:
116
+ # Ini adalah obrolan yang sudah ada
117
+ current_convo = chat_history_collection.find_one({
118
+ "_id": ObjectId(conversation_id),
119
+ "userId": user_id
120
+ })
121
+ if current_convo:
122
+ history = current_convo.get("messages", [])[-6:]
123
+
124
+ # --- 2. (RAG) - Lakukan RAG (Sama seperti sebelumnya) ---
125
+ print(f"Mencari konteks untuk: \"{user_message}\"")
126
+ query_embedding = embedding_model.encode(user_message).tolist()
127
+ results = knowledge_collection.query(
128
+ query_embeddings=[query_embedding],
129
+ n_results=1 # <-- Anda bisa ganti ini ke 1 nanti
130
+ )
131
+ context = "\n\n".join(results['documents'][0])
132
+ print("Konteks ditemukan.")
133
+
134
+ # --- 3. (RAG) Buat prompt ---
135
+ # <-- Anda bisa perketat prompt ini nanti
136
+ system_prompt = """You are a precise assistant. Answer *strictly* and *only* based on the context provided.
137
+ Do not add any information, pollutants, or applications that are not *explicitly* mentioned in the text.
138
+ If the context provides conflicting information (like for different products), only use the information from the *single most relevant* chunk."""
139
+ formatted_history = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history])
140
+ user_prompt = f"Context:\n{context}\n\nChat History:\n{formatted_history}\n\nQuestion:\n{user_message}"
141
+ messages = [
142
+ {"role": "system", "content": system_prompt},
143
+ {"role": "user", "content": user_prompt}
144
+ ]
145
+
146
+ # --- 4. (RAG) Panggil API Llama 3 ---
147
+ print("Memanggil Hugging Face API...")
148
+ response = hf_client.chat_completion(messages=messages, max_tokens=250, temperature=0.1)
149
+ ai_response = response.choices[0].message.content
150
+
151
+ # --- 5. (CRUD) Simpan balasan AI ke MongoDB ---
152
+ ai_message_doc = {"role": "assistant", "content": ai_response, "timestamp": datetime.now()}
153
+
154
+ if conversation_id: # Obrolan lama
155
+ chat_history_collection.update_one(
156
+ {"_id": ObjectId(conversation_id)},
157
+ {
158
+ "$push": {"messages": {"$each": [user_message_doc, ai_message_doc]}},
159
+ "$set": {"updatedAt": datetime.now()}
160
+ }
161
+ )
162
+ else: # Obrolan baru
163
+ title = user_message[:30] + "..." if len(user_message) > 30 else user_message
164
+ new_convo_doc = {
165
+ "userId": user_id,
166
+ "title": title,
167
+ "messages": [user_message_doc, ai_message_doc], # Langsung tambahkan user & AI
168
+ "createdAt": datetime.now(),
169
+ "updatedAt": datetime.now()
170
+ }
171
+ insert_result = chat_history_collection.insert_one(new_convo_doc)
172
+ conversation_id = insert_result.inserted_id # Ambil _id baru
173
+
174
+ print("Percakapan berhasil disimpan ke MongoDB.")
175
+
176
+ # --- 6. Kirim balasan ---
177
+ return jsonify({"answer": ai_response, "conversationId": str(conversation_id)})
178
+
179
+ except Exception as e:
180
+ print(f"Error di /api/chat: {e}")
181
+ return jsonify({"error": "Terjadi kesalahan di server"}), 500
182
+
183
+ # --- 7. Jalankan Server ---
184
+ if __name__ == "__main__":
185
+ app.run(port=3001, debug=True)
contoh_struktur.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_id": "67f89abf8b4d... (MongoDB ID)",
3
+ "userId": "anon-uuid-12345-abcde", // Anonymous User ID Anda
4
+ "createdAt": "2025-10-28T12:00:00Z",
5
+ "updatedAt": "2025-10-28T12:05:00Z",
6
+ "title": "Percakapan tentang Air Quality", // (Opsional)
7
+ "messages": [
8
+ {
9
+ "role": "user",
10
+ "content": "Can you describe about the Air Quality Tester?",
11
+ "timestamp": "2025-10-28T12:00:00Z"
12
+ },
13
+ {
14
+ "role": "assistant",
15
+ "content": "Our Air Quality Tester is a compact, lightweight device...",
16
+ "timestamp": "2025-10-28T12:01:00Z"
17
+ },
18
+ {
19
+ "role": "user",
20
+ "content": "Does it work with Google Sheets?",
21
+ "timestamp": "2025-10-28T12:04:00Z"
22
+ }
23
+ ]
24
+ }
knowledge_base_FINAL_COMBINED.csv ADDED
The diff for this file is too large to render. See raw diff
 
load_data.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from sentence_transformers import SentenceTransformer
3
+ from chromadb import HttpClient
4
+ import sys
5
+
6
+ # --- 1. Inisialisasi Klien & Model ---
7
+ print("Menghubungkan ke ChromaDB di localhost:8000...")
8
+ try:
9
+ # Kita terhubung ke server yang sama dengan app.py
10
+ client = HttpClient(host='localhost', port=8000)
11
+ collection = client.get_or_create_collection(name="website_knowledge")
12
+ print("✅ Berhasil terhubung ke ChromaDB.")
13
+ except Exception as e:
14
+ print(f"❌ GAGAL terhubung ke ChromaDB. Pastikan server ChromaDB berjalan.")
15
+ print(f"Error: {e}")
16
+ sys.exit(1)
17
+
18
+ print("Memuat model embedding 'all-MiniLM-L6-v2' (ini mungkin butuh waktu)...")
19
+ model = SentenceTransformer('all-MiniLM-L6-v2')
20
+ print("✅ Model embedding berhasil dimuat.")
21
+
22
+ # --- 2. Baca File CSV ---
23
+ file_path = "knowledge_base_FINAL_COMBINED.csv" # Pastikan nama file ini benar
24
+ try:
25
+ df = pd.read_csv(file_path)
26
+ df = df.fillna('')
27
+ print(f"✅ Berhasil memuat {len(df)} chunk dari {file_path}.")
28
+ except FileNotFoundError:
29
+ print(f"❌ Error: File {file_path} tidak ditemukan.")
30
+ sys.exit(1)
31
+
32
+ # --- 3. Siapkan Data ---
33
+ documents = df['chunk_text'].tolist()
34
+ metadatas = df[['main_topic', 'chunk_title', 'src_url']].to_dict('records')
35
+ ids = df['chunk_id'].tolist()
36
+
37
+ # --- 4. BUAT EMBEDDING (Bagian Paling Penting) ---
38
+ # app.py membuat embedding 'on-the-fly' untuk 1 pertanyaan
39
+ # Script ini membuat embedding untuk SEMUA dokumen sekaligus
40
+ print(f"Memulai proses embedding untuk {len(documents)} dokumen...")
41
+ embeddings = model.encode(documents, show_progress_bar=True)
42
+ print("✅ Embedding selesai.")
43
+
44
+ # --- 5. Tambahkan ke ChromaDB ---
45
+ # Hapus data lama (jika ada) agar tidak duplikat
46
+ try:
47
+ collection.delete(ids=ids)
48
+ print("Data lama di collection berhasil dihapus.")
49
+ except Exception as e:
50
+ print("Tidak ada data lama untuk dihapus, melanjutkan...")
51
+
52
+ print("Menambahkan data baru ke ChromaDB (dalam batch)...")
53
+ batch_size = 100
54
+ for i in range(0, len(ids), batch_size):
55
+ print(f" Menambahkan batch {i//batch_size + 1}...")
56
+
57
+ collection.add(
58
+ embeddings=embeddings[i:i+batch_size].tolist(),
59
+ documents=documents[i:i+batch_size],
60
+ metadatas=metadatas[i:i+batch_size],
61
+ ids=ids[i:i+batch_size]
62
+ )
63
+
64
+ print(f"🎉 SEMUA SELESAI! {collection.count()} dokumen berhasil disimpan di ChromaDB.")
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ flask
2
+ flask-cors
3
+ chromadb
4
+ sentence-transformers
5
+ huggingface_hub
6
+ pymongo
7
+ python-dotenv
8
+ pandas
script.js DELETED
@@ -1,195 +0,0 @@
1
- document.addEventListener("DOMContentLoaded", () => {
2
- // === 1. MENANGKAP ELEMEN DOM ===
3
- const sendButton = document.getElementById("send-button");
4
- const chatInput = document.getElementById("chat-input");
5
- const historyList = document.getElementById("history-list");
6
- const chatLog = document.getElementById("chat-log-area");
7
- const newChatButton = document.querySelector(".new-chat-btn");
8
-
9
- // Simpan HTML untuk placeholder "chat kosong"
10
- const emptyChatPlaceholder = `
11
- <div class="empty-chat-placeholder">
12
- <i class="fa-solid fa-robot"></i>
13
- <h2>Hello Name</h2>
14
- </div>
15
- `;
16
-
17
- // === 2. DATA UTAMA APLIKASI (STATE) ===
18
- let conversations = []; // Menyimpan SEMUA percakapan
19
- let currentConversationId = null; // Melacak chat mana yang sedang AKTIF
20
-
21
- // === 3. EVENT LISTENERS ===
22
-
23
- // Klik tombol "New Chat"
24
- newChatButton.addEventListener("click", (e) => {
25
- e.preventDefault(); // Mencegah link <a> me-refresh halaman
26
- startNewChat();
27
- });
28
-
29
- // Klik tombol "Send"
30
- sendButton.addEventListener("click", sendMessage);
31
-
32
- // Tekan "Enter" di input
33
- chatInput.addEventListener("keydown", (event) => {
34
- if (event.key === "Enter") {
35
- sendMessage();
36
- }
37
- });
38
-
39
- // Klik salah satu item di history
40
- historyList.addEventListener("click", (e) => {
41
- const clickedLi = e.target.closest("li"); // Dapatkan elemen <li> yang diklik
42
- if (clickedLi) {
43
- const id = Number(clickedLi.dataset.id); // Ambil ID dari data-attribute
44
- switchConversation(id);
45
- }
46
- });
47
-
48
- // === 4. FUNGSI-FUNGSI UTAMA ===
49
-
50
- /**
51
- * Memulai chat baru (membersihkan area chat)
52
- */
53
- function startNewChat() {
54
- currentConversationId = null; // Set ID chat aktif ke null
55
- chatInput.value = ""; // Kosongkan input
56
- renderChatLog(); // Tampilkan placeholder
57
- renderHistory(); // Perbarui history (untuk hapus highlight 'active')
58
- }
59
-
60
- /**
61
- * Mengirim pesan
62
- */
63
- function sendMessage() {
64
- const messageText = chatInput.value.trim();
65
- if (messageText === "") return;
66
-
67
- let activeConversation;
68
-
69
- if (currentConversationId === null) {
70
- // Ini adalah chat BARU
71
- const newId = Date.now(); // Buat ID unik berdasarkan waktu
72
- activeConversation = {
73
- id: newId,
74
- title: messageText.length > 28 ? messageText.substring(0, 28) + "..." : messageText,
75
- messages: [] // Array pesan untuk chat ini
76
- };
77
- conversations.unshift(activeConversation); // Tambahkan ke awal array
78
- currentConversationId = newId; // Set sebagai chat aktif
79
- renderHistory(); // Gambar ulang seluruh history list
80
- } else {
81
- // Ini adalah chat LAMA
82
- activeConversation = conversations.find(c => c.id === currentConversationId);
83
- }
84
-
85
- // Tambahkan pesan user ke data
86
- activeConversation.messages.push({ sender: "user", text: messageText });
87
-
88
- // Tampilkan pesan di layar
89
- renderChatLog();
90
- chatInput.value = ""; // Kosongkan input
91
-
92
- // Simulasikan balasan bot
93
- simulateBotReply(activeConversation.id, messageText);
94
- }
95
-
96
- /**
97
- * Mengganti percakapan yang aktif
98
- * @param {number} id - ID percakapan yang ingin dibuka
99
- */
100
- function switchConversation(id) {
101
- if (currentConversationId === id) return; // Jangan lakukan apa-apa jika chat sudah aktif
102
-
103
- currentConversationId = id;
104
- renderChatLog();
105
- renderHistory();
106
- }
107
-
108
- /**
109
- * Menggambar ulang (me-render) seluruh daftar history di sidebar
110
- */
111
- function renderHistory() {
112
- historyList.innerHTML = ""; // Kosongkan list
113
- conversations.forEach(convo => {
114
- const li = document.createElement("li");
115
- li.dataset.id = convo.id; // Simpan ID di data-attribute
116
- li.innerHTML = `<i class="fa-regular fa-comment-dots"></i> ${convo.title}`;
117
-
118
- if (convo.id === currentConversationId) {
119
- li.classList.add("active"); // Beri highlight jika ini chat aktif
120
- }
121
- historyList.appendChild(li);
122
- });
123
- }
124
-
125
- /**
126
- * Menggambar ulang (me-render) seluruh log chat di area kanan
127
- */
128
- function renderChatLog() {
129
- if (currentConversationId === null) {
130
- // Jika tidak ada chat aktif, tampilkan placeholder
131
- chatLog.innerHTML = emptyChatPlaceholder;
132
- return;
133
- }
134
-
135
- // Cari data percakapan yang aktif
136
- const activeConversation = conversations.find(c => c.id === currentConversationId);
137
- if (!activeConversation) {
138
- // Jika tidak ketemu (seharusnya tidak terjadi), kembali ke state awal
139
- startNewChat();
140
- return;
141
- }
142
-
143
- chatLog.innerHTML = ""; // Kosongkan area chat
144
-
145
- // Loop melalui setiap pesan di percakapan aktif dan buat HTML-nya
146
- activeConversation.messages.forEach(message => {
147
- const messageDiv = document.createElement("div");
148
- messageDiv.classList.add("chat-message", message.sender);
149
-
150
- let avatar = message.sender === "user" ? "AN" : '<i class="fa-solid fa-robot"></i>';
151
- let name = message.sender === "user" ? "" : "<strong>CHAT A.I+</strong>";
152
-
153
- messageDiv.innerHTML = `
154
- <div class="avatar">${avatar}</div>
155
- <div class="message-content">
156
- ${name}
157
- <p>${message.text}</p>
158
- </div>
159
- `;
160
- chatLog.appendChild(messageDiv);
161
- });
162
-
163
- // Auto-scroll ke pesan terbaru
164
- chatLog.scrollTop = chatLog.scrollHeight;
165
- }
166
-
167
- /**
168
- * Simulasi balasan bot
169
- * @param {number} convoId - ID chat mana yang harus dibalas
170
- * @param {string} userMessage - Pesan dari user (untuk logika balasan)
171
- */
172
- function simulateBotReply(convoId, userMessage) {
173
- let botText = "Maaf, saya tidak mengerti."; // Balasan default
174
-
175
- if (userMessage.toLowerCase().includes("halo")) {
176
- botText = "Halo juga! Ada yang bisa saya bantu?";
177
- }
178
-
179
- setTimeout(() => {
180
- // Cari percakapan yang benar (bisa jadi user sudah pindah chat)
181
- const conversationToReply = conversations.find(c => c.id === convoId);
182
- if (conversationToReply) {
183
- conversationToReply.messages.push({ sender: "bot", text: botText });
184
-
185
- // HANYA render ulang jika chat yang dibalas masih aktif
186
- if (currentConversationId === convoId) {
187
- renderChatLog();
188
- }
189
- }
190
- }, 1000); // Balas setelah 1 detik
191
- }
192
-
193
- // --- Inisialisasi Aplikasi ---
194
- startNewChat(); // Mulai aplikasi dalam keadaan "New Chat"
195
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/script.js ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener("DOMContentLoaded", () => {
2
+ // === 1. MENANGKAP ELEMEN DOM ===
3
+ const sendButton = document.getElementById("send-button");
4
+ const chatInput = document.getElementById("chat-input");
5
+ const historyList = document.getElementById("history-list");
6
+ const chatLog = document.getElementById("chat-log-area");
7
+ const newChatButton = document.querySelector(".new-chat-btn");
8
+ const clearAllButton = document.querySelector(".clear-all"); // <-- TAMBAHAN BARU
9
+
10
+ const emptyChatPlaceholder = `
11
+ <div class="empty-chat-placeholder">
12
+ <i class="fa-solid fa-robot"></i>
13
+ <h2>Hello Name</h2>
14
+ </div>
15
+ `;
16
+
17
+ // === 2. DATA UTAMA APLIKASI (STATE) ===
18
+ let conversations = {};
19
+ let currentConversationId = null;
20
+ let currentUserId = getOrCreateUserId();
21
+
22
+ // === 3. EVENT LISTENERS ===
23
+ newChatButton.addEventListener("click", (e) => {
24
+ e.preventDefault();
25
+ startNewChat();
26
+ });
27
+ sendButton.addEventListener("click", sendMessage);
28
+ chatInput.addEventListener("keydown", (event) => {
29
+ if (event.key === "Enter" && !event.shiftKey) {
30
+ event.preventDefault();
31
+ sendMessage();
32
+ }
33
+ });
34
+ historyList.addEventListener("click", (e) => {
35
+ const clickedLi = e.target.closest("li");
36
+ if (clickedLi) {
37
+ const id = clickedLi.dataset.id;
38
+ switchConversation(id);
39
+ }
40
+ });
41
+
42
+ // <-- LISTENER BARU UNTUK "CLEAR ALL" ---
43
+ clearAllButton.addEventListener("click", (e) => {
44
+ e.preventDefault();
45
+ clearAllConversations();
46
+ });
47
+
48
+ // === 4. FUNGSI-FUNGSI UTAMA ===
49
+
50
+ /**
51
+ * FUNGSI BARU: Menghapus semua percakapan
52
+ */
53
+ async function clearAllConversations() {
54
+ if (!confirm("Apakah Anda yakin ingin menghapus semua percakapan? Tindakan ini tidak bisa dibatalkan.")) {
55
+ return;
56
+ }
57
+
58
+ try {
59
+ const response = await fetch("http://localhost:3001/api/conversations", {
60
+ method: "DELETE",
61
+ headers: { "Content-Type": "application/json" },
62
+ body: JSON.stringify({ userId: currentUserId }) // Kirim userId untuk dihapus
63
+ });
64
+
65
+ if (!response.ok) {
66
+ throw new Error("Gagal menghapus history di server.");
67
+ }
68
+
69
+ // Jika server berhasil, bersihkan state frontend
70
+ conversations = {};
71
+ startNewChat(); // Ini akan membersihkan UI (merender history & chat log)
72
+
73
+ console.log("Semua percakapan berhasil dihapus.");
74
+
75
+ } catch (error) {
76
+ console.error("Error clearing conversations:", error);
77
+ alert("Terjadi kesalahan saat menghapus history.");
78
+ }
79
+ }
80
+
81
+ // ... (Fungsi getOrCreateUserId, startNewChat, sendMessage, dll. tetap sama persis) ...
82
+
83
+ function getOrCreateUserId() {
84
+ let userId = localStorage.getItem('anonymousUserId');
85
+ if (!userId) {
86
+ userId = 'anon-' + Date.now() + '-' + Math.floor(Math.random() * 1000);
87
+ localStorage.setItem('anonymousUserId', userId);
88
+ }
89
+ return userId;
90
+ }
91
+
92
+ function startNewChat() {
93
+ currentConversationId = null;
94
+ chatInput.value = "";
95
+ renderChatLog();
96
+ renderHistory();
97
+ }
98
+
99
+ async function sendMessage() {
100
+ const messageText = chatInput.value.trim();
101
+ if (messageText === "") return;
102
+
103
+ let conversationIdToSend = currentConversationId;
104
+ let tempId = null;
105
+ let activeConversation;
106
+
107
+ if (currentConversationId === null) {
108
+ const title = messageText.length > 28 ? messageText.substring(0, 28) + "..." : messageText;
109
+ tempId = "temp-" + Date.now();
110
+ activeConversation = {
111
+ id: tempId,
112
+ title: title,
113
+ messages: []
114
+ };
115
+ conversations[tempId] = activeConversation;
116
+ currentConversationId = tempId;
117
+ } else {
118
+ activeConversation = conversations[currentConversationId];
119
+ }
120
+
121
+ activeConversation.messages.push({ role: "user", content: messageText });
122
+ renderChatLog();
123
+ renderHistory();
124
+ chatInput.value = "";
125
+
126
+ const loadingDiv = addMessageToLog("assistant", "...");
127
+ chatLog.scrollTop = chatLog.scrollHeight;
128
+
129
+ try {
130
+ const response = await fetch("http://localhost:3001/api/chat", {
131
+ method: "POST",
132
+ headers: { "Content-Type": "application/json" },
133
+ body: JSON.stringify({
134
+ message: messageText,
135
+ userId: currentUserId,
136
+ conversationId: conversationIdToSend
137
+ })
138
+ });
139
+
140
+ if (!response.ok) throw new Error("Network response was not ok.");
141
+ const data = await response.json();
142
+ if (data.error) throw new Error(data.error);
143
+
144
+ const botText = data.answer;
145
+ const realConversationId = data.conversationId;
146
+
147
+ activeConversation.messages.push({ role: "assistant", content: botText });
148
+ const p = loadingDiv.querySelector(".message-content p");
149
+ p.textContent = botText;
150
+
151
+ if (currentConversationId === tempId) {
152
+ activeConversation.id = realConversationId;
153
+ conversations[realConversationId] = activeConversation;
154
+ delete conversations[tempId];
155
+ currentConversationId = realConversationId;
156
+ renderHistory();
157
+ }
158
+
159
+ } catch (error) {
160
+ console.error("Error sending message:", error);
161
+ const p = loadingDiv.querySelector(".message-content p");
162
+ p.textContent = "Maaf, terjadi kesalahan. Coba lagi.";
163
+ }
164
+
165
+ chatLog.scrollTop = chatLog.scrollHeight;
166
+ }
167
+
168
+ function switchConversation(id) {
169
+ if (currentConversationId === id) return;
170
+ currentConversationId = id;
171
+ renderChatLog();
172
+ renderHistory();
173
+ }
174
+
175
+ function renderHistory() {
176
+ historyList.innerHTML = "";
177
+
178
+ const sortedConversations = Object.values(conversations).sort((a, b) => {
179
+ const lastMsgA = a.messages[a.messages.length - 1]?.timestamp || a.id;
180
+ const lastMsgB = b.messages[b.messages.length - 1]?.timestamp || b.id;
181
+ return new Date(lastMsgB) - new Date(lastMsgA);
182
+ });
183
+
184
+ sortedConversations.forEach(convo => {
185
+ const li = document.createElement("li");
186
+ li.dataset.id = convo.id;
187
+ li.innerHTML = `<i class="fa-regular fa-comment-dots"></i> ${convo.title}`;
188
+ if (convo.id === currentConversationId) {
189
+ li.classList.add("active");
190
+ }
191
+ historyList.appendChild(li);
192
+ });
193
+ }
194
+
195
+ function renderChatLog() {
196
+ if (currentConversationId === null) {
197
+ chatLog.innerHTML = emptyChatPlaceholder;
198
+ return;
199
+ }
200
+
201
+ const activeConversation = conversations[currentConversationId];
202
+ if (!activeConversation) {
203
+ startNewChat();
204
+ return;
205
+ }
206
+
207
+ chatLog.innerHTML = "";
208
+ activeConversation.messages.forEach(message => {
209
+ addMessageToLog(message.role, message.content);
210
+ });
211
+ chatLog.scrollTop = chatLog.scrollHeight;
212
+ }
213
+
214
+ function addMessageToLog(role, text) {
215
+ const messageDiv = document.createElement("div");
216
+ messageDiv.classList.add("chat-message", role);
217
+
218
+ let avatar = role === "user" ? "AN" : '<i class="fa-solid fa-robot"></i>';
219
+ let name = role === "user" ? "" : "<strong>CHAT A.I+</strong>";
220
+
221
+ messageDiv.innerHTML = `
222
+ <div class="avatar">${avatar}</div>
223
+ <div class="message-content">
224
+ ${name}
225
+ <p>${text}</p>
226
+ </div>
227
+ `;
228
+ chatLog.appendChild(messageDiv);
229
+ return messageDiv;
230
+ }
231
+
232
+ // --- Inisialisasi Aplikasi ---
233
+ async function initializeApp() {
234
+ if (!currentUserId) return;
235
+ try {
236
+ const response = await fetch(`http://localhost:3001/api/conversations?userId=${currentUserId}`);
237
+ if (!response.ok) {
238
+ throw new Error("Gagal memuat history");
239
+ }
240
+ const data = await response.json();
241
+ conversations = {};
242
+ data.forEach(convo => {
243
+ conversations[convo.id] = convo;
244
+ });
245
+ renderHistory();
246
+ startNewChat();
247
+ } catch (error) {
248
+ console.error("Error initializing app:", error);
249
+ startNewChat();
250
+ }
251
+ }
252
+
253
+ initializeApp();
254
+ });
style.css → static/style.css RENAMED
File without changes
chat.html → templates/chat.html RENAMED
@@ -5,7 +5,7 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Chatbot UI</title>
7
 
8
- <link rel="stylesheet" href="style.css">
9
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
10
  </head>
11
  <body>
@@ -68,6 +68,6 @@
68
 
69
  </section>
70
 
71
- <script src="script.js"></script>
72
  </body>
73
  </html>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Chatbot UI</title>
7
 
8
+ <link rel="stylesheet" href="/static/style.css">
9
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
10
  </head>
11
  <body>
 
68
 
69
  </section>
70
 
71
+ <script src="/static/script.js"></script>
72
  </body>
73
  </html>