add new function in diagnosaController

This commit is contained in:
unknown 2025-06-09 09:43:11 +07:00
parent f9ba830cfa
commit c012b08a5f
11 changed files with 149 additions and 54 deletions

View File

@ -152,6 +152,36 @@ function filterSingleSymptomPerfectMatch(results) {
return filtered;
}
// Helper function untuk memprioritaskan berdasarkan jumlah gejala cocok
function prioritizeBySymptomCount(results) {
// Kelompokkan hasil berdasarkan jumlah gejala cocok
const groupedBySymptoms = {};
results.forEach(result => {
const symptomCount = result.jumlah_gejala_cocok;
if (!groupedBySymptoms[symptomCount]) {
groupedBySymptoms[symptomCount] = [];
}
groupedBySymptoms[symptomCount].push(result);
});
// Ambil kelompok dengan jumlah gejala cocok terbanyak
const maxSymptomCount = Math.max(...Object.keys(groupedBySymptoms).map(Number));
const topSymptomMatches = groupedBySymptoms[maxSymptomCount];
// Jika ada lebih dari 1 hasil dengan gejala cocok terbanyak, urutkan berdasarkan probabilitas
const sortedTopMatches = topSymptomMatches.sort((a, b) => {
return b.probabilitas_persen - a.probabilitas_persen;
});
console.log(`Memprioritaskan ${sortedTopMatches.length} hasil dengan ${maxSymptomCount} gejala cocok`);
return {
prioritizedResults: sortedTopMatches,
maxSymptomCount: maxSymptomCount,
totalGroups: Object.keys(groupedBySymptoms).length
};
}
exports.diagnosa = async (req, res) => {
const { gejala } = req.body;
const userId = req.user?.id;
@ -236,6 +266,26 @@ exports.diagnosa = async (req, res) => {
filterInfo.fallback_to_symptom_count = true;
filterInfo.fallback_reason = 'Semua hasil memiliki 100% akurasi dengan hanya 1 gejala cocok';
} else if (filteredResults.length > 0) {
// ========== PRIORITAS BERDASARKAN JUMLAH GEJALA COCOK ==========
const priorityAnalysis = prioritizeBySymptomCount(filteredResults);
// Jika ada hasil dengan gejala cocok lebih banyak, prioritaskan mereka
if (priorityAnalysis.totalGroups > 1) {
console.log(`Menggunakan prioritas gejala: ${priorityAnalysis.maxSymptomCount} gejala cocok diprioritaskan`);
finalResults = priorityAnalysis.prioritizedResults;
filterInfo.symptom_priority_applied = true;
filterInfo.max_symptom_count = priorityAnalysis.maxSymptomCount;
filterInfo.prioritized_count = priorityAnalysis.prioritizedResults.length;
} else {
// Jika semua hasil memiliki jumlah gejala cocok yang sama, urutkan berdasarkan probabilitas
finalResults = filteredResults.sort((a, b) => {
return b.probabilitas_persen - a.probabilitas_persen;
});
filterInfo.sorted_by_probability = true;
}
}
// ========== PENANGANAN AMBIGUITAS ==========

View File

@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="frontend"
android:label="SIBAYAM"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
<activity

View File

@ -7,15 +7,15 @@ import 'package:image_picker/image_picker.dart';
import 'package:http_parser/http_parser.dart';
class ApiService {
static const String baseUrl = 'https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/auth';
static const String gejalaUrl = 'https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/gejala';
static const String hamaUrl = 'https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/hama';
static const String penyakitUrl = 'https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/penyakit';
static const String rulesPenyakitUrl ='https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/rules_penyakit';
static const String rulesHamaUrl = 'https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/rules_hama';
static const String userUrl = 'https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/users';
static const String diagnosaUrl = 'https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/diagnosa';
static const String historiUrl = 'https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/histori';
static const String baseUrl = 'https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/auth';
static const String gejalaUrl = 'https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/gejala';
static const String hamaUrl = 'https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/hama';
static const String penyakitUrl = 'https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/penyakit';
static const String rulesPenyakitUrl ='https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/rules_penyakit';
static const String rulesHamaUrl = 'https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/rules_hama';
static const String userUrl = 'https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/users';
static const String diagnosaUrl = 'https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/diagnosa';
static const String historiUrl = 'https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/histori';
static const Duration timeout = Duration(seconds: 15);
/// Fungsi untuk mengirim gejala dan menerima hasil diagnosa
@ -419,7 +419,7 @@ Future<List<Map<String, dynamic>>> getAllHistori() async {
Future<Uint8List?> getHamaImageBytesByFilename(String filename) async {
try {
final url = Uri.parse('https://backend-sistem-pakar-diagnosa-penya.vercel.app/image_hama/$filename');
final url = Uri.parse('https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/image_hama/$filename');
print('Fetching image from: $url');
final response = await http.get(url);
@ -721,7 +721,7 @@ Future<List<Map<String, dynamic>>> getAllHistori() async {
Future<Uint8List?> getPenyakitImageBytesByFilename(String filename) async {
try {
final url = Uri.parse('https://backend-sistem-pakar-diagnosa-penya.vercel.app/image_penyakit/$filename');
final url = Uri.parse('https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/image_penyakit/$filename');
print('Fetching image from: $url');
final response = await http.get(url);

View File

@ -41,7 +41,7 @@ class _MyAppState extends State<MyApp> {
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Sistem Pakar Bayam',
title: 'SIBAYAM',
home: _initialScreen,
);
}

View File

@ -153,7 +153,7 @@ class DetailRiwayatPage extends StatelessWidget {
Text('Gejala: $gejalaList', style: TextStyle(fontSize: 16)),
SizedBox(height: 8),
Text(
'Hasil: ${(detailRiwayat['hasil'] as num?)?.toStringAsFixed(2) ?? "-"}',
'Hasil: ${detailRiwayat['hasil'] != null ? "${(((detailRiwayat['hasil'] as num) * 1000).floor() / 10).toStringAsFixed(1)}%" : "-"}',
style: TextStyle(fontSize: 16),
),
SizedBox(height: 8),

View File

@ -56,9 +56,14 @@ class _DiagnosaPageState extends State<DiagnosaPage> {
}
void prosesHasilDiagnosa() async {
if (gejalaTerpilihIds.isEmpty) {
// Validasi minimal 3 gejala
if (gejalaTerpilihIds.length < 3) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Silakan pilih minimal satu gejala')),
SnackBar(
content: Text('Silakan pilih minimal 3 gejala untuk melakukan diagnosa'),
backgroundColor: Color(0xFF9DC08D),
duration: Duration(seconds: 3),
),
);
return;
}
@ -175,10 +180,30 @@ class _DiagnosaPageState extends State<DiagnosaPage> {
),
),
Divider(color: Colors.grey),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Gejala Terpilih",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: gejalaTerpilihIds.length >= 3 ? Colors.green : Colors.green,
borderRadius: BorderRadius.circular(12),
),
child: Text(
"${gejalaTerpilihIds.length}/min 3",
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
SizedBox(height: 5),
SizedBox(
height: 100,
@ -202,15 +227,21 @@ class _DiagnosaPageState extends State<DiagnosaPage> {
height: 30,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
backgroundColor: gejalaTerpilihIds.length >= 3 ? Colors.green : Colors.grey,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
onPressed: prosesHasilDiagnosa,
onPressed: gejalaTerpilihIds.length >= 3 ? prosesHasilDiagnosa : null,
child: Text(
"Lihat Hasil Diagnosa",
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
gejalaTerpilihIds.length >= 3
? "Lihat Hasil Diagnosa"
: "Pilih minimal 3 gejala",
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold
),
),
),
),

View File

@ -257,10 +257,10 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
return;
}
if (passwordController.text.length < 6) {
if (passwordController.text.length < 8) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Password minimal 6 karakter.'),
content: Text('Password minimal 8 karakter.'),
backgroundColor: Colors.red,
),
);

View File

@ -614,6 +614,12 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
return _buildEmptyResult('Tidak ada kemungkinan ${type} lainnya');
}
// Get the probability of the main diagnosis result
double? mainDiagnosisProbability;
if (hasilTertinggi != null) {
mainDiagnosisProbability = _getProbabilitas(hasilTertinggi);
}
// Filter out items with 100% probability and the top result
List otherItems = [];
@ -626,7 +632,7 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
topResultId = hasilTertinggi['id_hama']?.toString();
}
// Filter out the top result AND items with 100% probability
// Filter out the top result AND items with 100% probability AND items with higher probability than main diagnosis
otherItems = itemList.where((item) {
String? itemId;
if (type == 'penyakit') {
@ -640,12 +646,19 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
return false;
}
// Skip if this item has 100% probability
// Get item probability
double itemProbabilitas = _getProbabilitas(item);
// Skip if this item has 100% probability
if ((itemProbabilitas * 100).round() == 100) {
return false;
}
// Skip if this item has higher probability than main diagnosis
if (mainDiagnosisProbability != null && itemProbabilitas > mainDiagnosisProbability) {
return false;
}
return true;
}).toList();
} else {
@ -975,7 +988,7 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
),
child: Center(
child: Text(
'${(value * 100).toStringAsFixed(0)}%',
'${((value * 1000).floor()/10).toStringAsFixed(1)}%',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
@ -1055,7 +1068,7 @@ Widget _buildProbabilityIndicator(double value) {
),
child: Center(
child: Text(
'${(value * 100).toStringAsFixed(0)}%',
'${((value * 1000).floor()/10).toStringAsFixed(1)}%',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),

View File

@ -62,7 +62,7 @@ class _LoginPageState extends State<LoginPage> {
}
try {
var url = Uri.parse("https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/auth/login");
var url = Uri.parse("https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/auth/login");
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},

View File

@ -63,7 +63,7 @@ class _ProfilPageState extends State<ProfilPage> {
}
// Buat URL untuk endpoint user API
var url = Uri.parse("https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/users");
var url = Uri.parse("https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/users");
// Kirim permintaan GET dengan token autentikasi
var response = await http.get(
@ -135,7 +135,6 @@ class _ProfilPageState extends State<ProfilPage> {
_nameController.text = userData?['name'] ?? '';
_emailController.text = userData?['email'] ?? '';
_alamatController.text = userData?['alamat'] ?? '';
_nomorTeleponController.text = userData?['nomorTelepon'] ?? '';
_passwordController.text = ''; // Empty for security
showDialog(
@ -247,26 +246,26 @@ class _ProfilPageState extends State<ProfilPage> {
: null,
),
SizedBox(height: 16),
TextFormField(
controller: _nomorTeleponController,
decoration: InputDecoration(
labelText: 'Nomor Telepon',
prefixIcon: Icon(Icons.phone, color: Color(0xFF9DC08D)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Color(0xFF9DC08D)),
),
),
keyboardType: TextInputType.phone,
validator:
(value) =>
value?.isEmpty ?? true
? 'Nomor telepon tidak boleh kosong'
: null,
),
// TextFormField(
// controller: _nomorTeleponController,
// decoration: InputDecoration(
// labelText: 'Nomor Telepon',
// prefixIcon: Icon(Icons.phone, color: Color(0xFF9DC08D)),
// border: OutlineInputBorder(
// borderRadius: BorderRadius.circular(8),
// ),
// focusedBorder: OutlineInputBorder(
// borderRadius: BorderRadius.circular(8),
// borderSide: BorderSide(color: Color(0xFF9DC08D)),
// ),
// ),
// keyboardType: TextInputType.phone,
// validator:
// (value) =>
// value?.isEmpty ?? true
// ? 'Nomor telepon tidak boleh kosong'
// : null,
// ),
],
),
),

View File

@ -94,7 +94,9 @@ class _RiwayatDiagnosaPageState extends State<RiwayatDiagnosaPage> {
Future<void> _fetchUserData() async {
try {
// Buat URL untuk endpoint user API
var url = Uri.parse("https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/users");
var url = Uri.parse(
"https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/users",
);
// Kirim permintaan GET dengan token autentikasi
var response = await http.get(
@ -489,7 +491,7 @@ class _RiwayatDiagnosaPageState extends State<RiwayatDiagnosaPage> {
),
SizedBox(height: 4),
Text(
'Hasil: ${(riwayat['hasil'] as num?)?.toStringAsFixed(2) ?? "-"}',
'Hasil: ${riwayat['hasil'] != null ? "${(((riwayat['hasil'] as num) * 1000).floor() / 10).toStringAsFixed(1)}%" : "-"}',
style: TextStyle(fontSize: 14),
),
SizedBox(height: 12),