File size: 14,386 Bytes
7d8273c
 
2498457
7d8273c
 
1e384db
2498457
 
 
 
 
 
a13a62d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2498457
a13a62d
 
 
 
2498457
 
a13a62d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2498457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a13a62d
 
2498457
a13a62d
 
 
 
2498457
 
a13a62d
2498457
a13a62d
 
 
2498457
 
a13a62d
 
 
 
 
 
 
 
 
2498457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a13a62d
 
2498457
a13a62d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2498457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a13a62d
 
 
 
2498457
 
 
 
 
 
 
 
 
 
a13a62d
 
2498457
a13a62d
2498457
 
 
 
 
a13a62d
2498457
 
 
 
 
 
 
 
 
 
 
a13a62d
2498457
a13a62d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2498457
 
a13a62d
2498457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a13a62d
2498457
 
 
 
a13a62d
 
2498457
 
 
 
 
a13a62d
 
 
2498457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
const API_DETECT_URL = "http://localhost:8000/detect";
const API_ASK_URL = "http://localhost:8000/ask";

// const API_DETECT_URL = "https://r7sc5m17-8000.asse.devtunnels.ms/detect";
// const API_ASK_URL = "https://r7sc5m17-8000.asse.devtunnels.ms/ask";


let selectedFile = null;

function parseMarkdown(text) {
  if (!text) return "";
  let html = text;
  html = html.replace(/(?:^\|.*\|(?:\n|\r|$))+/gm, function (match) {
    let rows = match.trim().split("\n");
    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">';

    rows.forEach((row, index) => {
      if (row.match(/^\|[\s\-\:]+\|/)) return;

      let cleanRow = row.replace(/^\||\|$/g, "");
      let cols = cleanRow.split("|").map((c) => c.trim());

      tableHtml +=
        '<tr class="border-b border-slate-200 last:border-0 hover:bg-slate-50 transition-colors">';
      cols.forEach((col) => {
        if (index === 0) {
          tableHtml += `<th class="px-4 py-3 bg-slate-100 font-semibold text-slate-700 whitespace-nowrap">${col}</th>`;
        } else {
          tableHtml += `<td class="px-4 py-3 align-top">${col}</td>`;
        }
      });
      tableHtml += "</tr>";
    });
    tableHtml += "</table></div>";
    return tableHtml;
  });

  // 1. Headings (H2, H3, H4)
  html = html.replace(
    /^##\s+(.*$)/gim,
    '<h2 class="text-xl font-bold text-slate-800 mt-5 mb-2">$1</h2>',
  );
  html = html.replace(
    /^###\s+(.*$)/gim,
    '<h3 class="text-lg font-bold text-slate-800 mt-5 mb-2">$1</h3>',
  );
  html = html.replace(
    /^####\s+(.*$)/gim,
    '<h3 class="text-lg font-bold text-slate-800 mt-5 mb-2">$1</h3>',
  );

  // 2. Garis Pembatas (Horizontal Rule)
  html = html.replace(/^---$/gm, '<hr class="my-4 border-slate-200" />');

  // 3. Bold & Italic
  html = html.replace(
    /\*\*(.*?)\*\*/g,
    '<strong class="font-bold text-slate-800">$1</strong>',
  );
  html = html.replace(
    /(?<!^)\*(.*?)\*/g,
    '<em class="italic text-slate-700">$1</em>',
  );

  // 4. Bullet Points (Menangkap *, -, dan •)
  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>',
  );

  // 5. Numbered List (Menangkap 1., 2., 3., dst) agar rapi sejajar
  html = html.replace(
    /^(\d+)\.\s+(.*$)/gim,
    '<div class="flex gap-2 mt-1.5"><span class="text-rose-500 font-bold shrink-0">$1.</span><span>$2</span></div>',
  );

  // 6. Ubah Enter menjadi <br/>
  html = html.replace(/\n/g, "<br/>");

  // 7. PEMBERSIHAN EKSTREM: Hapus <br/> yang menumpuk di sekitar elemen UI
  // Bersihkan sekitar garis pembatas
  html = html.replace(/(<br\/>)+<hr/g, "<hr");
  html = html.replace(/<hr(.*?)>(<br\/>)+/g, "<hr$1>");

  // Bersihkan sekitar Headings
  html = html.replace(/(<br\/>)+<h2/g, "<h2");
  html = html.replace(/<\/h2>(<br\/>)+/g, "</h2>");
  html = html.replace(/(<br\/>)+<h3/g, "<h3");
  html = html.replace(/<\/h3>(<br\/>)+/g, "</h3>");

  // Bersihkan sekitar kotak list (bullet & angka)
  html = html.replace(/(<br\/>)+<div/g, "<div");
  html = html.replace(/<\/div>(<br\/>)+/g, "</div>");

  // Maksimal 2 <br/> berturut-turut untuk paragraf biasa
  html = html.replace(/(<br\/>){3,}/g, "<br/><br/>");

  return html;
}

