Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
livindra 2026-05-01 10:41:03 +07:00
parent 2e51686c13
commit 9eca98fdfd
8 changed files with 116 additions and 145 deletions

122
app.py
View File

@ -194,6 +194,20 @@ def _serialize_prediction_row(prediction_row):
}
def _history_sort_timestamp(value):
parsed = pd.to_datetime(value, errors='coerce')
if pd.isna(parsed):
return pd.Timestamp.min
return parsed
def _history_sort_id(value):
try:
return int(value or 0)
except (TypeError, ValueError):
return 0
@app.route('/get_data_riwayat_deteksi', methods=['POST'])
@app.route('/api/get_data_riwayat_deteksi', methods=['POST'])
def get_data_riwayat_deteksi():
@ -218,12 +232,7 @@ def get_data_riwayat_deteksi():
except (TypeError, ValueError):
continue
ordered_ids = []
seen = set()
for pred_id in normalized_ids:
if pred_id not in seen:
ordered_ids.append(pred_id)
seen.add(pred_id)
ordered_ids = sorted(set(normalized_ids), reverse=True)
recent = []
total_confidence = 0.0
@ -254,6 +263,15 @@ def get_data_riwayat_deteksi():
elif serialized['prediction'] == 'sehat':
negatif += 1
recent = sorted(
recent,
key=lambda item: (
_history_sort_id(item.get('id')),
_history_sort_timestamp(item.get('timestamp')),
),
reverse=True,
)
total = len(recent)
stats = {
'total': total,
@ -607,6 +625,10 @@ def result_sick():
@app.route('/expert-system', methods=['GET', 'POST'])
def expert_system_page():
"""Halaman sistem pakar forward chaining"""
mode = request.form.get('mode') if request.method == 'POST' else request.args.get('mode', 'manual')
mode = (mode or 'manual').strip().lower()
use_image_context = mode != 'manual'
if request.method == 'POST':
# Dapatkan gejala yang dipilih
gejala_terpilih = request.form.getlist('gejala')
@ -618,59 +640,10 @@ def expert_system_page():
# Dapatkan diagnosis
diagnosis = expert_system.get_diagnosis()
# Get prediction_id dari session untuk Foreign Key
prediction_id = session.get('last_prediction', {}).get('db_id')
prediction_id = session.get('last_prediction', {}).get('db_id') if use_image_context else None
# Jika tidak ada prediction_id (diagnosis langsung dari sistem pakar tanpa scan)
# Buatkan entry dummy di predictions table
if not prediction_id and diagnosis.get('status') == 'terdiagnosis' and MYSQL_AVAILABLE:
try:
# Ambil diagnosis utama
diag_list = diagnosis['diagnosis']
main_diag = diag_list[0]
# Diagnosis dari sistem pakar berarti PMK terdeteksi,
# jadi hasil deteksi disimpan sebagai sakit/positif PMK.
severity = main_diag.get('severity', 'sedang')
prediction = 'sakit'
confidence = main_diag.get('score', 0.5)
# Buat entry yang menunjukkan diagnosis manual dari sistem pakar
features_dict = {
'diagnosis_method': 'manual_expert_system',
'severity': severity,
'gejala_selected': gejala_terpilih
}
# Save prediction untuk diagnosis manual
prediction_id = save_prediction_mysql(
original_filename='Diagnosis Sistem Pakar (Manual)',
filename='manual_expert_system',
image_path='manual_expert_system',
prediction=prediction,
confidence=float(confidence),
features_dict=features_dict
)
try:
session['last_prediction'] = {
'db_id': int(prediction_id),
'filename': 'manual_expert_system',
'original_filename': 'Diagnosis Sistem Pakar (Manual)',
'prediction': prediction,
'confidence': float(confidence),
'source': 'manual_expert_system'
}
except Exception:
pass
print(f"✓ Created prediction entry for manual expert system diagnosis, id={prediction_id}")
except Exception as e:
print(f"✗ Error creating prediction entry for manual diagnosis: {e}")
import traceback
traceback.print_exc()
# Save diagnosis ke database dengan FK ke predictions
if MYSQL_AVAILABLE and prediction_id and diagnosis.get('status') == 'terdiagnosis' and diagnosis.get('diagnosis'):
# Save diagnosis ke database. Jalur manual tidak lagi dipaksa terikat ke gambar.
if MYSQL_AVAILABLE and diagnosis.get('status') == 'terdiagnosis' and diagnosis.get('diagnosis'):
try:
diag_list = diagnosis['diagnosis']
main_diag = diag_list[0]
@ -704,8 +677,11 @@ def expert_system_page():
timestamp=datetime.datetime.utcnow()
)
diagnosis['saved_diagnosis_id'] = diag_id
if prediction_id:
diagnosis['saved_prediction_id'] = int(prediction_id)
print(f"✓ Diagnosis saved to database, id={diag_id}, linked to prediction_id={prediction_id}, severity={severity}")
else:
print(f"✓ Manual diagnosis saved to database, id={diag_id}, severity={severity}")
except Exception as e:
print(f"✗ Error saving diagnosis to MySQL: {e}")
import traceback
@ -714,7 +690,7 @@ def expert_system_page():
# Jika ada hasil prediksi berbasis image processing sebelumnya, tambahkan ke konteks
last_prediction = session.get('last_prediction', {})
image_info = None
if last_prediction.get('source') == 'image_processing':
if use_image_context and last_prediction.get('source') == 'image_processing':
image_info = {
'filename': last_prediction.get('original_filename', ''),
'prediction': last_prediction.get('prediction', ''),
@ -726,13 +702,14 @@ def expert_system_page():
gejala_groups=expert_system.get_gejala_groups(),
diagnosis=diagnosis,
selected_gejala=gejala_terpilih,
image_info=image_info)
image_info=image_info,
context_mode='image' if use_image_context else 'manual')
# GET request - tampilkan form
# Ambil informasi gambar dari session jika hasil sebelumnya berasal dari image processing
last_prediction = session.get('last_prediction', {})
image_info = None
if last_prediction.get('source') == 'image_processing':
if use_image_context and last_prediction.get('source') == 'image_processing':
image_info = {
'filename': last_prediction.get('original_filename', ''),
'prediction': last_prediction.get('prediction', ''),
@ -742,14 +719,15 @@ def expert_system_page():
return render_template('expert_system.html',
gejala_list=expert_system.get_gejala_list(),
gejala_groups=expert_system.get_gejala_groups(),
image_info=image_info)
image_info=image_info,
context_mode='image' if use_image_context else 'manual')
@app.route('/expert-system/from-prediction')
def expert_system_from_prediction():
"""Redirect ke sistem pakar dari hasil prediksi"""
# Bisa tambahkan logika untuk pre-fill gejala berdasarkan hasil prediksi
return redirect(url_for('expert_system_page'))
return redirect(url_for('expert_system_page', mode='image'))
@app.route('/api/diagnosis', methods=['POST'])
@ -763,8 +741,8 @@ def api_diagnosis():
expert_system.tambah_gejala(gejala)
diagnosis = expert_system.get_diagnosis()
# Save diagnosis ke database jika MYSQL_AVAILABLE dan ada prediction_id
if MYSQL_AVAILABLE and prediction_id:
# Save diagnosis ke database jika MYSQL_AVAILABLE
if MYSQL_AVAILABLE:
try:
# Prepare diagnosis data
if diagnosis.get('status') == 'terdiagnosis' and diagnosis.get('diagnosis'):
@ -798,11 +776,13 @@ def api_diagnosis():
prediction_id=prediction_id,
diagnosis_dict=diagnosis_details,
severity=severity,
confidence=float(score),
timestamp=datetime.datetime.utcnow()
)
diagnosis['saved_diagnosis_id'] = diag_id
if prediction_id:
print(f"✓ Diagnosis saved to database, id={diag_id}, linked to prediction_id={prediction_id}, severity={severity}")
else:
print(f"✓ Manual diagnosis saved to database, id={diag_id}, severity={severity}")
except Exception as e:
print(f"✗ Error saving diagnosis to database: {e}")
import traceback
@ -843,6 +823,11 @@ def riwayat_diagnosis():
'severity': r.get('severity', 'sedang'),
'rekomendasi': '|'.join(solusi_list) if isinstance(solusi_list, list) else str(solusi_list)
})
diagnosis_history = sorted(
diagnosis_history,
key=lambda item: _history_sort_timestamp(item.get('timestamp')),
reverse=True,
)
data_source = 'mysql'
return render_template('diagnosis_history.html', history=diagnosis_history, data_source=data_source)
except Exception as e:
@ -855,6 +840,11 @@ def riwayat_diagnosis():
if not df.empty:
df_recent = df.sort_values('timestamp', ascending=False).head(50)
diagnosis_history = df_recent.to_dict(orient='records')
diagnosis_history = sorted(
diagnosis_history,
key=lambda item: _history_sort_timestamp(item.get('timestamp')),
reverse=True,
)
except Exception as e:
print(f"Error reading diagnosis history: {e}")

