feryms commited on
Commit
a13a62d
·
1 Parent(s): 6234335

big update

Browse files
backend/__pycache__/api.cpython-312.pyc CHANGED
Binary files a/backend/__pycache__/api.cpython-312.pyc and b/backend/__pycache__/api.cpython-312.pyc differ
 
backend/api.py CHANGED
@@ -8,6 +8,7 @@ import base64
8
  import cv2
9
  import numpy as np
10
  from pydantic import BaseModel
 
11
  from src.chains.rag import generate_narrative
12
  from src.chains.chain import create_rag_chain
13
 
@@ -98,13 +99,19 @@ async def ask_expert(request: QuestionRequest):
98
  try:
99
  rag_chain = create_rag_chain()
100
 
101
- jawaban = rag_chain.invoke(request.question)
 
 
 
 
 
 
 
 
 
 
 
102
 
103
- return {
104
- "status": "success",
105
- "question": request.question,
106
- "answer": jawaban
107
- }
108
  except Exception as e:
109
  raise HTTPException(status_code=500, detail=f"Terjadi kesalahan pada LLM: {str(e)}")
110
 
 
8
  import cv2
9
  import numpy as np
10
  from pydantic import BaseModel
11
+ from fastapi.responses import StreamingResponse
12
  from src.chains.rag import generate_narrative
13
  from src.chains.chain import create_rag_chain
14
 
 
99
  try:
100
  rag_chain = create_rag_chain()
101
 
102
+ # Buat fungsi generator untuk memecah respons menjadi potongan (chunks)
103
+ def generate_response():
104
+ # rag_chain.stream() menggantikan rag_chain.invoke()
105
+ for chunk in rag_chain.stream(request.question):
106
+ # yield mengirimkan potongan teks ke klien saat itu juga
107
+ yield chunk
108
+
109
+ # Kembalikan StreamingResponse, bukan dictionary JSON
110
+ return StreamingResponse(
111
+ generate_response(),
112
+ media_type="text/event-stream"
113
+ )
114
 
 
 
 
 
 
115
  except Exception as e:
116
  raise HTTPException(status_code=500, detail=f"Terjadi kesalahan pada LLM: {str(e)}")
117
 
chroma_data/chroma.sqlite3 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:ad9cde89338037f6a33e3f03b17c5337d35533f702336ccf16c70f0e92a9534d
3
- size 1761280
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:da26aef13c026e13fc24403eee01d7fb6bb603687a9a9a3e62482b16e1d21349
3
+ size 2543616
chroma_data/{cdcb1c1c-f374-4f62-9cc7-7e62dcdaccd0 → d6ddab1a-f591-4aa9-ac0b-3e6358c18ab5}/data_level0.bin RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:41b1d6558680207762d59e507f9dcba0cb9fbbd4c23c79e14025206d3742e17f
3
  size 423600
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3ac7a09f294a000c263e1c7ea232d164bbfb2c41fe75fda603c0d9b19bf21a22
3
  size 423600
chroma_data/{cdcb1c1c-f374-4f62-9cc7-7e62dcdaccd0 → d6ddab1a-f591-4aa9-ac0b-3e6358c18ab5}/header.bin RENAMED
File without changes
chroma_data/{cdcb1c1c-f374-4f62-9cc7-7e62dcdaccd0 → d6ddab1a-f591-4aa9-ac0b-3e6358c18ab5}/length.bin RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:7a12e561363385e9dfeeab326368731c030ed4b374e7f5897ac819159d2884c5
3
  size 400
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1c28d39adcc8e9aed92582147fa03b71df7dd3593121741113f68c715d2ec443
3
  size 400
chroma_data/{cdcb1c1c-f374-4f62-9cc7-7e62dcdaccd0 → d6ddab1a-f591-4aa9-ac0b-3e6358c18ab5}/link_lists.bin RENAMED
File without changes
data/budidaya_cabai_rawit.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5a6e345b22fa5dde1d7eb789b4abb9ae8a9fbe83d774eb3ee0e9f33468108032
3
+ size 165447
data/penyakit.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fa557f928195ae43186444b059dc4052e2f6390da484c5ca42a31e4e3d6a698d
3
+ size 696137
frontend/index.html CHANGED
@@ -215,32 +215,20 @@
215
  <div
216
  class="bg-white p-2 rounded-3xl shadow-sm ring-1 ring-slate-200 mb-6 md:mb-8 max-w-2xl mx-auto"
217
  >
