MIF_E31230988/client.html

155 lines
6.1 KiB
HTML

<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Analisis Sentimen Tokopedia - Streaming Progres</title>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; margin: 24px; color: #111; }
.card { border: 1px solid #ddd; border-radius: 12px; padding: 16px; max-width: 900px; }
.row { display: flex; gap: 8px; align-items: center; }
input[type="text"] { flex: 1; padding: 10px 12px; border: 1px solid #ccc; border-radius: 8px; }
button { padding: 10px 16px; border: 0; border-radius: 8px; cursor: pointer; background: #10b981; color: white; font-weight: 600; }
button:disabled { background: #94a3b8; cursor: not-allowed; }
.muted { color: #64748b; }
.log { white-space: pre-wrap; background: #0b1020; color: #c7d2fe; padding: 12px; border-radius: 8px; min-height: 120px; }
.progress { height: 10px; background: #e2e8f0; border-radius: 999px; overflow: hidden; }
.bar { height: 100%; width: 0%; background: linear-gradient(90deg, #60a5fa, #34d399); transition: width 200ms ease; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; background:#eef2ff; color:#3730a3; margin-right: 6px; }
.result { border-top: 1px dashed #e5e7eb; padding-top: 12px; margin-top: 12px; }
.item { padding: 8px 0; border-bottom: 1px solid #f1f5f9; }
</style>
</head>
<body>
<h1>Analisis Sentimen Tokopedia</h1>
<p class="muted">Masukkan shortlink Tokopedia (tk.tokopedia.com) lalu pantau progres secara realtime.</p>
<div class="card">
<div class="row">
<input id="shortlink" type="text" placeholder="https://tk.tokopedia.com/...." />
<button id="startBtn">Mulai Analisis</button>
</div>
<div style="margin-top:12px" class="progress"><div id="bar" class="bar"></div></div>
<div style="margin-top:8px"><span id="status" class="tag">idle</span><span id="message" class="muted"></span></div>
<div style="margin-top:16px" class="log" id="log"></div>
<div id="result" class="result" hidden>
<h3>Hasil</h3>
<div id="meta" class="muted"></div>
<div id="items"></div>
</div>
</div>
<script>
const baseUrl = 'http://127.0.0.1:5000'; // Pastikan API Flask berjalan di sini
const el = (id) => document.getElementById(id);
const shortlink = el('shortlink');
const startBtn = el('startBtn');
const bar = el('bar');
const statusEl = el('status');
const messageEl = el('message');
const logEl = el('log');
const resultWrap = el('result');
const metaEl = el('meta');
const itemsEl = el('items');
function setProgress(pct, status, msg) {
bar.style.width = Math.max(0, Math.min(100, pct||0)) + '%';
statusEl.textContent = status || 'running';
messageEl.textContent = msg || '';
}
function appendLog(line, tsStr) {
let ts
if (tsStr) {
// tsStr comes ISO-like from server, keep HH:MM:SS for compactness
try { ts = new Date(tsStr).toLocaleTimeString(); } catch (_) { ts = new Date().toLocaleTimeString(); }
} else {
ts = new Date().toLocaleTimeString();
}
logEl.textContent += `[${ts}] ${line}\n`;
logEl.scrollTop = logEl.scrollHeight;
}
function renderResult(res) {
resultWrap.hidden = false;
metaEl.textContent = `Kategori: ${res.category} (encoded: ${res.category_encoded}) • Jumlah: ${res.count}`;
itemsEl.innerHTML = '';
(res.items || []).forEach((it) => {
const div = document.createElement('div');
div.className = 'item';
div.textContent = `Sentimen: ${it.sentiment} | Review: ${it.review}`;
itemsEl.appendChild(div);
});
}
async function startAnalysis() {
const url = shortlink.value.trim();
if (!url) { alert('Masukkan shortlink terlebih dahulu'); return; }
startBtn.disabled = true;
resultWrap.hidden = true;
itemsEl.innerHTML = '';
logEl.textContent = '';
setProgress(0, 'queued', 'Job queued');
appendLog('Mengirim permintaan analisis...');
try {
const r = await fetch(`${baseUrl}/analyze`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ shortlink: url })
});
if (!r.ok) {
const err = await r.json().catch(() => ({}));
throw new Error(err.error || `Gagal memulai analisis (${r.status})`);
}
const { job_id } = await r.json();
appendLog(`Job dimulai • job_id=${job_id}`);
const es = new EventSource(`${baseUrl}/stream/${job_id}`);
es.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
// Final event may contain result for completed or failed
if (data.status === 'failed') {
setProgress(100, 'failed', data.message || 'Gagal');
appendLog('Gagal: ' + (data.result?.error || data.message || 'Unknown error'), data.ts);
if (data.result?.traceback) {
appendLog(data.result.traceback, data.ts);
}
es.close();
startBtn.disabled = false;
return;
}
if (data.result && data.status === 'completed') {
setProgress(100, 'completed', 'Selesai');
appendLog('Selesai, menampilkan hasil', data.ts);
renderResult(data.result);
es.close();
startBtn.disabled = false;
return;
}
setProgress(data.percent, data.status, data.message);
appendLog(`${data.status || ''} ${data.percent||0}% • ${data.message || ''}`.trim(), data.ts);
} catch (err) {
appendLog('Error parsing stream: ' + err.message);
}
};
es.onerror = () => {
appendLog('SSE terputus.');
es.close();
startBtn.disabled = false;
};
} catch (e) {
alert(e.message);
startBtn.disabled = false;
}
}
startBtn.addEventListener('click', startAnalysis);
</script>
</body>
</html>