| <!DOCTYPE html> |
| <html lang="id"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>SentiScope β Sentiment Analysis Dashboard</title> |
| <meta name="description" content="Dashboard analisis sentimen media sosial dengan scraping otomatis, word cloud, dan indoBERT."> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet"> |
| <style> |
| |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| :root { |
| --bg: #07071a; |
| --surface: #0e0e28; |
| --surface-2: #14143a; |
| --border: rgba(130, 100, 255, 0.18); |
| --border-hover: rgba(130, 100, 255, 0.42); |
| --purple: #7c3aed; |
| --purple-light: #a855f7; |
| --cyan: #06b6d4; |
| --text: #e2e8f0; |
| --text-muted: #8892a4; |
| --text-dim: #4b5563; |
| --radius: 14px; |
| --radius-sm: 8px; |
| --transition: 0.22s cubic-bezier(0.4, 0, 0.2, 1); |
| } |
| |
| html { scroll-behavior: smooth; } |
| |
| body { |
| font-family: 'Inter', system-ui, sans-serif; |
| background: var(--bg); |
| color: var(--text); |
| min-height: 100vh; |
| overflow-x: hidden; |
| } |
| |
| body::before { |
| content: ''; |
| position: fixed; |
| inset: 0; |
| background: |
| radial-gradient(ellipse 70% 50% at 15% 20%, rgba(124,58,237,0.12) 0%, transparent 60%), |
| radial-gradient(ellipse 50% 40% at 85% 75%, rgba(6,182,212,0.10) 0%, transparent 60%), |
| radial-gradient(ellipse 40% 35% at 50% 5%, rgba(168,85,247,0.08) 0%, transparent 55%); |
| pointer-events: none; |
| z-index: 0; |
| } |
| |
| |
| .wrapper { |
| position: relative; |
| z-index: 1; |
| max-width: 920px; |
| margin: 0 auto; |
| padding: 2.5rem 1.25rem 4rem; |
| } |
| |
| |
| .hero { text-align: center; margin-bottom: 2.5rem; } |
| |
| .hero-badge { |
| display: inline-flex; |
| align-items: center; |
| gap: 0.45rem; |
| background: rgba(124,58,237,0.15); |
| border: 1px solid rgba(124,58,237,0.35); |
| border-radius: 100px; |
| padding: 0.28rem 0.9rem; |
| font-size: 0.75rem; |
| font-weight: 600; |
| color: var(--purple-light); |
| letter-spacing: 0.04em; |
| text-transform: uppercase; |
| margin-bottom: 1rem; |
| } |
| |
| .hero h1 { |
| font-family: 'Space Grotesk', sans-serif; |
| font-size: clamp(2rem, 5vw, 3.2rem); |
| font-weight: 700; |
| line-height: 1.15; |
| background: linear-gradient(135deg, #c084fc 0%, #818cf8 40%, #38bdf8 100%); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| margin-bottom: 0.7rem; |
| } |
| |
| .hero p { |
| color: var(--text-muted); |
| font-size: 0.95rem; |
| max-width: 520px; |
| margin: 0 auto; |
| line-height: 1.6; |
| } |
| |
| |
| .tab-nav { |
| display: flex; |
| gap: 0.5rem; |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| padding: 0.4rem; |
| margin-bottom: 2rem; |
| } |
| |
| .tab-btn { |
| flex: 1; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 0.5rem; |
| padding: 0.7rem 1.2rem; |
| border: none; |
| border-radius: var(--radius-sm); |
| background: transparent; |
| color: var(--text-muted); |
| font-family: 'Inter', sans-serif; |
| font-size: 0.88rem; |
| font-weight: 500; |
| cursor: pointer; |
| transition: var(--transition); |
| } |
| |
| .tab-btn:hover { color: var(--text); background: rgba(255,255,255,0.05); } |
| |
| .tab-btn.active { |
| background: linear-gradient(135deg, rgba(124,58,237,0.35), rgba(6,182,212,0.2)); |
| color: #fff; |
| font-weight: 600; |
| box-shadow: 0 0 0 1px rgba(124,58,237,0.5) inset; |
| } |
| |
| |
| .tab-panel { display: none; } |
| .tab-panel.active { display: block; } |
| |
| |
| .card { |
| background: linear-gradient(135deg, rgba(14,14,40,0.9) 0%, rgba(20,20,58,0.75) 100%); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| padding: 1.6rem; |
| margin-bottom: 1.25rem; |
| backdrop-filter: blur(12px); |
| transition: border-color var(--transition), box-shadow var(--transition); |
| } |
| |
| .card:hover { border-color: var(--border-hover); } |
| |
| |
| .platform-header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| margin-bottom: 1.1rem; |
| } |
| |
| .platform-title { |
| display: flex; |
| align-items: center; |
| gap: 0.6rem; |
| font-family: 'Space Grotesk', sans-serif; |
| font-size: 1rem; |
| font-weight: 600; |
| color: #c4b5fd; |
| } |
| |
| .platform-icon { |
| width: 32px; |
| height: 32px; |
| border-radius: 8px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 1rem; |
| } |
| |
| .pi-instagram { background: linear-gradient(135deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888); } |
| .pi-tiktok { background: #161823; border: 1px solid #333; } |
| .pi-facebook { background: #1877f2; } |
| .pi-news { background: linear-gradient(135deg, #0ea5e9, #6366f1); } |
| .pi-dataset { background: linear-gradient(135deg, #059669, #0891b2); } |
| |
| |
| .toggle-wrap { display: flex; align-items: center; gap: 0.6rem; } |
| |
| .toggle-label { font-size: 0.78rem; color: var(--text-dim); font-weight: 500; } |
| |
| .toggle { position: relative; width: 42px; height: 24px; } |
| |
| .toggle input { opacity: 0; width: 0; height: 0; } |
| |
| .slider { |
| position: absolute; |
| inset: 0; |
| background: rgba(255,255,255,0.1); |
| border-radius: 100px; |
| cursor: pointer; |
| transition: var(--transition); |
| } |
| |
| .slider::before { |
| content: ''; |
| position: absolute; |
| width: 18px; |
| height: 18px; |
| left: 3px; |
| top: 3px; |
| background: white; |
| border-radius: 50%; |
| transition: var(--transition); |
| } |
| |
| .toggle input:checked + .slider { background: linear-gradient(135deg, var(--purple), var(--cyan)); } |
| .toggle input:checked + .slider::before { transform: translateX(18px); } |
| |
| .platform-fields { |
| overflow: hidden; |
| transition: max-height 0.35s ease, opacity 0.3s ease; |
| } |
| |
| .platform-fields.collapsed { |
| max-height: 0 !important; |
| opacity: 0; |
| pointer-events: none; |
| } |
| |
| |
| .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } |
| .form-row.cols-3 { grid-template-columns: 1fr 1fr 1fr; } |
| .form-group { display: flex; flex-direction: column; gap: 0.3rem; } |
| .form-group.full { grid-column: 1 / -1; } |
| |
| label { font-size: 0.78rem; color: var(--text-muted); font-weight: 500; letter-spacing: 0.01em; } |
| |
| input[type="text"], |
| input[type="password"], |
| input[type="number"], |
| textarea, |
| select { |
| background: rgba(7,7,26,0.7); |
| border: 1px solid rgba(130,100,255,0.2); |
| border-radius: var(--radius-sm); |
| color: var(--text); |
| padding: 0.65rem 0.9rem; |
| font-family: 'Inter', sans-serif; |
| font-size: 0.88rem; |
| width: 100%; |
| transition: border-color var(--transition), box-shadow var(--transition); |
| outline: none; |
| } |
| |
| input::placeholder, textarea::placeholder { color: var(--text-dim); } |
| |
| input:focus, textarea:focus, select:focus { |
| border-color: var(--purple); |
| box-shadow: 0 0 0 3px rgba(124,58,237,0.2); |
| } |
| |
| select option { background: var(--surface-2); } |
| textarea { resize: vertical; min-height: 88px; line-height: 1.5; } |
| |
| .field-hint { font-size: 0.72rem; color: var(--text-dim); line-height: 1.4; margin-top: 0.2rem; } |
| |
| |
| .cookie-tabs { display: flex; gap: 0.3rem; margin-bottom: 0.5rem; } |
| |
| .cookie-tab-btn { |
| padding: 0.25rem 0.7rem; |
| font-size: 0.72rem; |
| font-weight: 600; |
| border: 1px solid rgba(130,100,255,0.25); |
| border-radius: 6px; |
| background: transparent; |
| color: var(--text-muted); |
| cursor: pointer; |
| transition: var(--transition); |
| } |
| |
| .cookie-tab-btn.active { |
| background: rgba(124,58,237,0.25); |
| color: #c4b5fd; |
| border-color: rgba(124,58,237,0.5); |
| } |
| |
| |
| .tag-hint { |
| display: inline-flex; |
| align-items: center; |
| gap: 0.3rem; |
| font-size: 0.72rem; |
| color: var(--cyan); |
| background: rgba(6,182,212,0.1); |
| border: 1px solid rgba(6,182,212,0.25); |
| border-radius: 6px; |
| padding: 0.15rem 0.55rem; |
| margin-top: 0.3rem; |
| } |
| |
| |
| .portal-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); |
| gap: 0.5rem; |
| } |
| |
| .portal-chip { |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| padding: 0.55rem 0.75rem; |
| border: 1px solid rgba(130,100,255,0.2); |
| border-radius: var(--radius-sm); |
| cursor: pointer; |
| background: rgba(7,7,26,0.5); |
| transition: var(--transition); |
| user-select: none; |
| } |
| |
| .portal-chip:hover { border-color: rgba(130,100,255,0.45); background: rgba(124,58,237,0.1); } |
| .portal-chip input[type="checkbox"] { display: none; } |
| .portal-chip.checked { border-color: var(--purple); background: rgba(124,58,237,0.2); } |
| |
| .chip-label { font-size: 0.82rem; font-weight: 500; color: var(--text-muted); } |
| .portal-chip.checked .chip-label { color: var(--text); } |
| |
| .chip-dot { |
| width: 8px; |
| height: 8px; |
| border-radius: 50%; |
| background: var(--text-dim); |
| flex-shrink: 0; |
| transition: var(--transition); |
| } |
| |
| .portal-chip.checked .chip-dot { background: var(--purple-light); } |
| |
| |
| .btn-submit { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 0.6rem; |
| width: 100%; |
| padding: 1rem; |
| background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 50%, #0891b2 100%); |
| border: none; |
| border-radius: var(--radius); |
| color: #fff; |
| font-family: 'Space Grotesk', sans-serif; |
| font-size: 1rem; |
| font-weight: 600; |
| cursor: pointer; |
| transition: opacity var(--transition), transform var(--transition), box-shadow var(--transition); |
| letter-spacing: 0.02em; |
| margin-top: 0.5rem; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .btn-submit::before { |
| content: ''; |
| position: absolute; |
| inset: 0; |
| background: linear-gradient(135deg, rgba(255,255,255,0.12), transparent); |
| opacity: 0; |
| transition: opacity var(--transition); |
| } |
| |
| .btn-submit:hover::before { opacity: 1; } |
| .btn-submit:hover { transform: translateY(-2px); box-shadow: 0 8px 32px rgba(124,58,237,0.45); } |
| .btn-submit:active { transform: translateY(0); } |
| .btn-submit:disabled { opacity: 0.65; pointer-events: none; cursor: not-allowed; transform: none; } |
| |
| |
| .spinner { |
| display: none; |
| width: 18px; |
| height: 18px; |
| border: 2.5px solid rgba(255,255,255,0.3); |
| border-top-color: #fff; |
| border-radius: 50%; |
| animation: spin 0.7s linear infinite; |
| flex-shrink: 0; |
| } |
| |
| @keyframes spin { to { transform: rotate(360deg); } } |
| |
| |
| .alert { |
| border-radius: var(--radius); |
| padding: 1rem 1.25rem; |
| margin-bottom: 1.5rem; |
| font-size: 0.88rem; |
| border: 1px solid; |
| display: flex; |
| gap: 0.6rem; |
| align-items: flex-start; |
| } |
| |
| .alert-error { |
| background: rgba(239,68,68,0.08); |
| border-color: rgba(239,68,68,0.3); |
| color: #fca5a5; |
| } |
| |
| |
| .results-section { margin-top: 2.5rem; } |
| |
| .results-header { |
| display: flex; |
| align-items: center; |
| gap: 0.6rem; |
| margin-bottom: 1.5rem; |
| } |
| |
| .results-header h2 { |
| font-family: 'Space Grotesk', sans-serif; |
| font-size: 1.3rem; |
| font-weight: 700; |
| background: linear-gradient(135deg, var(--cyan), var(--purple-light)); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| |
| .stats-strip { |
| font-size: 0.8rem; |
| color: var(--text-dim); |
| background: rgba(255,255,255,0.04); |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| padding: 0.4rem 0.9rem; |
| margin-left: auto; |
| } |
| |
| |
| .sentiment-grid { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| gap: 1rem; |
| margin-bottom: 1.5rem; |
| } |
| |
| .s-card { |
| border-radius: var(--radius); |
| padding: 1.4rem 1rem; |
| text-align: center; |
| border: 1px solid; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .s-card::before { content: ''; position: absolute; inset: 0; opacity: 0.06; border-radius: inherit; } |
| |
| .s-card.positif { background: rgba(34,197,94,0.08); border-color: rgba(34,197,94,0.3); } |
| .s-card.positif::before { background: #22c55e; } |
| .s-card.negatif { background: rgba(239,68,68,0.08); border-color: rgba(239,68,68,0.3); } |
| .s-card.negatif::before { background: #ef4444; } |
| .s-card.netral { background: rgba(148,163,184,0.06); border-color: rgba(148,163,184,0.2); } |
| .s-card.netral::before { background: #94a3b8; } |
| |
| .s-count { font-family: 'Space Grotesk', sans-serif; font-size: 2.8rem; font-weight: 700; line-height: 1; margin-bottom: 0.3rem; } |
| .s-card.positif .s-count { color: #4ade80; } |
| .s-card.negatif .s-count { color: #f87171; } |
| .s-card.netral .s-count { color: #94a3b8; } |
| |
| .s-label { font-size: 0.82rem; color: var(--text-muted); font-weight: 500; } |
| |
| .s-bar-wrap { margin-top: 0.8rem; height: 4px; background: rgba(255,255,255,0.08); border-radius: 100px; overflow: hidden; } |
| .s-bar { height: 100%; border-radius: 100px; transition: width 1.2s cubic-bezier(0.4,0,0.2,1); } |
| .s-card.positif .s-bar { background: linear-gradient(90deg, #16a34a, #4ade80); } |
| .s-card.negatif .s-bar { background: linear-gradient(90deg, #b91c1c, #f87171); } |
| .s-card.netral .s-bar { background: linear-gradient(90deg, #475569, #94a3b8); } |
| |
| |
| .wordcloud-card { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| padding: 1.5rem; |
| text-align: center; |
| } |
| |
| .wordcloud-card h3 { |
| font-family: 'Space Grotesk', sans-serif; |
| font-size: 1rem; |
| color: var(--purple-light); |
| margin-bottom: 1rem; |
| } |
| |
| .wordcloud-img { max-width: 100%; border-radius: 10px; border: 1px solid var(--border); } |
| |
| |
| .divider { |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| color: var(--text-dim); |
| font-size: 0.75rem; |
| margin: 0.75rem 0; |
| } |
| |
| .divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: var(--border); } |
| |
| |
| .section-label { |
| font-size: 0.7rem; |
| font-weight: 700; |
| text-transform: uppercase; |
| letter-spacing: 0.08em; |
| color: var(--text-dim); |
| margin-bottom: 0.6rem; |
| } |
| |
| |
| .upload-zone { |
| border: 2px dashed rgba(130,100,255,0.28); |
| border-radius: var(--radius); |
| padding: 2.5rem 1.5rem; |
| text-align: center; |
| transition: var(--transition); |
| cursor: pointer; |
| background: rgba(124,58,237,0.04); |
| position: relative; |
| } |
| |
| .upload-zone:hover, .upload-zone.drag-over { border-color: var(--purple); background: rgba(124,58,237,0.1); } |
| |
| .upload-zone input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; } |
| |
| .upload-icon { font-size: 2rem; margin-bottom: 0.5rem; } |
| .upload-text { font-size: 0.9rem; color: var(--text-muted); } |
| .upload-sub { font-size: 0.78rem; color: var(--text-dim); margin-top: 0.3rem; } |
| .upload-filename { display: none; margin-top: 0.6rem; font-size: 0.82rem; color: var(--cyan); font-weight: 500; } |
| |
| |
| @media (max-width: 640px) { |
| .form-row { grid-template-columns: 1fr; } |
| .form-row.cols-3 { grid-template-columns: 1fr 1fr; } |
| .sentiment-grid { grid-template-columns: 1fr; } |
| .tab-btn span.tab-text { display: none; } |
| .hero h1 { font-size: 1.8rem; } |
| } |
| |
| |
| @keyframes fadeUp { |
| from { opacity: 0; transform: translateY(20px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| .animate-in { animation: fadeUp 0.5s ease both; } |
| .delay-1 { animation-delay: 0.05s; } |
| .delay-2 { animation-delay: 0.10s; } |
| .delay-3 { animation-delay: 0.15s; } |
| .delay-4 { animation-delay: 0.20s; } |
| .delay-5 { animation-delay: 0.25s; } |
| </style> |
| </head> |
| <body> |
| <div class="wrapper"> |
|
|
| |
| <header class="hero animate-in"> |
| <div class="hero-badge">π¬ AI-Powered</div> |
| <h1>SentiScope</h1> |
| <p>Analisis sentimen media sosial otomatis dengan IndoBERT β Instagram, TikTok, Facebook & Berita Online.</p> |
| </header> |
|
|
| |
| {% if error %} |
| <div class="alert alert-error animate-in" role="alert"> |
| <span>β οΈ</span> |
| <span>{{ error }}</span> |
| </div> |
| {% endif %} |
|
|
| |
| <nav class="tab-nav animate-in delay-1" role="tablist"> |
| <button class="tab-btn {% if active_tab != 'dataset' %}active{% endif %}" |
| id="tab-scraping" role="tab" onclick="switchTab('scraping')"> |
| <span class="tab-icon">π·οΈ</span> |
| <span class="tab-text">Scraping Otomatis</span> |
| </button> |
| <button class="tab-btn {% if active_tab == 'dataset' %}active{% endif %}" |
| id="tab-dataset" role="tab" onclick="switchTab('dataset')"> |
| <span class="tab-icon">π</span> |
| <span class="tab-text">Upload Dataset</span> |
| </button> |
| </nav> |
|
|
| |
| <div class="tab-panel {% if active_tab != 'dataset' %}active{% endif %}" id="panel-scraping"> |
| <form id="scraping-form" action="/process" method="post"> |
|
|
| |
| <input type="hidden" id="enable_instagram" name="enable_instagram" value=""> |
| <input type="hidden" id="enable_tiktok" name="enable_tiktok" value=""> |
| <input type="hidden" id="enable_facebook" name="enable_facebook" value=""> |
| <input type="hidden" id="enable_news" name="enable_news" value=""> |
|
|
| |
| <div class="card animate-in delay-2"> |
| <div class="platform-header"> |
| <div class="platform-title"> |
| <div class="platform-icon pi-instagram">πΈ</div> |
| Instagram |
| </div> |
| <div class="toggle-wrap"> |
| <span class="toggle-label" id="ig-toggle-label">Nonaktif</span> |
| <label class="toggle"> |
| <input type="checkbox" id="ig-toggle" onchange="togglePlatform('ig')"> |
| <span class="slider"></span> |
| </label> |
| </div> |
| </div> |
| <div class="platform-fields collapsed" id="ig-fields" style="max-height:600px;"> |
| <div class="form-row" style="margin-bottom:0.9rem;"> |
| <div class="form-group"> |
| <label for="ig_username">Username Instagram</label> |
| <input id="ig_username" type="text" name="ig_username" placeholder="akun_instagram" autocomplete="username"> |
| </div> |
| <div class="form-group"> |
| <label for="ig_password">Password Instagram</label> |
| <input id="ig_password" type="password" name="ig_password" placeholder="β’β’β’β’β’β’β’β’" autocomplete="current-password"> |
| </div> |
| </div> |
| <div class="form-row"> |
| <div class="form-group full"> |
| <label for="target_accounts">Target Akun / #Hashtag (satu per baris)</label> |
| <textarea id="target_accounts" name="target_accounts" |
| placeholder="cirebonkab @rctvcirebon #jalanrusak"></textarea> |
| <span class="tag-hint">β΅ Satu target per baris, @ dan # opsional</span> |
| </div> |
| <div class="form-group"> |
| <label for="mode">Mode Waktu</label> |
| <select id="mode" name="mode"> |
| <option value="all">Semua Postingan</option> |
| <option value="date">7 Bulan Terakhir</option> |
| </select> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="card animate-in delay-3"> |
| <div class="platform-header"> |
| <div class="platform-title"> |
| <div class="platform-icon pi-tiktok">π΅</div> |
| TikTok |
| </div> |
| <div class="toggle-wrap"> |
| <span class="toggle-label" id="tt-toggle-label">Nonaktif</span> |
| <label class="toggle"> |
| <input type="checkbox" id="tt-toggle" onchange="togglePlatform('tt')"> |
| <span class="slider"></span> |
| </label> |
| </div> |
| </div> |
| <div class="platform-fields collapsed" id="tt-fields" style="max-height:500px;"> |
| <div class="form-group" style="margin-bottom:0.9rem;"> |
| <label>Format Cookie TikTok</label> |
| <div class="cookie-tabs"> |
| <button type="button" class="cookie-tab-btn active" onclick="setCookieHint('raw',this)">String Mentah</button> |
| <button type="button" class="cookie-tab-btn" onclick="setCookieHint('json_arr',this)">JSON Array</button> |
| <button type="button" class="cookie-tab-btn" onclick="setCookieHint('json_obj',this)">JSON Object</button> |
| </div> |
| <textarea id="tiktok_cookie" name="tiktok_cookie" |
| placeholder="sessionid=xxx; tt_webid=yyy; ..." |
| style="min-height:70px;font-family:monospace;font-size:0.8rem;"></textarea> |
| <p class="field-hint" id="cookie-hint"> |
| Format: <code>sessionid=ABC; tt_webid=123</code> β ambil dari DevTools β Application β Cookies β tiktok.com |
| </p> |
| </div> |
| <div class="form-group"> |
| <label for="tiktok_targets">Target Username TikTok (satu per baris)</label> |
| <textarea id="tiktok_targets" name="tiktok_targets" |
| placeholder="@rctvcirebon @cirebonnews kuningan_update"></textarea> |
| <span class="tag-hint">β΅ Satu username per baris, @ opsional</span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="card animate-in delay-3"> |
| <div class="platform-header"> |
| <div class="platform-title"> |
| <div class="platform-icon pi-facebook">π</div> |
| Facebook |
| </div> |
| <div class="toggle-wrap"> |
| <span class="toggle-label" id="fb-toggle-label">Nonaktif</span> |
| <label class="toggle"> |
| <input type="checkbox" id="fb-toggle" onchange="togglePlatform('fb')"> |
| <span class="slider"></span> |
| </label> |
| </div> |
| </div> |
| <div class="platform-fields collapsed" id="fb-fields" style="max-height:500px;"> |
| <div class="form-row" style="margin-bottom:0.9rem;"> |
| <div class="form-group"> |
| <label for="fb_username">Email / No. HP Facebook</label> |
| <input id="fb_username" type="text" name="fb_username" placeholder="email@contoh.com" autocomplete="username"> |
| </div> |
| <div class="form-group"> |
| <label for="fb_password">Password Facebook</label> |
| <input id="fb_password" type="password" name="fb_password" placeholder="β’β’β’β’β’β’β’β’" autocomplete="current-password"> |
| </div> |
| </div> |
| <div class="form-group"> |
| <label for="facebook_groups">URL Grup Facebook (satu per baris, wajib diisi)</label> |
| <textarea id="facebook_groups" name="facebook_groups" |
| placeholder="https://web.facebook.com/groups/123456 https://web.facebook.com/groups/teraswarga"></textarea> |
| <p class="field-hint">β οΈ Harus diisi β tidak ada grup default. Jika kosong, Facebook tidak akan di-scrape.</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="card animate-in delay-4"> |
| <div class="platform-header"> |
| <div class="platform-title"> |
| <div class="platform-icon pi-news">π°</div> |
| Berita Online |
| </div> |
| <div class="toggle-wrap"> |
| <span class="toggle-label" id="news-toggle-label">Nonaktif</span> |
| <label class="toggle"> |
| <input type="checkbox" id="news-toggle" onchange="togglePlatform('news')"> |
| <span class="slider"></span> |
| </label> |
| </div> |
| </div> |
| <div class="platform-fields collapsed" id="news-fields" style="max-height:500px;"> |
| <div class="section-label">Pilih Portal (bisa lebih dari satu)</div> |
| <div class="portal-grid" id="portal-grid"> |
| <label class="portal-chip" onclick="toggleChip(this)"> |
| <input type="checkbox" name="_portal_detik" value="detik"> |
| <span class="chip-dot"></span><span class="chip-label">Detik.com</span> |
| </label> |
| <label class="portal-chip" onclick="toggleChip(this)"> |
| <input type="checkbox" name="_portal_antara" value="antara"> |
| <span class="chip-dot"></span><span class="chip-label">Antara News</span> |
| </label> |
| <label class="portal-chip" onclick="toggleChip(this)"> |
| <input type="checkbox" name="_portal_radar" value="radar"> |
| <span class="chip-dot"></span><span class="chip-label">Radar (Disway)</span> |
| </label> |
| <label class="portal-chip" onclick="toggleChip(this)"> |
| <input type="checkbox" name="_portal_radarcirebon" value="radarcirebon"> |
| <span class="chip-dot"></span><span class="chip-label">Radar Cirebon ID</span> |
| </label> |
| <label class="portal-chip" onclick="toggleChip(this)"> |
| <input type="checkbox" name="_portal_cnn" value="cnn"> |
| <span class="chip-dot"></span><span class="chip-label">CNN Indonesia</span> |
| </label> |
| </div> |
| |
| <input type="hidden" id="news_portals" name="news_portals" value=""> |
| <div class="form-row" style="margin-top:1rem;"> |
| <div class="form-group"> |
| <label for="news_keyword">Keyword Pencarian</label> |
| <input id="news_keyword" type="text" name="news_keyword" value="kabupaten cirebon" placeholder="kabupaten cirebon"> |
| </div> |
| <div class="form-group"> |
| <label for="news_pages">Jumlah Halaman per Portal</label> |
| <input id="news_pages" type="number" name="news_pages" value="1" min="1" max="20"> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <button class="btn-submit animate-in delay-5" type="submit" id="scraping-submit"> |
| <span class="spinner" id="scraping-spinner"></span> |
| <span id="scraping-btn-text">β‘ Mulai Scraping & Analisis</span> |
| </button> |
| </form> |
| </div> |
|
|
| |
| <div class="tab-panel {% if active_tab == 'dataset' %}active{% endif %}" id="panel-dataset"> |
| <form id="dataset-form" action="/wordcloud-dataset" method="post" enctype="multipart/form-data"> |
| <div class="card animate-in"> |
| <div class="platform-header"> |
| <div class="platform-title"> |
| <div class="platform-icon pi-dataset">π</div> |
| Upload Dataset |
| </div> |
| </div> |
|
|
| <div class="form-group" style="margin-bottom:1.25rem;"> |
| <label>File Dataset (CSV, JSON, atau TXT)</label> |
| <div class="upload-zone" id="upload-zone"> |
| <input type="file" name="dataset_file" id="dataset_file" |
| accept=".csv,.json,.txt,.tsv" |
| onchange="showFilename(this)"> |
| <div class="upload-icon">π</div> |
| <div class="upload-text">Klik atau seret file ke sini</div> |
| <div class="upload-sub">Mendukung .csv, .json, .txt β maks 50 MB</div> |
| <div class="upload-filename" id="upload-filename">β <span></span></div> |
| </div> |
| </div> |
|
|
| <div class="form-group" style="margin-bottom:1.25rem;"> |
| <label for="text_column">Nama Kolom Teks (untuk CSV/JSON)</label> |
| <input id="text_column" type="text" name="text_column" value="text" placeholder="text / content / komentar"> |
| <p class="field-hint">Kolom yang berisi teks yang akan dianalisis. Kosongkan untuk pakai kolom pertama.</p> |
| </div> |
|
|
| <div class="divider">atau paste teks langsung</div> |
|
|
| <div class="form-group"> |
| <label for="dataset_text">Teks Dataset (satu dokumen/kalimat per baris)</label> |
| <textarea id="dataset_text" name="dataset_text" style="min-height:140px;" |
| placeholder="Masukkan teks di sini, satu kalimat per baris... Cirebon semakin maju dengan infrastruktur yang baik Jalan di daerah X masih rusak parah"></textarea> |
| </div> |
| </div> |
|
|
| <button class="btn-submit" type="submit" id="dataset-submit"> |
| <span class="spinner" id="dataset-spinner"></span> |
| <span id="dataset-btn-text">βοΈ Buat Word Cloud & Analisis Sentimen</span> |
| </button> |
| </form> |
| </div> |
|
|
| |
| {% if result %} |
| <section class="results-section animate-in"> |
| <div class="results-header"> |
| <h2>π Hasil Analisis Sentimen</h2> |
| <span class="stats-strip">{{ total_scraped }} teks dikumpulkan Β· {{ result.total }} dianalisis</span> |
| </div> |
|
|
| {% if csv_filename %} |
| <div style="margin-bottom: 1.5rem;"> |
| <a href="{{ csv_filename }}" download class="btn-submit" style="display:inline-flex; width:auto; padding:0.7rem 1.25rem; background:linear-gradient(135deg, #059669, #10b981); text-decoration:none; font-size:0.9rem;"> |
| π₯ Download Data Scraping (CSV) |
| </a> |
| </div> |
| {% endif %} |
|
|
| <div class="sentiment-grid"> |
| {% set total = result.total if result.total > 0 else 1 %} |
| <div class="s-card positif"> |
| <div class="s-count" id="count-pos">0</div> |
| <div class="s-label">π Positif</div> |
| <div class="s-bar-wrap"><div class="s-bar" id="bar-pos" style="width:0%"></div></div> |
| </div> |
| <div class="s-card negatif"> |
| <div class="s-count" id="count-neg">0</div> |
| <div class="s-label">π Negatif</div> |
| <div class="s-bar-wrap"><div class="s-bar" id="bar-neg" style="width:0%"></div></div> |
| </div> |
| <div class="s-card netral"> |
| <div class="s-count" id="count-neu">0</div> |
| <div class="s-label">π Netral</div> |
| <div class="s-bar-wrap"><div class="s-bar" id="bar-neu" style="width:0%"></div></div> |
| </div> |
| </div> |
|
|
| {% if image %} |
| <div class="wordcloud-card"> |
| <h3>βοΈ Word Cloud</h3> |
| <img class="wordcloud-img" src="data:image/png;base64,{{ image }}" alt="Word Cloud"> |
| </div> |
| {% endif %} |
| </section> |
|
|
| <script> |
| (function () { |
| var pos = {{ result.positif }}; |
| var neg = {{ result.negatif }}; |
| var neu = {{ result.netral }}; |
| var total = {{ result.total if result.total > 0 else 1 }}; |
| |
| function animCount(el, target) { |
| var start = 0; |
| var step = Math.max(1, Math.ceil(target / 40)); |
| var timer = setInterval(function () { |
| start = Math.min(start + step, target); |
| el.textContent = start; |
| if (start >= target) clearInterval(timer); |
| }, 25); |
| } |
| |
| setTimeout(function () { |
| animCount(document.getElementById('count-pos'), pos); |
| animCount(document.getElementById('count-neg'), neg); |
| animCount(document.getElementById('count-neu'), neu); |
| document.getElementById('bar-pos').style.width = (pos / total * 100).toFixed(1) + '%'; |
| document.getElementById('bar-neg').style.width = (neg / total * 100).toFixed(1) + '%'; |
| document.getElementById('bar-neu').style.width = (neu / total * 100).toFixed(1) + '%'; |
| }, 300); |
| })(); |
| </script> |
| {% endif %} |
|
|
| </div> |
|
|
| <script> |
| |
| function switchTab(name) { |
| document.querySelectorAll('.tab-btn').forEach(function (b) { b.classList.remove('active'); }); |
| document.querySelectorAll('.tab-panel').forEach(function (p) { p.classList.remove('active'); }); |
| document.getElementById('tab-' + name).classList.add('active'); |
| document.getElementById('panel-' + name).classList.add('active'); |
| } |
| |
| |
| function togglePlatform(id) { |
| var fields = document.getElementById(id + '-fields'); |
| var toggle = document.getElementById(id + '-toggle'); |
| var label = document.getElementById(id + '-toggle-label'); |
| var flagMap = { ig: 'enable_instagram', tt: 'enable_tiktok', fb: 'enable_facebook', news: 'enable_news' }; |
| |
| if (toggle.checked) { |
| fields.classList.remove('collapsed'); |
| if (label) label.textContent = 'Aktif'; |
| document.getElementById(flagMap[id]).value = '1'; |
| } else { |
| fields.classList.add('collapsed'); |
| if (label) label.textContent = 'Nonaktif'; |
| document.getElementById(flagMap[id]).value = ''; |
| } |
| } |
| |
| |
| function toggleChip(label) { |
| var cb = label.querySelector('input[type="checkbox"]'); |
| cb.checked = !cb.checked; |
| label.classList.toggle('checked', cb.checked); |
| updatePortalField(); |
| } |
| |
| function updatePortalField() { |
| var vals = []; |
| document.querySelectorAll('#portal-grid .portal-chip.checked input').forEach(function (cb) { |
| vals.push(cb.value); |
| }); |
| document.getElementById('news_portals').value = vals.join(','); |
| } |
| |
| |
| var cookieHints = { |
| raw: 'Format: <code>sessionid=ABC; tt_webid=123</code> β ambil dari DevTools β Application β Cookies β tiktok.com', |
| json_arr: 'Format JSON Array: <code>[{"name":"sessionid","value":"ABC","domain":".tiktok.com"}]</code>', |
| json_obj: 'Format JSON Object: <code>{"sessionid": "ABC", "tt_webid": "123"}</code>', |
| }; |
| |
| var cookiePlaceholders = { |
| raw: 'sessionid=xxx; tt_webid=yyy; ...', |
| json_arr: '[{"name":"sessionid","value":"xxx","domain":".tiktok.com"},...]', |
| json_obj: '{"sessionid": "xxx", "tt_webid": "yyy"}', |
| }; |
| |
| function setCookieHint(fmt, btn) { |
| document.querySelectorAll('.cookie-tab-btn').forEach(function (b) { b.classList.remove('active'); }); |
| btn.classList.add('active'); |
| document.getElementById('cookie-hint').innerHTML = cookieHints[fmt]; |
| document.getElementById('tiktok_cookie').placeholder = cookiePlaceholders[fmt]; |
| } |
| |
| |
| function showFilename(input) { |
| var wrap = document.getElementById('upload-filename'); |
| if (input.files && input.files[0]) { |
| wrap.style.display = 'block'; |
| wrap.querySelector('span').textContent = input.files[0].name; |
| } else { |
| wrap.style.display = 'none'; |
| } |
| } |
| |
| |
| var zone = document.getElementById('upload-zone'); |
| if (zone) { |
| zone.addEventListener('dragover', function (e) { e.preventDefault(); zone.classList.add('drag-over'); }); |
| zone.addEventListener('dragleave', function () { zone.classList.remove('drag-over'); }); |
| zone.addEventListener('drop', function () { zone.classList.remove('drag-over'); }); |
| } |
| |
| |
| function bindSubmit(formId, spinnerId, btnTextId, btnId, loadingText) { |
| var form = document.getElementById(formId); |
| if (!form) return; |
| form.addEventListener('submit', function () { |
| document.getElementById(btnId).disabled = true; |
| document.getElementById(spinnerId).style.display = 'inline-block'; |
| document.getElementById(btnTextId).innerHTML = loadingText + '<span class="dots"><span></span><span></span><span></span></span>'; |
| }); |
| } |
| |
| bindSubmit('scraping-form', 'scraping-spinner', 'scraping-btn-text', 'scraping-submit', 'Memproses (mungkin beberapa menit)'); |
| bindSubmit('dataset-form', 'dataset-spinner', 'dataset-btn-text', 'dataset-submit', 'Memproses dataset'); |
| |
| |
| var sf = document.getElementById('scraping-form'); |
| if (sf) sf.addEventListener('submit', updatePortalField, true); |
| </script> |
| </body> |
| </html> |