izzatakhsan commited on
Commit
2dc9583
·
verified ·
1 Parent(s): b13bcfc

Upload 12 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ static/images/ikn.png filter=lfs diff=lfs merge=lfs -text
37
+ static/images/whoosh.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gunakan Python 3.9 sebagai dasar
2
+ FROM python:3.9
3
+
4
+ # Set working directory
5
+ WORKDIR /code
6
+
7
+ # Copy requirements.txt ke dalam container
8
+ COPY ./requirements.txt /code/requirements.txt
9
+
10
+ # Install dependencies
11
+ # --no-cache-dir agar image tidak terlalu besar
12
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
13
+
14
+ # Buat user baru 'user' dengan ID 1000 (Syarat keamanan Hugging Face)
15
+ RUN useradd -m -u 1000 user
16
+
17
+ # Ganti user ke 'user'
18
+ USER user
19
+
20
+ # Set environment variables
21
+ ENV HOME=/home/user \
22
+ PATH=/home/user/.local/bin:$PATH
23
+
24
+ # Pindah ke direktori kerja user
25
+ WORKDIR $HOME/app
26
+
27
+ # Copy semua file proyek ke dalam container dengan izin user yang benar
28
+ COPY --chown=user . $HOME/app
29
+
30
+ # Buat folder uploads agar tidak error saat upload file
31
+ RUN mkdir -p $HOME/app/uploads
32
+
33
+ # Jalankan aplikasi menggunakan Gunicorn pada port 7860
34
+ # Port 7860 adalah port wajib untuk Hugging Face Spaces
35
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
app.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, url_for, flash, redirect
2
+ from model import predict_sentiment, analyze_sentiment_from_file, get_sentiment_counts, generate_wordcloud_base64
3
+ import os
4
+ from werkzeug.utils import secure_filename
5
+
6
+ # Konfigurasi folder upload
7
+ UPLOAD_FOLDER = 'uploads'
8
+ ALLOWED_EXTENSIONS = {'csv', 'xlsx'}
9
+
10
+ app = Flask(__name__)
11
+ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
12
+ app.config['SECRET_KEY'] = 'gantidenganyangaman'
13
+
14
+ # Pastikan folder upload ada
15
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
16
+
17
+ def allowed_file(filename):
18
+ """Memeriksa apakah ekstensi file diizinkan."""
19
+ return '.' in filename and \
20
+ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
21
+
22
+ @app.route('/')
23
+ def home():
24
+ """Merender halaman home."""
25
+ return render_template('home.html')
26
+
27
+ @app.route('/dashboard')
28
+ def dashboard():
29
+ """
30
+ Merender halaman dashboard dengan data visualisasi IKN dan Whoosh.
31
+ """
32
+ # Data Hardcoded sesuai permintaan
33
+ dashboard_data = {
34
+ 'ikn': {
35
+ 'title': 'Sentimen Masyarakat terhadap IKN',
36
+ 'counts': {'Positif': 633, 'Netral': 387, 'Negatif': 452},
37
+ 'image': 'ikn.png' # Pastikan file ini ada di static/images/
38
+ },
39
+ 'whoosh': {
40
+ 'title': 'Sentimen Masyarakat terhadap Whoosh',
41
+ 'counts': {'Positif': 1122, 'Netral': 4270, 'Negatif': 2108},
42
+ 'image': 'whoosh.png' # Pastikan file ini ada di static/images/
43
+ }
44
+ }
45
+
46
+ return render_template('dashboard.html', dashboard_data=dashboard_data)
47
+
48
+ @app.route('/analysis', methods=['GET', 'POST'])
49
+ def analysis():
50
+ """
51
+ Merender halaman analisis. Menangani analisis teks tunggal DAN upload file.
52
+ """
53
+ prediction_result = None
54
+ input_text = ""
55
+ results_table = None
56
+ wordcloud = None
57
+ sentiment_counts = None
58
+
59
+ if request.method == 'POST':
60
+ if 'text_input' in request.form:
61
+ input_text = request.form.get('text_input', '')
62
+ if input_text:
63
+ prediction_result = predict_sentiment(input_text)
64
+
65
+ elif 'file' in request.files:
66
+ file = request.files['file']
67
+ text_column = request.form.get('text_column')
68
+
69
+ if file.filename == '':
70
+ flash('Tidak ada file yang dipilih', 'danger')
71
+ return redirect(url_for('analysis'))
72
+
73
+ if not text_column:
74
+ flash('Nama kolom teks tidak boleh kosong', 'danger')
75
+ return redirect(url_for('analysis'))
76
+
77
+ if file and allowed_file(file.filename):
78
+ filename = secure_filename(file.filename)
79
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
80
+ file.save(file_path)
81
+
82
+ df, error = analyze_sentiment_from_file(file_path, text_column)
83
+
84
+ if error:
85
+ flash(error, 'danger')
86
+ os.remove(file_path)
87
+ return redirect(url_for('analysis'))
88
+
89
+ sentiment_counts = get_sentiment_counts(df)
90
+ wordcloud = generate_wordcloud_base64(df, text_column)
91
+ results_table = df.to_html(classes='table table-striped table-hover', index=False, border=0)
92
+ os.remove(file_path)
93
+ else:
94
+ flash('Format file tidak diizinkan. Gunakan .csv atau .xlsx', 'danger')
95
+ return redirect(url_for('analysis'))
96
+
97
+ return render_template('analysis.html',
98
+ prediction=prediction_result,
99
+ text=input_text,
100
+ sentiment_counts=sentiment_counts,
101
+ results_table=results_table,
102
+ wordcloud=wordcloud)
103
+
104
+ @app.route('/about')
105
+ def about():
106
+ """Merender halaman about."""
107
+ return render_template('about.html')
108
+
109
+ if __name__ == '__main__':
110
+ app.run(debug=True)
model.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
3
+ import pandas as pd
4
+ import matplotlib
5
+ matplotlib.use('Agg') # Gunakan backend 'Agg' agar tidak error di server
6
+ import matplotlib.pyplot as plt
7
+ import io
8
+ import base64
9
+ import re
10
+ from wordcloud import WordCloud, STOPWORDS
11
+
12
+ # --- Bagian 1: Pemuatan Model IndoBERT ---
13
+
14
+ MODEL_NAME = "crypter70/IndoBERT-Sentiment-Analysis"
15
+ tokenizer = None
16
+ model = None
17
+
18
+ def load_model():
19
+ """
20
+ Lazy-load the tokenizer and model. This avoids long blocking downloads during module import
21
+ and prevents the Flask app from hanging when importing `model.py`.
22
+ """
23
+ global tokenizer, model
24
+ if tokenizer is not None and model is not None:
25
+ return
26
+ try:
27
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
28
+ model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME)
29
+ model.eval()
30
+ print(f"Model {MODEL_NAME} loaded successfully.")
31
+ except Exception as e:
32
+ print(f"Error loading model {MODEL_NAME}: {e}")
33
+ print("Model and tokenizer set to None. Predictions will not work.")
34
+ tokenizer = None
35
+ model = None
36
+
37
+ # Definisikan label berdasarkan konfigurasi model
38
+ labels = ['Positif', 'Netral', 'Negatif']
39
+
40
+ def predict_sentiment(text):
41
+ """
42
+ Memprediksi sentimen dari satu string teks.
43
+ """
44
+ # Pastikan model dimuat secara lazy jika belum
45
+ if model is None or tokenizer is None:
46
+ load_model()
47
+ if not model or not tokenizer:
48
+ print("Model or tokenizer is not loaded. Cannot predict.")
49
+ return "Error: Model not loaded."
50
+
51
+ try:
52
+ # Ubah input menjadi string jika bukan (penting untuk data dari file)
53
+ text = str(text)
54
+
55
+ # Tokenisasi teks
56
+ inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=512, padding=True)
57
+
58
+ # Dapatkan prediksi model
59
+ with torch.no_grad(): # Nonaktifkan perhitungan gradien untuk inferensi
60
+ outputs = model(**inputs)
61
+
62
+ # Dapatkan probabilitas (logits) dan cari ID kelas dengan prob tertinggi
63
+ logits = outputs.logits
64
+ predicted_class_id = torch.argmax(logits, dim=1).item()
65
+
66
+ # Petakan ID ke label yang sesuai
67
+ return labels[predicted_class_id]
68
+
69
+ except Exception as e:
70
+ print(f"Error during prediction: {e}")
71
+ return "Error: Prediction failed."
72
+
73
+ # --- Bagian 2: Analisis File ---
74
+
75
+ def analyze_sentiment_from_file(file_path, text_column_name):
76
+ """
77
+ Membaca file (CSV atau XLSX), menerapkan prediksi sentimen ke kolom yang ditentukan.
78
+ """
79
+ try:
80
+ # Baca file berdasarkan ekstensinya
81
+ if file_path.endswith('.csv'):
82
+ df = pd.read_csv(file_path)
83
+ elif file_path.endswith('.xlsx'):
84
+ df = pd.read_excel(file_path)
85
+ else:
86
+ return None, "Error: Unsupported file format."
87
+
88
+ # Cek apakah kolom yang diminta ada di DataFrame
89
+ if text_column_name not in df.columns:
90
+ return None, f"Error: Column '{text_column_name}' not found in the file."
91
+
92
+ # Pastikan kolom teks adalah string (untuk menghindari error .apply())
93
+ df[text_column_name] = df[text_column_name].astype(str)
94
+
95
+ # Terapkan fungsi prediksi sentimen ke kolom yang ditentukan
96
+ # Membuat kolom baru 'Sentiment_Prediction'
97
+ df['Sentiment_Prediction'] = df[text_column_name].apply(predict_sentiment)
98
+
99
+ return df, None # Kembalikan DataFrame dan tidak ada error
100
+
101
+ except FileNotFoundError:
102
+ return None, "Error: File not found."
103
+ except Exception as e:
104
+ print(f"Error processing file: {e}")
105
+ return None, "Error: Could not process the file."
106
+
107
+ # --- Bagian 3: Visualisasi (DIPERBARUI) ---
108
+
109
+ def get_sentiment_counts(df, sentiment_column='Sentiment_Prediction'):
110
+ """
111
+ Menghitung jumlah sentimen dan mengembalikannya sebagai kamus (dictionary).
112
+ Siap untuk digunakan oleh Chart.js.
113
+ """
114
+ if sentiment_column not in df.columns:
115
+ print(f"Error: Sentiment column '{sentiment_column}' not found.")
116
+ return {}
117
+
118
+ sentiment_counts = df[sentiment_column].value_counts()
119
+
120
+ # Pastikan semua label ada, bahkan jika jumlahnya 0
121
+ all_labels = ['Positif', 'Netral', 'Negatif']
122
+ for label in all_labels:
123
+ if label not in sentiment_counts:
124
+ sentiment_counts[label] = 0
125
+
126
+ # Mengembalikan sebagai kamus standar
127
+ return sentiment_counts.to_dict()
128
+
129
+ def generate_wordcloud_base64(df, text_column_name):
130
+ """
131
+ Menghasilkan Word Cloud dari kolom teks dan mengembalikannya sebagai string base64.
132
+ """
133
+ wordcloud_base64 = None
134
+ try:
135
+ if text_column_name in df.columns:
136
+ # Gabungkan semua teks menjadi satu string besar
137
+ text = " ".join(review for review in df[text_column_name].astype(str))
138
+
139
+ # Tambahkan stopwords dasar bahasa Indonesia
140
+ stopwords_indonesia = set(STOPWORDS)
141
+ stopwords_indonesia.update([
142
+ 'yg', 'dg', 'rt', 'dgn', 'ny', 'd', 'k', 'ke', 'di', 'dari', 'dan', 'ini', 'itu',
143
+ 'atau', 'pada', 'untuk', 'juga', 'dengan', 'yang', 'ke', 'ya', 'ga', 'gak', 'tidak',
144
+ 'ada', 'adalah', 'saya', 'dia', 'kamu', 'kita', 'mereka', 'saja', 'seperti',
145
+ 'telah', 'akan', 'tapi', 'namun', 'karena', 'oleh', 'saat', 'sebagai', 'bahwa'
146
+ ])
147
+
148
+ # Bersihkan teks (opsional, tapi disarankan)
149
+ text = re.sub(r'http\S+', '', text) # Hapus URL
150
+ text = re.sub(r'[^a-zA-Z\s]', '', text.lower()) # Hapus non-alfabet
151
+
152
+ # Buat Word Cloud
153
+ wordcloud = WordCloud(
154
+ width=800,
155
+ height=400,
156
+ background_color='white',
157
+ stopwords=stopwords_indonesia,
158
+ min_font_size=10,
159
+ colormap='viridis' # Ganti palet warna
160
+ ).generate(text)
161
+
162
+ # Simpan ke buffer
163
+ buf_wc = io.BytesIO()
164
+ wordcloud.to_image().save(buf_wc, format='PNG')
165
+ buf_wc.seek(0)
166
+ wordcloud_base64 = base64.b64encode(buf_wc.getvalue()).decode('utf-8')
167
+ plt.close()
168
+
169
+ except Exception as e:
170
+ print(f"Error creating word cloud: {e}")
171
+ wordcloud_base64 = None
172
+
173
+ return wordcloud_base64
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ flask
2
+ torch
3
+ transformers
4
+ pandas
5
+ matplotlib
6
+ openpyxl
7
+ werkzeug
8
+ wordcloud
9
+ gunicorn
static/css/style.css ADDED
@@ -0,0 +1,481 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* --- 1. Import Font & Variabel Global Neo-Brutalism --- */
2
+ :root {
3
+ /* Palette Warna Neo-Brutalism */
4
+ --bg-color: #f0f0f0; /* Abu-abu sangat muda untuk background */
5
+ --card-bg: #ffffff; /* Putih untuk kartu */
6
+ --border-color: #000000; /* Hitam pekat untuk border */
7
+
8
+ --primary-color: #FFEB3B; /* Kuning Terang (Main Action) */
9
+ --secondary-color: #FF90E8; /* Pink Neon (Accent) */
10
+ --accent-color: #23A6D5; /* Cyan (Accent 2) */
11
+ --success-color: #00E676; /* Hijau Neon */
12
+ --danger-color: #FF5252; /* Merah Terang */
13
+
14
+ --text-main: #000000;
15
+ --text-muted: #444444;
16
+
17
+ /* Dimensi Brutal */
18
+ --border-width: 3px;
19
+ --shadow-offset: 5px;
20
+ --radius: 0px; /* Sudut tajam (bisa diubah ke 4px-8px jika ingin sedikit lengkung) */
21
+
22
+ --navbar-height: 80px;
23
+ --transition-speed: 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
24
+ }
25
+
26
+ /* --- 2. Reset & Setup Global --- */
27
+ * {
28
+ margin: 0;
29
+ padding: 0;
30
+ box-sizing: border-box;
31
+ }
32
+
33
+ body {
34
+ font-family: 'Inter', sans-serif;
35
+ background-color: var(--bg-color);
36
+ background-image: radial-gradient(#000 1px, transparent 1px); /* Pola titik-titik halus */
37
+ background-size: 20px 20px;
38
+ color: var(--text-main);
39
+ line-height: 1.6;
40
+ }
41
+
42
+ h1, h2, h3 {
43
+ font-family: 'Space Grotesk', sans-serif;
44
+ color: var(--text-main);
45
+ font-weight: 700;
46
+ text-transform: uppercase;
47
+ letter-spacing: -0.5px;
48
+ }
49
+
50
+ a {
51
+ text-decoration: none;
52
+ color: var(--text-main);
53
+ font-weight: 600;
54
+ }
55
+
56
+ /* --- 3. Navbar Neo-Brutal --- */
57
+ .navbar {
58
+ width: 100%;
59
+ height: var(--navbar-height);
60
+ background-color: var(--card-bg);
61
+ border-bottom: var(--border-width) solid var(--border-color);
62
+ padding: 0 30px;
63
+ position: fixed;
64
+ top: 0;
65
+ left: 0;
66
+ z-index: 1000;
67
+ display: flex;
68
+ align-items: center;
69
+ }
70
+
71
+ .navbar-container {
72
+ width: 100%;
73
+ height: 100%;
74
+ display: flex;
75
+ justify-content: space-between;
76
+ align-items: center;
77
+ max-width: 1400px;
78
+ margin: 0 auto;
79
+ }
80
+
81
+ .navbar-brand {
82
+ font-family: 'Space Grotesk', sans-serif;
83
+ font-size: 1.8rem;
84
+ font-weight: 800;
85
+ color: var(--text-main);
86
+ background-color: var(--primary-color);
87
+ padding: 5px 15px;
88
+ border: var(--border-width) solid var(--border-color);
89
+ box-shadow: 4px 4px 0px var(--border-color);
90
+ transition: transform 0.1s;
91
+ }
92
+
93
+ .navbar-brand:hover {
94
+ transform: translate(2px, 2px);
95
+ box-shadow: 2px 2px 0px var(--border-color);
96
+ }
97
+
98
+ .navbar-nav {
99
+ list-style-type: none;
100
+ display: flex;
101
+ gap: 20px;
102
+ }
103
+
104
+ .navbar-nav li a {
105
+ font-family: 'Space Grotesk', sans-serif;
106
+ padding: 8px 16px;
107
+ border: var(--border-width) solid transparent; /* Invisible border untuk layout */
108
+ transition: all var(--transition-speed);
109
+ font-size: 1.1rem;
110
+ }
111
+
112
+ .navbar-nav li a:hover {
113
+ background-color: var(--secondary-color);
114
+ border: var(--border-width) solid var(--border-color);
115
+ box-shadow: 4px 4px 0px var(--border-color);
116
+ transform: translate(-2px, -2px);
117
+ }
118
+
119
+ /* --- 4. Layout Konten --- */
120
+ .content-wrapper {
121
+ margin-top: var(--navbar-height);
122
+ padding: 40px 20px;
123
+ max-width: 1200px;
124
+ margin-left: auto;
125
+ margin-right: auto;
126
+ }
127
+
128
+ .page-header {
129
+ margin-bottom: 30px;
130
+ background-color: var(--accent-color);
131
+ border: var(--border-width) solid var(--border-color);
132
+ box-shadow: var(--shadow-offset) var(--shadow-offset) 0px var(--border-color);
133
+ padding: 20px;
134
+ display: inline-block;
135
+ }
136
+
137
+ .page-header h1 {
138
+ margin: 0;
139
+ font-size: 2.5rem;
140
+ color: #fff;
141
+ text-shadow: 2px 2px 0px var(--border-color);
142
+ }
143
+
144
+ /* --- 5. Komponen Card (Kotak) --- */
145
+ .card {
146
+ background: var(--card-bg);
147
+ border: var(--border-width) solid var(--border-color);
148
+ padding: 30px;
149
+ margin-bottom: 40px;
150
+ box-shadow: var(--shadow-offset) var(--shadow-offset) 0px var(--border-color);
151
+ transition: transform var(--transition-speed);
152
+ }
153
+
154
+ .card:hover {
155
+ /* Efek hover halus */
156
+ transform: translate(-2px, -2px);
157
+ box-shadow: 7px 7px 0px var(--border-color);
158
+ }
159
+
160
+ .card h2 {
161
+ font-size: 1.8rem;
162
+ margin-bottom: 1.5rem;
163
+ border-bottom: var(--border-width) solid var(--border-color);
164
+ padding-bottom: 10px;
165
+ display: inline-block;
166
+ background-color: var(--primary-color);
167
+ padding: 5px 15px;
168
+ }
169
+
170
+ /* --- 6. Form & Input --- */
171
+ .form-group {
172
+ margin-bottom: 1.5rem;
173
+ }
174
+
175
+ .form-group label {
176
+ display: block;
177
+ margin-bottom: 10px;
178
+ font-weight: 700;
179
+ font-family: 'Space Grotesk', sans-serif;
180
+ font-size: 1.1rem;
181
+ }
182
+
183
+ .form-control, .form-control-file {
184
+ width: 100%;
185
+ padding: 15px;
186
+ font-size: 1rem;
187
+ font-family: 'Inter', sans-serif;
188
+ border: var(--border-width) solid var(--border-color);
189
+ background-color: #fff;
190
+ border-radius: 0; /* Kotak tajam */
191
+ box-shadow: 4px 4px 0px rgba(0,0,0,0.1);
192
+ transition: all 0.2s;
193
+ }
194
+
195
+ .form-control:focus {
196
+ outline: none;
197
+ box-shadow: 6px 6px 0px var(--border-color);
198
+ background-color: #fffef0;
199
+ transform: translate(-2px, -2px);
200
+ }
201
+
202
+ textarea.form-control {
203
+ min-height: 150px;
204
+ resize: vertical;
205
+ }
206
+
207
+ /* Kustomisasi Input File Button */
208
+ .form-control-file::file-selector-button {
209
+ padding: 8px 15px;
210
+ margin-right: 15px;
211
+ border: var(--border-width) solid var(--border-color);
212
+ background-color: var(--secondary-color);
213
+ color: var(--text-main);
214
+ font-weight: 700;
215
+ cursor: pointer;
216
+ box-shadow: 2px 2px 0px var(--border-color);
217
+ transition: all 0.1s;
218
+ }
219
+
220
+ .form-control-file::file-selector-button:hover {
221
+ transform: translate(1px, 1px);
222
+ box-shadow: 1px 1px 0px var(--border-color);
223
+ }
224
+
225
+ /* --- 7. Tombol (Buttons) --- */
226
+ .btn {
227
+ padding: 12px 30px;
228
+ font-size: 1.1rem;
229
+ font-weight: 700;
230
+ font-family: 'Space Grotesk', sans-serif;
231
+ text-transform: uppercase;
232
+ border: var(--border-width) solid var(--border-color);
233
+ cursor: pointer;
234
+ display: inline-block;
235
+ text-align: center;
236
+ box-shadow: 5px 5px 0px var(--border-color);
237
+ transition: all 0.1s;
238
+ position: relative;
239
+ }
240
+
241
+ .btn:active {
242
+ transform: translate(4px, 4px);
243
+ box-shadow: 1px 1px 0px var(--border-color);
244
+ }
245
+
246
+ .btn-primary {
247
+ background-color: var(--primary-color); /* Kuning */
248
+ color: var(--text-main);
249
+ }
250
+
251
+ .btn-primary:hover {
252
+ background-color: #ffe600;
253
+ transform: translate(-2px, -2px);
254
+ box-shadow: 7px 7px 0px var(--border-color);
255
+ }
256
+
257
+ .btn-success {
258
+ background-color: var(--success-color); /* Hijau */
259
+ color: var(--text-main);
260
+ }
261
+
262
+ .btn-success:hover {
263
+ background-color: #00c853;
264
+ transform: translate(-2px, -2px);
265
+ box-shadow: 7px 7px 0px var(--border-color);
266
+ }
267
+
268
+ /* --- 8. Alerts / Flash Messages --- */
269
+ .alert {
270
+ padding: 20px;
271
+ margin-bottom: 30px;
272
+ border: var(--border-width) solid var(--border-color);
273
+ display: flex;
274
+ justify-content: space-between;
275
+ align-items: center;
276
+ font-weight: 600;
277
+ box-shadow: 5px 5px 0px var(--border-color);
278
+ }
279
+
280
+ .alert-danger {
281
+ background-color: var(--danger-color);
282
+ color: #fff;
283
+ }
284
+
285
+ .alert-success {
286
+ background-color: var(--success-color);
287
+ color: #000;
288
+ }
289
+
290
+ .alert-close {
291
+ background: none;
292
+ border: 2px solid currentColor;
293
+ width: 30px;
294
+ height: 30px;
295
+ font-size: 1.2rem;
296
+ cursor: pointer;
297
+ display: flex;
298
+ align-items: center;
299
+ justify-content: center;
300
+ border-radius: 50%;
301
+ }
302
+
303
+ .alert-close:hover {
304
+ background-color: rgba(0,0,0,0.1);
305
+ }
306
+
307
+ /* --- 9. Visualisasi & Hasil --- */
308
+ .result-block {
309
+ padding: 20px;
310
+ background-color: #fff;
311
+ border: var(--border-width) solid var(--border-color);
312
+ margin-top: 2rem;
313
+ box-shadow: 4px 4px 0px var(--border-color);
314
+ background-image: linear-gradient(45deg, #f0f0f0 25%, transparent 25%, transparent 75%, #f0f0f0 75%, #f0f0f0), linear-gradient(45deg, #f0f0f0 25%, transparent 25%, transparent 75%, #f0f0f0 75%, #f0f0f0);
315
+ background-size: 20px 20px;
316
+ background-position: 0 0, 10px 10px;
317
+ }
318
+
319
+ .result-block h3 {
320
+ background-color: #fff;
321
+ border: var(--border-width) solid var(--border-color);
322
+ padding: 10px;
323
+ display: inline-block;
324
+ margin: 0;
325
+ font-size: 2rem;
326
+ }
327
+
328
+ .charts-grid {
329
+ display: flex;
330
+ flex-wrap: wrap;
331
+ gap: 30px;
332
+ margin-bottom: 30px;
333
+ }
334
+
335
+ .chart-box {
336
+ flex: 1;
337
+ min-width: 300px;
338
+ padding: 20px;
339
+ border: var(--border-width) solid var(--border-color);
340
+ box-shadow: 5px 5px 0px var(--border-color);
341
+ text-align: center;
342
+ background: #fff;
343
+ }
344
+
345
+ .chart-box h3 {
346
+ background-color: var(--accent-color);
347
+ color: #fff;
348
+ display: inline-block;
349
+ padding: 5px 10px;
350
+ border: 2px solid var(--border-color);
351
+ margin-bottom: 20px;
352
+ font-size: 1.2rem;
353
+ }
354
+
355
+ .table-responsive {
356
+ max-height: 500px;
357
+ overflow-y: auto;
358
+ border: var(--border-width) solid var(--border-color);
359
+ box-shadow: 5px 5px 0px var(--border-color);
360
+ }
361
+
362
+ /* Table Styling */
363
+ .table {
364
+ width: 100%;
365
+ border-collapse: collapse;
366
+ }
367
+ .table th, .table td {
368
+ border: 2px solid var(--border-color);
369
+ padding: 12px;
370
+ text-align: left;
371
+ }
372
+ .table th {
373
+ background-color: var(--primary-color);
374
+ font-family: 'Space Grotesk', sans-serif;
375
+ text-transform: uppercase;
376
+ }
377
+ .table-striped tbody tr:nth-of-type(odd) {
378
+ background-color: #f9f9f9;
379
+ }
380
+
381
+ /* --- 10. Spinner --- */
382
+ .spinner-overlay {
383
+ display: none;
384
+ position: fixed;
385
+ top: 0;
386
+ left: 0;
387
+ width: 100%;
388
+ height: 100%;
389
+ background-color: rgba(255, 255, 255, 0.9);
390
+ z-index: 9999;
391
+ align-items: center;
392
+ justify-content: center;
393
+ }
394
+
395
+ .spinner-container {
396
+ text-align: center;
397
+ background: var(--primary-color);
398
+ padding: 40px;
399
+ border: var(--border-width) solid var(--border-color);
400
+ box-shadow: 10px 10px 0px var(--border-color);
401
+ }
402
+
403
+ .spinner-container p {
404
+ margin-top: 20px;
405
+ font-weight: 800;
406
+ font-family: 'Space Grotesk', sans-serif;
407
+ }
408
+
409
+ .spinner {
410
+ width: 60px;
411
+ height: 60px;
412
+ border: 8px solid #fff;
413
+ border-left-color: var(--border-color);
414
+ border-bottom-color: var(--border-color);
415
+ border-radius: 50%;
416
+ animation: spin 0.8s linear infinite;
417
+ margin: 0 auto;
418
+ }
419
+
420
+ @keyframes spin {
421
+ to { transform: rotate(360deg); }
422
+ }
423
+
424
+ .hidden { display: none !important; }
425
+
426
+ /* --- 11. Responsive Mobile --- */
427
+ .hamburger-btn {
428
+ display: none;
429
+ background: var(--secondary-color);
430
+ border: var(--border-width) solid var(--border-color);
431
+ color: var(--text-main);
432
+ font-size: 1.5rem;
433
+ padding: 5px 10px;
434
+ box-shadow: 3px 3px 0px var(--border-color);
435
+ cursor: pointer;
436
+ }
437
+
438
+ .hamburger-btn:active {
439
+ transform: translate(2px, 2px);
440
+ box-shadow: 1px 1px 0px var(--border-color);
441
+ }
442
+
443
+ @media (max-width: 768px) {
444
+ .hamburger-btn { display: block; }
445
+
446
+ .navbar-container { padding: 0 20px; }
447
+
448
+ .navbar-nav-container {
449
+ position: absolute;
450
+ top: var(--navbar-height);
451
+ left: 0;
452
+ width: 100%;
453
+ background-color: var(--card-bg);
454
+ border-bottom: var(--border-width) solid var(--border-color);
455
+ max-height: 0;
456
+ overflow: hidden;
457
+ transition: max-height 0.3s ease-out;
458
+ }
459
+
460
+ .navbar-nav-container.active {
461
+ max-height: 300px;
462
+ border-top: var(--border-width) solid var(--border-color);
463
+ }
464
+
465
+ .navbar-nav {
466
+ flex-direction: column;
467
+ padding: 20px;
468
+ gap: 10px;
469
+ }
470
+
471
+ .navbar-nav li a {
472
+ display: block;
473
+ border: 2px solid var(--border-color);
474
+ text-align: center;
475
+ background-color: #f9f9f9;
476
+ }
477
+
478
+ .page-header h1 { font-size: 1.8rem; }
479
+
480
+ .charts-grid { flex-direction: column; }
481
+ }
static/images/ikn.png ADDED

Git LFS Details

  • SHA256: b77e3c56cb57f9aeffd51ef3e586a4225782abd1f0bdbcacec6c73a9c89e81e2
  • Pointer size: 131 Bytes
  • Size of remote file: 195 kB
static/images/whoosh.png ADDED

Git LFS Details

  • SHA256: 8aac2d5c1a0d0024cdc7550e0d41f6cbf015755908b7f24cd62ed0b5b23d6ec1
  • Pointer size: 131 Bytes
  • Size of remote file: 151 kB
templates/about.html ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'layout.html' %}
2
+
3
+ {% block title %}Tentang Sentimenter{% endblock %}
4
+
5
+ <!-- TAMBAHKAN BLOCK INI untuk mengisi H1 yang sudah diperbaiki -->
6
+ {% block page_title %}Tentang Sentimenter{% endblock %}
7
+
8
+ {% block content %}
9
+ <div class="card">
10
+ <p style="font-size: 1.1em; margin-bottom: 25px;">
11
+ Aplikasi ini adalah prototipe untuk mendemonstrasikan kekuatan model pemrosesan bahasa alami (NLP)
12
+ dalam menganalisis sentimen teks berbahasa Indonesia.
13
+ </p>
14
+
15
+ <h2>Teknologi yang Digunakan</h2>
16
+ <ul style="list-style-type: none; padding-left: 0;">
17
+ <li style="margin-bottom: 10px; font-size: 1.1em;"><i class="fab fa-python" style="color: #3498db; margin-right: 10px; width: 20px;"></i> <strong>Backend:</strong> Flask (Python)</li>
18
+ <li style="margin-bottom: 10px; font-size: 1.1em;"><i class="fab fa-html5" style="color: #e74c3c; margin-right: 10px; width: 20px;"></i> <strong>Frontend:</strong> HTML & CSS Kustom</li>
19
+ <li style="margin-bottom: 10px; font-size: 1.1em;"><i class="fas fa-brain" style="color: #9b59b6; margin-right: 10px; width: 20px;"></i> <strong>Model ML:</strong> IndoBERT (Transformers)</li>
20
+ <li style="margin-bottom: 10px; font-size: 1.1em;"><i class="fas fa-database" style="color: #f39c12; margin-right: 10px; width: 20px;"></i> <strong>Analisis Data:</strong> Pandas</li>
21
+ <li style="margin-bottom: 10px; font-size: 1.1em;"><i class="fas fa-chart-pie" style="color: #2ecc71; margin-right: 10px; width: 20px;"></i> <strong>Visualisasi:</strong> Matplotlib, WordCloud</li>
22
+ </ul>
23
+ </div>
24
+
25
+ <div class="card">
26
+ <h2>Profil Developer (BRIN devs)</h2>
27
+ <p>
28
+ Website ini dikembangkan oleh BRIN devs, sekumpulan mahasiswa Sains Data yang kepo tentang data science, machine learning, dan AI.
29
+ Kami bersemangat untuk mengeksplorasi bagaimana teknologi ini dapat digunakan untuk menciptakan solusi inovatif.
30
+ Ikuti terus perjalanan kami di media sosial dan GitHub untuk update terbaru tentang proyek-proyek kami!
31
+ </p>
32
+ </div>
33
+ {% endblock %}
templates/analysis.html ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'layout.html' %}
2
+
3
+ {% block title %}Analisis Sentimen{% endblock %}
4
+
5
+ {% block page_title %}Analisis Sentimen{% endblock %}
6
+
7
+ {% block content %}
8
+ <p style="font-size: 1.1em; margin-top: -1rem; margin-bottom: 2rem; font-weight: 500;">
9
+ Gunakan bagian ini untuk menganalisis sentimen dari teks tunggal atau mengunggah file untuk analisis massal.
10
+ </p>
11
+
12
+ <!-- Menampilkan Flash Messages -->
13
+ {% with messages = get_flashed_messages(with_categories=true) %}
14
+ {% if messages %}
15
+ {% for category, message in messages %}
16
+ <div class="alert alert-{{ category }}" role="alert">
17
+ <div>
18
+ <i class="alert-icon fas {% if category == 'danger' %}fa-exclamation-triangle{% else %}fa-check-circle{% endif %}"></i>
19
+ {{ message }}
20
+ </div>
21
+ <button type="button" class="alert-close">&times;</button>
22
+ </div>
23
+ {% endfor %}
24
+ {% endif %}
25
+ {% endwith %}
26
+
27
+ {# Embed JSON data #}
28
+ <script id="sentiment-data" type="application/json">{% if sentiment_counts %}{{ sentiment_counts | tojson | safe }}{% else %}null{% endif %}</script>
29
+
30
+ <!-- Bagian 1: Analisis Teks Tunggal -->
31
+ <div class="card">
32
+ <h2>Analisis Teks Tunggal</h2>
33
+ <p>Masukkan sebuah kalimat atau paragraf dalam bahasa Indonesia untuk dianalisis.</p>
34
+
35
+ <form method="POST" action="{{ url_for('analysis') }}">
36
+ <div class="form-group">
37
+ <label for="text_input_area">Teks Anda:</label>
38
+ <textarea name="text_input" id="text_input_area" rows="6" class="form-control"
39
+ placeholder="Ketik atau tempel teks Anda di sini...">{{ text }}</textarea>
40
+ </div>
41
+ <button type="submit" class="btn btn-primary">
42
+ <i class="fas fa-bolt"></i> Analisis Teks
43
+ </button>
44
+ </form>
45
+
46
+ <!-- Hasil Prediksi -->
47
+ {% if prediction %}
48
+ <div class="result-block">
49
+ <h2 style="font-size: 1.2rem; margin-bottom: 1rem; border: none; background: none; padding: 0;">HASIL ANALISIS:</h2>
50
+ <h3>{{ prediction }}</h3>
51
+ </div>
52
+ {% endif %}
53
+ </div>
54
+
55
+ <!-- Bagian 2: Analisis File Massal -->
56
+ <div class="card">
57
+ <h2>Analisis File (CSV/XLSX)</h2>
58
+ <p>Upload file untuk analisis sentimen massal.</p>
59
+
60
+ <form method="POST" enctype="multipart/form-data" action="{{ url_for('analysis') }}" id="file-analysis-form">
61
+ <div class="form-group">
62
+ <label for="file">Pilih File:</label>
63
+ <input type="file" name="file" id="file" class="form-control-file" required>
64
+ </div>
65
+ <div class="form-group">
66
+ <label for="text_column">Nama Kolom Teks:</label>
67
+ <input type="text" name="text_column" id="text_column" class="form-control" placeholder="Contoh: 'tweet_text'" required>
68
+ </div>
69
+ <button type="submit" class="btn btn-success">
70
+ <i class="fas fa-file-upload"></i> Upload & Proses
71
+ </button>
72
+ </form>
73
+
74
+ {% if sentiment_counts %}
75
+ <div class="results-container" style="margin-top: 3rem;">
76
+ <h2 style="font-size: 2rem; margin-bottom: 20px; text-decoration: underline;">VISUALISASI HASIL</h2>
77
+ <div class="charts-grid">
78
+
79
+ <!-- Bar Chart -->
80
+ <div class="chart-box">
81
+ <h3>Distribusi Sentimen</h3>
82
+ <canvas id="barChartCanvas"></canvas>
83
+ </div>
84
+
85
+ <!-- Pie Chart -->
86
+ <div class="chart-box">
87
+ <h3>Proporsi Sentimen</h3>
88
+ <div style="position: relative; max-height: 400px; margin: auto;">
89
+ <canvas id="pieChartCanvas"></canvas>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ {% if wordcloud %}
95
+ <div class="chart-box" style="width: 100%; margin-bottom: 30px;">
96
+ <h3>Word Cloud</h3>
97
+ <img src="data:image/png;base64,{{ wordcloud }}" alt="Word Cloud" style="max-width: 100%; border: 2px solid #000;">
98
+ </div>
99
+ {% endif %}
100
+
101
+ <h2 style="font-size: 1.5rem; margin-top: 2rem;">Data Tabel</h2>
102
+ <div class="table-responsive">
103
+ {{ results_table|safe }}
104
+ </div>
105
+ </div>
106
+ {% endif %}
107
+ </div>
108
+
109
+ {% endblock %}
110
+
111
+ <!-- Script Khusus untuk Chart Neo-Brutalism -->
112
+ {% block scripts %}
113
+ <script>
114
+ document.addEventListener('DOMContentLoaded', function() {
115
+ const fileForm = document.getElementById('file-analysis-form');
116
+ const spinnerOverlay = document.getElementById('spinner-overlay');
117
+
118
+ if (fileForm) {
119
+ fileForm.addEventListener('submit', function() {
120
+ const fileInput = document.getElementById('file');
121
+ const columnInput = document.getElementById('text_column');
122
+ if (fileInput.value && columnInput.value && spinnerOverlay) {
123
+ spinnerOverlay.style.display = 'flex'; // Gunakan flex agar center
124
+ spinnerOverlay.classList.remove('hidden');
125
+ }
126
+ });
127
+ }
128
+
129
+ const countsEl = document.getElementById('sentiment-data');
130
+ let counts = null;
131
+
132
+ if (countsEl) {
133
+ try {
134
+ counts = JSON.parse(countsEl.textContent || 'null');
135
+ } catch (e) {
136
+ console.error("Gagal mem-parsing data sentimen:", e);
137
+ counts = null;
138
+ }
139
+ }
140
+
141
+ if (counts && Object.keys(counts).length > 0) {
142
+ // Setup Data
143
+ const labels = ['Positif', 'Netral', 'Negatif'];
144
+ const data = [
145
+ counts['Positif'] || 0,
146
+ counts['Netral'] || 0,
147
+ counts['Negatif'] || 0
148
+ ];
149
+
150
+ // Warna Neo-Brutalism
151
+ const bgColors = [
152
+ '#00E676', // Hijau
153
+ '#d1d1d1', // Abu
154
+ '#FF5252' // Merah
155
+ ];
156
+
157
+ const borderColors = '#000000'; // Border Hitam Pekat
158
+ const borderWidth = 3; // Border Tebal
159
+
160
+ // Config Font
161
+ Chart.defaults.font.family = "'Space Grotesk', sans-serif";
162
+ Chart.defaults.color = '#000';
163
+
164
+ // 4. Buat Bar Chart
165
+ const barCtx = document.getElementById('barChartCanvas');
166
+ if (barCtx) {
167
+ new Chart(barCtx, {
168
+ type: 'bar',
169
+ data: {
170
+ labels: labels,
171
+ datasets: [{
172
+ label: 'Jumlah',
173
+ data: data,
174
+ backgroundColor: bgColors,
175
+ borderColor: borderColors,
176
+ borderWidth: borderWidth,
177
+ hoverOffset: 4
178
+ }]
179
+ },
180
+ options: {
181
+ responsive: true,
182
+ plugins: {
183
+ legend: { display: false }
184
+ },
185
+ scales: {
186
+ y: {
187
+ beginAtZero: true,
188
+ grid: { color: '#000', lineWidth: 1 },
189
+ ticks: { color: '#000', font: { weight: 'bold' } }
190
+ },
191
+ x: {
192
+ grid: { display: false },
193
+ ticks: { color: '#000', font: { weight: 'bold' } }
194
+ }
195
+ }
196
+ }
197
+ });
198
+ }
199
+
200
+ // 5. Buat Pie Chart
201
+ const pieCtx = document.getElementById('pieChartCanvas');
202
+ if (pieCtx) {
203
+ new Chart(pieCtx, {
204
+ type: 'pie',
205
+ data: {
206
+ labels: labels,
207
+ datasets: [{
208
+ data: data,
209
+ backgroundColor: bgColors,
210
+ borderColor: borderColors,
211
+ borderWidth: borderWidth
212
+ }]
213
+ },
214
+ options: {
215
+ responsive: true,
216
+ plugins: {
217
+ legend: {
218
+ position: 'bottom',
219
+ labels: { color: '#000', font: { weight: 'bold' } }
220
+ }
221
+ }
222
+ }
223
+ });
224
+ }
225
+ }
226
+ });
227
+ </script>
228
+ {% endblock %}
templates/dashboard.html ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'layout.html' %}
2
+
3
+ {% block title %}Dashboard Sentimen{% endblock %}
4
+
5
+ {% block page_title %}DASHBOARD SENTIMEN{% endblock %}
6
+
7
+ {% block content %}
8
+ <p style="font-size: 1.1em; margin-top: -1rem; margin-bottom: 2rem; font-weight: 500;">
9
+ Analisis sentimen terkini mengenai topik-topik hangat di Indonesia.
10
+ </p>
11
+
12
+ <div id="dashboard-container">
13
+ <!-- Loop data dari Flask -->
14
+ {% for key, data in dashboard_data.items() %}
15
+ <div class="card" style="border-width: 4px; box-shadow: 8px 8px 0px #000;">
16
+ <div style="background-color: var(--primary-color); border-bottom: 4px solid #000; padding: 15px; margin: -30px -30px 20px -30px;">
17
+ <h2 style="margin: 0; border: none; font-size: 1.5rem; padding: 0; background: none;">{{ data.title }}</h2>
18
+ </div>
19
+
20
+ <div class="charts-grid" style="margin-top: 30px;">
21
+ <!-- Bar Chart Container -->
22
+ <div class="chart-box">
23
+ <h3>Distribusi</h3>
24
+ <canvas id="bar-{{ key }}"></canvas>
25
+ </div>
26
+
27
+ <!-- Pie Chart Container -->
28
+ <div class="chart-box">
29
+ <h3>Proporsi</h3>
30
+ <div style="position: relative; max-height: 300px; margin: auto;">
31
+ <canvas id="pie-{{ key }}"></canvas>
32
+ </div>
33
+ </div>
34
+ </div>
35
+
36
+ <!-- Word Cloud Image Section -->
37
+ <div class="chart-box" style="width: 100%; margin-top: 30px; border: 3px solid #000;">
38
+ <h3>Word Cloud</h3>
39
+ <!-- Menggunakan url_for static -->
40
+ <img src="{{ url_for('static', filename='images/' + data.image) }}"
41
+ alt="Word Cloud {{ data.title }}"
42
+ style="max-width: 100%; height: auto; display: block; margin: 0 auto;"
43
+ onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
44
+
45
+ <!-- Fallback jika gambar tidak ditemukan -->
46
+ <div style="display: none; padding: 40px; color: #666;">
47
+ <i class="fas fa-image" style="font-size: 3rem; margin-bottom: 10px;"></i><br>
48
+ Simpan file <strong>{{ data.image }}</strong> di folder <code>static/images/</code>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ {% endfor %}
53
+ </div>
54
+
55
+ {% endblock %}
56
+
57
+ {% block scripts %}
58
+ <script>
59
+ document.addEventListener('DOMContentLoaded', function() {
60
+ // 1. Ambil data lengkap dari Flask
61
+ const dashboardData = {{ dashboard_data | tojson }};
62
+
63
+ // Konfigurasi Font & Warna Neo-Brutalism
64
+ Chart.defaults.font.family = "'Space Grotesk', sans-serif";
65
+ Chart.defaults.color = '#000';
66
+
67
+ const bgColors = ['#00E676', '#d1d1d1', '#FF5252']; // Hijau, Abu, Merah
68
+ const borderColors = '#000000';
69
+ const borderWidth = 3;
70
+
71
+ // 2. Loop setiap topik (IKN, Whoosh)
72
+ for (const [key, item] of Object.entries(dashboardData)) {
73
+ const counts = item.counts;
74
+ const labels = ['Positif', 'Netral', 'Negatif'];
75
+ const dataValues = [
76
+ counts['Positif'] || 0,
77
+ counts['Netral'] || 0,
78
+ counts['Negatif'] || 0
79
+ ];
80
+
81
+ // Render Bar Chart
82
+ const barCtx = document.getElementById(`bar-${key}`);
83
+ if (barCtx) {
84
+ new Chart(barCtx, {
85
+ type: 'bar',
86
+ data: {
87
+ labels: labels,
88
+ datasets: [{
89
+ label: 'Jumlah',
90
+ data: dataValues,
91
+ backgroundColor: bgColors,
92
+ borderColor: borderColors,
93
+ borderWidth: borderWidth
94
+ }]
95
+ },
96
+ options: {
97
+ responsive: true,
98
+ plugins: { legend: { display: false } },
99
+ scales: {
100
+ y: {
101
+ beginAtZero: true,
102
+ grid: { color: '#000', lineWidth: 1 },
103
+ ticks: { color: '#000', font: { weight: 'bold' } }
104
+ },
105
+ x: {
106
+ grid: { display: false },
107
+ ticks: { color: '#000', font: { weight: 'bold' } }
108
+ }
109
+ }
110
+ }
111
+ });
112
+ }
113
+
114
+ // Render Pie Chart
115
+ const pieCtx = document.getElementById(`pie-${key}`);
116
+ if (pieCtx) {
117
+ new Chart(pieCtx, {
118
+ type: 'pie',
119
+ data: {
120
+ labels: labels,
121
+ datasets: [{
122
+ data: dataValues,
123
+ backgroundColor: bgColors,
124
+ borderColor: borderColors,
125
+ borderWidth: borderWidth
126
+ }]
127
+ },
128
+ options: {
129
+ responsive: true,
130
+ plugins: {
131
+ legend: {
132
+ position: 'bottom',
133
+ labels: { color: '#000', font: { weight: 'bold' } }
134
+ }
135
+ }
136
+ }
137
+ });
138
+ }
139
+ }
140
+ });
141
+ </script>
142
+ {% endblock %}
templates/home.html ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'layout.html' %}
2
+
3
+ {% block title %}Home{% endblock %}
4
+
5
+ {% block page_title %}{% endblock %} <!-- Kosongkan default title, kita gunakan Hero section -->
6
+
7
+ {% block content %}
8
+ <!-- Hero Section -->
9
+ <div style="text-align: center; padding: 60px 20px; margin-bottom: 40px; background: linear-gradient(180deg, rgba(52,152,219,0.05) 0%, rgba(255,255,255,0) 100%); border-radius: 20px;">
10
+ <h1 style="font-size: 3.5rem; margin-bottom: 20px; background: -webkit-linear-gradient(#2980b9, #3498db); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
11
+ Sentimenter AI
12
+ </h1>
13
+ <p style="font-size: 1.3rem; max-width: 700px; margin: 0 auto 40px auto; color: #7f8c8d;">
14
+ Pahami emosi di balik kata-kata dengan kekuatan Artificial Intelligence.
15
+ Analisis ulasan pelanggan, komentar media sosial, dan dokumen teks secara instan.
16
+ </p>
17
+ <a href="{{ url_for('analysis') }}" class="btn btn-primary" style="padding: 16px 40px; font-size: 1.1rem; border-radius: 50px;">
18
+ Mulai Analisis <i class="fas fa-arrow-right" style="margin-left: 10px;"></i>
19
+ </a>
20
+ </div>
21
+
22
+ <!-- Features Grid -->
23
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px;">
24
+
25
+ <div class="card" style="text-align: center;">
26
+ <div style="width: 60px; height: 60px; background: rgba(52, 152, 219, 0.1); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px auto;">
27
+ <i class="fas fa-bolt" style="font-size: 1.5rem; color: #3498db;"></i>
28
+ </div>
29
+ <h3>Cepat & Akurat</h3>
30
+ <p>Didukung oleh model IndoBERT yang telah dilatih khusus untuk memahami konteks bahasa Indonesia, termasuk bahasa gaul.</p>
31
+ </div>
32
+
33
+ <div class="card" style="text-align: center;">
34
+ <div style="width: 60px; height: 60px; background: rgba(46, 204, 113, 0.1); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px auto;">
35
+ <i class="fas fa-layer-group" style="font-size: 1.5rem; color: #2ecc71;"></i>
36
+ </div>
37
+ <h3>Analisis Massal</h3>
38
+ <p>Hemat waktu dengan mengunggah file CSV atau Excel. Proses ribuan baris data hanya dalam sekali klik.</p>
39
+ </div>
40
+
41
+ <div class="card" style="text-align: center;">
42
+ <div style="width: 60px; height: 60px; background: rgba(231, 76, 60, 0.1); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px auto;">
43
+ <i class="fas fa-chart-bar" style="font-size: 1.5rem; color: #e74c3c;"></i>
44
+ </div>
45
+ <h3>Visualisasi Data</h3>
46
+ <p>Dapatkan wawasan mendalam melalui grafik interaktif dan Word Cloud yang estetis dan mudah dipahami.</p>
47
+ </div>
48
+
49
+ </div>
50
+ {% endblock %}
templates/layout.html ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}Sentimenter{% endblock %} - Analisis Sentimen</title>
7
+
8
+ <!-- 1. Google Font: Space Grotesk (Judul) & Inter (Body) -->
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
12
+
13
+ <!-- 2. Font Awesome (untuk Ikon) -->
14
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
15
+ xintegrity="sha512-Fo3rlrZj/k7ujTnHg4CGR2D7kSs0v4LLanw2qksYuRlEzO+tcaEPQogQ0KaoGN26/zrn20ImR1DfuLWnOo7aBA=="
16
+ crossorigin="anonymous" referrerpolicy="no-referrer" />
17
+
18
+ <!-- 3. Link ke File CSS Eksternal -->
19
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
20
+
21
+ </head>
22
+ <body>
23
+
24
+ <!-- 5. Loading Spinner (Neo-Brutalism Style) -->
25
+ <div class_alias="spinner-overlay" id="spinner-overlay" class="hidden">
26
+ <div class="spinner-container">
27
+ <div class="spinner"></div>
28
+ <p>MEMPROSES DATA...</p>
29
+ </div>
30
+ </div>
31
+
32
+ <!-- 4. Navbar Atas -->
33
+ <nav class="navbar">
34
+ <div class="navbar-container">
35
+ <a href="{{ url_for('home') }}" class="navbar-brand">
36
+ <i class="fas fa-robot"></i> SENTIMENTER
37
+ </a>
38
+
39
+ <button class="hamburger-btn" id="hamburger-btn">
40
+ <i class="fas fa-bars"></i>
41
+ </button>
42
+
43
+ <div class="navbar-nav-container" id="navbar-nav-container">
44
+ <ul class="navbar-nav">
45
+ <li><a href="{{ url_for('home') }}">Home</a></li>
46
+ <li><a href="{{ url_for('dashboard') }}">Dashboard</a></li>
47
+ <li><a href="{{ url_for('analysis') }}">Analysis</a></li>
48
+ <li><a href="{{ url_for('about') }}">About</a></li>
49
+ </ul>
50
+ </div>
51
+ </div>
52
+ </nav>
53
+
54
+ <!-- 6. Konten Utama -->
55
+ <div class="content-wrapper">
56
+ <!-- Judul Halaman -->
57
+ <div class="page-header">
58
+ <h1>{% block page_title %}{% endblock %}</h1>
59
+ </div>
60
+
61
+ <!-- 7. Blok Konten Utama -->
62
+ {% block content %}{% endblock %}
63
+ </div>
64
+
65
+ <!-- 8. JavaScript untuk Toggle Navbar & Alert Close -->
66
+ <script>
67
+ // Toggle Navbar Mobile
68
+ const hamburgerBtn = document.getElementById('hamburger-btn');
69
+ const navContainer = document.getElementById('navbar-nav-container');
70
+
71
+ hamburgerBtn.addEventListener('click', () => {
72
+ navContainer.classList.toggle('active');
73
+ });
74
+
75
+ // Tutup Alert
76
+ document.body.addEventListener('click', function(event) {
77
+ if (event.target.classList.contains('alert-close')) {
78
+ event.target.closest('.alert').style.display = 'none';
79
+ }
80
+ });
81
+ </script>
82
+
83
+ <!-- 9. Tambahkan CDN Chart.js -->
84
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
85
+
86
+ <!-- 10. Blok Skrip Khusus Halaman -->
87
+ {% block scripts %}{% endblock %}
88
+
89
+ </body>
90
+ </html>