function switchMenu(menu) {
  const secDetect = document.getElementById("section-detect");
  const secChat = document.getElementById("section-chat");
  const btnDetect = document.getElementById("btn-menu-detect");
  const btnChat = document.getElementById("btn-menu-chat");

  const activeClass =
    "flex-1 md:w-full flex items-center justify-center md:justify-start gap-2 md:gap-3 px-4 py-2.5 md:py-3 rounded-xl font-medium transition-all bg-rose-50 text-rose-600 text-sm md:text-base whitespace-nowrap shadow-sm md:shadow-none";
  const inactiveClass =
    "flex-1 md:w-full flex items-center justify-center md:justify-start gap-2 md:gap-3 px-4 py-2.5 md:py-3 rounded-xl font-medium transition-all text-slate-500 hover:bg-slate-50 hover:text-slate-700 text-sm md:text-base whitespace-nowrap";

  if (menu === "detect") {
    secDetect.classList.remove("hidden");
    secChat.classList.add("hidden");
    secChat.classList.remove("flex");
    btnDetect.className = activeClass;
    btnChat.className = inactiveClass;
  } else {
    secDetect.classList.add("hidden");
    secChat.classList.remove("hidden");
    secChat.classList.add("flex");
    btnChat.className = activeClass;
    btnDetect.className = inactiveClass;
  }
}

function handleFileChange(event) {
  const file = event.target.files[0];
  if (file) {
    selectedFile = file;

    // 1. Tampilkan nama file & tombol analisis
    const fileNameEl = document.getElementById("file-name");
    if (fileNameEl) {
      fileNameEl.textContent = `File: ${file.name}`;
      fileNameEl.classList.remove("hidden");
    }
    document.getElementById("btn-upload").classList.remove("hidden");

    // 2. KONTROL TAMPILAN KOTAK UPLOAD
    const imagePreview = document.getElementById("image-preview");
    const uploadPlaceholder = document.getElementById("upload-placeholder");

    // Masukkan sumber gambar
    imagePreview.src = URL.createObjectURL(file);

    // Sembunyikan ikon/teks, lalu tampilkan gambarnya
    if (uploadPlaceholder) uploadPlaceholder.classList.add("hidden");
    if (imagePreview) imagePreview.classList.remove("hidden");

    // 3. Reset hasil deteksi sebelumnya jika ada
    const detectResult = document.getElementById("detect-result");
    const detectError = document.getElementById("detect-error");
    if (detectResult) detectResult.classList.add("hidden");
    if (detectError) detectError.classList.add("hidden");
  }
}

