-
- Video by {video.user.name} on Pexels
-
+
+
+
+
+
+
+
Live decision preview
+
Penjualan turun 40%
+
+
Critical
+
+
+ {['Impact 90', 'Efficiency 80', 'Speed 85'].map((item) => (
+
+ {item}
-
)
- }
- };
-
- return (
-
-
-
{getPageTitle('Starter Page')}
-
-
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
-
+
+
+
Rekomendasi #1
+
Score 86
+
+
Optimasi funnel penjualan 7 hari dengan eksperimen pesan, channel, dan evaluasi harian.
+
+ {[92, 84, 76].map((value, index) => (
+
+
+ {['Root cause: konversi', 'Marketing signal', 'Risiko eksekusi'][index]}
+ {value}%
+
+
+
+ ))}
+
+
+
-
-
-
-
-
-
+
-
-
-
-
© 2026 {title} . All rights reserved
-
- Privacy Policy
-
-
+
-
- );
-}
+
+
+ {pillars.map((pillar) => (
+
+
+
+
+ {pillar.title}
+ {pillar.text}
+
+ ))}
+
+
+
+ >
+);
-Starter.getLayout = function getLayout(page: ReactElement) {
+Landing.getLayout = function getLayout(page: ReactElement) {
return
{page} ;
};
+export default Landing;
diff --git a/frontend/src/pages/optema-ai.tsx b/frontend/src/pages/optema-ai.tsx
new file mode 100644
index 0000000..e66d101
--- /dev/null
+++ b/frontend/src/pages/optema-ai.tsx
@@ -0,0 +1,2088 @@
+import {
+ mdiBrain,
+ mdiChartTimelineVariant,
+ mdiClipboardTextClockOutline,
+ mdiLightbulbOnOutline,
+ mdiPlus,
+} from '@mdi/js';
+import axios from 'axios';
+import Head from 'next/head';
+import React, { ReactElement, useEffect, useMemo, useState } from 'react';
+import BaseButton from '../components/BaseButton';
+import BaseIcon from '../components/BaseIcon';
+import CardBox from '../components/CardBox';
+import SectionMain from '../components/SectionMain';
+import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
+import { getPageTitle } from '../config';
+import LayoutAuthenticated from '../layouts/Authenticated';
+import { useAppSelector } from '../stores/hooks';
+
+type Cause = {
+ id: string;
+ label: string;
+ category:
+ | 'people'
+ | 'process'
+ | 'technology'
+ | 'policy'
+ | 'environment'
+ | 'materials'
+ | 'measurement'
+ | 'other';
+ contribution: number;
+ description: string;
+};
+
+type DataSource = {
+ id: string;
+ title: string;
+ category: 'input' | 'metric' | 'pattern';
+ summary: string;
+ confidence: number;
+};
+
+type Solution = {
+ id: string;
+ title: string;
+ description: string;
+ rationale: string;
+ executionSteps: string[];
+ kpis: string[];
+ risks: string[];
+ resources: string[];
+ type:
+ | 'marketing'
+ | 'sales'
+ | 'operations'
+ | 'finance'
+ | 'product'
+ | 'hr'
+ | 'technology'
+ | 'other';
+ impact: number;
+ efficiency: number;
+ speed: number;
+ lowRisk: number;
+ score: number;
+ success: number;
+ rootCauseId: string;
+ duration: number;
+};
+
+type PlanTask = {
+ id: string;
+ day: number;
+ title: string;
+ description: string;
+};
+
+type Run = {
+ id: string;
+ caseId: string;
+ planId: string;
+ createdAt: string;
+ title: string;
+ problem: string;
+ urgency: number;
+ financialImpact: 'low' | 'medium' | 'high' | 'unknown';
+ priority: 'low' | 'medium' | 'high' | 'critical';
+ priorityScore: number;
+ causes: Cause[];
+ dataSources: DataSource[];
+ solutions: Solution[];
+ selectedSolution: Solution;
+ tasks: PlanTask[];
+ conclusion: string;
+};
+
+type ProblemKnowledge = {
+ key: 'pengangguran' | 'kemiskinan';
+ label: string;
+ keywords: string[];
+ severity: 'Tinggi' | 'Kritis';
+ penyebab: string[];
+ hipotesis: string[];
+ solusi: string[];
+ kesimpulan: string;
+ causeCategories: Cause['category'][];
+};
+
+type SolutionDraft = Omit<
+ Solution,
+ 'id' | 'score' | 'success' | 'rootCauseId'
+> & {
+ rootCauseId?: string;
+};
+
+type QuestionSolutionFit = {
+ focus: string;
+ keywords: string[];
+};
+
+const STORAGE_KEY = 'optema-ai-runs';
+const MAX_DATA_SOURCES = 3;
+
+const PROBLEM_KNOWLEDGE: ProblemKnowledge[] = [
+ {
+ key: 'pengangguran',
+ label: 'Pengangguran',
+ keywords: [
+ 'pengangguran',
+ 'menganggur',
+ 'nganggur',
+ 'pencari kerja',
+ 'cari kerja',
+ 'pekerjaan',
+ 'lamaran',
+ 'cv',
+ 'interview',
+ 'wawancara',
+ 'lowongan kerja',
+ 'lapangan kerja',
+ 'jobless',
+ ],
+ severity: 'Tinggi',
+ penyebab: [
+ 'Kurangnya lapangan kerja',
+ 'Skill tidak sesuai kebutuhan industri',
+ 'Kurangnya pengalaman',
+ 'Pendidikan rendah',
+ 'Otomatisasi pekerjaan',
+ 'Pertumbuhan ekonomi lambat',
+ 'Kurangnya informasi lowongan',
+ 'Mobilitas rendah',
+ 'Kurangnya sertifikasi',
+ 'Persaingan tinggi',
+ ],
+ hipotesis: [
+ 'Skill tidak relevan',
+ 'Lokasi pekerjaan terbatas',
+ 'Kurangnya pelatihan',
+ 'Kurangnya akses informasi',
+ 'Ekonomi melambat',
+ 'Perusahaan mengurangi perekrutan',
+ 'Kurangnya jaringan profesional',
+ 'Kurangnya pengalaman kerja',
+ 'Teknologi menggantikan pekerjaan',
+ 'Sertifikasi belum memadai',
+ ],
+ solusi: [
+ 'Pelatihan keterampilan',
+ 'Mengikuti sertifikasi',
+ 'Membuat CV profesional',
+ 'Magang',
+ 'Networking',
+ 'Pelatihan digital',
+ 'Job matching',
+ 'Wirausaha',
+ 'Peningkatan pendidikan',
+ 'Pencarian kerja aktif',
+ ],
+ kesimpulan:
+ 'Pengangguran dapat dikurangi dengan peningkatan kompetensi, akses informasi kerja, dan penciptaan lapangan kerja.',
+ causeCategories: ['policy', 'people', 'process', 'technology'],
+ },
+ {
+ key: 'kemiskinan',
+ label: 'Kemiskinan',
+ keywords: [
+ 'kemiskinan',
+ 'miskin',
+ 'pendapatan rendah',
+ 'kredit mikro',
+ 'modal usaha',
+ 'bantuan modal',
+ 'usaha kecil',
+ 'umkm',
+ 'ekonomi keluarga',
+ ],
+ severity: 'Kritis',
+ penyebab: [
+ 'Pendapatan rendah',
+ 'Pendidikan rendah',
+ 'Pengangguran',
+ 'Kurangnya modal',
+ 'Akses layanan terbatas',
+ 'Produktivitas rendah',
+ 'Ketimpangan ekonomi',
+ 'Inflasi',
+ 'Beban keluarga tinggi',
+ 'Kurangnya keterampilan',
+ ],
+ hipotesis: [
+ 'Kurangnya peluang ekonomi',
+ 'Kurangnya akses pendidikan',
+ 'Tidak memiliki aset produktif',
+ 'Akses modal sulit',
+ 'Kesehatan buruk',
+ 'Kondisi wilayah tertinggal',
+ 'Kurangnya pelatihan',
+ 'Harga kebutuhan meningkat',
+ 'Pendapatan tidak stabil',
+ 'Kesempatan kerja terbatas',
+ ],
+ solusi: [
+ 'Pelatihan kerja',
+ 'Bantuan modal usaha',
+ 'Pendidikan vokasi',
+ 'Pendampingan UMKM',
+ 'Program padat karya',
+ 'Akses kredit mikro',
+ 'Peningkatan produktivitas',
+ 'Digitalisasi usaha',
+ 'Kemitraan usaha',
+ 'Penciptaan lapangan kerja',
+ ],
+ kesimpulan:
+ 'Kemiskinan membutuhkan kombinasi peningkatan pendapatan, pendidikan, keterampilan, dan akses ekonomi.',
+ causeCategories: ['environment', 'people', 'policy', 'process'],
+ },
+];
+
+const uuid = () =>
+ typeof crypto !== 'undefined' && 'randomUUID' in crypto
+ ? crypto.randomUUID()
+ : `${Date.now()}-${Math.random().toString(16).slice(2)}`;
+
+const clamp = (value: number) => Math.max(1, Math.min(100, Math.round(value)));
+
+const scoreSolution = (
+ impact: number,
+ efficiency: number,
+ speed: number,
+ lowRisk: number,
+) => Math.round(impact * 0.4 + efficiency * 0.3 + speed * 0.2 + lowRisk * 0.1);
+
+const titleFromProblem = (problem: string) => {
+ const clean = problem.trim().replace(/\s+/g, ' ');
+ if (!clean) return 'Kasus keputusan baru';
+ return clean.length > 64 ? `${clean.slice(0, 61)}...` : clean;
+};
+
+const shortList = (items: string[], limit = 3) => {
+ const selected = items.slice(0, limit);
+
+ return selected.length ? selected.join(', ') : 'data belum tersedia';
+};
+
+const cleanText = (value: string) => value.trim().replace(/\s+/g, ' ');
+
+const problemSnippet = (problem: string, maxLength = 150) => {
+ const clean = cleanText(problem);
+
+ return clean.length > maxLength ? `${clean.slice(0, maxLength - 3)}...` : clean;
+};
+
+const trimSentenceEnd = (value: string) =>
+ cleanText(value).replace(/[.!?]+$/u, '');
+
+const keywordFitScore = (problem: string, keywords: string[]) => {
+ const text = cleanText(problem).toLowerCase();
+
+ return keywords.reduce((score, keyword) => {
+ const normalizedKeyword = cleanText(keyword).toLowerCase();
+
+ if (!normalizedKeyword || !text.includes(normalizedKeyword)) {
+ return score;
+ }
+
+ return score + (normalizedKeyword.includes(' ') ? 3 : 1);
+ }, 0);
+};
+
+const applyQuestionFitToDrafts = (
+ drafts: SolutionDraft[],
+ fits: QuestionSolutionFit[],
+ problem: string,
+): SolutionDraft[] => {
+ const scores = fits.map((fit) => keywordFitScore(problem, fit.keywords));
+ const maxScore = Math.max(...scores, 0);
+ const hasExplicitFit = maxScore > 0;
+ const snippet = problemSnippet(problem);
+
+ return drafts.map((draft, index) => {
+ const fit = fits[index];
+ const fitScore = scores[index] || 0;
+ const isBestFit = hasExplicitFit && fitScore === maxScore && fitScore > 0;
+ const scoreBoost = isBestFit
+ ? 10 + Math.min(fitScore, 8)
+ : fitScore > 0
+ ? 4 + Math.min(fitScore, 4)
+ : hasExplicitFit
+ ? -3
+ : 0;
+ const questionContext = fit
+ ? fitScore > 0
+ ? `Kesesuaian pertanyaan: solusi ini menjawab fokus “${fit.focus}” yang muncul pada input pengguna: “${snippet}”.`
+ : hasExplicitFit
+ ? 'Catatan kesesuaian: solusi ini hanya alternatif pendukung, bukan jawaban paling langsung dari pertanyaan pengguna.'
+ : `Kesesuaian pertanyaan: solusi ini disusun dari pola masalah yang ditulis pengguna: “${snippet}”.`
+ : '';
+
+ return {
+ ...draft,
+ title: isBestFit ? `${draft.title} — paling sesuai pertanyaan` : draft.title,
+ rationale: [questionContext, draft.rationale].filter(Boolean).join(' '),
+ impact: draft.impact + scoreBoost,
+ efficiency: draft.efficiency + (isBestFit ? 4 : fitScore > 0 ? 2 : 0),
+ speed: draft.speed + (isBestFit ? 3 : fitScore > 0 ? 1 : 0),
+ lowRisk: draft.lowRisk + (isBestFit ? 2 : 0),
+ };
+ });
+};
+
+const findProblemKnowledge = (problem: string) => {
+ const text = problem.toLowerCase();
+
+ return PROBLEM_KNOWLEDGE.find((knowledge) =>
+ knowledge.keywords.some((keyword) => text.includes(keyword)),
+ );
+};
+
+const buildKnowledgeCauses = (
+ knowledge: ProblemKnowledge,
+ urgency: number,
+ financialImpact: Run['financialImpact'],
+): Cause[] => {
+ const impactBoost =
+ financialImpact === 'high' ? 9 : financialImpact === 'medium' ? 5 : 2;
+ const severityBoost = knowledge.severity === 'Kritis' ? 10 : 6;
+ const base = 56 + urgency * 5 + impactBoost + severityBoost;
+
+ return knowledge.penyebab.slice(0, 4).map((cause, index) => ({
+ id: uuid(),
+ label: cause,
+ category: knowledge.causeCategories[index] || 'other',
+ contribution: clamp(base + 14 - index * 7),
+ description: `Penyebab dari knowledge base ${knowledge.label}. Hipotesis pendukung: ${
+ knowledge.hipotesis[index] || knowledge.hipotesis[0]
+ }.`,
+ }));
+};
+
+const finalizeSolutions = (
+ raw: SolutionDraft[],
+ causes: Cause[],
+): Solution[] =>
+ raw
+ .map((solution) => {
+ const score = scoreSolution(
+ solution.impact,
+ solution.efficiency,
+ solution.speed,
+ solution.lowRisk,
+ );
+
+ return {
+ ...solution,
+ id: uuid(),
+ rootCauseId: solution.rootCauseId || causes[0]?.id || uuid(),
+ impact: clamp(solution.impact),
+ efficiency: clamp(solution.efficiency),
+ speed: clamp(solution.speed),
+ lowRisk: clamp(solution.lowRisk),
+ score,
+ success: clamp(score + (solution.lowRisk - 70) * 0.25),
+ } as Solution;
+ })
+ .sort((a, b) => b.score - a.score);
+
+const inferCauses = (
+ problem: string,
+ urgency: number,
+ financialImpact: Run['financialImpact'],
+): Cause[] => {
+ const matchedKnowledge = findProblemKnowledge(problem);
+
+ if (matchedKnowledge) {
+ return buildKnowledgeCauses(matchedKnowledge, urgency, financialImpact);
+ }
+
+ const text = problem.toLowerCase();
+ const impactBoost =
+ financialImpact === 'high' ? 8 : financialImpact === 'medium' ? 4 : 0;
+ const base = 58 + urgency * 5 + impactBoost;
+
+ const candidates: Cause[] = [
+ {
+ id: uuid(),
+ label: text.match(/jual|sales|revenue|omzet|penjualan|conversion/)
+ ? 'Akuisisi & konversi penjualan melemah'
+ : 'Sinyal performa utama belum stabil',
+ category: 'sales' as Cause['category'],
+ contribution: clamp(base + 16),
+ description:
+ 'Indikasi utama datang dari funnel permintaan, kualitas prospek, atau kemampuan tim mengubah peluang menjadi transaksi.',
+ },
+ {
+ id: uuid(),
+ label: text.match(/iklan|marketing|campaign|brand|traffic/)
+ ? 'Pesan marketing kurang tepat sasaran'
+ : 'Eksperimen marketing belum cukup terukur',
+ category: 'measurement',
+ contribution: clamp(base + 9),
+ description:
+ 'Channel, pesan, dan metrik evaluasi perlu dipisahkan agar keputusan tidak hanya berdasarkan asumsi.',
+ },
+ {
+ id: uuid(),
+ label: text.match(/harga|margin|biaya|cash|modal/)
+ ? 'Tekanan harga, margin, atau biaya'
+ : 'Prioritas operasional belum selaras dengan target',
+ category: 'process',
+ contribution: clamp(base + 3),
+ description:
+ 'Ada peluang menata ulang proses, prioritas kerja, dan batasan biaya sebelum menjalankan solusi besar.',
+ },
+ {
+ id: uuid(),
+ label: text.match(/produk|fitur|quality|kualitas|stok/)
+ ? 'Kesesuaian produk dan pengalaman pelanggan perlu divalidasi'
+ : 'Risiko eksekusi perlu dikendalikan sejak awal',
+ category: 'product' as Cause['category'],
+ contribution: clamp(base - 5),
+ description:
+ 'Solusi harus diuji cepat agar tim tidak menghabiskan biaya pada arah yang belum terbukti.',
+ },
+ ];
+
+ return candidates
+ .map((cause) => ({
+ ...cause,
+ category:
+ cause.category === 'sales' || cause.category === 'product'
+ ? 'other'
+ : cause.category,
+ }))
+ .sort((a, b) => b.contribution - a.contribution);
+};
+
+const buildDataSources = (
+ problem: string,
+ urgency: number,
+ financialImpact: Run['financialImpact'],
+ causes: Cause[],
+): DataSource[] => {
+ const cleanProblem = problem.trim().replace(/\s+/g, ' ');
+ const financialLabel =
+ financialImpact === 'high'
+ ? 'besar'
+ : financialImpact === 'medium'
+ ? 'sedang'
+ : financialImpact === 'low'
+ ? 'rendah'
+ : 'belum diketahui';
+ const topCauses = causes
+ .slice(0, 2)
+ .map((cause) => cause.label.toLowerCase())
+ .join(' dan ');
+ const matchedKnowledge = findProblemKnowledge(problem);
+
+ if (matchedKnowledge) {
+ const sources: DataSource[] = [
+ {
+ id: uuid(),
+ title: 'Input masalah dari pengguna',
+ category: 'input',
+ summary: `Narasi utama terdeteksi sebagai topik ${matchedKnowledge.label}: “${
+ cleanProblem.length > 132
+ ? `${cleanProblem.slice(0, 129)}...`
+ : cleanProblem
+ }”.`,
+ confidence: clamp(90 + Math.min(cleanProblem.length / 50, 6)),
+ },
+ {
+ id: uuid(),
+ title: `Knowledge base ${matchedKnowledge.label}`,
+ category: 'pattern',
+ summary: `Severity ${matchedKnowledge.severity}. Penyebab dominan: ${shortList(
+ matchedKnowledge.penyebab,
+ MAX_DATA_SOURCES,
+ )}.`,
+ confidence: matchedKnowledge.severity === 'Kritis' ? 94 : 91,
+ },
+ {
+ id: uuid(),
+ title: 'Hipotesis dan prioritas eksekusi',
+ category: 'metric',
+ summary: `Hipotesis utama: ${shortList(
+ matchedKnowledge.hipotesis,
+ MAX_DATA_SOURCES,
+ )}. Urgensi ${urgency}/5 dan dampak finansial ${financialLabel}.`,
+ confidence: clamp(84 + urgency * 2),
+ },
+ ];
+
+ return sources.slice(0, MAX_DATA_SOURCES);
+ }
+
+ const sources: DataSource[] = [
+ {
+ id: uuid(),
+ title: 'Input masalah dari pengguna',
+ category: 'input',
+ summary: `Narasi utama yang dianalisis: “${cleanProblem.length > 132 ? `${cleanProblem.slice(0, 129)}...` : cleanProblem}”.`,
+ confidence: clamp(88 + Math.min(cleanProblem.length / 40, 8)),
+ },
+ {
+ id: uuid(),
+ title: 'Parameter prioritas bisnis',
+ category: 'metric',
+ summary: `Urgensi berada di level ${urgency}/5 dengan dampak finansial ${financialLabel}; parameter ini menaikkan bobot prioritas dan kecepatan eksekusi.`,
+ confidence: clamp(82 + urgency * 2),
+ },
+ {
+ id: uuid(),
+ title: 'Pola akar masalah terdeteksi',
+ category: 'pattern',
+ summary: topCauses
+ ? `OPTEMA membaca pola dominan pada ${topCauses}; pola ini menjadi dasar pemilihan solusi dan action plan.`
+ : 'OPTEMA belum menemukan pola dominan yang kuat; solusi diarahkan ke validasi cepat agar data berikutnya lebih presisi.',
+ confidence: clamp(causes[0]?.contribution || 72),
+ },
+ ];
+
+ return sources.slice(0, MAX_DATA_SOURCES);
+};
+
+const buildKnowledgeSolutionDrafts = (
+ problem: string,
+ knowledge: ProblemKnowledge,
+ urgency: number,
+ causes: Cause[],
+): SolutionDraft[] => {
+ const allSolutionOptions = shortList(knowledge.solusi, 10);
+ const topHypotheses = shortList(knowledge.hipotesis, MAX_DATA_SOURCES);
+ const topCauses = shortList(knowledge.penyebab, MAX_DATA_SOURCES);
+ const withQuestionFit = (
+ drafts: SolutionDraft[],
+ fits: QuestionSolutionFit[],
+ ) => applyQuestionFitToDrafts(drafts, fits, problem);
+
+ if (knowledge.key === 'pengangguran') {
+ return withQuestionFit([
+ {
+ title: 'Pelatihan keterampilan dan sertifikasi kerja terarah',
+ description:
+ 'Membangun jalur peningkatan kompetensi yang langsung dihubungkan dengan kebutuhan industri, sertifikasi, dan target penempatan kerja.',
+ rationale: `Diprioritaskan karena penyebab utama adalah ${topCauses}. Hipotesis yang diuji: ${topHypotheses}. Paket solusi dasar: ${allSolutionOptions}.`,
+ executionSteps: [
+ 'Petakan 3–5 sektor industri yang masih membuka lowongan dan daftar skill yang paling sering diminta.',
+ 'Kelompokkan pencari kerja berdasarkan level skill, pengalaman, pendidikan, dan kesiapan mengikuti sertifikasi.',
+ 'Jalankan pelatihan digital/vokasi pendek berbasis proyek agar peserta punya portofolio nyata.',
+ 'Fasilitasi sertifikasi yang relevan dengan lowongan prioritas, bukan sertifikasi umum yang sulit dikonversi menjadi pekerjaan.',
+ 'Hubungkan peserta tersertifikasi ke job matching, magang, dan interview terjadwal dengan perusahaan/UMKM lokal.',
+ 'Review hasil setiap 2 minggu: peserta lulus, lamaran terkirim, interview, penempatan, dan gap skill yang tersisa.',
+ ],
+ kpis: [
+ 'Jumlah peserta menyelesaikan pelatihan',
+ 'Persentase peserta memperoleh sertifikasi',
+ 'Jumlah peserta masuk interview/magang',
+ 'Tingkat penempatan kerja setelah 30–60 hari',
+ ],
+ risks: [
+ 'Pelatihan tidak sesuai kebutuhan industri; mitigasi: validasi kurikulum dengan lowongan aktif dan HR/perusahaan lokal.',
+ 'Peserta berhenti di tengah program; mitigasi: gunakan kelas pendek, mentor, dan target portofolio mingguan.',
+ 'Sertifikasi tidak meningkatkan peluang kerja; mitigasi: pilih sertifikasi yang disebut eksplisit dalam lowongan prioritas.',
+ ],
+ resources: [
+ 'Data lowongan aktif',
+ 'Trainer vokasi/digital',
+ 'Mitra sertifikasi',
+ 'PIC job matching',
+ ],
+ type: 'hr',
+ impact: 92 + urgency,
+ efficiency: 86,
+ speed: 78,
+ lowRisk: 82,
+ duration: 30,
+ rootCauseId: causes[1]?.id || causes[0]?.id,
+ },
+ {
+ title: 'Job matching, CV profesional, dan pencarian kerja aktif',
+ description:
+ 'Memperbaiki akses informasi kerja, kualitas CV, kesiapan interview, dan ritme lamaran agar peluang kerja meningkat cepat.',
+ rationale:
+ 'Cocok untuk mengatasi kurangnya informasi lowongan, jaringan profesional terbatas, dan rendahnya pengalaman mencari kerja secara terstruktur.',
+ executionSteps: [
+ 'Bangun database lowongan yang dikurasi berdasarkan lokasi, skill minimum, gaji, dan peluang entry-level.',
+ 'Buat template CV profesional, LinkedIn/profil digital, dan surat lamaran yang disesuaikan per jenis posisi.',
+ 'Latih simulasi interview, komunikasi profesional, dan cara menjelaskan pengalaman nonformal atau proyek portofolio.',
+ 'Tetapkan target pencarian kerja aktif: jumlah lamaran, follow-up, networking, dan evaluasi penolakan per minggu.',
+ 'Pasangkan kandidat dengan mentor atau alumni agar mereka mendapat referensi dan informasi lowongan lebih cepat.',
+ ],
+ kpis: [
+ 'Jumlah CV/profil diperbaiki',
+ 'Jumlah lamaran berkualitas per minggu',
+ 'Rasio panggilan interview',
+ 'Jumlah kandidat mendapat offer/magang',
+ ],
+ risks: [
+ 'Lamaran banyak tetapi tidak relevan; mitigasi: pakai daftar lowongan terkurasi dan scoring kecocokan skill.',
+ 'Kandidat kurang percaya diri saat interview; mitigasi: lakukan simulasi dan feedback sebelum interview nyata.',
+ ],
+ resources: [
+ 'Database lowongan',
+ 'Template CV dan profil digital',
+ 'Mentor karier',
+ 'Kemitraan perusahaan/HR',
+ ],
+ type: 'hr',
+ impact: 87,
+ efficiency: 89,
+ speed: 92,
+ lowRisk: 78,
+ duration: 14,
+ rootCauseId: causes[2]?.id || causes[0]?.id,
+ },
+ {
+ title: 'Magang, networking, dan wirausaha mikro',
+ description:
+ 'Membuka jalur pengalaman kerja melalui magang, jejaring profesional, dan opsi wirausaha kecil untuk mengurangi ketergantungan pada lowongan formal.',
+ rationale:
+ 'Menjawab hambatan kurang pengalaman, persaingan tinggi, dan keterbatasan lapangan kerja dengan jalur pengalaman serta pendapatan alternatif.',
+ executionSteps: [
+ 'Identifikasi UMKM/perusahaan lokal yang bisa menerima magang pendek berbasis proyek dan target output jelas.',
+ 'Bentuk komunitas networking pencari kerja, mentor, pelaku usaha, dan perusahaan lokal.',
+ 'Siapkan proyek mikro: jasa digital, produksi kecil, reseller, atau layanan lokal yang bisa menghasilkan portofolio dan pendapatan awal.',
+ 'Berikan pendampingan dasar keuangan, pemasaran, dan layanan pelanggan untuk peserta yang memilih wirausaha.',
+ 'Dokumentasikan pengalaman peserta menjadi portofolio agar tetap berguna untuk melamar kerja formal.',
+ ],
+ kpis: [
+ 'Jumlah slot magang aktif',
+ 'Jumlah proyek portofolio selesai',
+ 'Pendapatan awal dari usaha mikro',
+ 'Jumlah koneksi profesional baru',
+ ],
+ risks: [
+ 'Magang tidak berkualitas; mitigasi: tetapkan output, mentor, dan evaluasi mingguan.',
+ 'Usaha mikro tidak berlanjut; mitigasi: mulai kecil, validasi permintaan, dan batasi modal awal.',
+ ],
+ resources: [
+ 'Mitra UMKM/perusahaan',
+ 'Mentor bisnis/karier',
+ 'Modul wirausaha mikro',
+ 'Komunitas alumni/pencari kerja',
+ ],
+ type: 'hr',
+ impact: 82,
+ efficiency: 76,
+ speed: 84,
+ lowRisk: 74,
+ duration: 21,
+ rootCauseId: causes[3]?.id || causes[0]?.id,
+ },
+ ], [
+ {
+ focus: 'peningkatan skill, pelatihan, sertifikasi, dan kesiapan industri',
+ keywords: [
+ 'skill',
+ 'keterampilan',
+ 'kompetensi',
+ 'sertifikasi',
+ 'pelatihan',
+ 'pendidikan',
+ 'industri',
+ 'otomatisasi',
+ 'digital',
+ ],
+ },
+ {
+ focus: 'akses informasi lowongan, CV, lamaran, interview, dan job matching',
+ keywords: [
+ 'lowongan',
+ 'informasi',
+ 'cv',
+ 'lamaran',
+ 'interview',
+ 'wawancara',
+ 'job matching',
+ 'pencarian kerja',
+ 'pencari kerja',
+ 'lokasi',
+ ],
+ },
+ {
+ focus: 'pengalaman kerja, magang, networking, portofolio, atau wirausaha',
+ keywords: [
+ 'pengalaman',
+ 'magang',
+ 'networking',
+ 'jaringan',
+ 'wirausaha',
+ 'usaha',
+ 'umkm',
+ 'lapangan kerja',
+ 'modal',
+ 'portofolio',
+ ],
+ },
+ ]);
+ }
+
+ return withQuestionFit([
+ {
+ title: 'Pendampingan UMKM, modal mikro, dan digitalisasi usaha',
+ description:
+ 'Menaikkan pendapatan keluarga melalui akses modal produktif, pendampingan usaha, pencatatan keuangan, dan kanal penjualan digital.',
+ rationale: `Diprioritaskan karena penyebab utama adalah ${topCauses}. Hipotesis yang diuji: ${topHypotheses}. Paket solusi dasar: ${allSolutionOptions}.`,
+ executionSteps: [
+ 'Identifikasi keluarga/UMKM sasaran berdasarkan pendapatan, aset produktif, jenis usaha, dan kesiapan menjalankan usaha.',
+ 'Tentukan kebutuhan modal produktif yang spesifik: stok, alat kerja, bahan baku, atau akses distribusi, bukan bantuan konsumtif umum.',
+ 'Berikan kredit mikro/bantuan modal bertahap dengan pendampingan pencatatan cash-in, cash-out, margin, dan stok.',
+ 'Digitalisasikan usaha secara sederhana: katalog WhatsApp, marketplace lokal, pembayaran digital, dan promosi komunitas.',
+ 'Pasangkan UMKM dengan mentor dan kemitraan penjualan agar modal berubah menjadi omzet, bukan hanya tambahan utang.',
+ 'Evaluasi setiap 2 minggu: omzet, margin, repeat order, arus kas, dan kemampuan membayar kembali modal.',
+ ],
+ kpis: [
+ 'Kenaikan pendapatan bersih keluarga',
+ 'Jumlah UMKM aktif setelah pendampingan',
+ 'Rasio modal menjadi omzet',
+ 'Jumlah transaksi digital/kemitraan penjualan',
+ ],
+ risks: [
+ 'Modal dipakai untuk konsumsi; mitigasi: salurkan bertahap berdasarkan rencana usaha dan bukti pembelian produktif.',
+ 'Usaha tidak punya pasar; mitigasi: validasi pembeli sebelum modal besar diberikan.',
+ 'Pencatatan keuangan tidak disiplin; mitigasi: gunakan format harian sangat sederhana dan review mingguan.',
+ ],
+ resources: [
+ 'Dana/kredit mikro',
+ 'Mentor UMKM',
+ 'Template pencatatan keuangan',
+ 'Kanal penjualan digital',
+ ],
+ type: 'finance',
+ impact: 94 + urgency,
+ efficiency: 84,
+ speed: 76,
+ lowRisk: 76,
+ duration: 45,
+ rootCauseId: causes[3]?.id || causes[0]?.id,
+ },
+ {
+ title: 'Pelatihan kerja, pendidikan vokasi, dan program padat karya',
+ description:
+ 'Menghubungkan keluarga berpendapatan rendah dengan keterampilan kerja cepat, pekerjaan sementara, dan jalur vokasi menuju pekerjaan lebih stabil.',
+ rationale:
+ 'Menjawab pengangguran, pendidikan rendah, keterampilan terbatas, dan pendapatan tidak stabil dengan kombinasi income cepat dan skill jangka menengah.',
+ executionSteps: [
+ 'Pilih skill vokasi yang cepat diserap pasar lokal: konstruksi ringan, kuliner, perawatan, administrasi, digital dasar, atau logistik.',
+ 'Jalankan program padat karya untuk pendapatan cepat sambil peserta mengikuti pelatihan terjadwal.',
+ 'Hubungkan peserta dengan sertifikasi sederhana, magang, atau kontrak kerja lokal setelah pelatihan.',
+ 'Sediakan dukungan dasar seperti transportasi, jadwal fleksibel, atau childcare bila menjadi penghalang partisipasi.',
+ 'Monitor transisi dari padat karya ke kerja tetap/usaha produktif agar bantuan tidak berhenti sebagai program sementara.',
+ ],
+ kpis: [
+ 'Jumlah peserta mendapat pendapatan cepat',
+ 'Jumlah peserta menyelesaikan vokasi',
+ 'Jumlah penempatan kerja/magang',
+ 'Kenaikan pendapatan 30–90 hari',
+ ],
+ risks: [
+ 'Program padat karya berhenti tanpa transisi; mitigasi: sejak awal pasangkan dengan pelatihan dan job matching.',
+ 'Peserta tidak bisa hadir konsisten; mitigasi: jadwal fleksibel dan dukungan transportasi/keluarga.',
+ ],
+ resources: [
+ 'Mitra pelatihan vokasi',
+ 'Anggaran padat karya',
+ 'Instruktur/mentor',
+ 'Mitra penempatan kerja',
+ ],
+ type: 'hr',
+ impact: 90,
+ efficiency: 80,
+ speed: 86,
+ lowRisk: 78,
+ duration: 30,
+ rootCauseId: causes[1]?.id || causes[0]?.id,
+ },
+ {
+ title: 'Kemitraan usaha dan peningkatan produktivitas komunitas',
+ description:
+ 'Membangun akses ekonomi melalui kemitraan pemasok, koperasi/kelompok usaha, peningkatan produktivitas, dan akses layanan pendukung.',
+ rationale:
+ 'Cocok untuk wilayah dengan akses layanan terbatas, produktivitas rendah, dan ketimpangan ekonomi yang tidak selesai hanya dengan bantuan tunai.',
+ executionSteps: [
+ 'Petakan komoditas, jasa, atau keterampilan lokal yang bisa dikembangkan sebagai produk komunitas.',
+ 'Bentuk kelompok usaha/koperasi kecil untuk pembelian bahan baku, produksi, pemasaran, dan negosiasi harga bersama.',
+ 'Cari kemitraan dengan off-taker, toko lokal, BUMDes, komunitas, atau marketplace agar ada permintaan yang jelas.',
+ 'Perbaiki produktivitas melalui alat sederhana, standar kualitas, jadwal produksi, dan pembagian peran.',
+ 'Hubungkan keluarga miskin dengan layanan pendukung: kesehatan dasar, administrasi, pendidikan, dan akses modal lanjutan.',
+ ],
+ kpis: [
+ 'Jumlah kemitraan/off-taker aktif',
+ 'Produktivitas per kelompok usaha',
+ 'Jumlah keluarga mendapat akses layanan pendukung',
+ 'Kenaikan margin/pendapatan komunitas',
+ ],
+ risks: [
+ 'Kelompok usaha konflik internal; mitigasi: aturan peran, pembukuan transparan, dan fasilitator netral.',
+ 'Permintaan pasar tidak stabil; mitigasi: mulai dengan kontrak kecil/off-taker yang sudah teridentifikasi.',
+ ],
+ resources: [
+ 'Fasilitator komunitas',
+ 'Mitra off-taker',
+ 'Alat produktivitas sederhana',
+ 'Akses layanan sosial/ekonomi',
+ ],
+ type: 'operations',
+ impact: 84,
+ efficiency: 78,
+ speed: 70,
+ lowRisk: 82,
+ duration: 60,
+ rootCauseId: causes[2]?.id || causes[0]?.id,
+ },
+ ], [
+ {
+ focus: 'modal usaha, kredit mikro, UMKM, dan digitalisasi pendapatan',
+ keywords: [
+ 'modal',
+ 'modal usaha',
+ 'kredit mikro',
+ 'bantuan modal',
+ 'umkm',
+ 'usaha',
+ 'pendapatan',
+ 'ekonomi keluarga',
+ 'digitalisasi',
+ 'produktif',
+ ],
+ },
+ {
+ focus: 'pelatihan kerja, pendidikan vokasi, pekerjaan cepat, dan padat karya',
+ keywords: [
+ 'pelatihan',
+ 'kerja',
+ 'pendidikan',
+ 'vokasi',
+ 'padat karya',
+ 'skill',
+ 'keterampilan',
+ 'pengangguran',
+ 'pekerjaan',
+ 'pendapatan tidak stabil',
+ ],
+ },
+ {
+ focus: 'kemitraan usaha, produktivitas komunitas, akses layanan, dan pasar',
+ keywords: [
+ 'kemitraan',
+ 'produktifitas',
+ 'produktivitas',
+ 'komunitas',
+ 'koperasi',
+ 'pasar',
+ 'akses layanan',
+ 'ketimpangan',
+ 'wilayah tertinggal',
+ 'aset produktif',
+ ],
+ },
+ ]);
+};
+
+const buildSolutions = (
+ problem: string,
+ urgency: number,
+ causes: Cause[],
+): Solution[] => {
+ const matchedKnowledge = findProblemKnowledge(problem);
+
+ if (matchedKnowledge) {
+ return finalizeSolutions(
+ buildKnowledgeSolutionDrafts(problem, matchedKnowledge, urgency, causes),
+ causes,
+ );
+ }
+
+ const text = problem.toLowerCase();
+ const isSales = Boolean(
+ text.match(/jual|sales|omzet|penjualan|conversion|revenue/),
+ );
+ const isOps = Boolean(
+ text.match(/operasi|stok|logistik|produksi|delivery|proses/),
+ );
+ const isCost = Boolean(text.match(/biaya|cash|margin|modal|cost|budget/));
+
+ const raw = [
+ {
+ title: isSales
+ ? 'Optimasi funnel penjualan 7 hari'
+ : 'Sprint validasi peluang tertinggi',
+ description:
+ 'Audit cepat data, segmentasi pelanggan, perbaiki pesan utama, lalu jalankan eksperimen kecil dengan metrik harian.',
+ rationale: isSales
+ ? 'Dipilih karena masalah penjualan biasanya paling cepat diperbaiki dengan memperjelas segmen, memperketat follow-up, dan mengukur conversion rate harian.'
+ : 'Dipilih karena membantu tim menguji peluang dengan biaya kecil sebelum mengambil keputusan yang lebih besar.',
+ executionSteps: [
+ 'Tarik data 30–90 hari terakhir: sumber lead, penawaran, conversion, nilai transaksi, dan alasan gagal closing.',
+ 'Kelompokkan pelanggan/lead berdasarkan segmen paling bernilai, lalu pilih satu segmen prioritas untuk eksperimen cepat.',
+ 'Perbaiki value proposition, CTA, skrip follow-up, dan paket penawaran agar hambatan utama langsung dijawab.',
+ 'Jalankan eksperimen 7 hari dengan dashboard harian untuk melihat lead masuk, conversion, biaya, dan omzet tambahan.',
+ ],
+ kpis: [
+ 'Conversion rate harian',
+ 'Cost per qualified lead',
+ 'Omzet tambahan',
+ 'Jumlah follow-up selesai',
+ ],
+ risks: [
+ 'Data penjualan tidak lengkap; mitigasi: gunakan baseline sederhana dan catat manual selama 7 hari.',
+ 'Tim terlalu banyak mencoba kanal; mitigasi: fokus hanya pada 1–2 kanal utama dulu.',
+ ],
+ resources: [
+ '1 PIC sales/marketing',
+ 'Data transaksi dan lead',
+ 'Template pesan/skrip follow-up',
+ 'Budget eksperimen kecil',
+ ],
+ type: isSales ? 'sales' : 'operations',
+ impact: 88 + urgency,
+ efficiency: 76,
+ speed: 90,
+ lowRisk: 72,
+ duration: 7,
+ rootCauseId: causes[0]?.id,
+ },
+ {
+ title: isCost
+ ? 'Kontrol biaya dan prioritas cash-flow'
+ : 'Program reseller/kemitraan mikro',
+ description: isCost
+ ? 'Pisahkan biaya wajib dan variabel, hentikan pengeluaran rendah ROI, lalu alihkan anggaran ke kanal yang terukur.'
+ : 'Bangun penawaran sederhana untuk partner, siapkan materi jual, dan ukur performa tiap calon kanal baru.',
+ rationale: isCost
+ ? 'Dipilih untuk menjaga ruang napas bisnis: arus kas diamankan lebih dulu, lalu biaya dialihkan ke aktivitas yang dampaknya bisa dibuktikan.'
+ : 'Dipilih karena kemitraan kecil bisa memperluas distribusi tanpa menambah beban fixed cost besar di awal.',
+ executionSteps: isCost
+ ? [
+ 'Klasifikasikan biaya menjadi wajib, pendukung pertumbuhan, eksperimen, dan biaya yang bisa dihentikan sementara.',
+ 'Tentukan batas biaya mingguan dan daftar pengeluaran yang harus disetujui sebelum berjalan.',
+ 'Alihkan budget dari aktivitas rendah ROI ke kanal/produk yang memiliki bukti penjualan terbaik.',
+ 'Review cash-in dan cash-out setiap 3 hari untuk memastikan keputusan biaya langsung terlihat efeknya.',
+ ]
+ : [
+ 'Definisikan profil partner ideal: reseller, komunitas, kreator, atau bisnis komplementer dengan audiens relevan.',
+ 'Siapkan paket kemitraan: margin, materi promosi, contoh pesan, aturan komisi, dan target minimum.',
+ 'Rekrut 5–10 partner mikro, mulai dari jaringan terdekat yang sudah dipercaya pelanggan.',
+ 'Bandingkan performa partner berdasarkan leads, transaksi, repeat order, dan biaya insentif.',
+ ],
+ kpis: isCost
+ ? [
+ 'Cash runway',
+ 'Biaya rendah ROI yang dihentikan',
+ 'Margin kontribusi',
+ 'ROI kanal prioritas',
+ ]
+ : [
+ 'Jumlah partner aktif',
+ 'Lead dari partner',
+ 'Penjualan per partner',
+ 'Biaya komisi per transaksi',
+ ],
+ risks: isCost
+ ? [
+ 'Pemotongan biaya mengganggu kualitas layanan; mitigasi: jangan hentikan biaya yang langsung menjaga pengalaman pelanggan.',
+ 'Tim kehilangan momentum growth; mitigasi: sisakan budget eksperimen kecil yang terukur.',
+ ]
+ : [
+ 'Partner tidak aktif setelah onboarding; mitigasi: berikan target 7 hari dan materi siap pakai.',
+ 'Komisi terlalu besar menekan margin; mitigasi: batasi promo pada produk/margin tertentu.',
+ ],
+ resources: isCost
+ ? [
+ 'Laporan biaya',
+ 'Data margin produk',
+ 'PIC finance/owner',
+ 'Dashboard cash-flow sederhana',
+ ]
+ : [
+ 'Materi penawaran',
+ 'Kode/referral partner',
+ 'PIC partner relations',
+ 'Template pelaporan mingguan',
+ ],
+ type: isCost ? 'finance' : 'marketing',
+ impact: 84,
+ efficiency: 84,
+ speed: 68,
+ lowRisk: 80,
+ duration: 14,
+ rootCauseId: causes[1]?.id,
+ },
+ {
+ title: isOps
+ ? 'Perbaikan proses operasional kritis'
+ : 'Promo bundling taktis berbasis data',
+ description: isOps
+ ? 'Petakan bottleneck, tetapkan SLA harian, dan buat ritme monitoring singkat agar hambatan terlihat lebih cepat.'
+ : 'Uji bundling dengan batas waktu, stok, dan target margin yang jelas agar efek terhadap omzet dan profit bisa dibandingkan.',
+ rationale: isOps
+ ? 'Dipilih karena bottleneck operasional sering membuat biaya naik, pelanggan kecewa, dan peluang penjualan hilang meski demand ada.'
+ : 'Dipilih untuk menghasilkan sinyal pasar cepat tanpa mengubah produk inti; cocok saat bisnis butuh dorongan omzet jangka pendek.',
+ executionSteps: isOps
+ ? [
+ 'Petakan proses dari permintaan masuk sampai delivery/penyelesaian, lalu tandai titik tunggu dan rework terbesar.',
+ 'Tetapkan SLA harian untuk titik kritis dan buat papan monitoring sederhana yang terlihat oleh semua PIC.',
+ 'Hilangkan satu hambatan terbesar terlebih dahulu melalui perubahan SOP, pembagian tugas, atau checklist kualitas.',
+ 'Lakukan review singkat setiap akhir hari untuk memutuskan lanjut, koreksi, atau eskalasi.',
+ ]
+ : [
+ 'Pilih produk/jasa dengan margin sehat dan produk pelengkap yang menaikkan nilai transaksi.',
+ 'Buat 2 variasi bundling: satu fokus volume cepat dan satu fokus margin/upsell.',
+ 'Jalankan promo terbatas 5 hari dengan batas stok, batas waktu, dan pesan manfaat yang jelas.',
+ 'Bandingkan hasil bundling dengan penjualan normal dari sisi omzet, margin, dan repeat interest.',
+ ],
+ kpis: isOps
+ ? [
+ 'Lead time proses',
+ 'Jumlah bottleneck terselesaikan',
+ 'SLA harian tercapai',
+ 'Keluhan pelanggan',
+ ]
+ : [
+ 'Average order value',
+ 'Margin per bundle',
+ 'Jumlah transaksi promo',
+ 'Repeat inquiry',
+ ],
+ risks: isOps
+ ? [
+ 'SOP baru tidak dipakai konsisten; mitigasi: jadikan checklist harian dan tunjuk satu owner.',
+ 'Perubahan terlalu banyak sekaligus; mitigasi: pilih satu bottleneck paling kritis dulu.',
+ ]
+ : [
+ 'Diskon menurunkan persepsi nilai; mitigasi: tekankan bonus/manfaat, bukan sekadar potongan harga.',
+ 'Stok tidak cukup; mitigasi: tetapkan kuota dan alternatif produk.',
+ ],
+ resources: isOps
+ ? [
+ 'Peta proses',
+ 'Checklist SLA',
+ 'PIC operasional',
+ 'Data komplain/lead time',
+ ]
+ : [
+ 'Data produk dan margin',
+ 'Materi promo',
+ 'Stok/kapasitas layanan',
+ 'Kanal komunikasi pelanggan',
+ ],
+ type: isOps ? 'operations' : 'sales',
+ impact: 76,
+ efficiency: 69,
+ speed: 95,
+ lowRisk: 88,
+ duration: 5,
+ rootCauseId: causes[2]?.id,
+ },
+ ] as SolutionDraft[];
+
+ const genericFits: QuestionSolutionFit[] = [
+ {
+ focus: isSales
+ ? 'penjualan, omzet, funnel, pelanggan, conversion, dan revenue'
+ : 'validasi peluang atau keputusan paling penting dari masalah pengguna',
+ keywords: isSales
+ ? [
+ 'jual',
+ 'sales',
+ 'omzet',
+ 'penjualan',
+ 'conversion',
+ 'revenue',
+ 'pelanggan',
+ 'lead',
+ 'toko',
+ 'promosi',
+ ]
+ : [
+ 'validasi',
+ 'peluang',
+ 'target',
+ 'strategi',
+ 'keputusan',
+ 'prioritas',
+ 'masalah',
+ ],
+ },
+ {
+ focus: isCost
+ ? 'biaya, modal, budget, margin, cash-flow, dan kondisi keuangan'
+ : 'kemitraan, reseller, partner, distribusi, dan kanal baru',
+ keywords: isCost
+ ? [
+ 'biaya',
+ 'cash',
+ 'cash-flow',
+ 'arus kas',
+ 'margin',
+ 'modal',
+ 'cost',
+ 'budget',
+ 'uang',
+ 'keuangan',
+ ]
+ : [
+ 'reseller',
+ 'kemitraan',
+ 'partner',
+ 'distribusi',
+ 'kanal',
+ 'channel',
+ 'komunitas',
+ 'affiliate',
+ ],
+ },
+ {
+ focus: isOps
+ ? 'operasi, stok, logistik, produksi, delivery, SOP, dan bottleneck proses'
+ : 'promo, bundling, penawaran, paket, dan dorongan transaksi cepat',
+ keywords: isOps
+ ? [
+ 'operasi',
+ 'stok',
+ 'stock',
+ 'logistik',
+ 'produksi',
+ 'delivery',
+ 'pengiriman',
+ 'proses',
+ 'sop',
+ 'bottleneck',
+ ]
+ : [
+ 'promo',
+ 'bundling',
+ 'diskon',
+ 'penawaran',
+ 'paket',
+ 'transaksi',
+ 'stok',
+ 'jualan',
+ ],
+ },
+ ];
+
+ return finalizeSolutions(applyQuestionFitToDrafts(raw, genericFits, problem), causes);
+};
+
+const buildTasks = (solution: Solution): PlanTask[] => {
+ const steps = solution.executionSteps || [];
+ const kpis = solution.kpis || [];
+ const risks = solution.risks || [];
+ const resources = solution.resources || [];
+ const firstStep = trimSentenceEnd(steps[0] || solution.description);
+ const secondStep = trimSentenceEnd(
+ steps[1] || steps[0] || solution.description,
+ );
+ const mainRisk = trimSentenceEnd(
+ risks[0] || 'risiko eksekusi yang muncul selama uji coba',
+ );
+
+ return [
+ {
+ id: uuid(),
+ day: 1,
+ title: 'Audit fakta dan baseline',
+ description:
+ `Kumpulkan data awal yang langsung terkait solusi “${solution.title}”: ${resources.slice(0, 3).join(', ') || 'data performa, PIC, dan baseline masalah'}.`,
+ },
+ {
+ id: uuid(),
+ day: 2,
+ title: 'Susun eksperimen solusi sesuai pertanyaan',
+ description: `Turunkan strategi “${solution.title}” menjadi langkah pertama yang spesifik: ${firstStep}.`,
+ },
+ {
+ id: uuid(),
+ day: 3,
+ title: 'Eksekusi batch pertama',
+ description:
+ `Jalankan batch kecil sesuai langkah implementasi: ${secondStep}. Catat hasil dan hambatan.`,
+ },
+ {
+ id: uuid(),
+ day: 4,
+ title: 'Monitoring dan koreksi cepat',
+ description:
+ `Bandingkan hasil dengan KPI utama: ${kpis.slice(0, 3).join(', ') || 'progress, dampak, dan risiko'}. Koreksi aktivitas yang tidak menjawab masalah.`,
+ },
+ {
+ id: uuid(),
+ day: 5,
+ title: 'Review keputusan lanjut',
+ description:
+ `Putuskan scale, iterate, atau stop berdasarkan KPI dan risiko utama: ${mainRisk}.`,
+ },
+ ];
+};
+
+const buildConclusion = (
+ selectedSolution: Solution,
+ priority: Run['priority'],
+ priorityScore: number,
+ causes: Cause[],
+ dataSources: DataSource[],
+ knowledge?: ProblemKnowledge,
+) => {
+ const mainCause = causes[0]?.label || 'akar masalah utama belum tervalidasi';
+ const evidenceText =
+ dataSources.length > 0
+ ? `berdasarkan ${Math.min(dataSources.length, MAX_DATA_SOURCES)} sumber data ringkas yang tersedia`
+ : 'berdasarkan sinyal awal yang tersedia';
+ const knowledgeConclusion = knowledge
+ ? ` Knowledge base ${knowledge.label} memiliki severity ${knowledge.severity} dan kesimpulan domain: ${knowledge.kesimpulan}`
+ : '';
+
+ return `Kesimpulan: kasus ini berada pada prioritas ${priority} (${priorityScore}/100) dan perlu ditangani dengan eksperimen terukur, bukan keputusan besar tanpa validasi. ${evidenceText}, akar masalah paling dominan adalah ${mainCause.toLowerCase()}.${knowledgeConclusion} Rekomendasi utama adalah “${selectedSolution.title}” karena memiliki Decision Score ${selectedSolution.score}/100, estimasi success rate ${selectedSolution.success}%, dan dapat mulai dieksekusi dalam ${selectedSolution.duration} hari. Fokus 5 hari pertama adalah mengunci baseline, menjalankan eksperimen kecil, mengukur KPI harian, lalu memutuskan scale, iterate, atau stop.`;
+};
+
+const createRun = (
+ problem: string,
+ urgency: number,
+ financialImpact: Run['financialImpact'],
+): Run => {
+ const matchedKnowledge = findProblemKnowledge(problem);
+ const causes = inferCauses(problem, urgency, financialImpact);
+ const dataSources = buildDataSources(
+ problem,
+ urgency,
+ financialImpact,
+ causes,
+ );
+ const solutions = buildSolutions(problem, urgency, causes);
+ const selectedSolution = solutions[0];
+ const impactBoost =
+ financialImpact === 'high'
+ ? 18
+ : financialImpact === 'medium'
+ ? 10
+ : financialImpact === 'low'
+ ? 3
+ : 6;
+ const knowledgeBoost = matchedKnowledge
+ ? matchedKnowledge.severity === 'Kritis'
+ ? 10
+ : 6
+ : 0;
+ const priorityScore = clamp(
+ 52 +
+ urgency * 7 +
+ impactBoost +
+ knowledgeBoost +
+ Math.min(problem.length / 18, 8),
+ );
+ const priority: Run['priority'] =
+ priorityScore >= 88
+ ? 'critical'
+ : priorityScore >= 76
+ ? 'high'
+ : priorityScore >= 58
+ ? 'medium'
+ : 'low';
+ const conclusion = buildConclusion(
+ selectedSolution,
+ priority,
+ priorityScore,
+ causes,
+ dataSources,
+ matchedKnowledge,
+ );
+
+ return {
+ id: uuid(),
+ caseId: uuid(),
+ planId: uuid(),
+ createdAt: new Date().toISOString(),
+ title: titleFromProblem(problem),
+ problem,
+ urgency,
+ financialImpact,
+ priority,
+ priorityScore,
+ causes,
+ dataSources,
+ solutions,
+ selectedSolution,
+ tasks: buildTasks(selectedSolution),
+ conclusion,
+ };
+};
+
+const saveRunToBackend = async (run: Run, currentUserId?: string) => {
+ await axios.post('cases', {
+ data: {
+ id: run.caseId,
+ title: run.title,
+ problem_statement: run.problem,
+ status: 'ready',
+ priority: run.priority,
+ urgency_level: run.urgency,
+ priority_score: run.priorityScore,
+ financial_impact_level: run.financialImpact,
+ currency: 'IDR',
+ opened_at: run.createdAt,
+ notes: [
+ 'Generated from OPTEMA AI Decision Lab MVP workflow.',
+ run.conclusion,
+ `Sumber data: ${(run.dataSources || [])
+ .slice(0, MAX_DATA_SOURCES)
+ .map((source) => source.title)
+ .join(', ')}`,
+ ].join('\n'),
+ created_by_user: currentUserId || null,
+ },
+ });
+
+ await Promise.all(
+ run.causes.map((cause) =>
+ axios.post('root_cause_nodes', {
+ data: {
+ id: cause.id,
+ label: cause.label,
+ description: cause.description,
+ analysis_method: 'mixed',
+ category: cause.category,
+ depth_level: 1,
+ contribution_score: cause.contribution,
+ validated: false,
+ case: run.caseId,
+ },
+ }),
+ ),
+ );
+
+ await Promise.all(
+ run.solutions.map((solution) =>
+ axios.post('solutions', {
+ data: {
+ id: solution.id,
+ title: solution.title,
+ description: [
+ solution.description,
+ `Alasan: ${solution.rationale || '-'}`,
+ `Langkah utama: ${(solution.executionSteps || []).join(' | ')}`,
+ `KPI: ${(solution.kpis || []).join(', ')}`,
+ `Risiko/mitigasi: ${(solution.risks || []).join(' | ')}`,
+ `Resource: ${(solution.resources || []).join(', ')}`,
+ ].join('\n'),
+ solution_type: solution.type,
+ impact_score: solution.impact,
+ efficiency_score: solution.efficiency,
+ speed_score: solution.speed,
+ low_risk_score: solution.lowRisk,
+ decision_score: solution.score,
+ success_probability: solution.success,
+ status:
+ solution.id === run.selectedSolution.id ? 'selected' : 'proposed',
+ expected_duration_days: solution.duration,
+ case: run.caseId,
+ linked_root_cause: solution.rootCauseId,
+ },
+ }),
+ ),
+ );
+
+ await axios.post('action_plans', {
+ data: {
+ id: run.planId,
+ title: `Rencana aksi: ${run.selectedSolution.title}`,
+ objective: `Menjalankan solusi terpilih untuk kasus “${run.title}” dengan ritme 5 hari kerja. ${run.conclusion}`,
+ status: 'active',
+ start_at: run.createdAt,
+ budget_currency: 'IDR',
+ solution: run.selectedSolution.id,
+ owner_user: currentUserId || null,
+ },
+ });
+
+ await Promise.all(
+ run.tasks.map((task) =>
+ axios.post('action_tasks', {
+ data: {
+ id: task.id,
+ title: task.title,
+ description: task.description,
+ status: task.day === 1 ? 'doing' : 'todo',
+ priority: task.day <= 2 ? 'high' : 'medium',
+ day_number: task.day,
+ estimated_hours: task.day === 1 ? 3 : 2,
+ action_plan: run.planId,
+ },
+ }),
+ ),
+ );
+};
+
+const loadRuns = (): Run[] => {
+ if (typeof window === 'undefined') return [];
+ try {
+ const raw = window.localStorage.getItem(STORAGE_KEY);
+ return raw ? (JSON.parse(raw) as Run[]) : [];
+ } catch (error) {
+ console.error('Failed to load OPTEMA AI local history', error);
+ return [];
+ }
+};
+
+const persistRuns = (runs: Run[]) => {
+ if (typeof window === 'undefined') return;
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(runs.slice(0, 8)));
+};
+
+const Pill = ({
+ children,
+ tone = 'blue',
+}: {
+ children: React.ReactNode;
+ tone?: 'blue' | 'green' | 'orange' | 'slate';
+}) => {
+ const tones = {
+ blue: 'bg-blue-50 text-blue-700 ring-blue-200 dark:bg-blue-900/30 dark:text-blue-200',
+ green:
+ 'bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-200',
+ orange:
+ 'bg-orange-50 text-orange-700 ring-orange-200 dark:bg-orange-900/30 dark:text-orange-200',
+ slate:
+ 'bg-slate-100 text-slate-700 ring-slate-200 dark:bg-slate-800 dark:text-slate-200',
+ };
+ return (
+
+ {children}
+
+ );
+};
+
+const OptemaAiPage = () => {
+ const { currentUser } = useAppSelector((state) => state.auth);
+ const [problem, setProblem] = useState(
+ 'Pengangguran di wilayah saya meningkat karena skill warga tidak sesuai kebutuhan industri dan informasi lowongan sulit diakses.',
+ );
+ const [urgency, setUrgency] = useState(3);
+ const [financialImpact, setFinancialImpact] =
+ useState
('high');
+ const [runs, setRuns] = useState([]);
+ const [selectedId, setSelectedId] = useState(null);
+ const [isSaving, setIsSaving] = useState(false);
+ const [notice, setNotice] = useState<{
+ type: 'success' | 'error';
+ message: string;
+ } | null>(null);
+
+ useEffect(() => {
+ const stored = loadRuns();
+ setRuns(stored);
+ setSelectedId(stored[0]?.id || null);
+ }, []);
+
+ const selectedRun = useMemo(
+ () => runs.find((run) => run.id === selectedId) || runs[0],
+ [runs, selectedId],
+ );
+ const selectedDataSources = useMemo(() => {
+ if (!selectedRun) return [];
+ const sources = selectedRun.dataSources?.length
+ ? selectedRun.dataSources
+ : buildDataSources(
+ selectedRun.problem,
+ selectedRun.urgency,
+ selectedRun.financialImpact,
+ selectedRun.causes || [],
+ );
+
+ return sources.slice(0, MAX_DATA_SOURCES);
+ }, [selectedRun]);
+ const selectedConclusion = useMemo(() => {
+ if (!selectedRun) return '';
+
+ return (
+ selectedRun.conclusion ||
+ buildConclusion(
+ selectedRun.selectedSolution,
+ selectedRun.priority,
+ selectedRun.priorityScore,
+ selectedRun.causes || [],
+ selectedDataSources,
+ findProblemKnowledge(selectedRun.problem),
+ )
+ );
+ }, [selectedRun, selectedDataSources]);
+
+ const handleAnalyze = async () => {
+ setNotice(null);
+ if (problem.trim().length < 20) {
+ setNotice({
+ type: 'error',
+ message:
+ 'Tuliskan masalah minimal 20 karakter agar analisis lebih bermakna.',
+ });
+ return;
+ }
+
+ const run = createRun(problem, urgency, financialImpact);
+ setIsSaving(true);
+
+ try {
+ await saveRunToBackend(run, currentUser?.id);
+ const nextRuns = [run, ...runs].slice(0, 8);
+ setRuns(nextRuns);
+ setSelectedId(run.id);
+ persistRuns(nextRuns);
+ setNotice({
+ type: 'success',
+ message:
+ 'Analisis berhasil dibuat dan disimpan sebagai Case, Solutions, Root Causes, Action Plan, dan Tasks.',
+ });
+ } catch (error) {
+ console.error('OPTEMA AI workflow failed', error);
+ setNotice({
+ type: 'error',
+ message:
+ 'Analisis gagal disimpan. Pastikan akun Anda memiliki izin membuat Cases, Solutions, Root Causes, Action Plans, dan Tasks.',
+ });
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ return (
+ <>
+
+ {getPageTitle('OPTEMA AI Decision Lab')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Problem Intake
+
+
+ Ubah masalah jadi keputusan.
+
+
+
+
+
+ Deskripsi masalah bisnis
+
+
+
+
+
+
+ Riwayat Decision Runs
+
+
{runs.length} lokal
+
+ {runs.length === 0 ? (
+
+ Belum ada riwayat. Jalankan analisis pertama untuk melihat
+ list dan detail keputusan di sini.
+
+ ) : (
+
+ {runs.map((run) => (
+
setSelectedId(run.id)}
+ className={`w-full rounded-2xl border p-4 text-left transition hover:-translate-y-0.5 hover:shadow-md ${selectedRun?.id === run.id ? 'border-cyan-400 bg-cyan-50 dark:bg-cyan-950/40' : 'border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900'}`}
+ >
+
+
+ {run.title}
+
+
+ {run.priorityScore}/100
+
+
+
+ {new Date(run.createdAt).toLocaleString('id-ID')}
+
+
+ ))}
+
+ )}
+
+
+
+
+ {!selectedRun ? (
+
+
+
+
+
+ Belum ada analisis
+
+
+ Isi masalah bisnis di panel kiri untuk menghasilkan detection
+ score, root cause, solusi terurut, dan action plan 5 hari.
+
+
+ ) : (
+ <>
+
+
+
+
+ Detection Engine
+
+
+ {selectedRun.title}
+
+
+ {selectedRun.problem}
+
+
+
+ Prioritas {selectedRun.priority}
+
+
+
+
+
+
Priority Score
+
+ {selectedRun.priorityScore}
+
+
Skala 1–100
+
+
+
Solusi terbaik
+
+ {selectedRun.selectedSolution.score}
+
+
Decision Score
+
+
+
Success Rate
+
+ {selectedRun.selectedSolution.success}%
+
+
Simulasi awal
+
+
+
+
+
+
+
+
+
+ Sumber Data Analisis
+
+
+
Maks. {MAX_DATA_SOURCES} data
+
+
+ {selectedDataSources.map((source, index) => (
+
+
+
+ Data {index + 1}
+
+
+ Confidence {source.confidence}%
+
+
+
+ {source.title}
+
+
+ {source.summary}
+
+
+ ))}
+
+
+
+
+
+
+
+ Root Cause Map
+
+
+
+ {selectedRun.causes.map((cause) => (
+
+
+
+ {cause.label}
+
+
+ {cause.contribution}%
+
+
+
+
+ {cause.description}
+
+
+ ))}
+
+
+
+
+
+
+
+ Solution & Decision Scoring
+
+
+
+
+
+
+ Solusi
+ I
+ E
+ S
+ Low Risk
+ Score
+
+
+
+ {selectedRun.solutions.map((solution) => (
+
+
+
+ {solution.title}
+
+
+ {solution.description}
+
+
+
+ {solution.impact}
+
+
+ {solution.efficiency}
+
+
+ {solution.speed}
+
+
+ {solution.lowRisk}
+
+
+ {solution.score}
+
+
+ ))}
+
+
+
+
+
+ {selectedRun.solutions.map((solution, index) => (
+
+
+
+
+ Detail Solusi {index + 1}
+
+
+ {solution.title}
+
+
+
+ {solution.id === selectedRun.selectedSolution.id
+ ? 'Rekomendasi utama'
+ : `Score ${solution.score}`}
+
+
+
+ {solution.rationale || solution.description}
+
+
+
+
+ Langkah implementasi
+
+
+ {(
+ solution.executionSteps || [
+ solution.description,
+ ]
+ ).map((step) => (
+ {step}
+ ))}
+
+
+
+
+ KPI kontrol
+
+
+ {(
+ solution.kpis || [
+ 'Decision Score',
+ 'Progress action plan',
+ ]
+ ).map((kpi) => (
+ {kpi}
+ ))}
+
+
+
+
+ Risiko, mitigasi & resource
+
+
+ {(
+ solution.risks || [
+ 'Validasi hasil harian agar risiko eksekusi cepat terlihat.',
+ ]
+ ).map((risk) => (
+ {risk}
+ ))}
+
+
+ Resource:{' '}
+ {(
+ solution.resources || [
+ 'Owner/PIC',
+ 'Data performa',
+ 'Budget eksperimen',
+ ]
+ ).join(', ')}
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ Automated Action Plan
+
+
+
+ {selectedRun.tasks.map((task) => (
+
+
+ Hari {task.day}
+
+
+ {task.title}
+
+
+ {task.description}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ Executive Summary
+
+
+ Kesimpulan OPTEMA AI
+
+
+
+
Next: validasi 5 hari
+
+
+ {selectedConclusion}
+
+
+ >
+ )}
+
+
+
+ >
+ );
+};
+
+OptemaAiPage.getLayout = function getLayout(page: ReactElement) {
+ return {page} ;
+};
+
+export default OptemaAiPage;
diff --git a/frontend/src/pages/search.tsx b/frontend/src/pages/search.tsx
index 00f5168..005eb07 100644
--- a/frontend/src/pages/search.tsx
+++ b/frontend/src/pages/search.tsx
@@ -1,9 +1,7 @@
import React, { ReactElement, useEffect, useState } from 'react';
import Head from 'next/head';
import 'react-datepicker/dist/react-datepicker.css';
-import { useAppDispatch } from '../stores/hooks';
-
-import { useAppSelector } from '../stores/hooks';
+import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { useRouter } from 'next/router';
import LayoutAuthenticated from '../layouts/Authenticated';