218
- <label
219
- for="file-upload"
220
- class="group flex flex-col items-center justify-center w-full h-40 md:h-64 border-2 border-dashed border-slate-200 rounded-[1.25rem] bg-slate-50/50 hover:bg-rose-50/50 hover:border-rose-300 transition-all cursor-pointer relative overflow-hidden"
221
- >
222
- <div
223
- class="w-12 h-12 md:w-16 md:h-16 mb-3 md:mb-4 rounded-full bg-white shadow-sm ring-1 ring-slate-100 flex items-center justify-center group-hover:scale-110 transition-all duration-300"
224
- >
225
- <svg
226
- class="w-6 h-6 md:w-8 md:h-8 text-rose-500"
227
- fill="none"
228
- stroke="currentColor"
229
- viewBox="0 0 24 24"
230
- >
231
- <path
232
- stroke-linecap="round"
233
- stroke-linejoin="round"
234
- stroke-width="2"
235
- d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
236
- ></path>
237
- </svg>
238
- </div>
239
- <span
240
- class="text-sm md:text-base text-slate-600 font-medium group-hover:text-rose-600 transition-colors"
241
- >Pilih gambar daun</span
242
- >
243
- </label>
244
  <input
245
  type="file"
246
  accept="image/jpeg, image/png, image/jpg"
 
215
  <div
216
  class="bg-white p-2 rounded-3xl shadow-sm ring-1 ring-slate-200 mb-6 md:mb-8 max-w-2xl mx-auto"
217
  >
218
+ <label for="file-upload" class="relative flex flex-col items-center justify-center w-full h-64 border-2 border-dashed border-slate-300 rounded-2xl cursor-pointer hover:bg-slate-50 transition-colors overflow-hidden group">
219
+
220
+ <div id="upload-placeholder" class="flex flex-col items-center justify-center transition-opacity duration-300">
221
+ <div class="p-3 bg-white shadow-sm ring-1 ring-slate-100 rounded-full mb-3 group-hover:scale-110 transition-transform">
222
+ <svg class="w-6 h-6 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>
223
+ </div>
224
+ <p class="text-sm text-slate-600 font-medium">Pilih gambar daun</p>
225
+ </div>
226
+
227
+ <img id="image-preview" class="hidden absolute inset-0 w-full h-full object-cover z-10" alt="Preview Daun" />
228
+
229
+ </label>
230
+
231
+ <input id="file-upload" type="file" accept="image/*" class="hidden" onchange="handleFileChange(event)" />
 
 
 
 
 
 
 
 
 
 
 
 
232
  <input
233
  type="file"
234
  accept="image/jpeg, image/png, image/jpg"