async function handleUpload() {
  if (!selectedFile) return;

  const btnUpload = document.getElementById("btn-upload");
  const errorContainer = document.getElementById("detect-error");
  const resultContainer = document.getElementById("detect-result");

  btnUpload.disabled = true;
  btnUpload.textContent = "Memproses...";
  errorContainer.classList.add("hidden");
  resultContainer.classList.add("hidden");

  const formData = new FormData();
  formData.append("file", selectedFile);

  try {
    const response = await fetch(API_DETECT_URL, {
      method: "POST",
      body: formData,
    });

    if (!response.ok) throw new Error(`Error server: ${response.status}`);

    const data = await response.json();

    // Tampilkan hasil deteksi (gambar ber-bounding box dan teksnya)
    tampilkanHasilDeteksi(data);

    // --- KODE RESET KOTAK UPLOAD DITAMBAHKAN DI SINI ---
    // 1. Sembunyikan gambar preview daun dan tampilkan kembali ikon placeholder
    const imagePreview = document.getElementById("image-preview");
    const uploadPlaceholder = document.getElementById("upload-placeholder");

    if (imagePreview) imagePreview.classList.add("hidden");
    if (uploadPlaceholder) uploadPlaceholder.classList.remove("hidden");

    // 2. Sembunyikan teks nama file dan tombol analisis agar bersih
    document.getElementById("file-name").classList.add("hidden");
    btnUpload.classList.add("hidden");

    // 3. Bersihkan memori agar user bisa mengunggah file yang sama lagi jika perlu
    selectedFile = null;
    document.getElementById("file-upload").value = "";
    // --------------------------------------------------
  } catch (err) {
    errorContainer.textContent = "Gagal terhubung ke server.";
    errorContainer.classList.remove("hidden");
  } finally {
    btnUpload.disabled = false;
    btnUpload.textContent = "Mulai Analisis AI";
  }
}

function tampilkanHasilDeteksi(data) {
  const resultContainer = document.getElementById("detect-result");
  const resultImage = document.getElementById("result-image");
  const resultDetails = document.getElementById("result-details");

  if (data.image_base64) {
    resultImage.src = `data:image/jpeg;base64,${data.image_base64}`;
  }

  resultDetails.innerHTML = "";
  if (data.total_detections > 0) {
    data.results.forEach((item) => {
      const confidencePercent = (item.confidence * 100).toFixed(0);

      // MENGGUNAKAN FUNGSI PARSE MARKDOWN
      const formattedNarrative = parseMarkdown(item.narrative);

      const div = document.createElement("div");
      div.className =
        "bg-white ring-1 ring-slate-200 shadow-sm rounded-2xl p-4 md:p-6 relative overflow-hidden";

      div.innerHTML = `
                <div class="absolute top-0 left-0 w-1 md:w-1.5 h-full bg-rose-500"></div>
                <h4 class="text-base md:text-lg font-bold text-slate-800 mb-2 flex flex-col md:flex-row md:items-center justify-between gap-1">
                    <span>${item.class}</span>
                    <span class="text-xs font-semibold bg-slate-100 text-slate-500 px-2 py-1 rounded-md w-fit">Keyakinan: ${confidencePercent}%</span>
                </h4>
                <div class="text-slate-600 text-xs md:text-sm leading-relaxed mt-2 md:mt-3">
                    ${formattedNarrative}
                </div>
            `;
      resultDetails.appendChild(div);
    });
  } else {
    resultDetails.innerHTML = `
            <div class="bg-emerald-50 text-emerald-700 p-4 rounded-2xl ring-1 ring-emerald-200 font-medium flex items-center gap-3 text-sm md:text-base">
                <span class="text-xl">✅</span> Tidak ada penyakit terdeteksi. Daun tampak sehat!
            </div>
        `;
  }

  resultContainer.classList.remove("hidden");

  setTimeout(() => {
    resultContainer.scrollIntoView({ behavior: "smooth", block: "start" });
  }, 500);
}

async function handleSendChat(event) {
  event.preventDefault();

  const inputEl = document.getElementById("chat-input");
  const btnSend = document.getElementById("btn-chat-send");
  const question = inputEl.value.trim();
  if (!question) return;

  const chatEmpty = document.getElementById("chat-empty");
  if (chatEmpty) chatEmpty.classList.add("hidden");

  // 1. Tampilkan pertanyaan User
  appendMessage("user", question);
  inputEl.value = "";
  inputEl.disabled = true;
  btnSend.disabled = true;

  // 2. Tampilkan animasi loading (...) sebelum AI mulai mengetik
  const loadingId = showChatLoading();

  try {
    const response = await fetch(API_ASK_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ question: question }),
    });

    if (!response.ok) throw new Error("Gagal");

    // AI mulai membalas, hilangkan animasi loading
    removeChatLoading(loadingId);

