MahfudAwal commited on
Commit
d3bd4e2
·
1 Parent(s): c0fe0dc
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gunakan base image Python yang ringan
2
+ FROM python:3.11-slim
3
+
4
+ # Tetapkan direktori kerja di dalam container
5
+ WORKDIR /app
6
+
7
+ # Perbarui pip dan instal pustaka sistem yang mungkin diperlukan
8
+ RUN pip install --no-cache-dir --upgrade pip
9
+
10
+ # Salin file requirements terlebih dahulu untuk caching yang lebih baik
11
+ COPY requirements.txt requirements.txt
12
+
13
+ # Instal dependensi Python
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Download data NLTK yang dibutuhkan oleh helper.py
17
+ RUN python -m nltk.downloader stopwords
18
+
19
+ # Salin semua file proyek ke dalam direktori kerja di container
20
+ COPY . .
21
+
22
+ # Beri tahu Docker bahwa aplikasi akan berjalan di port 7860 (port default HF Spaces)
23
+ EXPOSE 7860
24
+
25
+ # Perintah untuk menjalankan aplikasi Flask saat container dimulai
26
+ # Menggunakan host 0.0.0.0 agar dapat diakses dari luar container
27
+ CMD ["flask", "run", "--host=0.0.0.0", "--port=7860"]
Model/a.txt ADDED
File without changes
README.md CHANGED
@@ -1,12 +1,12 @@
1
  ---
2
- title: Keluh Cerdas
3
- emoji: 👀
4
  colorFrom: blue
5
- colorTo: purple
6
  sdk: docker
7
  pinned: false
8
  license: apache-2.0
9
- short_description: app
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Keluhcerdas
3
+ emoji: 🌍
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
  pinned: false
8
  license: apache-2.0