frontend/script.js CHANGED
@@ -10,47 +10,91 @@ let selectedFile = null;
10
  function parseMarkdown(text) {
11
  if (!text) return "";
12
  let html = text;
13
- html = html.replace(/(?:^\|.*\|(?:\n|\r|$))+/gm, function(match) {
14
- let rows = match.trim().split('\n');
15
- let tableHtml = '<div class="overflow-x-auto my-5 rounded-xl ring-1 ring-slate-200 shadow-sm"><table class="w-full text-sm text-left text-slate-600">';
16
-
17
- rows.forEach((row, index) => {
18
- if (row.match(/^\|[\s\-\:]+\|/)) return;
19
-
20
- let cleanRow = row.replace(/^\||\|$/g, '');
21
- let cols = cleanRow.split('|').map(c => c.trim());
22
-
23
- tableHtml += '<tr class="border-b border-slate-200 last:border-0 hover:bg-slate-50 transition-colors">';
24
- cols.forEach(col => {
25
- if (index === 0) {
26
- tableHtml += `<th class="px-4 py-3 bg-slate-100 font-semibold text-slate-700 whitespace-nowrap">${col}</th>`;
27
- } else {
28
- tableHtml += `<td class="px-4 py-3 align-top">${col}</td>`;
29
- }
30
- });
31
- tableHtml += '</tr>';
32
  });
33
- tableHtml += '</table></div>';
34
- return tableHtml;
 
 
35
  });
36
 
37
- html = html.replace(/^###\s+(.*$)/gim, '<h3 class="text-lg font-bold text-slate-800 mt-5 mb-2">$1</h3>');
38
-
39
- html = html.replace(/^##\s+(.*$)/gim, '<h2 class="text-xl font-bold text-slate-800 mt-5 mb-2">$1</h2>');
40
-
41
- html = html.replace(/\*\*(.*?)\*\*/g, '<strong class="font-bold text-slate-800">$1</strong>');
42
-
43
- html = html.replace(/^[\*\-]\s+(.*$)/gim, '<div class="flex gap-2 mt-1.5"><span class="text-rose-500 font-bold shrink-0">•</span><span>$1</span></div>');
44
-
45
- html = html.replace(/(?<!^)\*(.*?)\*/g, '<em class="italic text-slate-700">$1</em>');
46
-
47
- html = html.replace(/\n/g, '<br/>');
48
-
49
- html = html.replace(/<\/h3><br\/>/g, '</h3>');
50
- html = html.replace(/<\/h2><br\/>/g, '</h2>');
51
- html = html.replace(/<\/div><br\/>/g, '</div>');
52
- html = html.replace(/<\/div><br\/><br\/>/g, '</div>');
53
- html = html.replace(/(<br\/>){3,}/g, '<br/><br/>');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  return html;
56
  }
@@ -85,18 +129,31 @@ function handleFileChange(event) {
85
  const file = event.target.files[0];
86
  if (file) {
87
  selectedFile = file;
 
 
88
  const fileNameEl = document.getElementById("file-name");
89
- fileNameEl.textContent = `File: ${file.name}`;
90
- fileNameEl.classList.remove("hidden");
 
 
91
  document.getElementById("btn-upload").classList.remove("hidden");
92
 
93
- const previewContainer = document.getElementById("preview-container");
94
  const imagePreview = document.getElementById("image-preview");
 
 
 
95
  imagePreview.src = URL.createObjectURL(file);
96
- previewContainer.classList.remove("hidden");
97
 
98
- document.getElementById("detect-result").classList.add("hidden");
99
- document.getElementById("detect-error").classList.add("hidden");
 
 
 
 
 
 
 
100
  }
101
  }
102
 
@@ -124,7 +181,26 @@ async function handleUpload() {
124
  if (!response.ok) throw new Error(`Error server: ${response.status}`);
125
 
126
  const data = await response.json();
 
 
127
  tampilkanHasilDeteksi(data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  } catch (err) {
129
  errorContainer.textContent = "Gagal terhubung ke server.";
130
  errorContainer.classList.remove("hidden");
@@ -176,6 +252,10 @@ function tampilkanHasilDeteksi(data) {
176
  }
177
 
178
  resultContainer.classList.remove("hidden");
 
 
 
 
179
  }
180
 
181
  async function handleSendChat(event) {
@@ -186,14 +266,16 @@ async function handleSendChat(event) {
186
  const question = inputEl.value.trim();
187
  if (!question) return;
188
 
189
- document.getElementById("chat-empty").classList.add("hidden");
 
190
 
 
191
  appendMessage("user", question);
192
  inputEl.value = "";
193
-
194
  inputEl.disabled = true;
195
  btnSend.disabled = true;
196
 
 
197
  const loadingId = showChatLoading();
198
 
199
  try {
@@ -205,12 +287,57 @@ async function handleSendChat(event) {
205
 
206
  if (!response.ok) throw new Error("Gagal");
207
 
208
- const data = await response.json();
209
  removeChatLoading(loadingId);
210
- appendMessage("assistant", data.answer);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  } catch (err) {
212
  removeChatLoading(loadingId);
213
- appendMessage("assistant", "❌ Gagal terhubung ke server.");
214
  } finally {
215
  inputEl.disabled = false;
216
  btnSend.disabled = false;
@@ -227,17 +354,21 @@ function appendMessage(role, content) {
227
  if (role === "user") {
228
  bubble.className =
229
  "max-w-[90%] md:max-w-[80%] p-3 md:p-4 rounded-2xl rounded-tr-sm bg-rose-500 text-white shadow-md shadow-rose-200 leading-relaxed text-sm md:text-[15px]";
230
- bubble.textContent = content; // User input tetap menggunakan text content murni demi keamanan
231
  } else {
232
  bubble.className =
233
  "max-w-[90%] md:max-w-[80%] p-3 md:p-4 rounded-2xl rounded-tl-sm bg-white ring-1 ring-slate-200 text-slate-700 shadow-sm leading-relaxed text-sm md:text-[15px]";
234
 
235
- bubble.innerHTML = parseMarkdown(content);
 
236
  }
237
 
238
  wrapper.appendChild(bubble);
239
  chatBox.appendChild(wrapper);
240
  scrollToBottom(chatBox);
 
 
 
241
  }
242
 
243
  function showChatLoading() {
 
10
  function parseMarkdown(text) {
11
  if (!text) return "";
12
  let html = text;
13
+ html = html.replace(/(?:^\|.*\|(?:\n|\r|$))+/gm, function (match) {
14
+ let rows = match.trim().split("\n");
15
+ let tableHtml =
16
+ '<div class="overflow-x-auto my-5 rounded-xl ring-1 ring-slate-200 shadow-sm"><table class="w-full text-sm text-left text-slate-600">';
17
+
18
+ rows.forEach((row, index) => {
19
+ if (row.match(/^\|[\s\-\:]+\|/)) return;
20
+
21
+ let cleanRow = row.replace(/^\||\|$/g, "");
22
+ let cols = cleanRow.split("|").map((c) => c.trim());
23
+
24
+ tableHtml +=
25
+ '<tr class="border-b border-slate-200 last:border-0 hover:bg-slate-50 transition-colors">';
26
+ cols.forEach((col) => {
27
+ if (index === 0) {
28
+ tableHtml += `<th class="px-4 py-3 bg-slate-100 font-semibold text-slate-700 whitespace-nowrap">${col}</th>`;
29
+ } else {
30
+ tableHtml += `<td class="px-4 py-3 align-top">${col}</td>`;
31
+ }
32
  });
33
+ tableHtml += "</tr>";
34
+ });
35
+ tableHtml += "</table></div>";
36
+ return tableHtml;
37
  });
38
 
39
+ // 1. Headings (H2, H3, H4)
40
+ html = html.replace(
41
+ /^##\s+(.*$)/gim,
42
+ '<h2 class="text-xl font-bold text-slate-800 mt-5 mb-2">$1</h2>',
43
+ );
44
+ html = html.replace(
45
+ /^###\s+(.*$)/gim,
46
+ '<h3 class="text-lg font-bold text-slate-800 mt-5 mb-2">$1</h3>',
47
+ );
48
+ html = html.replace(
49
+ /^####\s+(.*$)/gim,
50
+ '<h3 class="text-lg font-bold text-slate-800 mt-5 mb-2">$1</h3>',
51
+ );
52
+
53
+ // 2. Garis Pembatas (Horizontal Rule)
54
+ html = html.replace(/^---$/gm, '<hr class="my-4 border-slate-200" />');
55
+
56
+ // 3. Bold & Italic
57
+ html = html.replace(
58
+ /\*\*(.*?)\*\*/g,
59
+ '<strong class="font-bold text-slate-800">$1</strong>',
60
+ );
61
+ html = html.replace(
62
+ /(?<!^)\*(.*?)\*/g,
63
+ '<em class="italic text-slate-700">$1</em>',
64
+ );
65
+
66
+ // 4. Bullet Points (Menangkap *, -, dan •)
67
+ html = html.replace(
68
+ /^[\*\-•]\s+(.*$)/gim,
69
+ '<div class="flex gap-2 mt-1.5"><span class="text-rose-500 font-bold shrink-0">•</span><span>$1</span></div>',
70
+ );
71
+
72
+ // 5. Numbered List (Menangkap 1., 2., 3., dst) agar rapi sejajar
73
+ html = html.replace(
74
+ /^(\d+)\.\s+(.*$)/gim,
75
+ '<div class="flex gap-2 mt-1.5"><span class="text-rose-500 font-bold shrink-0">$1.</span><span>$2</span></div>',
76
+ );
77
+
78
+ // 6. Ubah Enter menjadi <br/>
79
+ html = html.replace(/\n/g, "<br/>");
80
+
81
+ // 7. PEMBERSIHAN EKSTREM: Hapus <br/> yang menumpuk di sekitar elemen UI
82
+ // Bersihkan sekitar garis pembatas
83
+ html = html.replace(/(<br\/>)+<hr/g, "<hr");
84
+ html = html.replace(/<hr(.*?)>(<br\/>)+/g, "<hr$1>");
85
+
86
+ // Bersihkan sekitar Headings
87
+ html = html.replace(/(<br\/>)+<h2/g, "<h2");
88
+ html = html.replace(/<\/h2>(<br\/>)+/g, "</h2>");
89
+ html = html.replace(/(<br\/>)+<h3/g, "<h3");
90
+ html = html.replace(/<\/h3>(<br\/>)+/g, "</h3>");
91
+
92
+ // Bersihkan sekitar kotak list (bullet & angka)
93
+ html = html.replace(/(<br\/>)+<div/g, "<div");
94
+ html = html.replace(/<\/div>(<br\/>)+/g, "</div>");
95
+
96
+ // Maksimal 2 <br/> berturut-turut untuk paragraf biasa
97
+ html = html.replace(/(<br\/>){3,}/g, "<br/><br/>");
98
 
99
  return html;
100
  }
 
129
  const file = event.target.files[0];
130
  if (file) {
131
  selectedFile = file;
132
+
133
+ // 1. Tampilkan nama file & tombol analisis
134
  const fileNameEl = document.getElementById("file-name");
135
+ if (fileNameEl) {
136
+ fileNameEl.textContent = `File: ${file.name}`;
137
+ fileNameEl.classList.remove("hidden");
138
+ }
139
  document.getElementById("btn-upload").classList.remove("hidden");
140
 
141
+ // 2. KONTROL TAMPILAN KOTAK UPLOAD
142
  const imagePreview = document.getElementById("image-preview");
143
+ const uploadPlaceholder = document.getElementById("upload-placeholder");
144
+
145
+ // Masukkan sumber gambar
146
  imagePreview.src = URL.createObjectURL(file);
 
147
 
148
+ // Sembunyikan ikon/teks, lalu tampilkan gambarnya
149
+ if (uploadPlaceholder) uploadPlaceholder.classList.add("hidden");
150
+ if (imagePreview) imagePreview.classList.remove("hidden");
151
+
152
+ // 3. Reset hasil deteksi sebelumnya jika ada
153
+ const detectResult = document.getElementById("detect-result");
154
+ const detectError = document.getElementById("detect-error");
155
+ if (detectResult) detectResult.classList.add("hidden");
156
+ if (detectError) detectError.classList.add("hidden");
157
  }
158
  }
159
 
 
181
  if (!response.ok) throw new Error(`Error server: ${response.status}`);
182
 
183
  const data = await response.json();
184
+
185
+ // Tampilkan hasil deteksi (gambar ber-bounding box dan teksnya)
186
  tampilkanHasilDeteksi(data);
187
+
188
+ // --- KODE RESET KOTAK UPLOAD DITAMBAHKAN DI SINI ---
189
+ // 1. Sembunyikan gambar preview daun dan tampilkan kembali ikon placeholder
190
+ const imagePreview = document.getElementById("image-preview");
191
+ const uploadPlaceholder = document.getElementById("upload-placeholder");
192
+
193
+ if (imagePreview) imagePreview.classList.add("hidden");
194
+ if (uploadPlaceholder) uploadPlaceholder.classList.remove("hidden");
195
+
196
+ // 2. Sembunyikan teks nama file dan tombol analisis agar bersih
197
+ document.getElementById("file-name").classList.add("hidden");
198
+ btnUpload.classList.add("hidden");
199
+
200
+ // 3. Bersihkan memori agar user bisa mengunggah file yang sama lagi jika perlu
201
+ selectedFile = null;
202
+ document.getElementById("file-upload").value = "";
203
+ // --------------------------------------------------
204
  } catch (err) {
205
  errorContainer.textContent = "Gagal terhubung ke server.";
206
  errorContainer.classList.remove("hidden");
 
252
  }
253
 
254
  resultContainer.classList.remove("hidden");
255
+
256
+ setTimeout(() => {
257
+ resultContainer.scrollIntoView({ behavior: "smooth", block: "start" });
258
+ }, 500);
259
  }
260
 
261
  async function handleSendChat(event) {
 
266
  const question = inputEl.value.trim();
267
  if (!question) return;
268
 
269
+ const chatEmpty = document.getElementById("chat-empty");
270
+ if (chatEmpty) chatEmpty.classList.add("hidden");
271
 
272
+ // 1. Tampilkan pertanyaan User
273
  appendMessage("user", question);
274
  inputEl.value = "";
 
275
  inputEl.disabled = true;
276
  btnSend.disabled = true;
277
 
278
+ // 2. Tampilkan animasi loading (...) sebelum AI mulai mengetik
279
  const loadingId = showChatLoading();
280
 
281
  try {
 
287
 
288
  if (!response.ok) throw new Error("Gagal");
289
 
290
+ // AI mulai membalas, hilangkan animasi loading
291
  removeChatLoading(loadingId);
292
+
293
+ const aiBubble = appendMessage("assistant", "");
294
+ const chatBox = document.getElementById("chat-messages");
295
+
296
+ // 4. LOGIKA STREAMING DENGAN "REM MESIN TIK" (TYPEWRITER EFFECT)
297
+ const reader = response.body.getReader();
298
+ const decoder = new TextDecoder("utf-8");
299
+
300
+ let fullTextFromAPI = ""; // Tangki penampung teks super cepat dari server
301
+ let textToDisplay = ""; // Teks yang akan diteteskan ke layar perlahan-lahan
302
+ let charIndex = 0;
303
+
304
+ // A. Fungsi pengetik independen (Jalan di latar belakang)
305
+ // Angka 15 adalah kecepatan ketik (15 milidetik per huruf). Bisa Anda perbesar jika ingin lebih lambat.
306
+ const typingInterval = setInterval(() => {
307
+ // Jika masih ada huruf di tangki yang belum ditampilkan
308
+ if (charIndex < fullTextFromAPI.length) {
309
+ // Keluarkan 2 huruf sekaligus agar tidak terlalu lambat
310
+ textToDisplay += fullTextFromAPI.slice(charIndex, charIndex + 4);
311
+ charIndex += 4;
312
+
313
+ aiBubble.innerHTML = parseMarkdown(textToDisplay);
314
+ scrollToBottom(chatBox);
315
+ }
316
+ }, 15);
317
+
318
+ // B. Pipa penyedot dari API Server (Berjalan secepat kilat)
319
+ while (true) {
320
+ const { done, value } = await reader.read();
321
+
322
+ if (done) {
323
+ // Matikan interval pengetik HANYA JIKA semua teks sudah berhasil diketik ke layar
324
+ const waitComplete = setInterval(() => {
325
+ if (charIndex >= fullTextFromAPI.length) {
326
+ clearInterval(typingInterval);
327
+ clearInterval(waitComplete);
328
+ }
329
+ }, 50);
330
+ break;
331
+ }
332
+
333
+ // Terjemahkan byte dan masukkan langsung ke tangki penampung
334
+ const chunkText = decoder.decode(value, { stream: true });
335
+ fullTextFromAPI += chunkText;
336
+ }
337
+
338
  } catch (err) {
339
  removeChatLoading(loadingId);
340
+ appendMessage("assistant", "❌ Gagal terhubung ke server. Silakan coba lagi.");
341
  } finally {
342
  inputEl.disabled = false;
343
  btnSend.disabled = false;
 
354
  if (role === "user") {
355
  bubble.className =
356
  "max-w-[90%] md:max-w-[80%] p-3 md:p-4 rounded-2xl rounded-tr-sm bg-rose-500 text-white shadow-md shadow-rose-200 leading-relaxed text-sm md:text-[15px]";
357
+ bubble.textContent = content;
358
  } else {
359
  bubble.className =
360
  "max-w-[90%] md:max-w-[80%] p-3 md:p-4 rounded-2xl rounded-tl-sm bg-white ring-1 ring-slate-200 text-slate-700 shadow-sm leading-relaxed text-sm md:text-[15px]";
361
 
362
+ // Jika ada konten (user history), parse markdown. Jika kosong (awal streaming), biarkan kosong.
363
+ bubble.innerHTML = content ? parseMarkdown(content) : "";
364
  }
365
 
366
  wrapper.appendChild(bubble);
367
  chatBox.appendChild(wrapper);
368
  scrollToBottom(chatBox);
369
+
370
+ // KUNCI PERUBAHAN: Kembalikan elemen bubble agar bisa di-update oleh fungsi streaming
371
+ return bubble;
372
  }
373
 
374
  function showChatLoading() {
ingest.py CHANGED
@@ -11,30 +11,36 @@ SOURCES = [
11
  "https://www.dgwfertilizer.co.id/8-hama-dan-penyakit-penting-pada-tanaman-cabai/",
12
  "https://mitrabertani.com/artikel/detail/Budidaya-Cabai-Sederhana-tapi-Penting-Cara-Tepat-Tanam-Cabai",
13
  "https://digitani.ipb.ac.id/bagaimana-langkah-langkah-budidaya-cabai/",
14
- "data/cabai.pdf"
 
 
15
  ]
16
 
17
  def run_ingestion_pipeline():
18
- print("Memulai Data Ingestion Pipeline\n")
19
  all_chunks = []
20
 
21
  for source in SOURCES:
22
  try:
 
23
  raw_docs = load_data(source)
24
- chunks = split_documents(raw_docs, chunk_size=1000, chunk_overlap=150)
 
 
 
25
  all_chunks.extend(chunks)
26
- print(f"Berhasil memproses: {source} ({len(chunks)} chunks)")
27
  except Exception as e:
28
- print(f"Gagal memproses {source}: {e}")
29
 
30
  if all_chunks:
31
- print(f"\nMenyiapkan model embedding dan menyimpan {len(all_chunks)} chunks ke ChromaDB...")
32
 
33
  db = get_vector_store()
34
-
35
  db.add_documents(all_chunks)
36
 
37
- print("\nSelesai! Semua data web telah masuk ke database dan siap digunakan oleh API/Streamlit.")
 
38
  else:
39
  print("\nTidak ada data yang berhasil diproses.")
40
 
 
11
  "https://www.dgwfertilizer.co.id/8-hama-dan-penyakit-penting-pada-tanaman-cabai/",
12
  "https://mitrabertani.com/artikel/detail/Budidaya-Cabai-Sederhana-tapi-Penting-Cara-Tepat-Tanam-Cabai",
13
  "https://digitani.ipb.ac.id/bagaimana-langkah-langkah-budidaya-cabai/",
14
+ "data/cabai.pdf",
15
+ "data/budidaya_cabai_rawit.pdf",
16
+ "data/penyakit.pdf"
17
  ]
18
 
19
  def run_ingestion_pipeline():
20
+ print("Memulai Data Ingestion Pipeline (Mode Semantik)\n")
21
  all_chunks = []
22
 
23
  for source in SOURCES:
24
  try:
25
+ print(f"Membaca sumber: {source}")
26
  raw_docs = load_data(source)
27
+
28
+ # MENGGUNAKAN SEMANTIC CHUNKING KHUSUS UNTUK WEB & PDF
29
+ chunks = split_documents(raw_docs, chunk_size=1000, chunk_overlap=200)
30
+
31
  all_chunks.extend(chunks)
32
+ print(f"Berhasil memproses menjadi {len(chunks)} semantic chunks.\n")
33
  except Exception as e:
34
+ print(f"Gagal memproses {source}: {e}\n")
35
 
36
  if all_chunks:
37
+ print(f"Menyiapkan penyisipan {len(all_chunks)} semantic chunks ke ChromaDB...")
38
 
39
  db = get_vector_store()
 
40
  db.add_documents(all_chunks)
41
 
42
+ print("\nSelesai! Semua data web & PDF telah diproses secara semantik.")
43
+ print("Database vektor siap digunakan oleh API FastAPI.")
44
  else:
45
  print("\nTidak ada data yang berhasil diproses.")
46
 
requirements.txt CHANGED
@@ -13,6 +13,7 @@ langchain_core
13
  opencv-python-headless
14
  langchain
15
  langchain_community
 
16
  langchain-core
17
  langchain-openai
18
  langchain-chroma
 
13
  opencv-python-headless
14
  langchain
15
  langchain_community
16
+ langchain-experimental
17
  langchain-core
18
  langchain-openai
19
  langchain-chroma
src/chains/__pycache__/chain.cpython-312.pyc CHANGED
Binary files a/src/chains/__pycache__/chain.cpython-312.pyc and b/src/chains/__pycache__/chain.cpython-312.pyc differ
 
src/chains/__pycache__/rag.cpython-312.pyc CHANGED
Binary files a/src/chains/__pycache__/rag.cpython-312.pyc and b/src/chains/__pycache__/rag.cpython-312.pyc differ
 
src/chains/chain.py CHANGED
@@ -8,6 +8,7 @@ sys.path.append(root_dir)
8
  from langchain_community.cross_encoders import HuggingFaceCrossEncoder
9
  from langchain_core.runnables import RunnablePassthrough
10
  from langchain_core.output_parsers import StrOutputParser
 
11
  from langchain_openai import ChatOpenAI
12
  from langchain_classic.retrievers.document_compressors import CrossEncoderReranker
13
  from langchain_core.messages import SystemMessage
@@ -19,11 +20,11 @@ from src.chains.prompt import get_rag_prompt
19
  load_dotenv()
20
 
21
  def create_rag_chain(disease_label=None):
22
- llm = ChatOpenAI(
23
- model="nvidia/nemotron-3-nano-30b-a3b:free",
 
24
  temperature=0.2,
25
- openai_api_key=os.getenv("OPENROUTER_API_KEY"),
26
- openai_api_base="https://openrouter.ai/api/v1",
27
  )
28
 
29
  vs = get_vector_store()
 
8
  from langchain_community.cross_encoders import HuggingFaceCrossEncoder
9
  from langchain_core.runnables import RunnablePassthrough
10
  from langchain_core.output_parsers import StrOutputParser
11
+ from langchain_groq import ChatGroq
12
  from langchain_openai import ChatOpenAI
13
  from langchain_classic.retrievers.document_compressors import CrossEncoderReranker
14
  from langchain_core.messages import SystemMessage
 
20
  load_dotenv()
21
 
22
  def create_rag_chain(disease_label=None):
23
+ llm = ChatGroq(
24
+ model="openai/gpt-oss-20b",
25
+ streaming=True,
26
  temperature=0.2,
27
+ api_key=os.getenv("GROQ_API_KEY"),
 
28
  )
29
 
30
  vs = get_vector_store()
src/chains/rag.py CHANGED
@@ -1,4 +1,6 @@
1
  import os
 
 
2
  from langchain_openai import ChatOpenAI
3
  from langchain_chroma import Chroma
4
  from langchain_huggingface import HuggingFaceEmbeddings
@@ -7,13 +9,13 @@ from langchain_classic.retrievers import EnsembleRetriever
7
  from langchain_core.documents import Document
8
  from src.chains.prompt import DISEASE_PROMPT_TEMPLATE
9
 
10
- llm = ChatOpenAI(
11
- base_url="https://openrouter.ai/api/v1",
12
- api_key=os.getenv("OPENROUTER_API_KEY"),
13
- model="nvidia/nemotron-3-nano-30b-a3b:free",
14
- temperature=0.4,
15
- max_tokens=1500
16
- )
17
 
18
  print("Memuat koneksi ke Database...")
19
  embeddings = HuggingFaceEmbeddings(model_name="Qwen/Qwen3-Embedding-0.6B")
 
1
  import os
2
+ from dotenv import load_dotenv
3
+ from langchain_groq import ChatGroq
4
  from langchain_openai import ChatOpenAI
5
  from langchain_chroma import Chroma
6
  from langchain_huggingface import HuggingFaceEmbeddings
 
9
  from langchain_core.documents import Document
10
  from src.chains.prompt import DISEASE_PROMPT_TEMPLATE
11
 
12
+ load_dotenv()
13
+
14
+ llm = ChatGroq(
15
+ model="openai/gpt-oss-20b",
16
+ temperature=0.2,
17
+ api_key=os.getenv("GROQ_API_KEY"),
18
+ )
19
 
20
  print("Memuat koneksi ke Database...")
21
  embeddings = HuggingFaceEmbeddings(model_name="Qwen/Qwen3-Embedding-0.6B")
src/ingestion/__pycache__/chunker.cpython-312.pyc CHANGED
Binary files a/src/ingestion/__pycache__/chunker.cpython-312.pyc and b/src/ingestion/__pycache__/chunker.cpython-312.pyc differ
 
src/ingestion/__pycache__/embedder.cpython-312.pyc CHANGED
Binary files a/src/ingestion/__pycache__/embedder.cpython-312.pyc and b/src/ingestion/__pycache__/embedder.cpython-312.pyc differ
 
src/ingestion/__pycache__/loader.cpython-312.pyc CHANGED
Binary files a/src/ingestion/__pycache__/loader.cpython-312.pyc and b/src/ingestion/__pycache__/loader.cpython-312.pyc differ
 
src/ingestion/chunker.py CHANGED
@@ -1,16 +1,12 @@
1
  from langchain_text_splitters import RecursiveCharacterTextSplitter
 
2
  from langchain_core.documents import Document
3
  from typing import List
4
-
5
- # chunk_overlap: Jumlah karakter yang tumpang tindih antar potongan.
6
- chunk_size = 1000
7
- chunk_overlap = 200
8
 
9
  def split_documents(documents: List[Document], chunk_size, chunk_overlap):
10
  print(f"Memulai proses pemotongan {len(documents)} dokumen...")
11
 
12
- # Inisialisasi splitter
13
- # Kita menggunakan karakter [\n\n, \n, " ", ""] sebagai prioritas pemisah
14
  text_splitter = RecursiveCharacterTextSplitter(
15
  chunk_size=chunk_size,
16
  chunk_overlap=chunk_overlap,
@@ -21,4 +17,18 @@ def split_documents(documents: List[Document], chunk_size, chunk_overlap):
21
  chunks = text_splitter.split_documents(documents)
22
 
23
  print(f"Berhasil memecah dokumen menjadi {len(chunks)} potongan teks.")
24
- return chunks
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from langchain_text_splitters import RecursiveCharacterTextSplitter
2
+ from langchain_experimental.text_splitter import SemanticChunker
3
  from langchain_core.documents import Document
4
  from typing import List
5
+ from src.ingestion.embedder import get_embedding_model
 
 
 
6
 
7
  def split_documents(documents: List[Document], chunk_size, chunk_overlap):
8
  print(f"Memulai proses pemotongan {len(documents)} dokumen...")
9
 
 
 
10
  text_splitter = RecursiveCharacterTextSplitter(
11
  chunk_size=chunk_size,
12
  chunk_overlap=chunk_overlap,
 
17
  chunks = text_splitter.split_documents(documents)
18
 
19
  print(f"Berhasil memecah dokumen menjadi {len(chunks)} potongan teks.")
20
+ return chunks
21
+
22
+ # def split_documents_semantically(documents: List[Document]):
23
+ # print(f"Memulai analisis semantik pada {len(documents)} dokumen")
24
+
25
+ # embeddings = get_embedding_model()
26
+
27
+ # # Inisialisasi Semantic Chunker
28
+ # semantic_splitter = SemanticChunker(
29
+ # embeddings,
30
+ # breakpoint_threshold_type="percentile"
31
+ # )
32
+
33
+ # chunks = semantic_splitter.split_documents(documents)
34
+ # return chunks
src/ingestion/embedder.py CHANGED
@@ -1,7 +1,9 @@
 
1
  from langchain_huggingface import HuggingFaceEmbeddings
2
  import torch
3
 
4
- model_name = "Qwen/Qwen3-Embedding-0.6B"
 
5
 
6
  def get_embedding_model(model_name: str = model_name):
7
  print(f"Mempersiapkan model embedding: {model_name}")
 
1
+ import os
2
  from langchain_huggingface import HuggingFaceEmbeddings
3
  import torch
4
 
5
+
6
+ model_name = "LazarusNLP/all-indo-e5-small-v4"
7
 
8
  def get_embedding_model(model_name: str = model_name):
9
  print(f"Mempersiapkan model embedding: {model_name}")