View File

@ -172,7 +172,7 @@
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'expert_system_page' %}active{% endif %}" href="{{ url_for('expert_system_page') }}">
<a class="nav-link {% if request.endpoint == 'expert_system_page' %}active{% endif %}" href="{{ url_for('expert_system_page', mode='manual') }}">
<i class="fas fa-brain me-1"></i>Sistem Pakar
</a>
</li>
@ -214,7 +214,7 @@
<ul class="list-unstyled">
<li><a href="{{ url_for('index') }}" class="text-white">Home</a></li>
<li><a href="{{ url_for('riwayat_deteksi') }}" class="text-white">Riwayat Deteksi</a></li>
<li><a href="{{ url_for('expert_system_page') }}" class="text-white">Sistem Pakar</a></li>
<li><a href="{{ url_for('expert_system_page', mode='manual') }}" class="text-white">Sistem Pakar</a></li>
</ul>
<hr class="bg-white">
<div class="text-right">

View File

@ -176,20 +176,6 @@
</ul>
</div>
{% endif %}
<!-- Waktu Diagnosis -->
<div class="row">
<div class="col-md-12">
<h6 class="text-muted mb-2">Waktu Diagnosis</h6>
<p class="fw-bold">
{% if diagnosis.timestamp %}
{{ diagnosis.timestamp[:19] | replace('T', ' ') }}
{% else %}
N/A
{% endif %}
</p>
</div>
</div>
</div>
</div>
{% else %}

