From 60f601a85b7538fbbc698ad5f2cb4e89210cd0d9 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 19 Jun 2026 15:42:30 +0000 Subject: [PATCH] OPTEMA AI --- core/__pycache__/views.cpython-311.pyc | Bin 21854 -> 43786 bytes core/templates/core/case_detail.html | 49 ++++ core/views.py | 365 ++++++++++++++++++++----- 3 files changed, 352 insertions(+), 62 deletions(-) diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 733da46296207059f6a4d1b6187adb8a2dbd7fd1..2512f3d02fbf6848789e876cf4643b39d0232edd 100644 GIT binary patch literal 43786 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 + {% if case_insights %} +
+
Data & Kalkulasi
+

1B. Data yang Terdeteksi dari Pertanyaan

+

Bagian ini dibuat dari angka/konteks yang Anda tulis, supaya jawaban tidak generik dan bisa dicek hitungannya.

+
+ {% for item in case_insights.detected %} +
+ {{ item.label }} + {{ item.value }} + {{ item.note }} +
+ {% endfor %} +
+ {% if case_insights.calculations %} +
+ + + + {% for calc in case_insights.calculations %} + + {% endfor %} + +
KalkulasiRumusHasil
{{ calc.label }}{{ calc.formula }}{{ calc.result }}
+
+ {% endif %} + {% if case_insights.comparisons %} +

Perbandingan Opsi Berdasarkan Data

+
+ + + + {% for row in case_insights.comparisons %} + + {% endfor %} + +
OpsiEstimasi TotalKecocokan BudgetCatatan
{{ row.option }}{{ row.estimate }}{{ row.budget_fit }}{{ row.note }}
+
+

{{ case_insights.assumption_note }}

+ {% endif %} + {% if case_insights.recommendation %} +
{{ case_insights.recommendation }}
+ {% endif %} + {% if case_insights.missing %} +
Data tambahan yang akan membuat jawaban lebih akurat:
    {% for item in case_insights.missing %}
  • {{ item }}
  • {% endfor %}