9
+ short_description: keluhcerdas
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, redirect, url_for, flash
2
+ from datetime import date, datetime
3
+ import os, json
4
+ import numpy as np
5
+ import pandas as pd
6
+ from wordcloud import WordCloud
7
+ from helper import predict_emotion,keyword, load_tflite_model
8
+
9
+ app = Flask(__name__)
10
+
11
+ @app.route('/')
12
+ @app.route('/dashboard')
13
+ def dashboard():
14
+ base_path = os.path.join('data')
15
+ dash_df = pd.read_excel(os.path.join(base_path, 'dataset_dash.xlsx'))
16
+ emosi_df = pd.read_excel(os.path.join(base_path, 'final_dataset.xlsx'))
17
+
18
+ # Info utama
19
+ total_keluhan = dash_df.shape[0]
20
+ topik_terbanyak = dash_df['Topik'].value_counts().idxmax()
21
+ instansi_terbanyak = dash_df['Instansi'].value_counts().idxmax()
22
+
23
+ # Proses tanggal
24
+ dash_df['tanggal_keluhan'] = pd.to_datetime(dash_df['tanggal_keluhan']).dt.normalize()
25
+ today = pd.to_datetime(datetime.today().date())
26
+ keluhan_hari_ini = dash_df[dash_df['tanggal_keluhan'] == today].shape[0]
27
+
28
+ # Data keluhan harian (last 7 days)
29
+ start_date = today - pd.Timedelta(days=6)
30
+ last_7_days_df = dash_df[(dash_df['tanggal_keluhan'] >= start_date) & (dash_df['tanggal_keluhan'] <= today)].copy()
31
+
32
+ last_7_days_df['nama_hari'] = last_7_days_df['tanggal_keluhan'].dt.day_name()
33
+ hari_en_to_id = {
34
+ 'Monday': 'Senin',
35
+ 'Tuesday': 'Selasa',
36
+ 'Wednesday': 'Rabu',
37
+ 'Thursday': 'Kamis',
38
+ 'Friday': 'Jumat',
39
+ 'Saturday': 'Sabtu',
40
+ 'Sunday': 'Minggu'
41
+ }
42
+ last_7_days_df['nama_hari'] = last_7_days_df['nama_hari'].map(hari_en_to_id)
43
+ hari_urut = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu']
44
+ keluhan_per_hari = last_7_days_df.groupby('nama_hari').size().reindex(hari_urut, fill_value=0)
45
+ keluhan_harian_labels = keluhan_per_hari.index.tolist()
46
+ keluhan_harian_values = keluhan_per_hari.values.tolist()
47
+
48
+ # Data emosi
49
+ emosi_dist = emosi_df['emosi'].value_counts()
50
+ emosi_values = emosi_dist.values.tolist()
51
+
52
+ # Data keluhan bulanan dengan sorting berdasarkan tanggal
53
+ # Buat kolom untuk menampung data bulanan dan kelompokkan berdasarkan bulan dan tahun
54
+ dash_df['year'] = dash_df['tanggal_keluhan'].dt.year
55
+ dash_df['month'] = dash_df['tanggal_keluhan'].dt.month
56
+
57
+ # Kelompokkan berdasarkan bulan dan tahun
58
+ keluhan_bulanan = dash_df.groupby(['year', 'month']).size().reset_index()
59
+ keluhan_bulanan.columns = ['year', 'month', 'count']
60
+
61
+ # Urutkan berdasarkan tahun dan bulan
62
+ keluhan_bulanan = keluhan_bulanan.sort_values(['year', 'month'])
63
+
64
+ # Format label bulan-tahun untuk tampilan
65
+ import calendar
66
+ keluhan_bulanan['bulan_nama'] = keluhan_bulanan.apply(
67
+ lambda row: f"{calendar.month_abbr[row['month']]} {row['year']}",
68
+ axis=1
69
+ )
70
+ keluhan_bulanan_labels = keluhan_bulanan['bulan_nama'].tolist()
71
+ keluhan_bulanan_values = keluhan_bulanan['count'].tolist()
72
+
73
+ # ----- Top 5 Topik & Instansi ---------------------------------------
74
+ top_topik = dash_df['Topik'].value_counts().head(5).reset_index()
75
+ top_topik.columns = ['Topik', 'Jumlah']
76
+
77
+ top_instansi = dash_df['Instansi'].value_counts().head(5).reset_index()
78
+ top_instansi.columns = ['Instansi', 'Jumlah']
79
+
80
+ # ----- Word-cloud ----------------------------------------------------
81
+ text_wc = ' '.join(dash_df['keluhan'].dropna().astype(str))
82
+ wc_img = WordCloud(width=800, height=400, background_color="white").generate(text_wc)
83
+
84
+ wc_path = os.path.join('static', 'wordcloud.png')
85
+ os.makedirs(os.path.dirname(wc_path), exist_ok=True)
86
+ wc_img.to_file(wc_path) # simpan ⇒ static/wordcloud.png
87
+
88
+ return render_template(
89
+ 'dashboard.html',
90
+ total_keluhan=total_keluhan,
91
+ topik_terbanyak=topik_terbanyak,
92
+ instansi_terbanyak=instansi_terbanyak,
93
+ keluhan_hari_ini=keluhan_hari_ini,
94
+ keluhan_harian_labels=keluhan_harian_labels,
95
+ keluhan_harian_values=keluhan_harian_values,
96
+ emosi_labels=emosi_dist.index.tolist(),
97
+ emosi_values=emosi_values,
98
+ keluhan_bulanan_labels=keluhan_bulanan_labels,
99
+ keluhan_bulanan_values=keluhan_bulanan_values,
100
+ wordcloud_image='wordcloud.png',
101
+ top_topik = top_topik.itertuples(index=False),
102
+ top_instansi = top_instansi.itertuples(index=False),
103
+ )
104
+
105
+ @app.route('/ubah_status', methods=['POST'])
106
+ def ubah_status():
107
+ keluhan_id = int(request.form['id'])
108
+
109
+ base_path = os.path.join('data')
110
+ file_path = os.path.join(base_path, 'vikor_fix.xlsx')
111
+ df = pd.read_excel(file_path)
112
+
113
+ # Update status menjadi 'selesai'
114
+ df.loc[df['id'] == keluhan_id, 'status'] = 'selesai'
115
+
116
+ # Simpan kembali
117
+ df.to_excel(file_path, index=False)
118
+
119
+ return redirect(url_for('leaderboard'))
120
+
121
+
122
+ @app.route('/leaderboard')
123
+ def leaderboard():
124
+ base_path = os.path.join('data')
125
+ final_df = pd.read_excel(os.path.join(base_path, 'vikor_fix.xlsx'))
126
+
127
+ # Filter hanya data yang belum selesai
128
+ df_pending = final_df[final_df['status'] != 'selesai']
129
+
130
+ # ----- Hitung VIKOR -----
131
+ f_emosi_plus = df_pending['new_emosi'].max()
132
+ f_emosi_min = df_pending['new_emosi'].min()
133
+ f_ranking_plus= df_pending['new_keyword'].max()
134
+ f_ranking_min = df_pending['new_keyword'].min()
135
+
136
+ emosi_denom = f_emosi_plus - f_emosi_min
137
+ ranking_denom = f_ranking_plus - f_ranking_min
138
+
139
+ df_pending['normalisasi_emosi'] = (f_emosi_plus - df_pending['new_emosi']) / (emosi_denom if emosi_denom != 0 else 1)
140
+ df_pending['normalisasi_ranking'] = (f_ranking_plus - df_pending['new_keyword']) / (ranking_denom if ranking_denom != 0 else 1)
141
+
142
+ df_pending['normalisasi_bobot_emosi'] = 0.5 * df_pending['normalisasi_emosi']
143
+ df_pending['normalisasi_bobot_ranking'] = 0.5 * df_pending['normalisasi_ranking']
144
+
145
+ df_pending['ultility'] = df_pending['normalisasi_bobot_emosi'] + df_pending['normalisasi_bobot_ranking']
146
+ df_pending['regret'] = df_pending[['normalisasi_bobot_emosi', 'normalisasi_bobot_ranking']].max(axis=1)
147
+
148
+ s_plus = df_pending['ultility'].max()
149
+ s_min = df_pending['ultility'].min()
150
+ r_plus = df_pending['regret'].max()
151
+ r_min = df_pending['regret'].min()
152
+
153
+ df_pending['vikor'] = 0.5 * ((df_pending['ultility'] - s_min) / (s_plus - s_min)) + \
154
+ 0.5 * ((df_pending['regret'] - r_min) / (r_plus - r_min))
155
+
156
+ df_pending['rank'] = df_pending['vikor'].rank(ascending=True).astype(int)
157
+
158
+ # ----- Keluhan prioritas (10 skor vikor tertinggi) -------------------
159
+ prioritas_df = df_pending.sort_values(by='vikor', ascending=True).head(10)
160
+
161
+ # ----- Render ke template -------------------------------------------
162
+ return render_template(
163
+ 'leaderboard.html',
164
+ keluhan_prioritas = prioritas_df
165
+ )
166
+
167
+ @app.route('/form', methods=['GET', 'POST'])
168
+ def form():
169
+ # Load dataframes
170
+ base_path = os.path.join('data')
171
+ instansi_df = pd.read_csv(os.path.join(base_path, 'mediacenter_instansi_202311220929.csv'), sep=';')
172
+ kecamatan_df = pd.read_csv(os.path.join(base_path, 'mediacenter_kecamatan_202311220929.csv'), sep=';')
173
+ kelurahan_df = pd.read_csv(os.path.join(base_path, 'mediacenter_kelurahan_202311220929.csv'), sep=';')
174
+ topik_df = pd.read_csv(os.path.join(base_path, 'mediacenter_topik_202311230834.csv'), sep=';')
175
+ # Join antar dataframe agar dapatkan nama kecamatan pada kelurahan
176
+ kecamatan_dict = kecamatan_df.set_index('id')['name'].to_dict()
177
+ kelurahan_df['kecamatan_name'] = kelurahan_df['kecamatan_id'].map(kecamatan_dict)
178
+
179
+ # Buat mapping: kecamatan_name -> list of kelurahan names
180
+ kelurahan_map = kelurahan_df.groupby('kecamatan_name')['name'].apply(list).to_dict()
181
+
182
+ message = None
183
+ if request.method == 'POST':
184
+ # Form
185
+ keluhan = request.form.get('keluhan')
186
+ instansi = request.form.get('instansi')
187
+ tanggal_keluhan = request.form.get('tanggal_keluhan')
188
+ kecamatan = request.form.get('kecamatan')
189
+ kelurahan = request.form.get('kelurahan')
190
+ topik = request.form.get('topik')
191
+
192
+ # Prediksi emosi dan ekstrak keyword
193
+ interpreter = load_tflite_model()
194
+ emosi = predict_emotion(keluhan, interpreter)
195
+ keywords, ranked_keywords = keyword(keluhan)
196
+ keywords_str = ', '.join(keywords)
197
+ emotion_mapping = {
198
+ 'anger': 3,
199
+ 'fear': 2,
200
+ 'sadness': 1
201
+ }
202
+ new_emosi = emotion_mapping.get(emosi, emosi)
203
+
204
+ # Buat dictionary data baru
205
+ new_data = {
206
+ 'keluhan': keluhan,
207
+ 'instansi': instansi,
208
+ 'tanggal_keluhan': tanggal_keluhan,
209
+ 'kecamatan': kecamatan,
210
+ 'kelurahan': kelurahan,
211
+ 'topik': topik,
212
+ 'emosi': emosi,
213
+ 'new_emosi': new_emosi,
214
+ 'new_keyword': ranked_keywords,
215
+ 'keywords': keywords_str,
216
+ 'status': 'belum_selesai'
217
+ }
218
+
219
+ # Simpan ke final_dataset.xlsx
220
+ # Cek apakah file sudah ada, jika tidak buat baru
221
+ dataset_path = os.path.join('data', 'vikor_fix.xlsx')
222
+ if not os.path.exists(dataset_path):
223
+ df = pd.DataFrame([new_data])
224
+ else:
225
+ df = pd.read_excel(dataset_path)
226
+ df = pd.concat([df, pd.DataFrame([new_data])], ignore_index=True)
227
+ df.to_excel(dataset_path, index=False)
228
+
229
+ message = "✅ Keluhan berhasil disimpan!"
230
+
231
+ # Pass dataframes to the template
232
+ return render_template('form.html', instansi=instansi_df,
233
+ kecamatan=kecamatan_df, kelurahan=kelurahan_df,
234
+ topik=topik_df, kelurahan_map=json.dumps(kelurahan_map), message=message)
235
+
236
+ if __name__ == "__main__":
237
+ port = int(os.environ.get('PORT', 5000))
238
+ app.run(host="0.0.0.0", port=port, debug=True) # debug=True opsional
data/a.txt ADDED
File without changes
helper.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import demoji
2
+ import re
3
+ import pandas as pd
4
+ import numpy as np
5
+ import nltk
6
+ import keras
7
+ from transformers import BertTokenizer, TFBertModel
8
+ import tensorflow as tf
9
+ from string import punctuation
10
+ from keybert import KeyBERT
11
+ from nltk.corpus import stopwords
12
+ from sentence_transformers import SentenceTransformer
13
+ import os
14
+ import warnings
15
+ warnings.filterwarnings("ignore")
16
+
17
+ # --- Configuration & Global Variables ---
18
+ MAX_LENGTH = 128
19
+ base_path = os.path.join('data')
20
+ model_path = os.path.join('Model')
21
+
22
+ # --- Helper: Download NLTK data ---
23
+ nltk.download('stopwords')
24
+
25
+ # --- Load Resources ---
26
+ alay_dict = pd.read_csv(os.path.join(base_path, 'kamus_alay.csv'), names=['alay', 'normal'], encoding='latin-1')
27
+ alay_dict_map = dict(zip(alay_dict['alay'], alay_dict['normal']))
28
+ stop_words = set(stopwords.words('indonesian'))
29
+ tokenizer = BertTokenizer.from_pretrained("indobenchmark/indobert-large-p1")
30
+ bert_model = TFBertModel.from_pretrained("indobenchmark/indobert-large-p1")
31
+ lstm_model = keras.models.load_model(os.path.join(model_path, 'indobert_lstm_model.keras'))
32
+
33
+ # --- Preprocessing Functions ---
34
+ def process_text(text):
35
+ # Baca kamus CSV ke dalam DataFrame
36
+ global alay_dict_map
37
+ text = str(text) # Convert Object to str
38
+ text = text.lower() # Lowercase text
39
+ text = re.sub(r'\d+', '', text) # Remove number
40
+ text = text.replace('\\n\\n\\n', ' ')
41
+ text = text.replace('\\n\\n', ' ')
42
+ text = text.replace('\\n', ' ')
43
+ text = re.sub(r'^https?:\/\/.*[\r\n]*', '', text, flags=re.MULTILINE) # Remove link
44
+ text = re.sub(f"[{re.escape(punctuation)}]", " ", text) # Remove punctuation
45
+ text = demoji.replace(text, "") # Remove emoji
46
+ text = " ".join(text.split()) # Remove extra spaces, tabs, and new lines
47
+ text = text.split()
48
+ text = [alay_dict_map[word] if word in alay_dict_map else word for word in text]
49
+ text = ' '.join(text)
50
+
51
+ return text
52
+
53
+ # --- Emotion Prediction ---
54
+ def load_tflite_model(tflite_path="Model/indobert_lstm_model.tflite"):
55
+ interpreter = tf.lite.Interpreter(model_path=tflite_path)
56
+ interpreter.allocate_tensors()
57
+ return interpreter
58
+
59
+ def predict_emotion(text, interpreter):
60
+ cleaned = process_text(text)
61
+ tokens = tokenizer(cleaned, return_tensors="tf", padding='max_length', truncation=True, max_length=128)
62
+
63
+ # Ambil seluruh token embeddings (bukan hanya CLS token)
64
+ outputs = bert_model(**tokens)
65
+ embeddings = outputs.last_hidden_state # shape (1, 128, 1024)
66
+
67
+ input_data = embeddings.numpy().astype(np.float32) # sesuai shape TFLite
68
+ input_details = interpreter.get_input_details()
69
+ output_details = interpreter.get_output_details()
70
+
71
+ interpreter.set_tensor(input_details[0]['index'], input_data)
72
+ interpreter.invoke()
73
+ output = interpreter.get_tensor(output_details[0]['index'])
74
+
75
+ label = np.argmax(output, axis=1)[0]
76
+ emotions = ['anger', 'fear', 'sadness']
77
+ return emotions[label]
78
+
79
+ # --- Keyword Extraction & Ranking ---
80
+ # Load rank keyword
81
+ df_rank_keyword = pd.read_excel(os.path.join(base_path, 'Keyword_KeyBERT.xlsx'))
82
+ df_rank_keyword['keyword'] = df_rank_keyword['keyword'].apply(process_text)
83
+ df_rank_keyword['new_rank'] = df_rank_keyword['rank'].max() - df_rank_keyword['rank'] + 1
84
+
85
+ def rank_keywords(row):
86
+ total_ranking = 0
87
+ total_keyword = 0
88
+ for keyword in row:
89
+ frekuensi_rank = df_rank_keyword.loc[df_rank_keyword['keyword'] == keyword]
90
+ if not frekuensi_rank.empty:
91
+ total_ranking += frekuensi_rank['new_rank'].values[0]
92
+ total_keyword += 1
93
+ if total_keyword > 0:
94
+ return total_ranking / total_keyword
95
+ else:
96
+ return 0
97
+
98
+ def keyword(text):
99
+ # Model Keyword
100
+ sentence_model = SentenceTransformer("denaya/indoSBERT-large", trust_remote_code=True)
101
+
102
+ # Buat objek KeyBERT
103
+ kw_model = KeyBERT(model=sentence_model)
104
+
105
+ # Proses Keyword
106
+ stop_words = set(stopwords.words('indonesian'))
107
+ text = text.split()
108
+ text = [w for w in text if not w in stop_words]
109
+ text = ' '.join(text)
110
+ text = process_text(text)
111
+ keywords = kw_model.extract_keywords(text, top_n=5)
112
+ keyword = [keyword for keyword, _ in keywords]
113
+ rank = rank_keywords(keyword)
114
+
115
+ return keyword, rank
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ demoji==1.1.0
2
+ Flask==3.1.1
3
+ keras==3.10.0
4
+ keybert==0.9.0
5
+ matplotlib==3.9.4
6
+ nltk==3.9.1
7
+ numpy==2.0.2
8
+ openpyxl==3.1.5
9
+ pandas==2.3.0
10
+ scikit-learn==1.6.1
11
+ tensorflow==2.19.0
12
+ tf_keras==2.19.0
13
+ transformers==4.52.4
14
+ wordcloud==1.9.4
15
+
static/img/a.txt ADDED
File without changes
templates/base.html ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- templates/base.html -->
2
+ <!DOCTYPE html>
3
+ <html lang="id">
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>KeluhCerdas - {% block title %}{% endblock %}</title>
8
+ <link href="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css" rel="stylesheet"/>
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
10
+ <style>
11
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; }
12
+ .navbar-brand { font-weight: bold; font-size: 1.2rem; }
13
+ .container-xl {
14
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
15
+ }
16
+ </style>
17
+ {% block extra_head %}{% endblock %}
18
+ </head>
19
+ <body>
20
+ <div class="page">
21
+ <!-- Navbar -->
22
+ <header class="navbar navbar-expand-md navbar-dark bg-primary d-print-none">
23
+ <div class="container-xl">
24
+ <a class="navbar-brand" href="{{ url_for('dashboard') }}">KeluhCerdas</a>
25
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
26
+ <span class="navbar-toggler-icon"></span>
27
+ </button>
28
+ <div class="collapse navbar-collapse" id="navbar-menu">
29
+ <ul class="navbar-nav me-auto">
30
+ <li class="nav-item">
31
+ <a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %} border-bottom-2 border-primary" href="{{ url_for('dashboard') }}">Dashboard</a>
32
+ </li>
33
+ <li class="nav-item">
34
+ <a class="nav-link {% if request.endpoint == 'leaderboard' %}active{% endif %} border-bottom-2 border-primary" href="{{ url_for('leaderboard') }}">Leaderboard</a>
35
+ </li>
36
+ <li class="nav-item">
37
+ <a class="nav-link {% if request.endpoint == 'form' %}active{% endif %} border-bottom-2 border-primary" href="{{ url_for('form') }}">Form Keluhan</a>
38
+ </li>
39
+ </ul>
40
+ <div class="navbar-nav flex-row">
41
+ <a href="#" class="nav-link px-2 text-white">Admin</a>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </header>
46
+
47
+ <!-- Content -->
48
+ <main class="page-wrapper">
49
+ <div class="page-body">
50
+ <div class="container-xl mt-4">
51
+ {% block content %}{% endblock %}
52
+ </div>
53
+ </div>
54
+ </main>
55
+
56
+ <!-- Footer -->
57
+ <footer class="footer footer-transparent d-print-none mt-5">
58
+ <div class="container-xl text-center">
59
+ <p class="mb-0 text-light bg-dark p-2 rounded">© 2025 KeluhCerdas - AI-based Complaint Prioritization</p>
60
+ </div>
61
+ </footer>
62
+ </div>
63
+
64
+ <script src="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/js/tabler.min.js"></script>
65
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
66
+ {% block extra_js %}{% endblock %}
67
+ </body>
68
+ </html>
templates/dashboard.html ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Dashboard{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container-fluid">
7
+ <div class="row g-4 mb-4">
8
+ <!-- Card: Total Keluhan -->
9
+ <div class="col-12 col-sm-6 col-lg-3">
10
+ <div class="card shadow-sm h-100">
11
+ <div class="card-body text-center">
12
+ <div class="h1 mb-2 fw-bold">{{ total_keluhan }}</div>
13
+ <div class="text-muted">Total Keluhan</div>
14
+ </div>
15
+ </div>
16
+ </div>
17
+ <!-- Card: Topik Terbanyak -->
18
+ <div class="col-12 col-sm-6 col-lg-3">
19
+ <div class="card shadow-sm h-100">
20
+ <div class="card-body text-center">
21
+ <div class="h1 mb-2 fw-bold">{{ topik_terbanyak }}</div>
22
+ <div class="text-muted">Topik Terbanyak</div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ <!-- Card: Instansi Terbanyak -->
27
+ <div class="col-12 col-sm-6 col-lg-3">
28
+ <div class="card shadow-sm h-100">
29
+ <div class="card-body text-center">
30
+ <div class="h1 mb-2 fw-bold">{{ instansi_terbanyak }}</div>
31
+ <div class="text-muted">Instansi Terbanyak</div>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ <!-- Card: Keluhan Hari Ini -->
36
+ <div class="col-12 col-sm-6 col-lg-3">
37
+ <div class="card shadow-sm h-100">
38
+ <div class="card-body text-center">
39
+ <div class="h1 mb-2 fw-bold">{{ keluhan_hari_ini }}</div>
40
+ <div class="text-muted">Keluhan Hari Ini</div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- Chart Area -->
47
+ <div class="row g-4">
48
+ <!-- Grafik Keluhan Bulanan -->
49
+ <div class="col-12 col-lg-8">
50
+ <div class="card shadow-sm h-100">
51
+ <div class="card-header bg-white border-bottom-0">
52
+ <h3 class="card-title mb-0">Tren Keluhan per Bulan</h3>
53
+ </div>
54
+ <div class="card-body">
55
+ <canvas id="keluhanBulananChart" height="120"></canvas>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ <!-- Pie Chart Distribusi Emosi -->
60
+ <div class="col-12 col-lg-4">
61
+ <div class="card shadow-sm h-100">
62
+ <div class="card-header bg-white border-bottom-0">
63
+ <h3 class="card-title mb-0">Distribusi Emosi</h3>
64
+ </div>
65
+ <div class="card-body d-flex justify-content-center align-items-center" style="min-height:240px;">
66
+ <canvas id="emosiPieChart" height="200"></canvas>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ <div class="row mb-4">
72
+ <div class="col">
73
+ <div class="card">
74
+ <div class="card-header"><h3 class="card-title">Word-cloud Keluhan</h3></div>
75
+ <div class="card-body text-center">
76
+ <!-- gunakan variabel 'wordcloud_image' yang dikirim Flask -->
77
+ <img src="{{ url_for('static', filename=wordcloud_image) }}"
78
+ alt="Word-cloud Keluhan"
79
+ class="img-fluid" style="max-height:300px;">
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ <div class="row row-deck">
86
+ <!-- ========= Topik ========= -->
87
+ <div class="col-md-6">
88
+ <div class="card">
89
+ <div class="card-header"><h3 class="card-title">Topik Keluhan Terbanyak</h3></div>
90
+ <div class="card-body p-0">
91
+ <table class="table table-striped table-hover">
92
+ <thead><tr><th>#</th><th>Topik</th><th>Jumlah</th></tr></thead>
93
+ <tbody>
94
+ {% for row in top_topik %}
95
+ <tr>
96
+ <td>{{ loop.index }}</td>
97
+ <td>{{ row.Topik }}</td>
98
+ <td>{{ row.Jumlah }}</td>
99
+ </tr>
100
+ {% endfor %}
101
+ </tbody>
102
+ </table>
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ <!-- ========= Instansi ========= -->
108
+ <div class="col-md-6">
109
+ <div class="card">
110
+ <div class="card-header"><h3 class="card-title">Instansi Terbanyak Menerima Keluhan</h3></div>
111
+ <div class="card-body p-0">
112
+ <table class="table table-striped table-hover">
113
+ <thead><tr><th>#</th><th>Instansi</th><th>Jumlah</th></tr></thead>
114
+ <tbody>
115
+ {% for row in top_instansi %}
116
+ <tr>
117
+ <td>{{ loop.index }}</td>
118
+ <td>{{ row.Instansi }}</td>
119
+ <td>{{ row.Jumlah }}</td>
120
+ </tr>
121
+ {% endfor %}
122
+ </tbody>
123
+ </table>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ {% endblock %}
130
+
131
+ {% block extra_js %}
132
+ <!-- Chart.js -->
133
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
134
+ <script>
135
+ // Keluhan Bulanan (Line Chart)
136
+ const keluhanBulananChart = new Chart(document.getElementById('keluhanBulananChart'), {
137
+ type: 'line',
138
+ data: {
139
+ labels: {{ keluhan_bulanan_labels | tojson }},
140
+ datasets: [{
141
+ label: 'Jumlah Keluhan',
142
+ data: {{ keluhan_bulanan_values | tojson }},
143
+ fill: false,
144
+ borderColor: 'rgba(75, 192, 192, 1)',
145
+ backgroundColor: 'rgba(75, 192, 192, 0.2)',
146
+ tension: 0.3,
147
+ pointRadius: 5,
148
+ pointHoverRadius: 7
149
+ }]
150
+ },
151
+ options: {
152
+ responsive: true,
153
+ plugins: {
154
+ legend: { display: false }
155
+ },
156
+ scales: {
157
+ y: {
158
+ beginAtZero: true,
159
+ ticks: { precision: 0 }
160
+ },
161
+ x: {
162
+ ticks: { autoSkip: false }
163
+ }
164
+ }
165
+ }
166
+ });
167
+
168
+ // Emosi (Pie Chart)
169
+ const emosiPieChart = new Chart(document.getElementById('emosiPieChart'), {
170
+ type: 'doughnut',
171
+ data: {
172
+ labels: {{ emosi_labels | tojson }},
173
+ datasets: [{
174
+ data: {{ emosi_values | tojson }},
175
+ backgroundColor: [ '#74c0fc','#f03e3e','#fab005', '#69db7c'],
176
+ borderWidth: 1
177
+ }],
178
+ options: {
179
+ plugins: {
180
+ legend: {
181
+ display: true,
182
+ position: 'bottom'
183
+ }
184
+ }
185
+ }
186
+ }
187
+ });
188
+ </script>
189
+ {% endblock %}
templates/form.html ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Form Keluhan{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="row">
7
+ <div class="col-md-8 mx-auto">
8
+ <div class="card">
9
+ <div class="card-header"><h3 class="card-title">Form Tambah Keluhan</h3></div>
10
+ <div class="card-body">
11
+ <form action="#" method="post">
12
+
13
+ <!-- Tanggal -->
14
+ <div class="mb-3">
15
+ <label class="form-label">Tanggal Keluhan</label>
16
+ <input type="date" class="form-control" name="tanggal_keluhan" value="{{ current_date or '2025-06-05' }}">
17
+ </div>
18
+
19
+ <!-- Instansi -->
20
+ <div class="mb-3">
21
+ <label class="form-label">Instansi</label>
22
+ <select class="form-select" name="instansi" required>
23
+ <option selected disabled>Pilih instansi</option>
24
+ {% for index, row in instansi.iterrows() %}
25
+ <option value="{{ row['name'] }}">{{ row['name'] }}</option>
26
+ {% endfor %}
27
+ </select>
28
+ </div>
29
+
30
+ <!-- Kecamatan -->
31
+ <div class="mb-3">
32
+ <label class="form-label">Kecamatan</label>
33
+ <select class="form-select" name="kecamatan" id="kecamatan" required>
34
+ <option selected disabled>Pilih kecamatan</option>
35
+ {% for index, row in kecamatan.iterrows() %}
36
+ <option value="{{ row['name'] }}">{{ row['name'] }}</option>
37
+ {% endfor %}
38
+ </select>
39
+ </div>
40
+
41
+ <!-- Kelurahan -->
42
+ <div class="mb-3">
43
+ <label class="form-label">Kelurahan</label>
44
+ <select class="form-select" name="kelurahan" id="kelurahan" required>
45
+ <option selected disabled>Pilih kelurahan</option>
46
+ </select>
47
+ </div>
48
+
49
+ <!-- Topik -->
50
+ <div class="mb-3">
51
+ <label class="form-label">Topik</label>
52
+ <select class="form-select" name="topik" required>
53
+ <option selected disabled>Pilih topik</option>
54
+ {% for index, row in topik.iterrows() %}
55
+ <option value="{{ row['name'] }}">{{ row['name'] }}</option>
56
+ {% endfor %}
57
+ </select>
58
+ </div>
59
+
60
+ <!-- Keluhan -->
61
+ <div class="mb-3">
62
+ <label class="form-label">Keluhan</label>
63
+ <textarea class="form-control" name="keluhan" rows="3" placeholder="Masukkan keluhan Anda" required></textarea>
64
+ </div>
65
+
66
+ <button type="submit" class="btn btn-primary">Submit</button>
67
+ </form>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Kelurahan Map untuk JavaScript -->
74
+ <script>
75
+ const kelurahanMap = {{ kelurahan_map | safe }};
76
+
77
+ document.getElementById('kecamatan').addEventListener('change', function () {
78
+ const kecamatan = this.value;
79
+ const kelurahanSelect = document.getElementById('kelurahan');
80
+ kelurahanSelect.innerHTML = '<option selected disabled>Pilih kelurahan</option>';
81
+
82
+ if (kelurahanMap[kecamatan]) {
83
+ kelurahanMap[kecamatan].forEach(function (kel) {
84
+ const opt = document.createElement('option');
85
+ opt.value = kel;
86
+ opt.innerText = kel;
87
+ kelurahanSelect.appendChild(opt);
88
+ });
89
+ }
90
+ });
91
+ </script>
92
+ {% endblock %}
templates/leaderboard.html ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Leaderboard{% endblock %}
4
+
5
+ {% block content %}
6
+
7
+
8
+ <!-- ========= Keluhan Prioritas ========= -->
9
+ <div class="card mt-4">
10
+ <div class="card-header"><h3 class="card-title">Daftar Keluhan Prioritas</h3></div>
11
+ <div class="card-body p-0">
12
+ <div class="table-responsive">
13
+ <table class="table table-hover table-striped mb-0">
14
+ <thead>
15
+ <tr>
16
+ <th>ID</th><th>Keluhan</th><th>Topik</th><th>Instansi</th>
17
+ <th>Emosi</th><th>Skor Prioritas</th><th>Status</th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ {% for _, row in keluhan_prioritas.iterrows() %}
22
+ <tr>
23
+ <td>{{ row.keluhan }}</td>
24
+ <td>{{row.keyword}}}</td>
25
+ <td>{{ row.topik }}</td>
26
+ <td>{{ row.instansi }}</td>
27
+ <td>{{ row.emosi }}</td>
28
+ <td>
29
+ <span class="badge text-dark
30
+ {% if row.vikor <= 0.1 %}bg-danger
31
+ {% elif row.vikor <= 1 %}bg-warning
32
+ {% else %}bg-secondary
33
+ {% endif %}">
34
+ {{ "%.4f"|format(row.vikor) }}
35
+ </span>
36
+ </td>
37
+ <td
38
+ <span class="badge {% if row.status == 'belum_ditanggapi' %}bg-warning
39
+ {% elif row.status == 'selesai' %}bg-success
40
+ {% else %}bg-secondary
41
+ {% endif %}"
42
+ style="cursor: pointer;"
43
+ data-bs-toggle="modal"
44
+ data-bs-target="#ubahStatusModal{{ row.id }}">
45
+ {{ row.status }}
46
+ </span>
47
+
48
+ <!-- Modal konfirmasi -->
49
+ <div class="modal fade" id="ubahStatusModal{{ row.id }}" tabindex="-1" aria-labelledby="modalLabel{{ row.id }}" aria-hidden="true">
50
+ <div class="modal-dialog">
51
+ <div class="modal-content">
52
+ <form action="{{ url_for('ubah_status') }}" method="POST">
53
+ <div class="modal-header">
54
+ <h5 class="modal-title" id="modalLabel{{ row.id }}">Ubah Status Keluhan</h5>
55
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Tutup"></button>
56
+ </div>
57
+ <div class="modal-body">
58
+ <p>Ubah status keluhan dengan ID <strong>{{ row.id }}</strong> menjadi <strong>Selesai</strong>?</p>
59
+ <input type="hidden" name="id" value="{{ row.id }}">
60
+ </div>
61
+ <div class="modal-footer">
62
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
63
+ <button type="submit" class="btn btn-success">Ya, ubah</button>
64
+ </div>
65
+ </form>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </td>
70
+ </tr>
71
+ {% endfor %}
72
+ </tbody>
73
+ </table>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ {% endblock %}