View File

@ -10,6 +10,7 @@
<h4 class="mb-0"><i class="fas fa-stethoscope me-2"></i>Sistem Pakar Forward Chaining - Diagnosis PMK</h4>
</div>
<div class="card-body">
{% set is_image_mode = context_mode == 'image' %}
<!-- Breadcrumb -->
<nav aria-label="breadcrumb">
@ -22,8 +23,19 @@
<!-- Info Panel -->
<div class="alert alert-info mb-4">
<i class="fas fa-info-circle me-2"></i>
<strong>Cara Penggunaan:</strong> Pilih gejala yang Anda amati pada sapi, kemudian klik tombol "Diagnosis" untuk mendapatkan hasil.
<strong>Cara Penggunaan:</strong> Pilih gejala yang Anda amati pada sapi, kemudian klik tombol diagnosis untuk mendapatkan hasil.
</div>
{% if is_image_mode %}
<div class="alert alert-success mb-4">
<i class="fas fa-image me-2"></i>
Mode ini mengikuti hasil deteksi gambar sebelumnya.
</div>
{% else %}
<div class="alert alert-secondary mb-4">
<i class="fas fa-brain me-2"></i>
Mode ini berjalan langsung sebagai sistem pakar tanpa keterikatan gambar.
</div>
{% endif %}
{% if image_info %}
<div class="alert alert-success mb-4">
@ -143,7 +155,7 @@
<button class="btn btn-success" onclick="window.print()">
<i class="fas fa-print me-2"></i>Cetak Hasil
</button>
<a href="{{ url_for('expert_system_page') }}" class="btn btn-primary">
<a href="{{ url_for('expert_system_page', mode=context_mode) }}" class="btn btn-primary">
<i class="fas fa-redo me-2"></i>Konsultasi Lagi
</a>
<a href="{{ url_for('index') }}" class="btn btn-secondary">
@ -157,7 +169,7 @@
{{ diagnosis.message }}
</div>
<div class="text-center">
<a href="{{ url_for('expert_system_page') }}" class="btn btn-primary">
<a href="{{ url_for('expert_system_page', mode=context_mode) }}" class="btn btn-primary">
<i class="fas fa-redo me-2"></i>Coba Lagi
</a>
</div>
@ -166,7 +178,8 @@
{% else %}
<!-- Form Gejala -->
<form method="POST" action="{{ url_for('expert_system_page') }}" id="expertForm">
<form method="POST" action="{{ url_for('expert_system_page', mode=context_mode) }}" id="expertForm">
<input type="hidden" name="mode" value="{{ context_mode }}">
<div class="mb-4">
<h5 class="mb-3"><i class="fas fa-list-ul me-2"></i>Pilih Gejala yang Diamati:</h5>
@ -208,7 +221,7 @@
<!-- Action Buttons -->
<div class="mt-4 text-center">
<button type="submit" class="btn btn-success btn-lg">
<i class="fas fa-diagnoses me-2"></i>Diagnosis
<i class="fas fa-diagnoses me-2"></i>{% if is_image_mode %}Diagnosis dari Gambar{% else %}Diagnosis Langsung{% endif %}
</button>
<button type="reset" class="btn btn-secondary btn-lg">
<i class="fas fa-undo me-2"></i>Reset