+ {% endif %} +
+ {% endif %} +
diff --git a/core/views.py b/core/views.py index afb8c5b..8e32400 100644 --- a/core/views.py +++ b/core/views.py @@ -1,3 +1,4 @@ +import re from decimal import Decimal from django.contrib import messages @@ -28,6 +29,268 @@ def _case_title_from_description(description): return f"{first_line[:75].rstrip()}..." +MONEY_UNITS = { + "ribu": 1_000, + "rb": 1_000, + "juta": 1_000_000, + "jt": 1_000_000, + "miliar": 1_000_000_000, + "milyar": 1_000_000_000, +} + +NUMBER_WORDS = { + "satu": 1, + "dua": 2, + "tiga": 3, + "empat": 4, + "lima": 5, + "enam": 6, + "tujuh": 7, + "delapan": 8, + "sembilan": 9, + "sepuluh": 10, +} + +COUNTRY_KEYWORDS = { + "Singapura": ["singapura", "singapore"], + "Jepang": ["jepang", "japan"], + "Indonesia": ["indonesia", "lokal", "dalam negeri"], + "Malaysia": ["malaysia"], + "Australia": ["australia"], + "Korea Selatan": ["korea", "korea selatan"], +} + +EDUCATION_COST_ASSUMPTIONS = { + "Jepang": { + "tuition_year": (60_000_000, 130_000_000), + "living_month": (10_000_000, 18_000_000), + "setup": 25_000_000, + "note": "lebih realistis bila digabung beasiswa, kota hemat, dan kerja paruh waktu sesuai aturan visa", + }, + "Singapura": { + "tuition_year": (180_000_000, 450_000_000), + "living_month": (18_000_000, 35_000_000), + "setup": 35_000_000, + "note": "biasanya perlu scholarship/subsidy besar agar masuk budget ketat", + }, +} + + +def _format_idr(amount): + if amount is None: + return "Belum terdeteksi" + return f"Rp{int(round(amount)):,.0f}".replace(",", ".") + + +def _format_idr_range(low, high): + return f"{_format_idr(low)} – {_format_idr(high)}" + + +def _number_value(raw_number): + value = raw_number.lower().replace("rp", "").replace("idr", "").replace(" ", "").strip() + if "," in value and "." in value: + value = value.replace(".", "").replace(",", ".") if value.rfind(",") > value.rfind(".") else value.replace(",", "") + elif "," in value: + value = value.replace(",", ".") + elif "." in value: + parts = value.split(".") + if len(parts) > 1 and len(parts[-1]) == 3 and all(part.isdigit() for part in parts): + value = "".join(parts) + try: + return float(value) + except ValueError: + return None + + +def _money_to_idr(raw_number, unit=None): + number = _number_value(raw_number) + if number is None: + return None + unit = (unit or "").lower() + if unit in MONEY_UNITS: + return int(number * MONEY_UNITS[unit]) + return int(number) if number >= 10_000 else None + + +def _extract_money_amounts(text): + patterns = [ + r"(?:rp\.?|idr)\s*([0-9][0-9.,]*)\s*(miliar|milyar|juta|jt|ribu|rb)?", + r"(?:uang|dana|budget|anggaran|biaya|modal|tabungan|cash|kas|hutang|utang|gaji)\s*(?:hanya|ada|sekitar|kurang lebih|maksimal|max|sebesar|:|=)?\s*(?:rp\.?|idr)?\s*([0-9][0-9.,]*)\s*(miliar|milyar|juta|jt|ribu|rb)?", + r"([0-9][0-9.,]*)\s*(miliar|milyar|juta|jt|ribu|rb)\b", + ] + amounts = [] + for pattern in patterns: + for match in re.finditer(pattern, text, flags=re.IGNORECASE): + amount = _money_to_idr(match.group(1), match.group(2) if len(match.groups()) > 1 else None) + if amount: + amounts.append(amount) + return sorted(set(amounts)) + + +def _extract_duration_years(text): + match = re.search(r"([0-9]+(?:[,.][0-9]+)?)\s*(tahun|thn|year|years|yr|bulan|bln|month|months|semester)", text, flags=re.IGNORECASE) + if match: + value = _number_value(match.group(1)) + unit = match.group(2).lower() + if value: + if unit in {"bulan", "bln", "month", "months"}: + return value / 12 + if unit == "semester": + return value / 2 + return round(value, 2) + word_pattern = r"(" + "|".join(NUMBER_WORDS.keys()) + r")\s*(tahun|thn|year|years|bulan|bln|semester)" + match = re.search(word_pattern, text, flags=re.IGNORECASE) + if match: + value = NUMBER_WORDS[match.group(1).lower()] + unit = match.group(2).lower() + if unit in {"bulan", "bln"}: + return value / 12 + if unit == "semester": + return value / 2 + return float(value) + return None + + +def _format_years(years): + if years is None: + return "Belum terdeteksi" + if years < 1: + months = max(1, int(round(years * 12))) + return f"{months} bulan" + return f"{int(years)} tahun" if float(years).is_integer() else f"{years:.1f} tahun" + + +def _extract_countries(text): + return [country for country, keywords in COUNTRY_KEYWORDS.items() if any(keyword in text for keyword in keywords)] + + +def _extract_percentages(text): + percentages = [] + for match in re.finditer(r"([0-9]+(?:[,.][0-9]+)?)\s*(%|persen|percent)", text, flags=re.IGNORECASE): + value = _number_value(match.group(1)) + if value is not None: + percentages.append(value) + return sorted(set(percentages)) + + +def _extract_case_data(description): + text = description.lower() + money_amounts = _extract_money_amounts(text) + return { + "budget": max(money_amounts) if money_amounts else None, + "money_amounts": money_amounts, + "duration_years": _extract_duration_years(text), + "countries": _extract_countries(text), + "percentages": _extract_percentages(text), + } + + +def _case_constraint_note(case_data): + parts = [] + if case_data.get("budget"): + parts.append(f"dana {_format_idr(case_data['budget'])}") + if case_data.get("duration_years"): + parts.append(f"target {_format_years(case_data['duration_years'])}") + if case_data.get("countries"): + parts.append(f"opsi {', '.join(case_data['countries'])}") + return ", ".join(parts) if parts else "data yang tertulis di pertanyaan" + + +def _education_cost_rows(case_data, fallback_years=None): + years = case_data.get("duration_years") or fallback_years + if not years: + return [] + rows = [] + for country in [c for c in case_data.get("countries", []) if c in EDUCATION_COST_ASSUMPTIONS]: + assumption = EDUCATION_COST_ASSUMPTIONS[country] + tuition_low, tuition_high = assumption["tuition_year"] + living_low, living_high = assumption["living_month"] + months = years * 12 + total_low = int(round(tuition_low * years + living_low * months + assumption["setup"])) + total_high = int(round(tuition_high * years + living_high * months + assumption["setup"])) + budget = case_data.get("budget") + rows.append({ + "country": country, + "total_low": total_low, + "total_high": total_high, + "coverage": round((budget / total_low) * 100, 1) if budget else None, + "shortfall_low": max(0, total_low - budget) if budget else None, + "surplus_low": max(0, budget - total_low) if budget else None, + "note": assumption["note"], + }) + return rows + + +def _build_case_insights(description, category=None): + case_data = _extract_case_data(description) + budget = case_data.get("budget") + years = case_data.get("duration_years") + countries = case_data.get("countries") + percentages = case_data.get("percentages") or [] + detected = [ + {"label": "Dana/budget", "value": _format_idr(budget) if budget else "Belum disebut", "note": "angka terbesar yang terdeteksi sebagai uang" if budget else "tambahkan nominal agar kalkulasi lebih presisi"}, + {"label": "Target waktu", "value": _format_years(years) if years else "Belum disebut", "note": "diambil dari kata tahun/bulan/semester" if years else "tambahkan deadline atau durasi target"}, + {"label": "Persentase/rasio", "value": ", ".join(f"{value:g}%" for value in percentages) if percentages else "Belum disebut", "note": "persen yang muncul di pertanyaan" if percentages else "tambahkan rasio seperti turun 30% atau target naik 20%"}, + {"label": "Opsi/negara", "value": ", ".join(countries) if countries else "Belum spesifik", "note": "opsi yang muncul di pertanyaan" if countries else "tambahkan opsi yang ingin dibandingkan"}, + ] + calculations = [] + if budget and years: + months = years * 12 + reserve = budget * 0.10 + usable = budget - reserve + if years >= 1: + calculations.append({"label": "Budget per tahun", "formula": f"{_format_idr(budget)} ÷ {_format_years(years)}", "result": f"≈ {_format_idr(budget / years)} / tahun"}) + else: + calculations.append({"label": "Budget untuk periode target", "formula": f"{_format_idr(budget)} untuk {_format_years(years)}", "result": f"≈ {_format_idr(budget)} total periode"}) + calculations.extend([ + {"label": "Budget per bulan", "formula": f"{_format_idr(budget)} ÷ {int(round(months))} bulan", "result": f"≈ {_format_idr(budget / months)} / bulan"}, + {"label": "Dana aman setelah cadangan 10%", "formula": f"{_format_idr(budget)} − 10% cadangan ({_format_idr(reserve)})", "result": f"≈ {_format_idr(usable)} total atau {_format_idr(usable / months)} / bulan"}, + ]) + if percentages: + calculations.append({"label": "Persentase dari pertanyaan", "formula": f"Angka rasio terdeteksi: {', '.join(f'{value:g}%' for value in percentages)}", "result": "Tambahkan baseline nominal agar dampak rupiahnya bisa dihitung."}) + elif budget: + calculations.append({"label": "Cadangan minimum 10%", "formula": f"{_format_idr(budget)} × 10%", "result": f"≈ {_format_idr(budget * 0.10)} disisihkan sebagai buffer risiko"}) + elif years: + calculations.append({"label": "Target waktu terdeteksi", "formula": f"Durasi target = {_format_years(years)}", "result": "Masukkan nominal dana/biaya agar bisa dihitung budget per bulan dan gap biaya."}) + + comparisons = [] + if category == "Pendidikan": + for row in _education_cost_rows(case_data, fallback_years=years): + if budget: + fit = f"Budget menutup ±{row['coverage']}% dari estimasi minimum; gap minimum {_format_idr(row['shortfall_low'])}." if row["shortfall_low"] else f"Budget melewati estimasi minimum; sisa aman sekitar {_format_idr(row['surplus_low'])}." + else: + fit = "Budget belum disebut, jadi gap belum bisa dihitung." + comparisons.append({"option": row["country"], "estimate": _format_idr_range(row["total_low"], row["total_high"]), "budget_fit": fit, "note": row["note"]}) + + recommendation = "" + if category == "Pendidikan" and comparisons and budget: + rows = _education_cost_rows(case_data, fallback_years=years) + best = min(rows, key=lambda item: item["total_low"]) + recommendation = ( + f"Secara self-funded, {_format_idr(budget)} belum menutup estimasi minimum {best['country']} ({_format_idr(best['total_low'])}). Opsi paling dekat tetap {best['country']}, tetapi harus dikunci dengan beasiswa/tuition waiver, kota hemat, atau income legal." + if best["shortfall_low"] else + f"Opsi paling aman secara angka awal adalah {best['country']} karena estimasi minimumnya masih masuk budget." + ) + elif calculations: + recommendation = f"Analisis disesuaikan dengan {_case_constraint_note(case_data)}; gunakan angka ini sebagai batas saat memilih solusi." + + missing = [] + if not budget: + missing.append("Nominal dana/biaya/budget belum jelas.") + if not years: + missing.append("Target durasi atau deadline belum jelas.") + if category == "Pendidikan" and not countries: + missing.append("Negara/kampus pembanding belum disebut.") + return { + "detected": detected, + "calculations": calculations, + "comparisons": comparisons, + "recommendation": recommendation, + "missing": missing, + "assumption_note": "Estimasi biaya pendidikan adalah asumsi awal untuk screening; tetap verifikasi biaya resmi kampus, visa, kurs, beasiswa, dan aturan kerja terbaru.", + } + + ANALYSIS_DATABASE = [ { "kategori": "Pendidikan", @@ -245,63 +508,43 @@ def _write_analysis_records(problem, financial_impact, cause_profiles, options, def _build_education_analysis(problem): + case_data = _extract_case_data(problem.description) + constraint_note = _case_constraint_note(case_data) + budget = case_data.get("budget") + years = case_data.get("duration_years") or 3 + budget_text = _format_idr(budget) if budget else "budget yang tersedia" + years_text = _format_years(years) + row_by_country = {row["country"]: row for row in _education_cost_rows(case_data, fallback_years=years)} + japan_gap_text = "" + singapore_gap_text = "" + if budget and row_by_country.get("Jepang"): + gap = row_by_country["Jepang"].get("shortfall_low") or 0 + japan_gap_text = f" Estimasi minimum Jepang masih butuh tambahan sekitar {_format_idr(gap)} bila tanpa beasiswa." if gap else " Estimasi minimum Jepang masuk batas budget awal." + if budget and row_by_country.get("Singapura"): + gap = row_by_country["Singapura"].get("shortfall_low") or 0 + singapore_gap_text = f" Estimasi minimum Singapura butuh tambahan sekitar {_format_idr(gap)} bila tanpa scholarship besar." if gap else " Estimasi minimum Singapura masuk batas budget awal." + if budget: + budget_math = f"Dengan {budget_text} untuk {years_text}, batas kasar adalah {_format_idr(budget / years)}/tahun atau {_format_idr(budget / (years * 12))}/bulan sebelum cadangan." + else: + budget_math = "Nominal budget belum terbaca, jadi kalkulasi gap harus dilengkapi dengan angka dana/biaya." + cause_profiles = [ - ( - "Keterbatasan Dana", - 96, - "Budget perlu menutup tuition, biaya hidup, visa, tiket, asuransi, dan dana darurat; jadi pilihan negara/kampus harus disaring dari batas biaya dulu.", - ), - ( - "Target Lulus 3 Tahun", - 92, - "Target selesai cepat hanya realistis jika program, credit transfer, kalender akademik, dan syarat kelulusan cocok sejak awal.", - ), - ( - "Biaya Hidup Negara", - 88, - "Perbandingan Singapura vs Jepang harus dihitung dari total cost of study, bukan hanya uang kuliah; kota, tempat tinggal, dan transport sangat menentukan.", - ), - ( - "Bahasa & Admission", - 76, - "Risiko gagal masuk atau molor muncul jika syarat IELTS/JLPT, dokumen, deadline, dan kesiapan bahasa belum dipetakan.", - ), + ("Keterbatasan Dana", 96, f"Pertanyaan menyebut {constraint_note}. {budget_math} Karena itu, keputusan harus dimulai dari total biaya studi, bukan dari nama negara saja."), + ("Target Lulus Tepat Waktu", 92, f"Target {years_text} hanya realistis jika program, credit transfer, kalender akademik, dan syarat kelulusan cocok sejak awal."), + ("Gap Biaya Negara", 88, f"Perbandingan perlu memakai total tuition + living cost + setup cost. {japan_gap_text}{singapore_gap_text}".strip()), + ("Bahasa & Admission", 76, "Risiko gagal masuk atau molor muncul jika syarat IELTS/JLPT, dokumen, deadline, dan kesiapan bahasa belum dipetakan sebelum memilih negara."), ] options = [ - { - "title": "Prioritaskan Jepang + Beasiswa/Part-time Legal", - "impact": 88, - "efficiency": 86, - "speed": 72, - "low_risk": 78, - "success_rate": 84, - "rationale": "Lebih masuk akal untuk budget ketat bila shortlist difokuskan ke kampus/program yang punya beasiswa, kota yang lebih hemat, dan opsi kerja paruh waktu sesuai aturan visa.", - }, - { - "title": "Singapura Hanya Jika Ada Scholarship/Subsidy Besar", - "impact": 82, - "efficiency": 58, - "speed": 86, - "low_risk": 55, - "success_rate": 66, - "rationale": "Singapura bisa unggul untuk akses industri dan durasi cepat, tetapi budget 200 juta berisiko tidak cukup tanpa bantuan biaya yang jelas sejak awal.", - }, - { - "title": "Pathway Hemat: Mulai Lokal lalu Transfer/Credit Recognition", - "impact": 76, - "efficiency": 88, - "speed": 68, - "low_risk": 86, - "success_rate": 80, - "rationale": "Menekan cash-out awal sambil mengejar beasiswa dan kesiapan bahasa, tetapi harus dipastikan kredit bisa diakui agar target total 3 tahun tidak mundur.", - }, + {"title": "Prioritaskan Jepang + Beasiswa/Part-time Legal", "impact": 88, "efficiency": 86, "speed": 72, "low_risk": 78, "success_rate": 84, "rationale": f"Paling dekat dengan batas {budget_text} karena gap estimasi minimum Jepang biasanya lebih kecil daripada Singapura. Tetap wajib cari beasiswa/tuition waiver dan kota hemat; jangan asumsi {budget_text} cukup untuk self-funded penuh."}, + {"title": "Pathway Hemat: Mulai Lokal lalu Transfer/Credit Recognition", "impact": 76, "efficiency": 88, "speed": 68, "low_risk": 86, "success_rate": 80, "rationale": f"Menekan cash-out awal sambil mengejar beasiswa dan kesiapan bahasa. Cocok jika hitungan {budget_text} per {years_text} tidak menutup total biaya studi luar negeri penuh."}, + {"title": "Singapura Hanya Jika Ada Scholarship/Subsidy Besar", "impact": 82, "efficiency": 58, "speed": 86, "low_risk": 55, "success_rate": 66, "rationale": f"Singapura bisa unggul untuk akses industri dan durasi cepat, tetapi {budget_text} berisiko tidak cukup tanpa bantuan biaya yang jelas sejak awal.{singapore_gap_text}"}, ] steps = [ - (1, "Buat batas biaya final", "Pecah dana 200 juta menjadi tuition, living cost, visa, tiket, asuransi, dan dana darurat; coret opsi yang melewati batas aman."), - (2, "Shortlist program 3 tahun", "Cari minimal 10 program Jepang dan 3 program Singapura yang durasinya cocok, lalu tandai syarat bahasa, deadline, dan total biaya."), - (3, "Kejar funding", "Daftar beasiswa, tuition waiver, atau sponsor; jangan memilih Singapura kecuali kekurangan biaya sudah tertutup jelas."), + (1, "Buat batas biaya final", f"Pecah {budget_text} menjadi tuition, living cost, visa, tiket, asuransi, dan dana darurat 10%; coret opsi yang melewati batas aman."), + (2, "Shortlist program sesuai durasi", f"Cari minimal 10 program Jepang dan 3 program Singapura yang durasinya mendekati {years_text}, lalu catat syarat bahasa, deadline, dan total biaya."), + (3, "Hitung gap funding", "Bandingkan estimasi total biaya dengan dana tersedia; targetkan beasiswa, tuition waiver, sponsor, atau income legal untuk menutup gap."), (4, "Validasi aturan visa", "Cek izin kerja paruh waktu, syarat dokumen finansial, serta risiko jika kurs/biaya hidup naik."), - (5, "Decision gate", "Pilih Jepang jika total biaya paling aman; pilih Singapura hanya jika scholarship/subsidy membuat biaya 3 tahun masuk budget."), + (5, "Decision gate", "Pilih opsi dengan gap terkecil dan bukti funding paling jelas; Singapura hanya layak jika scholarship/subsidy membuat biaya total masuk budget."), ] _write_analysis_records(problem, "Budget ketat", cause_profiles, options, steps) @@ -310,16 +553,12 @@ def _build_category_analysis(problem, category): kategori = category["kategori"] causes = category["penyebab"] solutions = category["solusi"] - + case_data = _extract_case_data(problem.description) + constraint_note = _case_constraint_note(case_data) cause_profiles = [ - ( - cause, - _clamp(92 - index * 8), - f"Input terdeteksi sebagai kategori {kategori}. Penyebab ini perlu divalidasi karena dapat langsung memengaruhi target, batasan, dan keputusan berikutnya.", - ) + (cause, _clamp(92 - index * 8), f"Input terdeteksi sebagai kategori {kategori} dengan konteks: {constraint_note}. Penyebab ini perlu divalidasi karena dapat langsung memengaruhi target, batasan, dan keputusan berikutnya.") for index, cause in enumerate(causes) ] - options = [] for index, solution in enumerate(solutions): options.append({ @@ -329,11 +568,10 @@ def _build_category_analysis(problem, category): "speed": _clamp(84 - index * 5), "low_risk": _clamp(74 + index * 4), "success_rate": _clamp(82 - index * 3), - "rationale": f"Solusi ini cocok untuk kategori {kategori} karena langsung menargetkan penyebab utama: {causes[min(index, len(causes) - 1)]}.", + "rationale": f"Solusi ini dipilih karena sesuai kategori {kategori}, menargetkan penyebab '{causes[min(index, len(causes) - 1)]}', dan tetap mengikuti batasan pertanyaan: {constraint_note}.", }) - steps = [ - (1, "Kunci tujuan dan batasan", f"Kategori utama: {kategori}. Tulis target akhir, batasan dana/waktu, dan indikator sukses yang terukur."), + (1, "Kunci tujuan dan batasan", f"Kategori utama: {kategori}. Tulis target akhir, batasan dana/waktu, dan indikator sukses yang terukur dari konteks: {constraint_note}."), (2, "Validasi penyebab utama", f"Cek apakah penyebab paling dominan adalah: {causes[0]}. Kumpulkan bukti sederhana sebelum memilih solusi."), (3, "Bandingkan opsi solusi", f"Bandingkan opsi: {', '.join(solutions[:3])}. Pilih yang paling sesuai dengan budget, waktu, dan risiko."), (4, "Jalankan langkah kecil", "Mulai dari eksperimen paling kecil selama 1-3 hari agar cepat terlihat apakah arah solusi benar."), @@ -439,9 +677,12 @@ def case_detail(request, pk): pk=pk, ) top_solution = problem.solutions.first() + category = _detect_problem_category(problem.description) + case_insights = _build_case_insights(problem.description, category["kategori"]) return render(request, "core/case_detail.html", { "problem": problem, "top_solution": top_solution, + "case_insights": case_insights, "page_title": f"{problem.title} — Analisis OPTEMA AI", - "meta_description": f"Analisis prioritas, akar masalah, solusi berskor, dan action plan untuk {problem.title}.", + "meta_description": f"Analisis prioritas, akar masalah, solusi berskor, kalkulasi data, dan action plan untuk {problem.title}.", })