Spaces:
Sleeping
Sleeping
MahfudAwal
commited on
Commit
·
d3bd4e2
1
Parent(s):
c0fe0dc
push
Browse files- Dockerfile +27 -0
- Model/a.txt +0 -0
- README.md +4 -4
- app.py +238 -0
- data/a.txt +0 -0
- helper.py +115 -0
- requirements.txt +15 -0
- static/img/a.txt +0 -0
- templates/base.html +68 -0
- templates/dashboard.html +189 -0
- templates/form.html +92 -0
- templates/leaderboard.html +77 -0
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:
|
3 |
-
emoji:
|
4 |
colorFrom: blue
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
license: apache-2.0
|
9 |
-
short_description:
|
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 %}
|