View File

@ -47,6 +47,9 @@
<a href="{{ url_for('upload') }}" class="btn btn-success btn-lg w-100">
<i class="fas fa-upload me-2"></i>Upload Gambar
</a>
<a href="{{ url_for('expert_system_page', mode='manual') }}" class="btn btn-outline-success btn-lg w-100 mt-2">
<i class="fas fa-brain me-2"></i>Sistem Pakar Langsung
</a>
</div>
</div>
</div>
@ -79,56 +82,13 @@
<div class="card-body">
<div class="step-circle mx-auto">3</div>
<h5>Diagnosis Lanjutan</h5>
<p class="text-muted">Jika terdeteksi sakit, lanjut ke sistem pakar untuk diagnosis lengkap</p>
<p class="text-muted">Jika terdeteksi sakit, lanjut ke sistem pakar untuk diagnosis lengkap. Atau gunakan sistem pakar langsung tanpa gambar.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Riwayat Terbaru -->
{% if history %}
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="fas fa-history me-2"></i>Riwayat Deteksi Terbaru</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Waktu</th>
<th>Gambar</th>
<th>Hasil</th>
<th>Keyakinan</th>
</tr>
</thead>
<tbody>
{% for item in history %}
<tr>
<td>{{ item.timestamp }}</td>
<td>{{ item.image_path }}</td>
<td>
{% if item.prediction == 'sakit' %}
<span class="badge bg-warning text-dark">Positif PMK</span>
{% else %}
<span class="badge bg-success">Sehat</span>
{% endif %}
</td>
<td>{{ "%.2f"|format(item.confidence) }}%</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<style>
.feature-icon {
width: 100px;

View File

@ -21,7 +21,7 @@
{% set recommendation_icon = 'fa-arrow-right text-warning' if is_sick else 'fa-check-circle text-success' %}
{% set action_btn_class = 'btn-warning' if is_sick else 'btn-info' %}
{% set action_btn_icon = 'fa-arrow-right' if is_sick else 'fa-stethoscope' %}
{% set action_btn_text = 'Lanjut ke Sistem Pakar' if is_sick else 'Sistem Pakar' %}
{% set action_btn_text = 'Lanjut ke Sistem Pakar dari Gambar' if is_sick else 'Sistem Pakar' %}
{% set table_head_class = 'table-warning' if is_sick else 'table-success' %}
<div class="row justify-content-center">
@ -151,13 +151,11 @@
<a href="{{ url_for('upload') }}" class="btn btn-success btn-lg me-2">
<i class="fas fa-upload me-2"></i>Upload Gambar Lain
</a>
<a href="{{ url_for('expert_system_page') }}" class="btn {{ action_btn_class }} btn-lg me-2">
<a href="{{ url_for('expert_system_page', mode='image') }}" class="btn {{ action_btn_class }} btn-lg me-2">
<i class="fas {{ action_btn_icon }} me-2"></i>{{ action_btn_text }}
</a>
{% set last_db = session.get('last_prediction', {}).get('db_id') %}
<a href="{{ url_for('riwayat_deteksi') }}{% if last_db %}#pred-{{ last_db }}{% endif %}" class="btn btn-secondary btn-lg">
<i class="fas fa-history me-2"></i>Riwayat Deteksi
</a>
</div>
<div class="mt-3 text-center">

View File

@ -201,7 +201,17 @@ function exportData(format) {
function loadLocalStorageIds() {
try {
const value = JSON.parse(localStorage.getItem('pmk_predictions_ids') || '[]');
return Array.isArray(value) ? [...new Set(value.map(String))] : [];
if (!Array.isArray(value)) {
return [];
}
const numericIds = value
.map((id) => Number(id))
.filter((id) => Number.isFinite(id));
return [...new Set(numericIds)]
.sort((a, b) => b - a)
.map((id) => String(id));
} catch (e) {
return [];
}
@ -223,6 +233,17 @@ function formatTimestamp(timestamp) {
}
}
function compareHistoryItemsDesc(a, b) {
const idDiff = Number(b.id || 0) - Number(a.id || 0);
if (idDiff !== 0) {
return idDiff;
}
const timestampA = String(a.timestamp || '');
const timestampB = String(b.timestamp || '');
return timestampB.localeCompare(timestampA);
}
function createHistoryRow(item) {
const tr = document.createElement('tr');
tr.id = `pred-${item.id}`;
@ -315,6 +336,7 @@ async function loadRiwayatDeteksi() {
const recent = Array.isArray(payload.recent) ? payload.recent : [];
const stats = payload.stats || { total: 0, positif: 0, negatif: 0, akurasi_rata_rata: 0 };
recent.sort(compareHistoryItemsDesc);
if (tableBody) {
tableBody.innerHTML = '';

View File

@ -662,12 +662,12 @@ def get_prediction_by_id(pred_id):
return None
def save_diagnosis_mysql(prediction_id, diagnosis_dict, severity='sedang', timestamp=None):
def save_diagnosis_mysql(prediction_id=None, diagnosis_dict=None, severity='sedang', timestamp=None):
"""
Save diagnosis result from expert system to database
Args:
prediction_id: Foreign Key ke predictions table (prediction yang di-diagnosa)
prediction_id: Foreign Key ke predictions table (prediction yang di-diagnosa). Optional.
diagnosis_dict: Dictionary of diagnosis details from expert system
severity: 'ringan', 'sedang', or 'berat'
timestamp: Optional datetime, defaults to now
@ -678,6 +678,8 @@ def save_diagnosis_mysql(prediction_id, diagnosis_dict, severity='sedang', times
try:
session = Session()
diagnosis_dict = diagnosis_dict or {}
# Convert diagnosis dict to JSON string
diagnosis_json = json.dumps(diagnosis_dict, default=str)