const aiBubble = appendMessage("assistant", "");
    const chatBox = document.getElementById("chat-messages");

    // 4. LOGIKA STREAMING DENGAN "REM MESIN TIK" (TYPEWRITER EFFECT)
    const reader = response.body.getReader();
    const decoder = new TextDecoder("utf-8");
    
    let fullTextFromAPI = ""; // Tangki penampung teks super cepat dari server
    let textToDisplay = "";   // Teks yang akan diteteskan ke layar perlahan-lahan
    let charIndex = 0;

    // A. Fungsi pengetik independen (Jalan di latar belakang)
    // Angka 15 adalah kecepatan ketik (15 milidetik per huruf). Bisa Anda perbesar jika ingin lebih lambat.
    const typingInterval = setInterval(() => {
      // Jika masih ada huruf di tangki yang belum ditampilkan
      if (charIndex < fullTextFromAPI.length) {
        // Keluarkan 2 huruf sekaligus agar tidak terlalu lambat
        textToDisplay += fullTextFromAPI.slice(charIndex, charIndex + 4);
        charIndex += 4;
        
        aiBubble.innerHTML = parseMarkdown(textToDisplay);
        scrollToBottom(chatBox);
      }
    }, 15); 

    // B. Pipa penyedot dari API Server (Berjalan secepat kilat)
    while (true) {
      const { done, value } = await reader.read();
      
      if (done) {
        // Matikan interval pengetik HANYA JIKA semua teks sudah berhasil diketik ke layar
        const waitComplete = setInterval(() => {
          if (charIndex >= fullTextFromAPI.length) {
            clearInterval(typingInterval);
            clearInterval(waitComplete);
          }
        }, 50);
        break;
      }

      // Terjemahkan byte dan masukkan langsung ke tangki penampung
      const chunkText = decoder.decode(value, { stream: true });
      fullTextFromAPI += chunkText; 
    }

  } catch (err) {
    removeChatLoading(loadingId);
    appendMessage("assistant", "❌ Gagal terhubung ke server. Silakan coba lagi.");
  } finally {
    inputEl.disabled = false;
    btnSend.disabled = false;
    inputEl.focus();
  }
}

function appendMessage(role, content) {
  const chatBox = document.getElementById("chat-messages");
  const wrapper = document.createElement("div");
  wrapper.className = `flex ${role === "user" ? "justify-end" : "justify-start"}`;

  const bubble = document.createElement("div");
  if (role === "user") {
    bubble.className =
      "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]";
    bubble.textContent = content;
  } else {
    bubble.className =
      "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]";

    // Jika ada konten (user history), parse markdown. Jika kosong (awal streaming), biarkan kosong.
    bubble.innerHTML = content ? parseMarkdown(content) : "";
  }

  wrapper.appendChild(bubble);
  chatBox.appendChild(wrapper);
  scrollToBottom(chatBox);

  // KUNCI PERUBAHAN: Kembalikan elemen bubble agar bisa di-update oleh fungsi streaming
  return bubble;
}

function showChatLoading() {
  const chatBox = document.getElementById("chat-messages");
  const wrapper = document.createElement("div");
  const id = "loading-" + Date.now();
  wrapper.id = id;
  wrapper.className = `flex justify-start`;
  wrapper.innerHTML = `
        <div class="bg-white ring-1 ring-slate-200 text-slate-500 p-4 md:p-5 rounded-2xl rounded-tl-sm shadow-sm flex items-center">
            <div class="dot-flashing ml-2"></div>
        </div>
    `;
  chatBox.appendChild(wrapper);
  scrollToBottom(chatBox);
  return id;
}

function removeChatLoading(id) {
  const el = document.getElementById(id);
  if (el) el.remove();
}

function scrollToBottom(element) {
  element.scrollTo({
    top: element.scrollHeight,
    behavior: "smooth",
  });
}