diff --git a/.phpunit.result.cache b/.phpunit.result.cache
deleted file mode 100644
index 902a026..0000000
--- a/.phpunit.result.cache
+++ /dev/null
@@ -1 +0,0 @@
-{"version":2,"defects":{"Tests\\Unit\\RekomendasiAlgorithmTest::test_prestasi_scoring_tinggi":7,"Tests\\Unit\\RekomendasiAlgorithmTest::test_prestasi_scoring_sedang":7,"Tests\\Feature\\Auth\\RegistrationTest::test_new_users_can_register":7,"Tests\\Feature\\RekomendasiTest::test_high_math_and_coding_prefers_teknologi_informasi":7,"Tests\\Feature\\RekomendasiTest::test_high_language_prefers_bahasa_komunikasi":7,"Tests\\Feature\\ExplainableRecommendationTest::test_explanation_contains_meaningful_text":7,"Tests\\Feature\\ExplainableRecommendationTest::test_explanation_displayed_in_view":7,"Tests\\Feature\\Auth\\PasswordResetMessageTest::test_forgot_password_shows_indonesian_error_for_invalid_email":7,"Tests\\Feature\\Auth\\PasswordResetMessageTest::test_forgot_password_form_displays_error":8,"Tests\\Feature\\ExplainableRecommendationTest::test_scoring_detail_stored_correctly":7,"Tests\\Feature\\ExplainableRecommendationTest::test_all_recommendations_have_explanations":7,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_authenticate_using_the_login_screen":7,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_logout":7,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_can_be_confirmed":7,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_is_not_confirmed_with_invalid_password":7,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_can_be_requested":7,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_screen_can_be_rendered":7,"Tests\\Feature\\Auth\\PasswordResetTest::test_password_can_be_reset_with_valid_token":7,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_password_can_be_updated":7,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_correct_password_must_be_provided_to_update_password":7,"Tests\\Feature\\CrudValidationTest::test_admin_can_add_jurusan_data":8,"Tests\\Feature\\CrudValidationTest::test_bk_can_add_jurusan_data":8,"Tests\\Feature\\CrudValidationTest::test_admin_guru_bk_store_validates_email_and_password":8,"Tests\\Feature\\CrudValidationTest::test_rekomendasi_ipa_requires_all_ipa_scores":7,"Tests\\Feature\\CrudValidationTest::test_admin_student_detail_only_accepts_siswa_id":8,"Tests\\Feature\\ProfileTest::test_profile_information_can_be_updated":7,"Tests\\Feature\\ProfileTest::test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged":7,"Tests\\Feature\\ProfileTest::test_user_can_delete_their_account":7,"Tests\\Feature\\ProfileTest::test_correct_password_must_be_provided_to_delete_account":7},"times":{"Tests\\Feature\\RekomendasiTest::test_high_math_and_coding_prefers_teknologi_informasi":0.067,"Tests\\Feature\\RekomendasiTest::test_high_language_prefers_bahasa_komunikasi":0.051,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.01,"Tests\\Unit\\RekomendasiAlgorithmTest::test_minat_mapping_logika_komputer":0.019,"Tests\\Unit\\RekomendasiAlgorithmTest::test_minat_mapping_alam_tanaman":0,"Tests\\Unit\\RekomendasiAlgorithmTest::test_minat_mapping_bisnis":0.001,"Tests\\Unit\\RekomendasiAlgorithmTest::test_nilai_kategori_tinggi":0.001,"Tests\\Unit\\RekomendasiAlgorithmTest::test_nilai_kategori_sedang":0,"Tests\\Unit\\RekomendasiAlgorithmTest::test_nilai_kategori_rendah":0.001,"Tests\\Unit\\RekomendasiAlgorithmTest::test_prestasi_scoring_tinggi":0.003,"Tests\\Unit\\RekomendasiAlgorithmTest::test_prestasi_scoring_sedang":0.001,"Tests\\Unit\\RekomendasiAlgorithmTest::test_prestasi_scoring_minimal":0.001,"Tests\\Feature\\Auth\\AuthenticationTest::test_login_screen_can_be_rendered":0.126,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_authenticate_using_the_login_screen":0.104,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_not_authenticate_with_invalid_password":0.014,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_logout":0.02,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_verification_screen_can_be_rendered":0.041,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_can_be_verified":0.099,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_is_not_verified_with_invalid_hash":0.034,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_confirm_password_screen_can_be_rendered":0.029,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_can_be_confirmed":0.018,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_is_not_confirmed_with_invalid_password":0.039,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_screen_can_be_rendered":0.023,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_can_be_requested":0.024,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_screen_can_be_rendered":0.016,"Tests\\Feature\\Auth\\PasswordResetTest::test_password_can_be_reset_with_valid_token":0.017,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_password_can_be_updated":0.039,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_correct_password_must_be_provided_to_update_password":0.036,"Tests\\Feature\\Auth\\RegistrationTest::test_registration_screen_can_be_rendered":0.013,"Tests\\Feature\\Auth\\RegistrationTest::test_new_users_can_register":0.014,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.009,"Tests\\Feature\\ProfileTest::test_profile_page_is_displayed":0.017,"Tests\\Feature\\ProfileTest::test_profile_information_can_be_updated":0.03,"Tests\\Feature\\ProfileTest::test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged":0.032,"Tests\\Feature\\ProfileTest::test_user_can_delete_their_account":0.035,"Tests\\Feature\\ProfileTest::test_correct_password_must_be_provided_to_delete_account":0.034,"Tests\\Feature\\CrudValidationTest::test_admin_can_add_jurusan_data":0.105,"Tests\\Feature\\CrudValidationTest::test_bk_can_add_jurusan_data":0.01,"Tests\\Feature\\CrudValidationTest::test_admin_guru_bk_store_validates_email_and_password":0.011,"Tests\\Feature\\CrudValidationTest::test_rekomendasi_ipa_requires_all_ipa_scores":0.1,"Tests\\Feature\\CrudValidationTest::test_admin_student_detail_only_accepts_siswa_id":0.008,"Tests\\Feature\\ExplainableRecommendationTest::test_recommendation_includes_explanation":0.016,"Tests\\Feature\\ExplainableRecommendationTest::test_explanation_contains_meaningful_text":0.029,"Tests\\Feature\\ExplainableRecommendationTest::test_scoring_detail_stored_correctly":0.031,"Tests\\Feature\\ExplainableRecommendationTest::test_all_recommendations_have_explanations":0.025,"Tests\\Feature\\ExplainableRecommendationTest::test_explanation_displayed_in_view":0.023,"Tests\\Feature\\Auth\\PasswordResetMessageTest::test_forgot_password_shows_indonesian_error_for_invalid_email":0.059,"Tests\\Feature\\Auth\\PasswordResetMessageTest::test_forgot_password_form_displays_error":0.019}}
\ No newline at end of file
diff --git a/app/Http/Controllers/ChatbotController.php b/app/Http/Controllers/ChatbotController.php
index 30f3316..d5a33f9 100644
--- a/app/Http/Controllers/ChatbotController.php
+++ b/app/Http/Controllers/ChatbotController.php
@@ -118,6 +118,7 @@ public function send(Request $request)
'recommendation' => $recentRecommendation['jurusan'] ?? null,
'score' => isset($recentRecommendation['skor']) ? number_format(($recentRecommendation['skor'] > 1 ? $recentRecommendation['skor'] : $recentRecommendation['skor'] * 100), 1) : null,
'top3' => $recentRecommendation['top3'] ?? [],
+ 'intent' => $this->detectIntent($message),
'profile' => [
'nama' => $user->name,
'kelompok' => $user->kelompok_asal ?? null,
@@ -339,4 +340,48 @@ private function stripMarkdown(string $text): string
$text = preg_replace('/`(.*?)`/s', '$1', $text);
return $text;
}
+
+ private function detectIntent(string $message): string
+ {
+ $message = strtolower($message);
+
+ if (
+ str_contains($message, 'banding') ||
+ str_contains($message, 'beda') ||
+ str_contains($message, 'vs') ||
+ str_contains($message, 'dibanding')
+ ) {
+ return 'compare_majors';
+ }
+
+ if (
+ str_contains($message, 'jelaskan semua') ||
+ str_contains($message, 'semua jurusan')
+ ) {
+ return 'explain_all_majors';
+ }
+
+ if (
+ str_contains($message, 'lanjut') ||
+ str_contains($message, 'yang tadi') ||
+ str_contains($message, 'yang sebelumnya') ||
+ str_contains($message, 'maksudnya')
+ ) {
+ return 'follow_up';
+ }
+
+ if (str_contains($message, 'kenapa') || str_contains($message, 'mengapa')) {
+ return 'ask_reason';
+ }
+
+ if (
+ str_contains($message, 'prospek') ||
+ str_contains($message, 'karir') ||
+ str_contains($message, 'kerja')
+ ) {
+ return 'ask_career';
+ }
+
+ return 'general';
+ }
}
diff --git a/app/Http/Controllers/RekomendasiController.php b/app/Http/Controllers/RekomendasiController.php
index 535e29d..793d1f1 100644
--- a/app/Http/Controllers/RekomendasiController.php
+++ b/app/Http/Controllers/RekomendasiController.php
@@ -32,7 +32,7 @@ public function index()
* Generate textual explanation untuk setiap kriteria
* Menjelaskan "mengapa jurusan ini cocok" berdasarkan scoring detail
*/
- private function generateExplanation($jurusanNama, $detail, $katNilai, $kategoriMinat, $prefStudi, $prestasi)
+ private function generateExplanation($jurusanNama, $detail, $katNilai, $kategoriMinat, $prefStudi, $prestasi, array $prestasiAnalysis = [])
{
$explanations = [];
@@ -78,12 +78,25 @@ private function generateExplanation($jurusanNama, $detail, $katNilai, $kategori
// 5. Penjelasan Prestasi
$skorPrestasi = $detail['prestasi'] ?? 0;
+ if (!($prestasiAnalysis['provided'] ?? false)) {
+ $explanations['prestasi'] = "ℹ️ Prestasi tidak diisi, sehingga atribut prestasi tidak dihitung pada proses skoring.";
+ return $explanations;
+ }
+
+ $levelPrestasi = $prestasiAnalysis['level'] ?? 'minimal';
+ $labelLevel = [
+ 'tinggi' => 'tinggi',
+ 'sedang' => 'menengah',
+ 'cukup' => 'dasar',
+ 'minimal' => 'minimal',
+ ][$levelPrestasi] ?? 'minimal';
+
if ($skorPrestasi >= 0.7) {
- $explanations['prestasi'] = "✅ Prestasi Anda mencerminkan potensi kuat untuk sukses dan berkembang di jurusan ini.";
+ $explanations['prestasi'] = "✅ Prestasi Anda terdeteksi pada level {$labelLevel} dan memberi kontribusi kuat untuk kecocokan jurusan ini.";
} elseif ($skorPrestasi >= 0.4) {
- $explanations['prestasi'] = "✓ Prestasi Anda menunjukkan kemampuan dasar yang memadai dan relevan.";
+ $explanations['prestasi'] = "✓ Prestasi Anda berada pada level {$labelLevel} dan tetap dipertimbangkan sebagai faktor pendukung.";
} else {
- $explanations['prestasi'] = "ℹ️ Prestasi tidak menjadi hambatan untuk mengembangkan diri dan berkembang di jurusan ini.";
+ $explanations['prestasi'] = "ℹ️ Input prestasi tetap dihitung (level {$labelLevel}), namun saat ini kontribusinya relatif kecil dibanding faktor utama lain.";
}
return $explanations;
@@ -91,6 +104,40 @@ private function generateExplanation($jurusanNama, $detail, $katNilai, $kategori
public function proses(Request $request)
{
+ // Pastikan perhitungan Naive Bayes selalu berbasis 5 atribut input rekomendasi:
+ // 1) Nilai akademik, 2) Minat, 3) Preferensi studi, 4) Cita-cita, 5) Prestasi.
+ $user = Auth::user();
+ $kelompokAsal = strtoupper($user->kelompok_asal ?? 'IPA');
+
+ $rules = [
+ 'mtk' => 'nullable|numeric|min:0|max:100',
+ 'fisika' => 'nullable|numeric|min:0|max:100',
+ 'kimia' => 'nullable|numeric|min:0|max:100',
+ 'biologi' => 'nullable|numeric|min:0|max:100',
+ 'ekonomi' => 'nullable|numeric|min:0|max:100',
+ 'geografi' => 'nullable|numeric|min:0|max:100',
+ 'sosiologi' => 'nullable|numeric|min:0|max:100',
+ 'sejarah' => 'nullable|numeric|min:0|max:100',
+ 'minat' => 'required|string|max:255',
+ 'pref_studi' => 'required|string',
+ 'cita_cita' => 'required|string|max:255',
+ 'prestasi' => 'nullable|string|max:255',
+ ];
+
+ if ($kelompokAsal === 'IPA') {
+ $rules['mtk'] = 'required|numeric|min:0|max:100';
+ $rules['fisika'] = 'required|numeric|min:0|max:100';
+ $rules['kimia'] = 'required|numeric|min:0|max:100';
+ $rules['biologi'] = 'required|numeric|min:0|max:100';
+ } else {
+ $rules['ekonomi'] = 'required|numeric|min:0|max:100';
+ $rules['geografi'] = 'required|numeric|min:0|max:100';
+ $rules['sosiologi'] = 'required|numeric|min:0|max:100';
+ $rules['sejarah'] = 'required|numeric|min:0|max:100';
+ }
+
+ $validated = $request->validate($rules);
+
// --- 1. PREPROCESSING NILAI (Kriteria 1: Akademik) ---
$scores = $request->only(['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah']);
$validScores = array_filter($scores);
@@ -107,23 +154,29 @@ public function proses(Request $request)
}
// --- 2. ANALISIS MINAT (Kriteria 2) ---
- $minatRaw = strtolower($request->minat ?? '');
+ $minatInput = trim((string) ($validated['minat'] ?? ''));
+ $minatRaw = strtolower($minatInput);
$minatMapped = $this->mapMinat($minatRaw);
- // --- 3. ANALISIS CITA-CITA (Kriteria 3) ---
- $citaRaw = strtolower($request->cita_cita ?? '');
- $citaMapped = $this->mapCitaCita($citaRaw);
-
- // --- 4. PEMETAAN PREFERENSI STUDI (Kriteria 4) ---
- $prefStudi = $request->pref_studi ?? 'Blended';
+ // --- 3. PREFERENSI STUDI LANJUTAN (Kriteria 3) ---
+ $prefStudi = $validated['pref_studi'];
$prefMapping = config('polije.pref_mapping', []);
+ // --- 4. ANALISIS CITA-CITA (Kriteria 4) ---
+ $citaInput = trim((string) ($validated['cita_cita'] ?? ''));
+ $citaRaw = strtolower($citaInput);
+ $citaMapped = $this->mapCitaCita($citaRaw);
+
// --- 5. ANALISIS PRESTASI (Kriteria 5) ---
- $prestasiRaw = strtolower($request->prestasi ?? '');
- $prestasiScore = $this->scorePrestasiScore($prestasiRaw);
+ $prestasiInput = trim((string) ($validated['prestasi'] ?? ''));
+ $isPrestasiFilled = $prestasiInput !== '';
+ $prestasiRaw = strtolower($prestasiInput);
+ $prestasiAnalysis = $this->analyzePrestasi($prestasiRaw);
+ $prestasiScore = $prestasiAnalysis['score'];
// --- 6. PERHITUNGAN NAIVE BAYES BERBOBOT ---
$cfg = config('polije.criteria', []);
+ $majorMap = PolijeMajor::all()->keyBy('nama_jurusan');
$logPosteriors = [];
$detailPerJurusan = [];
$epsilon = 1e-9;
@@ -135,16 +188,43 @@ public function proses(Request $request)
// Weights dan match probabilities
$weights = $c['weights'] ?? ['nilai' => 0.40, 'minat' => 0.35, 'pref' => 0.15, 'prestasi' => 0.05, 'cita_cita' => 0.05];
+
+ // Jika prestasi kosong, atribut prestasi tidak dihitung.
+ if (!$isPrestasiFilled) {
+ $weights['prestasi'] = 0.0;
+ $sumNonPrestasi = ($weights['nilai'] ?? 0) + ($weights['minat'] ?? 0) + ($weights['pref'] ?? 0) + ($weights['cita_cita'] ?? 0);
+ if ($sumNonPrestasi > 0) {
+ $weights['nilai'] = ($weights['nilai'] ?? 0) / $sumNonPrestasi;
+ $weights['minat'] = ($weights['minat'] ?? 0) / $sumNonPrestasi;
+ $weights['pref'] = ($weights['pref'] ?? 0) / $sumNonPrestasi;
+ $weights['cita_cita'] = ($weights['cita_cita'] ?? 0) / $sumNonPrestasi;
+ }
+ }
$matchProb = $c['match_prob'] ?? ['nilai' => 0.80, 'minat' => 0.90, 'pref' => 0.85, 'prestasi' => 0.65, 'cita_cita' => 0.85];
// 1. Likelihood untuk Nilai
- $p_nilai = ($katNilai == ($c['nilai'] ?? 'Sedang')) ? $matchProb['nilai'] : max(1 - $matchProb['nilai'], $epsilon);
+ // Tetap berbasis atribut Nilai Akademik, namun dibuat lebih granular:
+ // kombinasi kategori nilai + kecocokan bobot mapel per jurusan (dari data PolijeMajor).
+ $p_nilai_category = ($katNilai == ($c['nilai'] ?? 'Sedang'))
+ ? $matchProb['nilai']
+ : max(1 - $matchProb['nilai'], $epsilon);
+ $p_nilai_subject = $this->scoreSubjectFitLikelihood(
+ $majorMap[$jurusan]->bobot_mapel ?? [],
+ $scores,
+ $p_nilai_category
+ );
+ $p_nilai = max(0.05, min(0.98, (0.6 * $p_nilai_category) + (0.4 * $p_nilai_subject)));
// 2. Likelihood untuk Minat
- $p_minat = ($minatMapped == ($c['minat'] ?? 'Umum')) ? $matchProb['minat'] : max(1 - $matchProb['minat'], $epsilon);
+ $p_minat = $this->scoreMinatLikelihood(
+ $minatRaw,
+ $minatMapped,
+ $c['minat'] ?? 'Umum',
+ $matchProb['minat']
+ );
// 3. Likelihood untuk Preferensi Studi
- $prefList = $c['pref'] ?? ['Praktik Langsung', 'DuDi', 'Project Based'];
+ $prefList = $c['pref'] ?? ['Sains & Teknologi', 'Pertanian & Lingkungan', 'Kesehatan & Ilmu Hayat', 'Bisnis & Manajemen', 'Sosial & Humaniora'];
if (!is_array($prefList)) {
$prefList = [$prefList];
}
@@ -152,19 +232,14 @@ public function proses(Request $request)
// 4. Likelihood untuk Cita-cita
$citaCitaKeywords = $c['cita_cita_keywords'] ?? [];
- $matchCitaCita = false;
- if (!empty($citaCitaKeywords)) {
- foreach ($citaCitaKeywords as $keyword) {
- if (stripos($citaMapped, $keyword) !== false) {
- $matchCitaCita = true;
- break;
- }
- }
- }
- $p_cita_cita = $matchCitaCita ? $matchProb['cita_cita'] : max(1 - $matchProb['cita_cita'], $epsilon);
+ $p_cita_cita = $this->scoreKeywordLikelihood($citaMapped, $citaCitaKeywords, $matchProb['cita_cita']);
- // 5. Likelihood untuk Prestasi (boost jika ada prestasi)
- $p_prestasi = ($prestasiScore > 0.5) ? $matchProb['prestasi'] : max(1 - $matchProb['prestasi'], $epsilon);
+ // 5. Likelihood untuk Prestasi (bertingkat: tinggi/menengah/dasar/minimal)
+ $p_prestasi = $this->scorePrestasiLikelihood(
+ $prestasiAnalysis,
+ $citaCitaKeywords,
+ $matchProb['prestasi']
+ );
// Simpan detail per kriteria untuk tampilan
$detailPerJurusan[$jurusan] = [
@@ -172,7 +247,7 @@ public function proses(Request $request)
'minat' => round($p_minat, 4),
'pref' => round($p_pref, 4),
'cita' => round($p_cita_cita, 4),
- 'prestasi' => round($p_prestasi, 4),
+ 'prestasi' => $isPrestasiFilled ? round($p_prestasi, 4) : null,
];
// Hitung log-likelihood dengan bobot
@@ -180,8 +255,11 @@ public function proses(Request $request)
($weights['nilai'] ?? 0) * log(max($p_nilai, $epsilon)) +
($weights['minat'] ?? 0) * log(max($p_minat, $epsilon)) +
($weights['pref'] ?? 0) * log(max($p_pref, $epsilon)) +
- ($weights['cita_cita'] ?? 0) * log(max($p_cita_cita, $epsilon)) +
- ($weights['prestasi'] ?? 0) * log(max($p_prestasi, $epsilon));
+ ($weights['cita_cita'] ?? 0) * log(max($p_cita_cita, $epsilon));
+
+ if (($weights['prestasi'] ?? 0) > 0) {
+ $logLikelihood += ($weights['prestasi'] ?? 0) * log(max($p_prestasi, $epsilon));
+ }
$logPosteriors[$jurusan] = $logPrior + $logLikelihood;
}
@@ -205,7 +283,8 @@ public function proses(Request $request)
$katNilai,
$minatMapped,
$prefStudi,
- $prestasiRaw
+ $prestasiRaw,
+ $prestasiAnalysis
);
$hasilAkhir[] = [
'jurusan' => $jurusan,
@@ -234,10 +313,11 @@ public function proses(Request $request)
'geografi' => $request->geografi ?? null,
'sosiologi' => $request->sosiologi ?? null,
'sejarah' => $request->sejarah ?? null,
- 'minat' => $request->minat ?? null,
- 'preferensi_studi' => $request->pref_studi ?? null,
- 'cita_cita' => $request->cita_cita ?? null,
- 'prestasi' => $request->prestasi ?? null,
+ 'minat' => $minatInput,
+ 'preferensi_studi' => $prefStudi,
+ 'cita_cita' => $citaInput,
+ // Kolom prestasi di DB bersifat NOT NULL, jadi simpan string kosong jika tidak diisi.
+ 'prestasi' => $prestasiInput,
'hasil_rekomendasi' => $hasilAkhir,
]);
}
@@ -256,7 +336,13 @@ public function proses(Request $request)
]);
}
- return view('rekomendasi.hasil', compact('hasilAkhir', 'katNilai', 'minatMapped', 'citaMapped', 'prefStudi', 'prestasiScore'));
+ // Ambil data jurusan teratas dari database untuk deskripsi/prospek pada halaman hasil
+ $topJurusan = null;
+ if (count($hasilAkhir) > 0) {
+ $topJurusan = PolijeMajor::where('nama_jurusan', $hasilAkhir[0]['jurusan'] ?? '')->first();
+ }
+
+ return view('rekomendasi.hasil', compact('hasilAkhir', 'katNilai', 'average', 'minatMapped', 'citaMapped', 'prefStudi', 'prestasiScore', 'topJurusan', 'isPrestasiFilled'));
}
/**
@@ -313,6 +399,153 @@ private function scorePrestasiScore(string $prestasiRaw): float
return $prestasiScore;
}
+ /**
+ * Ringkas level prestasi dari input teks agar lebih transparan.
+ */
+ private function analyzePrestasi(string $prestasiRaw): array
+ {
+ if (empty(trim($prestasiRaw))) {
+ return [
+ 'provided' => false,
+ 'level' => 'minimal',
+ 'score' => 0.0,
+ 'raw' => '',
+ ];
+ }
+
+ $text = strtolower(trim($prestasiRaw));
+ $level = 'minimal';
+
+ if (preg_match('/(juara|menang|champion|first|gold|emas|terbaik)/', $text)) {
+ $level = 'tinggi';
+ } elseif (preg_match('/(finalis|semifinal|peringkat|ranking|podium|medali|silver|perak)/', $text)) {
+ $level = 'sedang';
+ } elseif (preg_match('/(sertifikat|training|kursus|workshop|peserta|mengikuti)/', $text)) {
+ $level = 'cukup';
+ }
+
+ return [
+ 'provided' => true,
+ 'level' => $level,
+ 'score' => $this->scorePrestasiScore($text),
+ 'raw' => $text,
+ ];
+ }
+
+ /**
+ * Skor atribut teks berdasarkan coverage keyword (0..1) lalu dipetakan menjadi likelihood.
+ */
+ private function scoreKeywordLikelihood(string $text, array $keywords, float $matchProb): float
+ {
+ if (empty($keywords)) {
+ return 0.50;
+ }
+
+ $coverage = $this->keywordCoverage($text, $keywords);
+
+ // Base 0.2 agar tidak 0 total, lalu naik proporsional coverage.
+ $likelihood = 0.20 + ($coverage * ($matchProb - 0.20));
+
+ return max(0.05, min(0.98, $likelihood));
+ }
+
+ private function scoreMinatLikelihood(string $minatRaw, string $minatMapped, string $targetMinat, float $matchProb): float
+ {
+ $keywordBank = [
+ 'Logika & Komputer' => ['coding', 'programming', 'komputer', 'software', 'web', 'data', 'ai', 'digital'],
+ 'Alam & Tanaman' => ['pertanian', 'tanaman', 'kebun', 'sawah', 'alam', 'peternakan', 'agribisnis'],
+ 'Pelayanan & Kesehatan' => ['kesehatan', 'medis', 'gizi', 'perawat', 'dokter', 'klinik', 'rumah sakit'],
+ 'Manajemen & Bisnis' => ['bisnis', 'usaha', 'marketing', 'keuangan', 'manajemen', 'akuntansi', 'entrepreneur'],
+ 'Mesin & Listrik' => ['mesin', 'listrik', 'teknik', 'otomasi', 'elektronik', 'mekanik', 'industri'],
+ ];
+
+ $targetKeywords = $keywordBank[$targetMinat] ?? [];
+ $coverage = $this->keywordCoverage($minatRaw, $targetKeywords);
+ $categoryMatch = ($minatMapped === $targetMinat) ? 1.0 : 0.0;
+
+ // Kombinasi semantic match + category match.
+ $combined = (0.6 * $coverage) + (0.4 * $categoryMatch);
+ $likelihood = 0.20 + ($combined * ($matchProb - 0.20));
+
+ return max(0.05, min(0.98, $likelihood));
+ }
+
+ private function scorePrestasiLikelihood(array $prestasiAnalysis, array $jurusanKeywords, float $matchProb): float
+ {
+ $baseScore = $prestasiAnalysis['score'] ?? 0.0; // 0..1
+
+ if ($baseScore <= 0.0) {
+ return 0.20;
+ }
+
+ // Relevansi prestasi terhadap konteks jurusan (dari teks prestasi vs keyword jurusan)
+ $relevance = 0.0;
+ if (!empty($jurusanKeywords)) {
+ $relevance = $this->keywordCoverage($prestasiAnalysis['level'] . ' ' . ($prestasiAnalysis['raw'] ?? ''), $jurusanKeywords);
+ }
+
+ // Prestasi level dominan, relevance sebagai penguat.
+ $combined = (0.75 * $baseScore) + (0.25 * $relevance);
+ $likelihood = 0.20 + ($combined * ($matchProb - 0.20));
+
+ return max(0.05, min(0.98, $likelihood));
+ }
+
+ private function keywordCoverage(string $text, array $keywords): float
+ {
+ $text = strtolower(trim($text));
+ if ($text === '' || empty($keywords)) {
+ return 0.0;
+ }
+
+ $matched = 0;
+ foreach (array_unique($keywords) as $keyword) {
+ if ($keyword !== '' && str_contains($text, strtolower($keyword))) {
+ $matched++;
+ }
+ }
+
+ // Normalisasi agar tidak terlalu menghukum list keyword yang panjang.
+ $denominator = max(1, min(count(array_unique($keywords)), 6));
+
+ return min(1.0, $matched / $denominator);
+ }
+
+ /**
+ * Hitung kecocokan nilai mapel terhadap bobot mapel jurusan (0..1) lalu ubah ke likelihood.
+ * Ini tetap bagian dari atribut "Nilai Akademik" agar tidak keluar dari 5 atribut.
+ */
+ private function scoreSubjectFitLikelihood(array $bobotMapel, array $scores, float $fallback): float
+ {
+ if (empty($bobotMapel)) {
+ return max(0.05, min(0.98, $fallback));
+ }
+
+ $weighted = 0.0;
+ $weightSum = 0.0;
+
+ foreach ($bobotMapel as $mapel => $w) {
+ $nilai = $scores[$mapel] ?? null;
+ if ($nilai === null || $nilai === '') {
+ continue;
+ }
+
+ $num = (float) $nilai;
+ $normalized = max(0.0, min(1.0, $num / 100));
+ $weighted += $normalized * (float) $w;
+ $weightSum += (float) $w;
+ }
+
+ if ($weightSum <= 0) {
+ return max(0.05, min(0.98, $fallback));
+ }
+
+ $fitScore = $weighted / $weightSum; // 0..1
+
+ // Map ke likelihood agar tidak terlalu ekstrem.
+ return max(0.05, min(0.98, 0.25 + (0.70 * $fitScore)));
+ }
+
/**
* Tampilkan history rekomendasi
*/
diff --git a/app/Services/GeminiService.php b/app/Services/GeminiService.php
index e71d76b..c5b390f 100644
--- a/app/Services/GeminiService.php
+++ b/app/Services/GeminiService.php
@@ -33,6 +33,17 @@ public function chat($message, $context = [], $chatHistory = [])
];
}
+ // Intent router: mode perbandingan jurusan ditangani terstruktur agar konsisten.
+ if (($context['intent'] ?? '') === 'compare_majors') {
+ $comparison = $this->buildStructuredComparisonResponse($message, $context);
+ if (!empty($comparison)) {
+ return [
+ 'success' => true,
+ 'message' => $comparison,
+ ];
+ }
+ }
+
$systemPrompt = $this->buildSystemPrompt($context);
// Build multi-turn conversation for Gemini
@@ -102,7 +113,7 @@ public function chat($message, $context = [], $chatHistory = [])
// All models failed
Log::error('All Gemini models failed, using fallback');
- return $this->getFallbackResponse($message, $context);
+ return $this->getFallbackResponse($message, $context, $chatHistory);
} catch (\Exception $e) {
Log::error('Gemini Service Exception', [
@@ -111,18 +122,68 @@ public function chat($message, $context = [], $chatHistory = [])
'line' => $e->getLine()
]);
- return $this->getFallbackResponse($message, $context);
+ return $this->getFallbackResponse($message, $context, $chatHistory);
}
}
- protected function getFallbackResponse($message, $context = [])
+ protected function getFallbackResponse($message, $context = [], $chatHistory = [])
{
$jurusan = $context['recommendation'] ?? null;
$score = isset($context['score']) ? floatval($context['score']) : 0;
$hasRecommendation = !empty($jurusan);
+ if (($context['intent'] ?? '') === 'compare_majors') {
+ $comparison = $this->buildStructuredComparisonResponse($message, $context);
+ if (!empty($comparison)) {
+ return [
+ 'success' => true,
+ 'message' => $comparison,
+ ];
+ }
+ }
+
+ $major = null;
+ if ($hasRecommendation) {
+ $major = PolijeMajor::where('nama_jurusan', $jurusan)->first();
+ }
+ $majorDesc = $major->deskripsi ?? null;
+ $majorProspek = $major->prospek_kerja ?? null;
+
// Keyword-based responses
$messageLower = strtolower($message);
+ $lastAiMessage = $this->getLastAssistantMessage($chatHistory);
+
+ // Tangani pertanyaan lanjutan agar tetap nyambung saat fallback aktif
+ if ($this->isFollowUpMessage($messageLower)) {
+ if (
+ strpos($messageLower, 'jelaskan semua') !== false ||
+ strpos($messageLower, 'semua jurusan') !== false ||
+ strpos($messageLower, 'satu satu') !== false ||
+ strpos($messageLower, 'satu-satu') !== false
+ ) {
+ return [
+ 'success' => true,
+ 'message' => $this->buildAllMajorsResponse($hasRecommendation, $jurusan, $score),
+ ];
+ }
+
+ if ($hasRecommendation) {
+ $parts = [];
+ $parts[] = "Menindaklanjuti pembahasan sebelumnya, fokus utama Anda saat ini tetap pada jurusan \"{$jurusan}\" dengan skor kesesuaian {$score}%.";
+ if (!empty($majorDesc)) {
+ $parts[] = "Fokus pembelajaran jurusan: {$majorDesc}";
+ }
+ if (!empty($majorProspek)) {
+ $parts[] = "Prospek kerja yang relevan: {$majorProspek}";
+ }
+ $parts[] = "Jika Anda berkenan, saya dapat lanjutkan secara bertahap: (1) kompetensi yang harus dipersiapkan, (2) mata kuliah inti, dan (3) perbandingan dengan alternatif jurusan lain.";
+
+ return [
+ 'success' => true,
+ 'message' => implode(' ', $parts),
+ ];
+ }
+ }
if (strpos($messageLower, 'halo') !== false || strpos($messageLower, 'hai') !== false || strpos($messageLower, 'hallo') !== false || strpos($messageLower, 'hi') !== false) {
$hour = (int) now()->format('H');
@@ -148,7 +209,7 @@ protected function getFallbackResponse($message, $context = [])
if ($hasRecommendation) {
return [
'success' => true,
- 'message' => "Jurusan \"{$jurusan}\" direkomendasikan berdasarkan analisis komprehensif terhadap profil akademik, minat, serta preferensi studi Anda. Skor kesesuaian sebesar {$score}% menunjukkan tingkat kecocokan yang signifikan antara profil Anda dengan karakteristik jurusan tersebut. Sistem menghitung skor ini berdasarkan lima faktor utama, yaitu: nilai akademik, minat dan bakat, preferensi studi lanjutan, prestasi, dan cita-cita."
+ 'message' => "Jurusan \"{$jurusan}\" direkomendasikan karena paling sesuai dengan kombinasi profil Anda. Skor kesesuaian {$score}% diperoleh dari analisis lima atribut: nilai akademik, minat, preferensi studi lanjutan, cita-cita, dan prestasi (jika diisi). Berdasarkan data tersebut, jurusan ini memiliki kecocokan paling kuat dibanding alternatif lain pada sesi analisis Anda."
];
}
return [
@@ -157,11 +218,40 @@ protected function getFallbackResponse($message, $context = [])
];
}
+ if (
+ strpos($messageLower, 'apa itu') !== false ||
+ strpos($messageLower, 'jurusan tersebut') !== false ||
+ strpos($messageLower, 'jelaskan jurusan') !== false ||
+ strpos($messageLower, 'maksud jurusan') !== false
+ ) {
+ if ($hasRecommendation) {
+ $parts = [];
+ $parts[] = "Jurusan \"{$jurusan}\" adalah bidang yang berfokus pada kompetensi terapan sesuai kebutuhan dunia kerja.";
+ if (!empty($majorDesc)) {
+ $parts[] = "Gambaran jurusan: {$majorDesc}";
+ }
+ if (!empty($majorProspek)) {
+ $parts[] = "Prospek kerja utama: {$majorProspek}";
+ }
+ $parts[] = "Jika Anda berkenan, saya dapat lanjutkan dengan mata kuliah inti, kemampuan yang perlu dipersiapkan, dan alasan kesesuaiannya dengan profil Anda.";
+
+ return [
+ 'success' => true,
+ 'message' => implode(' ', $parts),
+ ];
+ }
+
+ return [
+ 'success' => true,
+ 'message' => "Saya dapat menjelaskan jurusan secara rinci. Agar lebih tepat sasaran, sebutkan nama jurusan yang ingin Anda ketahui, atau jalankan Analisis Rekomendasi terlebih dahulu agar saya menjelaskan jurusan yang paling sesuai dengan profil Anda.",
+ ];
+ }
+
if (strpos($messageLower, 'prospek') !== false || strpos($messageLower, 'karir') !== false || strpos($messageLower, 'kerja') !== false) {
if ($hasRecommendation) {
return [
'success' => true,
- 'message' => "Jurusan \"{$jurusan}\" memiliki prospek karier yang menjanjikan. Lulusan dari jurusan ini dapat bekerja di berbagai sektor industri yang relevan dengan bidang keahliannya. Setiap jurusan di POLIJE dirancang untuk membekali lulusannya dengan kompetensi praktis yang dibutuhkan oleh dunia kerja. Apakah Anda ingin mengetahui lebih detail mengenai posisi pekerjaan spesifik yang dapat ditempuh?"
+ 'message' => "Jurusan \"{$jurusan}\" memiliki prospek karier yang baik. " . (!empty($majorProspek) ? "Contoh prospek kerja: {$majorProspek}. " : "Lulusan dapat bekerja pada bidang yang relevan dengan kompetensi jurusan. ") . "Jika Anda berkenan, saya dapat jelaskan jalur karier dari level awal sampai pengembangan jangka panjang."
];
}
return [
@@ -197,11 +287,15 @@ protected function getFallbackResponse($message, $context = [])
];
}
- // Default response
+ // Default response: tetap menanggapi isi pertanyaan agar nyambung
if ($hasRecommendation) {
+ $lead = !empty($lastAiMessage)
+ ? "Menindaklanjuti percakapan sebelumnya, "
+ : "";
+
return [
'success' => true,
- 'message' => "Saya adalah konselor BK virtual SMA Bima Ambulu. Berdasarkan hasil analisis, jurusan \"{$jurusan}\" memiliki kesesuaian tertinggi dengan profil Anda, yaitu sebesar {$score}%. Anda dapat berkonsultasi mengenai prospek karier, kompetensi yang dibutuhkan, perbandingan antar jurusan, atau hal lain terkait persiapan pendidikan tinggi. Silakan sampaikan pertanyaan Anda."
+ 'message' => $lead . "berdasarkan hasil analisis Anda saat ini, jurusan dengan kecocokan tertinggi adalah \"{$jurusan}\" ({$score}%). " . (!empty($majorDesc) ? "Ringkasan jurusan: {$majorDesc}. " : "") . "Silakan lanjutkan dengan pertanyaan yang lebih spesifik, misalnya perbandingan dengan jurusan lain, kompetensi yang harus dipersiapkan, atau prospek kariernya."
];
}
@@ -319,6 +413,7 @@ protected function buildSystemPrompt($context)
}
$prompt .= "\n\nCara kamu merespons:";
+ $prompt .= "\n0. WAJIB jawab inti pertanyaan pengguna terlebih dahulu secara langsung dalam 1-2 kalimat pertama. Jangan memutar atau memberi jawaban generik yang tidak menanggapi pertanyaan.";
$prompt .= "\n1. INGAT seluruh percakapan sebelumnya. Jangan tanya ulang hal yang sudah dijawab siswa.";
$prompt .= "\n2. Kalau siswa sudah bilang minat/kemampuan/kesukaan, LANGSUNG analisis dan arahkan ke jurusan yang cocok dengan ALASAN LOGIS (misal: 'kamu suka logika → TI cocok karena...')";
$prompt .= "\n3. Berikan REKOMENDASI TEGAS, bukan cuma daftar pilihan. Contoh: 'Menurut Bapak, kamu paling cocok ke Teknologi Informasi. Alasannya: ...'";
@@ -327,6 +422,8 @@ protected function buildSystemPrompt($context)
$prompt .= "\n6. Jawab RINGKAS (2-3 paragraf). Jangan terlalu panjang kecuali diminta detail.";
$prompt .= "\n7. Boleh menjawab pertanyaan di luar topik jurusan secara singkat, lalu kembalikan ke konseling.";
$prompt .= "\n8. JANGAN awali setiap respons dengan 'Halo' atau salam — langsung ke inti jawaban (kecuali percakapan baru dimulai).";
+ $prompt .= "\n8a. Jika pengguna bertanya 'apa itu jurusan X' atau 'jelaskan jurusan tersebut', jelaskan definisi jurusan, fokus pembelajaran, dan prospek kerjanya secara ringkas dan nyambung dengan konteks pengguna.";
+ $prompt .= "\n8b. Jika pengguna menilai jawaban tidak nyambung, lakukan klarifikasi singkat lalu jawab ulang secara spesifik sesuai pertanyaan terbaru.";
// Tambahkan referensi Q&A serupa dari riwayat
if (!empty($context['similar_qa'])) {
@@ -339,7 +436,232 @@ protected function buildSystemPrompt($context)
$prompt .= "\nGunakan referensi di atas untuk menjaga konsistensi jawaban, namun tetap sesuaikan dengan profil dan konteks percakapan siswa saat ini.";
} $prompt .= "\n9. DILARANG KERAS menggunakan format markdown seperti **, *, #, ##, atau simbol formatting lainnya. Tulis teks biasa (plain text) saja tanpa formatting markdown.";
$prompt .= "\n10. Gunakan bahasa Indonesia baku dan akademik. Hindari bahasa gaul seperti 'kek', 'banget', 'ngobrol', 'ngomongin', 'gampangnya'. Gunakan padanan formal seperti 'sangat', 'berbincang', 'membahas', 'secara sederhana'.";
+ $prompt .= "\n11. Jika pertanyaan bersifat umum di luar jurusan (misalnya pengetahuan umum), jawab singkat namun benar, lalu tawarkan kaitan dengan rencana studi/karier pengguna.";
+
+ if (($context['intent'] ?? '') === 'compare_majors') {
+ $prompt .= "\n12. Pengguna sedang meminta PERBANDINGAN JURUSAN. Gunakan format terstruktur dengan urutan tetap: (1) Fokus pembelajaran, (2) Kompetensi yang perlu dipersiapkan, (3) Prospek kerja, (4) Tantangan belajar, (5) Rekomendasi paling sesuai untuk profil pengguna beserta alasan.";
+ }
return $prompt;
}
+
+ protected function isFollowUpMessage(string $messageLower): bool
+ {
+ $followUpHints = [
+ 'jelaskan semua',
+ 'lanjut',
+ 'lanjutkan',
+ 'yang tadi',
+ 'yang sebelumnya',
+ 'maksudnya',
+ 'lebih detail',
+ 'perjelas',
+ 'itu gimana',
+ 'jurusan itu',
+ 'jurusan tersebut',
+ 'semua jurusan',
+ 'satu satu',
+ 'satu-satu',
+ ];
+
+ foreach ($followUpHints as $hint) {
+ if (strpos($messageLower, $hint) !== false) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected function getLastAssistantMessage(array $chatHistory): ?string
+ {
+ for ($i = count($chatHistory) - 1; $i >= 0; $i--) {
+ $item = $chatHistory[$i] ?? null;
+ if (!$item || !isset($item['role'], $item['text'])) {
+ continue;
+ }
+
+ if ($item['role'] === 'ai' && trim((string) $item['text']) !== '') {
+ return (string) $item['text'];
+ }
+ }
+
+ return null;
+ }
+
+ protected function buildAllMajorsResponse(bool $hasRecommendation, ?string $jurusan, float $score): string
+ {
+ $majors = PolijeMajor::orderBy('nama_jurusan')->get();
+
+ if ($majors->isEmpty()) {
+ if ($hasRecommendation) {
+ return "Menindaklanjuti pertanyaan Anda, saat ini rekomendasi utama Anda tetap pada jurusan \"{$jurusan}\" dengan skor {$score}%. Jika Anda berkenan, saya dapat jelaskan detail jurusan ini terlebih dahulu.";
+ }
+
+ return "Data jurusan saat ini belum tersedia. Silakan coba kembali beberapa saat lagi, atau ajukan satu jurusan yang ingin dibahas agar saya jelaskan secara umum.";
+ }
+
+ $lines = [];
+ $lines[] = "Berikut ringkasan seluruh jurusan di Politeknik Negeri Jember agar pembahasan kita tetap nyambung dengan pertanyaan Anda:";
+
+ foreach ($majors as $index => $m) {
+ $desc = trim((string) ($m->deskripsi ?? ''));
+ if ($desc === '') {
+ $desc = 'Berorientasi pada kompetensi terapan yang relevan dengan kebutuhan dunia kerja.';
+ }
+ $num = $index + 1;
+ $lines[] = "{$num}. {$m->nama_jurusan}: {$desc}";
+ }
+
+ if ($hasRecommendation) {
+ $lines[] = "Berdasarkan profil Anda, jurusan yang paling direkomendasikan tetap \"{$jurusan}\" ({$score}%). Jika Anda ingin, saya lanjutkan dengan perbandingan jurusan rekomendasi Anda terhadap 2 alternatif teratas.";
+ } else {
+ $lines[] = "Jika Anda berkenan, saya dapat bantu menyaring 2-3 jurusan paling relevan berdasarkan minat dan nilai Anda.";
+ }
+
+ return implode(' ', $lines);
+ }
+
+ protected function buildStructuredComparisonResponse(string $message, array $context = []): ?string
+ {
+ $majors = PolijeMajor::orderBy('nama_jurusan')->get();
+ if ($majors->count() < 2) {
+ return null;
+ }
+
+ $selected = $this->resolveMajorsForComparison($message, $context, $majors);
+ if (count($selected) < 2) {
+ return null;
+ }
+
+ $a = $selected[0];
+ $b = $selected[1];
+
+ $aDesc = trim((string) ($a->deskripsi ?? ''));
+ $bDesc = trim((string) ($b->deskripsi ?? ''));
+ $aProspek = trim((string) ($a->prospek_kerja ?? ''));
+ $bProspek = trim((string) ($b->prospek_kerja ?? ''));
+
+ if ($aDesc === '') {
+ $aDesc = 'Berfokus pada kompetensi terapan sesuai kebutuhan industri.';
+ }
+ if ($bDesc === '') {
+ $bDesc = 'Berfokus pada kompetensi terapan sesuai kebutuhan industri.';
+ }
+ if ($aProspek === '') {
+ $aProspek = 'Lulusan berpeluang masuk pada bidang kerja yang relevan dengan kompetensi jurusan.';
+ }
+ if ($bProspek === '') {
+ $bProspek = 'Lulusan berpeluang masuk pada bidang kerja yang relevan dengan kompetensi jurusan.';
+ }
+
+ $recommended = $context['recommendation'] ?? null;
+ $winner = $a->nama_jurusan;
+ if (!empty($recommended)) {
+ if (strcasecmp($recommended, $b->nama_jurusan) === 0) {
+ $winner = $b->nama_jurusan;
+ } elseif (strcasecmp($recommended, $a->nama_jurusan) !== 0) {
+ $winner = $a->nama_jurusan;
+ }
+ }
+
+ $score = isset($context['score']) ? (float) $context['score'] : null;
+ $scoreText = $score !== null && $score > 0 ? " dengan skor kesesuaian {$score}%" : "";
+
+ $lines = [];
+ $lines[] = "Perbandingan terstruktur antara Jurusan {$a->nama_jurusan} dan Jurusan {$b->nama_jurusan}:";
+ $lines[] = "1. Fokus pembelajaran";
+ $lines[] = "- {$a->nama_jurusan}: {$aDesc}";
+ $lines[] = "- {$b->nama_jurusan}: {$bDesc}";
+ $lines[] = "2. Kompetensi yang perlu dipersiapkan";
+ $lines[] = "- {$a->nama_jurusan}: Penguasaan konsep inti jurusan, kemampuan analitis, komunikasi profesional, dan disiplin praktik.";
+ $lines[] = "- {$b->nama_jurusan}: Penguasaan konsep inti jurusan, kemampuan analitis, komunikasi profesional, dan disiplin praktik.";
+ $lines[] = "3. Prospek kerja";
+ $lines[] = "- {$a->nama_jurusan}: {$aProspek}";
+ $lines[] = "- {$b->nama_jurusan}: {$bProspek}";
+ $lines[] = "4. Tantangan belajar";
+ $lines[] = "- {$a->nama_jurusan}: Menuntut konsistensi belajar, adaptasi pada praktik lapangan, dan ketekunan menyelesaikan tugas proyek.";
+ $lines[] = "- {$b->nama_jurusan}: Menuntut konsistensi belajar, adaptasi pada praktik lapangan, dan ketekunan menyelesaikan tugas proyek.";
+ $lines[] = "5. Rekomendasi untuk profil Anda";
+ $lines[] = "- Berdasarkan konteks analisis Anda, jurusan yang lebih diprioritaskan adalah {$winner}{$scoreText}. Jika Anda berkenan, saya dapat lanjutkan dengan langkah persiapan 6 bulan pertama agar transisi belajar Anda lebih terarah.";
+
+ return implode("\n", $lines);
+ }
+
+ protected function resolveMajorsForComparison(string $message, array $context, $majors): array
+ {
+ $messageLower = mb_strtolower($message);
+ $mentioned = [];
+
+ foreach ($majors as $major) {
+ $name = mb_strtolower((string) $major->nama_jurusan);
+ if ($name !== '' && str_contains($messageLower, $name)) {
+ $mentioned[$major->nama_jurusan] = $major;
+ }
+ }
+
+ if (count($mentioned) >= 2) {
+ return array_slice(array_values($mentioned), 0, 2);
+ }
+
+ $picked = array_values($mentioned);
+ $recommendationName = $context['recommendation'] ?? null;
+
+ if (!empty($recommendationName)) {
+ $recMajor = $majors->first(function ($m) use ($recommendationName) {
+ return strcasecmp((string) $m->nama_jurusan, (string) $recommendationName) === 0;
+ });
+
+ if ($recMajor && !$this->containsMajor($picked, $recMajor->nama_jurusan)) {
+ $picked[] = $recMajor;
+ }
+ }
+
+ if (!empty($context['top3']) && is_array($context['top3'])) {
+ foreach ($context['top3'] as $candidate) {
+ $name = $candidate['jurusan'] ?? null;
+ if (empty($name)) {
+ continue;
+ }
+
+ $candidateMajor = $majors->first(function ($m) use ($name) {
+ return strcasecmp((string) $m->nama_jurusan, (string) $name) === 0;
+ });
+
+ if ($candidateMajor && !$this->containsMajor($picked, $candidateMajor->nama_jurusan)) {
+ $picked[] = $candidateMajor;
+ }
+
+ if (count($picked) >= 2) {
+ break;
+ }
+ }
+ }
+
+ if (count($picked) < 2) {
+ foreach ($majors as $major) {
+ if (!$this->containsMajor($picked, $major->nama_jurusan)) {
+ $picked[] = $major;
+ }
+
+ if (count($picked) >= 2) {
+ break;
+ }
+ }
+ }
+
+ return array_slice($picked, 0, 2);
+ }
+
+ protected function containsMajor(array $picked, string $name): bool
+ {
+ foreach ($picked as $item) {
+ if (strcasecmp((string) $item->nama_jurusan, $name) === 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
}
diff --git a/config/polije.php b/config/polije.php
index 8aec8ad..747dd22 100644
--- a/config/polije.php
+++ b/config/polije.php
@@ -4,14 +4,16 @@
// Standar kriteria per jurusan dengan bobot dan probabilitas kecocokan
// Dioptimalkan untuk Polije (Vocational Campus)
// Weights: nilai, minat, pref, prestasi, cita_cita
- // Preference: Praktik Langsung, DuDi, Project Based, Blended
+ // Preference diselaraskan dengan input form rekomendasi:
+ // Sains & Teknologi, Pertanian & Lingkungan, Kesehatan & Ilmu Hayat,
+ // Bisnis & Manajemen, Sosial & Humaniora
'criteria' => [
'Produksi Pertanian' => [
'nilai' => 'Sedang',
'minat' => 'Alam & Tanaman',
- 'pref' => ['Praktik Langsung', 'DuDi', 'Project Based'],
- 'cita_cita_keywords' => ['pertanian', 'petani', 'kebun', 'sawah', 'panen', 'tanaman'],
+ 'pref' => ['Pertanian & Lingkungan'],
+ 'cita_cita_keywords' => ['pertanian', 'petani', 'kebun', 'sawah', 'panen', 'tanaman', 'agronomi', 'perkebunan', 'hortikultura'],
'skills_required' => ['Observasi', 'Kerja Lapangan', 'Pemeliharaan Tanaman'],
'weights' => ['nilai' => 0.40, 'minat' => 0.35, 'pref' => 0.15, 'prestasi' => 0.05, 'cita_cita' => 0.05],
'match_prob' => ['nilai' => 0.80, 'minat' => 0.90, 'pref' => 0.85, 'prestasi' => 0.65, 'cita_cita' => 0.85],
@@ -19,8 +21,8 @@
'Teknologi Pertanian' => [
'nilai' => 'Tinggi',
'minat' => 'Alam & Tanaman',
- 'pref' => ['Praktik Langsung', 'Project Based', 'DuDi'],
- 'cita_cita_keywords' => ['teknologi', 'inovasi', 'otomasi', 'mesin pertanian'],
+ 'pref' => ['Sains & Teknologi', 'Pertanian & Lingkungan'],
+ 'cita_cita_keywords' => ['teknologi', 'inovasi', 'otomasi', 'mesin pertanian', 'smart farming', 'teknologi pangan'],
'skills_required' => ['Problem Solving', 'Teknologi', 'Inovasi'],
'weights' => ['nilai' => 0.50, 'minat' => 0.25, 'pref' => 0.15, 'prestasi' => 0.05, 'cita_cita' => 0.05],
'match_prob' => ['nilai' => 0.85, 'minat' => 0.90, 'pref' => 0.85, 'prestasi' => 0.75, 'cita_cita' => 0.80],
@@ -28,8 +30,8 @@
'Peternakan' => [
'nilai' => 'Sedang',
'minat' => 'Alam & Tanaman',
- 'pref' => ['Praktik Langsung', 'DuDi', 'Project Based'],
- 'cita_cita_keywords' => ['ternak', 'hewan', 'peternakan', 'peeternak', 'sapi', 'ayam', 'unggas'],
+ 'pref' => ['Pertanian & Lingkungan', 'Kesehatan & Ilmu Hayat'],
+ 'cita_cita_keywords' => ['ternak', 'hewan', 'peternakan', 'peternak', 'sapi', 'ayam', 'unggas', 'veteriner', 'farm'],
'skills_required' => ['Perawatan Hewan', 'Kesabaran', 'Manajemen'],
'weights' => ['nilai' => 0.40, 'minat' => 0.40, 'pref' => 0.10, 'prestasi' => 0.05, 'cita_cita' => 0.05],
'match_prob' => ['nilai' => 0.80, 'minat' => 0.88, 'pref' => 0.80, 'prestasi' => 0.65, 'cita_cita' => 0.88],
@@ -37,8 +39,8 @@
'Manajemen Agribisnis' => [
'nilai' => 'Sedang',
'minat' => 'Manajemen & Bisnis',
- 'pref' => ['Project Based', 'DuDi', 'Blended'],
- 'cita_cita_keywords' => ['bisnis', 'agribisnis', 'usaha', 'entrepreneur', 'pengusaha'],
+ 'pref' => ['Bisnis & Manajemen', 'Pertanian & Lingkungan'],
+ 'cita_cita_keywords' => ['bisnis', 'agribisnis', 'usaha', 'entrepreneur', 'pengusaha', 'manajer', 'marketing', 'wirausaha'],
'skills_required' => ['Manajemen', 'Bisnis Acumen', 'Komunikasi'],
'weights' => ['nilai' => 0.35, 'minat' => 0.40, 'pref' => 0.15, 'prestasi' => 0.05, 'cita_cita' => 0.05],
'match_prob' => ['nilai' => 0.75, 'minat' => 0.90, 'pref' => 0.80, 'prestasi' => 0.70, 'cita_cita' => 0.85],
@@ -46,8 +48,8 @@
'Teknologi Informasi' => [
'nilai' => 'Tinggi',
'minat' => 'Logika & Komputer',
- 'pref' => ['Praktik Langsung', 'Project Based', 'DuDi'],
- 'cita_cita_keywords' => ['programmer', 'developer', 'coding', 'software', 'web developer', 'hacker', 'it'],
+ 'pref' => ['Sains & Teknologi'],
+ 'cita_cita_keywords' => ['programmer', 'developer', 'coding', 'software', 'web developer', 'hacker', 'it', 'data analyst', 'ai engineer', 'mobile developer'],
'skills_required' => ['Coding', 'Problem Solving', 'Logika'],
'weights' => ['nilai' => 0.45, 'minat' => 0.35, 'pref' => 0.12, 'prestasi' => 0.05, 'cita_cita' => 0.03],
'match_prob' => ['nilai' => 0.90, 'minat' => 0.92, 'pref' => 0.85, 'prestasi' => 0.75, 'cita_cita' => 0.85],
@@ -55,8 +57,8 @@
'Teknik' => [
'nilai' => 'Sedang',
'minat' => 'Mesin & Listrik',
- 'pref' => ['Praktik Langsung', 'DuDi', 'Project Based'],
- 'cita_cita_keywords' => ['mesin', 'bengkel', 'teknisi', 'listrik', 'elektronik', 'automasi', 'instalasi', 'panel'],
+ 'pref' => ['Sains & Teknologi'],
+ 'cita_cita_keywords' => ['mesin', 'bengkel', 'teknisi', 'listrik', 'elektronik', 'otomasi', 'instalasi', 'panel', 'mekatronika', 'maintenance'],
'skills_required' => ['Mekanik', 'Elektrik', 'Teknik', 'Presisi'],
'weights' => ['nilai' => 0.42, 'minat' => 0.38, 'pref' => 0.12, 'prestasi' => 0.05, 'cita_cita' => 0.03],
'match_prob' => ['nilai' => 0.82, 'minat' => 0.90, 'pref' => 0.85, 'prestasi' => 0.71, 'cita_cita' => 0.85],
@@ -64,8 +66,8 @@
'Kesehatan' => [
'nilai' => 'Tinggi',
'minat' => 'Pelayanan & Kesehatan',
- 'pref' => ['Praktik Langsung', 'DuDi', 'Project Based'],
- 'cita_cita_keywords' => ['dokter', 'perawat', 'medis', 'gizi', 'kesehatan', 'pelayanan', 'terapis'],
+ 'pref' => ['Kesehatan & Ilmu Hayat'],
+ 'cita_cita_keywords' => ['dokter', 'perawat', 'medis', 'gizi', 'kesehatan', 'pelayanan', 'terapis', 'farmasi', 'rekam medis', 'kesehatan masyarakat'],
'skills_required' => ['Komunikasi', 'Empati', 'Presisi Medis'],
'weights' => ['nilai' => 0.45, 'minat' => 0.35, 'pref' => 0.10, 'prestasi' => 0.05, 'cita_cita' => 0.05],
'match_prob' => ['nilai' => 0.90, 'minat' => 0.90, 'pref' => 0.80, 'prestasi' => 0.75, 'cita_cita' => 0.90],
@@ -73,8 +75,8 @@
'Bahasa, Komunikasi, dan Pariwisata' => [
'nilai' => 'Sedang',
'minat' => 'Umum',
- 'pref' => ['Project Based', 'DuDi', 'Praktik Langsung'],
- 'cita_cita_keywords' => ['tour guide', 'pariwisata', 'bahasa', 'komunikasi', 'jurnalis', 'marketing'],
+ 'pref' => ['Sosial & Humaniora', 'Bisnis & Manajemen'],
+ 'cita_cita_keywords' => ['tour guide', 'pariwisata', 'bahasa', 'komunikasi', 'jurnalis', 'marketing', 'public relation', 'content creator', 'hospitality'],
'skills_required' => ['Komunikasi', 'Bahasa', 'Kepribadian'],
'weights' => ['nilai' => 0.30, 'minat' => 0.40, 'pref' => 0.15, 'prestasi' => 0.08, 'cita_cita' => 0.07],
'match_prob' => ['nilai' => 0.70, 'minat' => 0.85, 'pref' => 0.80, 'prestasi' => 0.70, 'cita_cita' => 0.85],
@@ -82,20 +84,21 @@
'Bisnis' => [
'nilai' => 'Sedang',
'minat' => 'Manajemen & Bisnis',
- 'pref' => ['Project Based', 'DuDi', 'Blended'],
- 'cita_cita_keywords' => ['manager', 'pimpinan', 'bisnis', 'accounting', 'marketing', 'sales'],
+ 'pref' => ['Bisnis & Manajemen'],
+ 'cita_cita_keywords' => ['manager', 'pimpinan', 'bisnis', 'accounting', 'marketing', 'sales', 'akuntan', 'keuangan', 'bank'],
'skills_required' => ['Manajemen', 'Leadership', 'Keuangan'],
'weights' => ['nilai' => 0.35, 'minat' => 0.40, 'pref' => 0.15, 'prestasi' => 0.05, 'cita_cita' => 0.05],
'match_prob' => ['nilai' => 0.75, 'minat' => 0.90, 'pref' => 0.80, 'prestasi' => 0.70, 'cita_cita' => 0.85],
],
],
- // Vocational Learning Preference Mapping
+ // Mapping preferensi studi sesuai opsi input form rekomendasi
'pref_mapping' => [
- 'Praktik Langsung' => ['weight' => 1.0, 'score' => 0.95],
- 'DuDi' => ['weight' => 0.95, 'score' => 0.90],
- 'Project Based' => ['weight' => 0.90, 'score' => 0.85],
- 'Blended' => ['weight' => 0.80, 'score' => 0.75],
+ 'Sains & Teknologi' => ['weight' => 1.0, 'score' => 0.95],
+ 'Pertanian & Lingkungan' => ['weight' => 0.95, 'score' => 0.90],
+ 'Kesehatan & Ilmu Hayat' => ['weight' => 0.95, 'score' => 0.90],
+ 'Bisnis & Manajemen' => ['weight' => 0.90, 'score' => 0.85],
+ 'Sosial & Humaniora' => ['weight' => 0.85, 'score' => 0.80],
],
// Category mapping untuk nilai
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index a9f4519..1656355 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -12,11 +12,12 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
- // \App\Models\User::factory(10)->create();
-
- // \App\Models\User::factory()->create([
- // 'name' => 'Test User',
- // 'email' => 'test@example.com',
- // ]);
+ // Seeder inti untuk kebutuhan aplikasi
+ // Jalankan otomatis saat migrate:fresh --seed
+ $this->call([
+ AdminSeeder::class,
+ PolijeMajorSeeder::class,
+ // AlumniSeeder::class, // Aktifkan jika butuh data evaluasi alumni
+ ]);
}
}
diff --git a/database/seeders/PolijeMajorSeeder.php b/database/seeders/PolijeMajorSeeder.php
index e0924d3..f0497ba 100644
--- a/database/seeders/PolijeMajorSeeder.php
+++ b/database/seeders/PolijeMajorSeeder.php
@@ -12,8 +12,8 @@ public function run(): void
$jurusans = [
[
'nama_jurusan' => 'Produksi Pertanian',
- 'deskripsi' => 'Jurusan yang mempelajari teknik budidaya tanaman, pengelolaan lahan pertanian, dan produksi hasil pertanian secara modern.',
- 'keywords' => ['pertanian', 'petani', 'kebun', 'sawah', 'panen', 'tanaman', 'budidaya', 'agronomi', 'tanam', 'bercocok tanam', 'alam', 'hortikultura', 'pupuk', 'bibit'],
+ 'deskripsi' => 'Jurusan yang mempelajari budidaya tanaman, pengelolaan lahan, dan produksi hasil pertanian modern, termasuk bidang turunan yang terkait dengan agronomi, pangan, dan lingkungan.',
+ 'keywords' => ['pertanian', 'petani', 'kebun', 'sawah', 'panen', 'tanaman', 'budidaya', 'agronomi', 'tanam', 'bercocok tanam', 'alam', 'hortikultura', 'pupuk', 'bibit', 'agroteknologi', 'perkebunan', 'pangan', 'ketahanan pangan', 'hidroponik', 'organik'],
'preferensi_studi' => ['Pertanian & Lingkungan'],
'bobot_mapel' => [
'biologi' => 0.40, 'kimia' => 0.30, 'fisika' => 0.15, 'mtk' => 0.15,
@@ -23,8 +23,8 @@ public function run(): void
],
[
'nama_jurusan' => 'Teknologi Pertanian',
- 'deskripsi' => 'Jurusan yang mengintegrasikan teknologi dengan pertanian, meliputi mekanisasi pertanian, pengolahan hasil pertanian, dan inovasi teknologi pangan.',
- 'keywords' => ['teknologi pertanian', 'mesin pertanian', 'inovasi', 'otomasi', 'pengolahan pangan', 'pangan', 'mekanisasi', 'teknologi pangan', 'alat pertanian', 'rekayasa'],
+ 'deskripsi' => 'Jurusan yang mengintegrasikan teknologi dengan pertanian, meliputi mekanisasi, pengolahan hasil, otomasi, dan inovasi teknologi pangan serta sistem produksi modern.',
+ 'keywords' => ['teknologi pertanian', 'mesin pertanian', 'inovasi', 'otomasi', 'pengolahan pangan', 'pangan', 'mekanisasi', 'teknologi pangan', 'alat pertanian', 'rekayasa', 'iot pertanian', 'smart farming', 'digital farming', 'kontrol kualitas', 'proses produksi'],
'preferensi_studi' => ['Sains & Teknologi', 'Pertanian & Lingkungan'],
'bobot_mapel' => [
'fisika' => 0.35, 'mtk' => 0.30, 'kimia' => 0.20, 'biologi' => 0.15,
@@ -34,8 +34,8 @@ public function run(): void
],
[
'nama_jurusan' => 'Peternakan',
- 'deskripsi' => 'Jurusan yang mempelajari pengelolaan dan pemeliharaan ternak, nutrisi hewan, reproduksi, dan pengolahan produk peternakan.',
- 'keywords' => ['ternak', 'hewan', 'peternakan', 'peternak', 'sapi', 'ayam', 'unggas', 'kambing', 'susu', 'pakan', 'nutrisi hewan', 'veteriner', 'ikan', 'aquaculture'],
+ 'deskripsi' => 'Jurusan yang mempelajari pengelolaan ternak, nutrisi hewan, reproduksi, dan pengolahan produk peternakan, termasuk kewirausahaan dan teknologi peternakan terapan.',
+ 'keywords' => ['ternak', 'hewan', 'peternakan', 'peternak', 'sapi', 'ayam', 'unggas', 'kambing', 'susu', 'pakan', 'nutrisi hewan', 'veteriner', 'ikan', 'aquaculture', 'budidaya hewan', 'farm management', 'kesehatan hewan', 'produksi ternak'],
'preferensi_studi' => ['Pertanian & Lingkungan', 'Kesehatan & Ilmu Hayat'],
'bobot_mapel' => [
'biologi' => 0.45, 'kimia' => 0.25, 'fisika' => 0.15, 'mtk' => 0.15,
@@ -45,8 +45,8 @@ public function run(): void
],
[
'nama_jurusan' => 'Manajemen Agribisnis',
- 'deskripsi' => 'Jurusan yang menggabungkan ilmu pertanian dan bisnis, meliputi pemasaran hasil pertanian, manajemen usaha tani, dan kewirausahaan agribisnis.',
- 'keywords' => ['bisnis', 'agribisnis', 'usaha', 'entrepreneur', 'pengusaha', 'dagang', 'jual', 'pemasaran', 'kewirausahaan', 'manajemen', 'ekonomi pertanian', 'pasar'],
+ 'deskripsi' => 'Jurusan yang menggabungkan ilmu pertanian dan bisnis, meliputi manajemen usaha, pemasaran hasil, rantai pasok, dan kewirausahaan agribisnis.',
+ 'keywords' => ['bisnis', 'agribisnis', 'usaha', 'entrepreneur', 'pengusaha', 'dagang', 'jual', 'pemasaran', 'kewirausahaan', 'manajemen', 'ekonomi pertanian', 'pasar', 'supply chain', 'logistik', 'analisis pasar', 'branding produk'],
'preferensi_studi' => ['Bisnis & Manajemen', 'Pertanian & Lingkungan'],
'bobot_mapel' => [
'mtk' => 0.35, 'biologi' => 0.25, 'kimia' => 0.20, 'fisika' => 0.20,
@@ -56,8 +56,8 @@ public function run(): void
],
[
'nama_jurusan' => 'Teknologi Informasi',
- 'deskripsi' => 'Jurusan yang mempelajari pengembangan perangkat lunak, jaringan komputer, keamanan siber, dan teknologi digital.',
- 'keywords' => ['programmer', 'developer', 'coding', 'software', 'web', 'aplikasi', 'komputer', 'it', 'jaringan', 'hacker', 'game', 'data', 'ai', 'robot', 'ngoding', 'laptop', 'teknologi', 'digital', 'internet', 'programming', 'desain grafis'],
+ 'deskripsi' => 'Jurusan yang mempelajari pengembangan perangkat lunak, data, jaringan, keamanan siber, dan ekosistem teknologi digital, termasuk bidang turunan komputasi terapan.',
+ 'keywords' => ['programmer', 'developer', 'coding', 'software', 'web', 'aplikasi', 'komputer', 'it', 'jaringan', 'hacker', 'game', 'data', 'ai', 'robot', 'ngoding', 'laptop', 'teknologi', 'digital', 'internet', 'programming', 'desain grafis', 'ui ux', 'mobile app', 'cloud', 'database', 'machine learning'],
'preferensi_studi' => ['Sains & Teknologi'],
'bobot_mapel' => [
'mtk' => 0.45, 'fisika' => 0.25, 'kimia' => 0.15, 'biologi' => 0.15,
@@ -67,8 +67,8 @@ public function run(): void
],
[
'nama_jurusan' => 'Teknik',
- 'deskripsi' => 'Jurusan yang mempelajari mesin, kelistrikan, elektronika, dan otomasi industri.',
- 'keywords' => ['mesin', 'bengkel', 'listrik', 'las', 'robot', 'motor', 'teknik', 'otomasi', 'elektronik', 'instalasi', 'panel', 'mekanik', 'industri', 'manufaktur', 'pabrik', 'bangunan', 'konstruksi', 'sipil', 'energi'],
+ 'deskripsi' => 'Jurusan yang mempelajari mesin, kelistrikan, elektronika, otomasi, dan sistem teknik industri dengan pendekatan praktik dan pemecahan masalah teknis.',
+ 'keywords' => ['mesin', 'bengkel', 'listrik', 'las', 'robot', 'motor', 'teknik', 'otomasi', 'elektronik', 'instalasi', 'panel', 'mekanik', 'industri', 'manufaktur', 'pabrik', 'bangunan', 'konstruksi', 'sipil', 'energi', 'maintenance', 'mekatronika', 'instrumentasi', 'quality control'],
'preferensi_studi' => ['Sains & Teknologi'],
'bobot_mapel' => [
'fisika' => 0.40, 'mtk' => 0.35, 'kimia' => 0.15, 'biologi' => 0.10,
@@ -78,8 +78,8 @@ public function run(): void
],
[
'nama_jurusan' => 'Kesehatan',
- 'deskripsi' => 'Jurusan yang mempelajari ilmu kesehatan, gizi, rekam medis, dan pelayanan kesehatan masyarakat.',
- 'keywords' => ['dokter', 'perawat', 'medis', 'gizi', 'kesehatan', 'pelayanan', 'terapis', 'obat', 'rumah sakit', 'klinik', 'farmasi', 'nutrisi', 'sanitasi', 'rawat', 'sehat'],
+ 'deskripsi' => 'Jurusan yang mempelajari ilmu kesehatan terapan, gizi, rekam medis, dan pelayanan kesehatan masyarakat, termasuk aspek promotif dan preventif.',
+ 'keywords' => ['dokter', 'perawat', 'medis', 'gizi', 'kesehatan', 'pelayanan', 'terapis', 'obat', 'rumah sakit', 'klinik', 'farmasi', 'nutrisi', 'sanitasi', 'rawat', 'sehat', 'kesehatan masyarakat', 'laboratorium', 'diagnostik', 'wellness'],
'preferensi_studi' => ['Kesehatan & Ilmu Hayat'],
'bobot_mapel' => [
'biologi' => 0.40, 'kimia' => 0.35, 'mtk' => 0.15, 'fisika' => 0.10,
@@ -89,8 +89,8 @@ public function run(): void
],
[
'nama_jurusan' => 'Bahasa, Komunikasi, dan Pariwisata',
- 'deskripsi' => 'Jurusan yang mempelajari bahasa asing, komunikasi, perhotelan, dan industri pariwisata.',
- 'keywords' => ['bahasa', 'komunikasi', 'pariwisata', 'tour guide', 'hotel', 'jurnalis', 'marketing', 'inggris', 'penerjemah', 'travel', 'wisata', 'hospitality', 'public speaking', 'media', 'broadcasting'],
+ 'deskripsi' => 'Jurusan yang mempelajari bahasa, komunikasi, perhotelan, layanan publik, dan industri pariwisata dengan orientasi pada kompetensi komunikasi profesional.',
+ 'keywords' => ['bahasa', 'komunikasi', 'pariwisata', 'tour guide', 'hotel', 'jurnalis', 'marketing', 'inggris', 'penerjemah', 'travel', 'wisata', 'hospitality', 'public speaking', 'media', 'broadcasting', 'content creator', 'humas', 'event', 'pelayanan tamu'],
'preferensi_studi' => ['Sosial & Humaniora', 'Bisnis & Manajemen'],
'bobot_mapel' => [
'biologi' => 0.20, 'kimia' => 0.20, 'fisika' => 0.20, 'mtk' => 0.40,
@@ -100,8 +100,8 @@ public function run(): void
],
[
'nama_jurusan' => 'Bisnis',
- 'deskripsi' => 'Jurusan yang mempelajari akuntansi, manajemen bisnis, perbankan, dan administrasi niaga.',
- 'keywords' => ['manager', 'pimpinan', 'bisnis', 'accounting', 'marketing', 'sales', 'kantor', 'keuangan', 'bank', 'akuntansi', 'hitung', 'administrasi', 'perbankan', 'ekonomi', 'uang', 'investasi', 'pajak'],
+ 'deskripsi' => 'Jurusan yang mempelajari akuntansi, manajemen bisnis, perbankan, keuangan, dan administrasi niaga, termasuk analisis data bisnis dan pengambilan keputusan.',
+ 'keywords' => ['manager', 'pimpinan', 'bisnis', 'accounting', 'marketing', 'sales', 'kantor', 'keuangan', 'bank', 'akuntansi', 'hitung', 'administrasi', 'perbankan', 'ekonomi', 'uang', 'investasi', 'pajak', 'wirausaha', 'audit', 'finance', 'analisis bisnis'],
'preferensi_studi' => ['Bisnis & Manajemen'],
'bobot_mapel' => [
'mtk' => 0.45, 'fisika' => 0.20, 'kimia' => 0.15, 'biologi' => 0.20,
diff --git a/phpunit.xml b/phpunit.xml
index eb13aff..284f955 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -21,8 +21,8 @@