OPTEMA AI
This commit is contained in:
parent
65e988640d
commit
60f601a85b
Binary file not shown.
@ -38,6 +38,55 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if case_insights %}
|
||||
<div class="analysis-card mb-4">
|
||||
<div class="section-kicker">Data & Kalkulasi</div>
|
||||
<h2>1B. Data yang Terdeteksi dari Pertanyaan</h2>
|
||||
<p class="formula-note">Bagian ini dibuat dari angka/konteks yang Anda tulis, supaya jawaban tidak generik dan bisa dicek hitungannya.</p>
|
||||
<div class="metric-grid mb-4">
|
||||
{% for item in case_insights.detected %}
|
||||
<div class="metric-card">
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
<small>{{ item.note }}</small>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if case_insights.calculations %}
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table align-middle solution-table">
|
||||
<thead><tr><th>Kalkulasi</th><th>Rumus</th><th>Hasil</th></tr></thead>
|
||||
<tbody>
|
||||
{% for calc in case_insights.calculations %}
|
||||
<tr><td><strong>{{ calc.label }}</strong></td><td>{{ calc.formula }}</td><td>{{ calc.result }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if case_insights.comparisons %}
|
||||
<h3 class="h5 mb-3">Perbandingan Opsi Berdasarkan Data</h3>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table align-middle solution-table">
|
||||
<thead><tr><th>Opsi</th><th>Estimasi Total</th><th>Kecocokan Budget</th><th>Catatan</th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in case_insights.comparisons %}
|
||||
<tr><td><strong>{{ row.option }}</strong></td><td>{{ row.estimate }}</td><td>{{ row.budget_fit }}</td><td>{{ row.note }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="formula-note mb-4">{{ case_insights.assumption_note }}</p>
|
||||
{% endif %}
|
||||
{% if case_insights.recommendation %}
|
||||
<div class="alert alert-info mb-3" role="status">{{ case_insights.recommendation }}</div>
|
||||
{% endif %}
|
||||
{% if case_insights.missing %}
|
||||
<div class="alert alert-warning mb-0" role="status"><strong>Data tambahan yang akan membuat jawaban lebih akurat:</strong><ul class="mb-0 mt-2">{% for item in case_insights.missing %}<li>{{ item }}</li>{% endfor %}</ul></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-5">
|
||||
<div class="analysis-card sticky-lg-top">
|
||||
|
||||
365
core/views.py
365
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}.",
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user