40291-vm/frontend/src/pages/optema-ai.tsx
Flatlogic Bot 7eba0ddde6 OPTEMA AI
2026-06-20 08:21:09 +00:00

2377 lines
90 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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' | 'keluarga_hukum_waris';
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'],
},
{
key: 'keluarga_hukum_waris',
label: 'Hukum keluarga, perceraian, dan warisan',
keywords: [
'selingkuh',
'perselingkuhan',
'cerai',
'perceraian',
'warisan',
'waris',
'ahli waris',
'harta waris',
'harta bersama',
'harta gono gini',
'gono gini',
'gono-gini',
'hak asuh',
'nafkah anak',
'hak anak',
'sengketa keluarga',
'rumah tangga',
'suami',
'istri',
],
severity: 'Kritis',
penyebab: [
'Konflik rumah tangga dan dugaan perselingkuhan',
'Perceraian, hak asuh, dan nafkah anak belum tertata',
'Pembagian harta bersama dan warisan belum jelas',
'Hak dan kebutuhan 10 anak berisiko terabaikan',
'Dokumen keluarga, aset, dan ahli waris belum diinventarisasi',
'Komunikasi keluarga rentan memanas tanpa mediator netral',
'Keputusan emosional berisiko memperburuk sengketa hukum',
'Belum ada rencana perlindungan anak jangka pendek',
'Potensi pengalihan aset sebelum status hukum jelas',
'Kebutuhan pendamping hukum dan psikososial belum dipenuhi',
],
hipotesis: [
'Isu perceraian, warisan, harta bersama, nafkah, dan hak anak masih tercampur menjadi satu masalah',
'Keluarga belum memiliki kronologi, bukti, dan dokumen pendukung yang rapi',
'Ahli waris, aset, utang, dan status harta perlu dipetakan sebelum mengambil keputusan',
'Mediasi awal dibutuhkan agar konflik tidak langsung merugikan anak-anak',
'Konsultasi advokat, pos bantuan hukum, mediator, atau notaris perlu dilakukan sebelum membuat kesepakatan',
'Anak-anak membutuhkan rencana sementara untuk biaya hidup, sekolah, kesehatan, dan tempat tinggal',
'Kesepakatan lisan berisiko lemah jika tidak dituangkan dalam dokumen yang benar',
'Ada risiko aset keluarga dipindahkan atau dipakai tanpa persetujuan pihak yang berhak',
'Perlu pemisahan antara masalah moral/perselingkuhan dan hak perdata keluarga',
'Keputusan paling aman adalah langkah hukum bertahap berbasis dokumen',
],
solusi: [
'Konsultasi hukum keluarga dan waris',
'Mediasi keluarga dengan pihak netral',
'Inventarisasi aset, utang, dan ahli waris',
'Rencana hak asuh dan nafkah anak',
'Pengamanan dokumen keluarga dan aset',
'Pendampingan psikososial anak',
'Penyusunan kronologi dan bukti',
'Opsi penyelesaian damai tertulis',
'Rujukan advokat atau pos bantuan hukum',
'Koordinasi notaris/PPAT untuk urusan aset bila diperlukan',
],
kesimpulan:
'Kasus perselingkuhan, perceraian, warisan, dan banyak anak harus dipisahkan menjadi isu hukum keluarga, harta/waris, dan perlindungan anak; langkah aman adalah dokumentasi, mediasi, dan konsultasi hukum profesional.',
causeCategories: ['people', 'policy', 'process', 'other'],
},
];
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 scoredKnowledge = PROBLEM_KNOWLEDGE.map((knowledge) => ({
knowledge,
score: keywordFitScore(problem, knowledge.keywords),
}))
.filter((item) => item.score > 0)
.sort((a, b) => b.score - a.score);
return scoredKnowledge[0]?.knowledge;
};
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 35 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 3060 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',
],
},
]);
}
if (knowledge.key === 'keluarga_hukum_waris') {
return withQuestionFit([
{
title: 'Konsultasi hukum keluarga dan waris terpadu',
description:
'Memisahkan isu perselingkuhan, perceraian, hak anak, harta bersama, dan warisan agar keluarga punya langkah hukum yang aman dan tidak merugikan 10 anak.',
rationale: `Diprioritaskan karena penyebab utama adalah ${topCauses}. Hipotesis yang diuji: ${topHypotheses}. Paket solusi dasar: ${allSolutionOptions}. Rekomendasi ini bersifat arahan awal dan perlu divalidasi dengan advokat, pos bantuan hukum, mediator, notaris, atau pejabat berwenang.`,
executionSteps: [
'Tulis kronologi singkat kejadian perselingkuhan, konflik rumah tangga, proses cerai, isu warisan, dan posisi 10 anak tanpa menyebarkan tuduhan ke pihak luar.',
'Kumpulkan dokumen inti: buku nikah/akta cerai bila ada, kartu keluarga, akta kelahiran anak, identitas pihak terkait, sertifikat aset, rekening, surat utang, dan dokumen waris.',
'Pisahkan masalah menjadi 4 jalur: status perkawinan/perceraian, hak asuh dan nafkah anak, harta bersama, serta warisan/ahli waris.',
'Buat daftar ahli waris, aset, utang, dan pihak yang menguasai aset saat ini agar pembahasan tidak bercampur dengan emosi konflik.',
'Jadwalkan konsultasi dengan advokat/pos bantuan hukum/mediator keluarga dan bawa dokumen serta pertanyaan tertulis.',
'Tentukan langkah aman berikutnya: mediasi tertulis, perlindungan hak anak sementara, pengamanan dokumen, atau jalur pengadilan bila damai tidak memungkinkan.',
],
kpis: [
'Kronologi dan dokumen inti terkumpul',
'Daftar aset, utang, ahli waris, dan kebutuhan 10 anak tersusun',
'Jadwal konsultasi hukum atau mediasi ditetapkan',
'Rencana sementara hak asuh, nafkah, sekolah, kesehatan, dan tempat tinggal anak tersedia',
],
risks: [
'Konflik keluarga memanas; mitigasi: gunakan mediator netral dan hindari keputusan sepihak saat emosi tinggi.',
'Dokumen atau aset hilang/dialihkan; mitigasi: buat salinan dokumen, catat penguasaan aset, dan konsultasikan langkah pengamanan hukum.',
'Hak anak terabaikan; mitigasi: dahulukan biaya hidup, sekolah, kesehatan, dan tempat tinggal anak sebelum sengketa harta diperluas.',
],
resources: [
'Dokumen keluarga dan aset',
'Advokat atau pos bantuan hukum',
'Mediator keluarga',
'Notaris/PPAT bila menyangkut aset',
'Pendamping psikososial anak',
],
type: 'other',
impact: 94 + urgency,
efficiency: 86,
speed: 82,
lowRisk: 78,
duration: 7,
rootCauseId: causes[0]?.id,
},
{
title: 'Mediasi keluarga dan rencana perlindungan anak',
description:
'Menurunkan eskalasi konflik dengan forum mediasi yang fokus pada keselamatan, kebutuhan, hak asuh, nafkah, dan stabilitas 10 anak.',
rationale:
'Cocok saat konflik rumah tangga dan perceraian mulai memengaruhi anak-anak; mediasi membantu membuat kesepakatan sementara sebelum proses hukum berjalan lebih jauh.',
executionSteps: [
'Tentukan pihak yang wajib hadir dan pihak netral yang dipercaya, seperti mediator keluarga, tokoh yang disepakati, konselor, atau pendamping hukum.',
'Buat agenda mediasi terbatas: kebutuhan anak, jadwal pengasuhan, nafkah sementara, akses sekolah/kesehatan, dan larangan mengalihkan aset tanpa persetujuan.',
'Pisahkan pembahasan dugaan perselingkuhan dari pembahasan kebutuhan anak agar anak tidak menjadi alat tekanan konflik orang tua.',
'Tuliskan hasil mediasi sementara secara jelas: siapa membayar apa, kapan, bukti pembayaran, akses komunikasi anak, dan tanggal review.',
'Jika mediasi gagal atau ada ancaman/kekerasan, eskalasi ke pendamping hukum atau lembaga perlindungan yang berwenang.',
],
kpis: [
'Agenda mediasi dan pihak netral disepakati',
'Kesepakatan sementara nafkah dan pengasuhan anak tertulis',
'Kebutuhan sekolah, kesehatan, dan tempat tinggal anak terpetakan',
'Risiko konflik atau ancaman berkurang',
],
risks: [
'Salah satu pihak menolak hadir; mitigasi: minta mediator mengirim undangan tertulis dan siapkan opsi konsultasi hukum.',
'Anak terseret konflik; mitigasi: batasi pembicaraan orang dewasa dan libatkan pendamping anak bila perlu.',
'Kesepakatan tidak dipatuhi; mitigasi: dokumentasikan pelanggaran dan konsultasikan penguatan formalnya.',
],
resources: [
'Mediator keluarga',
'Daftar kebutuhan anak',
'Catatan komunikasi para pihak',
'Pendamping hukum atau konselor',
],
type: 'other',
impact: 90,
efficiency: 84,
speed: 88,
lowRisk: 82,
duration: 5,
rootCauseId: causes[1]?.id || causes[0]?.id,
},
{
title: 'Inventarisasi harta bersama, warisan, dan ahli waris',
description:
'Membuat peta aset, utang, status kepemilikan, ahli waris, dan dokumen pendukung sebelum ada pembagian atau keputusan atas warisan.',
rationale:
'Cocok ketika perceraian dan warisan bercampur; inventarisasi mencegah aset diperebutkan tanpa data dan membantu menentukan isu mana yang perlu notaris, mediator, atau pengadilan.',
executionSteps: [
'Buat tabel aset: tanah, rumah, kendaraan, tabungan, usaha, barang berharga, piutang, dan utang beserta bukti dokumennya.',
'Tandai status setiap aset: milik pribadi, harta bersama, harta warisan, masih sengketa, atau belum jelas.',
'Susun daftar calon ahli waris dan hubungan keluarga berdasarkan dokumen resmi, lalu catat pihak yang masih perlu diverifikasi.',
'Amankan salinan dokumen dan bukti penguasaan aset agar tidak hilang ketika konflik meningkat.',
'Konsultasikan tabel aset dan ahli waris ke notaris/PPAT atau advokat agar langkah pembagian tidak salah prosedur.',
],
kpis: [
'Tabel aset dan utang selesai',
'Status harta bersama dan warisan dipisahkan',
'Daftar ahli waris awal tersusun',
'Dokumen aset tersalin dan aman',
],
risks: [
'Data aset disembunyikan; mitigasi: kumpulkan bukti dari dokumen resmi, saksi, catatan transaksi, dan konsultasi hukum.',
'Pembagian dilakukan sebelum status jelas; mitigasi: tahan keputusan pembagian sampai ada validasi pihak berwenang.',
'Utang tidak dihitung; mitigasi: masukkan semua kewajiban sebelum menghitung bagian bersih.',
],
resources: [
'Sertifikat dan bukti aset',
'Dokumen ahli waris',
'Catatan utang/piutang',
'Notaris/PPAT atau advokat',
],
type: 'other',
impact: 86,
efficiency: 82,
speed: 76,
lowRisk: 86,
duration: 10,
rootCauseId: causes[2]?.id || causes[0]?.id,
},
], [
{
focus: 'perselingkuhan, perceraian, warisan, hak anak, harta bersama, dan perlindungan 10 anak',
keywords: [
'selingkuh',
'perselingkuhan',
'cerai',
'perceraian',
'warisan',
'waris',
'10 anak',
'anak',
'hak anak',
'hak asuh',
'nafkah',
'nafkah anak',
'harta bersama',
'harta gono gini',
'gono gini',
'gono-gini',
'ahli waris',
],
},
{
focus: 'mediasi keluarga, konflik rumah tangga, pengasuhan, nafkah, dan stabilitas anak',
keywords: [
'mediasi',
'konflik',
'keluarga',
'rumah tangga',
'suami',
'istri',
'anak',
'hak asuh',
'nafkah',
'damai',
'konselor',
],
},
{
focus: 'inventarisasi aset, utang, harta warisan, ahli waris, sertifikat, dan prosedur notaris/pengadilan',
keywords: [
'aset',
'harta',
'warisan',
'waris',
'ahli waris',
'sertifikat',
'tanah',
'rumah',
'utang',
'notaris',
'ppat',
'pengadilan',
],
},
]);
}
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 3090 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 3090 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 12 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 510 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',
);
const isLegalFamilyCase = Boolean(
`${solution.title} ${solution.description}`.match(
/hukum|waris|cerai|perceraian|hak asuh|nafkah|mediasi keluarga|anak/i,
),
);
if (isLegalFamilyCase) {
return [
{
id: uuid(),
day: 1,
title: 'Amankan fakta dan dokumen inti',
description: `Kumpulkan fakta dan dokumen awal untuk solusi “${solution.title}”: ${resources.slice(0, 3).join(', ') || 'dokumen keluarga, dokumen aset, dan daftar pihak terkait'}.`,
},
{
id: uuid(),
day: 2,
title: 'Pisahkan isu hukum utama',
description: `Turunkan kasus menjadi langkah hukum pertama yang jelas: ${firstStep}. Pisahkan isu perceraian, warisan, harta, dan hak anak agar tidak bercampur.`,
},
{
id: uuid(),
day: 3,
title: 'Mulai konsultasi atau mediasi',
description: `Jalankan langkah awal yang aman: ${secondStep}. Catat posisi tiap pihak, dokumen yang kurang, dan risiko untuk anak.`,
},
{
id: uuid(),
day: 4,
title: 'Cek perlindungan anak dan aset',
description: `Bandingkan progress dengan indikator utama: ${kpis.slice(0, 3).join(', ') || 'dokumen, mediasi, perlindungan anak, dan status aset'}. Koreksi jika ada hak anak atau aset yang belum terlindungi.`,
},
{
id: uuid(),
day: 5,
title: 'Putuskan jalur damai atau hukum formal',
description: `Pilih langkah lanjut berdasarkan dokumen, hasil konsultasi/mediasi, dan risiko utama: ${mainRisk}.`,
},
];
}
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}`
: '';
if (knowledge?.key === 'keluarga_hukum_waris') {
return `Kesimpulan: kasus ini berada pada prioritas ${priority} (${priorityScore}/100) dan perlu ditangani dengan pendampingan hukum/mediasi bertahap, bukan keputusan emosional atau sepihak. ${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 mengamankan dokumen, memetakan perceraian/warisan/harta/hak anak, konsultasi dengan pihak profesional, lalu memilih jalur damai tertulis atau hukum formal.`;
}
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 (
<span
className={`inline-flex rounded-full px-3 py-1 text-xs font-semibold ring-1 ${tones[tone]}`}
>
{children}
</span>
);
};
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<Run['financialImpact']>('high');
const [runs, setRuns] = useState<Run[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(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 (
<>
<Head>
<title>{getPageTitle('OPTEMA AI Decision Lab')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiBrain}
title='OPTEMA AI Decision Lab'
main
>
<BaseButton
href='/cases/cases-list'
label='Buka Cases'
color='whiteDark'
small
/>
</SectionTitleLineWithButton>
<div className='grid gap-6 lg:grid-cols-[0.9fr_1.4fr]'>
<div className='space-y-6'>
<CardBox
className='overflow-hidden border-0 bg-gradient-to-br from-[#08111F] via-[#102A43] to-[#13B8A6] text-white shadow-xl'
cardBoxClassName='p-6'
>
<div className='mb-5 flex items-center gap-3'>
<div className='rounded-2xl bg-white/15 p-3 ring-1 ring-white/20'>
<BaseIcon path={mdiChartTimelineVariant} size={26} />
</div>
<div>
<p className='text-sm font-semibold text-cyan-100'>
Problem Intake
</p>
<h2 className='text-2xl font-bold'>
Ubah masalah jadi keputusan.
</h2>
</div>
</div>
<label
className='mb-2 block text-sm font-semibold text-cyan-50'
htmlFor='problem'
>
Deskripsi masalah bisnis
</label>
<textarea
id='problem'
value={problem}
onChange={(event) => setProblem(event.target.value)}
className='min-h-44 w-full rounded-2xl border border-white/20 bg-white/10 p-4 text-sm text-white placeholder:text-white/50 outline-none ring-cyan-200/40 transition focus:ring-4'
placeholder='Contoh: Penjualan toko saya turun 40% dalam 3 bulan terakhir.'
/>
<div className='mt-5 grid gap-4 sm:grid-cols-2'>
<div className='rounded-2xl bg-white/10 p-4 ring-1 ring-white/15'>
<label
className='mb-3 block text-sm font-semibold'
htmlFor='urgency'
>
Urgensi: {urgency}/5
</label>
<input
id='urgency'
type='range'
min='1'
max='5'
value={urgency}
onChange={(event) => setUrgency(Number(event.target.value))}
className='w-full accent-[#F97316]'
/>
</div>
<div className='rounded-2xl bg-white/10 p-4 ring-1 ring-white/15'>
<label
className='mb-3 block text-sm font-semibold'
htmlFor='impact'
>
Dampak finansial
</label>
<select
id='impact'
value={financialImpact}
onChange={(event) =>
setFinancialImpact(
event.target.value as Run['financialImpact'],
)
}
className='w-full rounded-xl border border-white/20 bg-[#102A43] px-3 py-2 text-sm text-white outline-none ring-cyan-200/40 focus:ring-4'
>
<option value='high'>Besar</option>
<option value='medium'>Sedang</option>
<option value='low'>Rendah</option>
<option value='unknown'>Belum diketahui</option>
</select>
</div>
</div>
{notice && (
<div
className={`mt-5 rounded-2xl p-4 text-sm ${notice.type === 'success' ? 'bg-emerald-400/15 text-emerald-50' : 'bg-red-400/15 text-red-50'}`}
>
{notice.message}
</div>
)}
<BaseButton
className='mt-5 w-full border-0 bg-[#F97316] py-3 font-bold text-white hover:bg-[#EA580C]'
icon={mdiPlus}
label={
isSaving
? 'Menyimpan analisis...'
: 'Jalankan Analisis & Simpan Workflow'
}
onClick={handleAnalyze}
disabled={isSaving}
/>
</CardBox>
<CardBox className='shadow-sm' cardBoxClassName='p-5'>
<div className='mb-4 flex items-center justify-between'>
<h3 className='font-bold text-slate-900 dark:text-white'>
Riwayat Decision Runs
</h3>
<Pill tone='slate'>{runs.length} lokal</Pill>
</div>
{runs.length === 0 ? (
<div className='rounded-2xl border border-dashed border-slate-300 p-5 text-sm text-slate-500 dark:border-slate-700 dark:text-slate-400'>
Belum ada riwayat. Jalankan analisis pertama untuk melihat
list dan detail keputusan di sini.
</div>
) : (
<div className='space-y-3'>
{runs.map((run) => (
<button
key={run.id}
type='button'
onClick={() => 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'}`}
>
<div className='flex items-center justify-between gap-3'>
<p className='line-clamp-1 font-semibold text-slate-900 dark:text-white'>
{run.title}
</p>
<Pill
tone={
run.priority === 'critical' ||
run.priority === 'high'
? 'orange'
: 'blue'
}
>
{run.priorityScore}/100
</Pill>
</div>
<p className='mt-2 text-xs text-slate-500'>
{new Date(run.createdAt).toLocaleString('id-ID')}
</p>
</button>
))}
</div>
)}
</CardBox>
</div>
<div className='space-y-6'>
{!selectedRun ? (
<CardBox
className='min-h-[520px] items-center justify-center border-dashed text-center'
cardBoxClassName='p-10'
>
<div className='mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-3xl bg-cyan-50 text-cyan-700'>
<BaseIcon path={mdiBrain} size={34} />
</div>
<h2 className='text-2xl font-bold text-slate-900 dark:text-white'>
Belum ada analisis
</h2>
<p className='mx-auto mt-2 max-w-md text-slate-500'>
Isi masalah bisnis di panel kiri untuk menghasilkan detection
score, root cause, solusi terurut, dan action plan 5 hari.
</p>
</CardBox>
) : (
<>
<CardBox className='shadow-sm' cardBoxClassName='p-6'>
<div className='mb-5 flex flex-wrap items-start justify-between gap-4'>
<div>
<p className='text-sm font-semibold text-cyan-600'>
Detection Engine
</p>
<h2 className='mt-1 text-2xl font-bold text-slate-950 dark:text-white'>
{selectedRun.title}
</h2>
<p className='mt-2 max-w-3xl text-sm text-slate-500 dark:text-slate-400'>
{selectedRun.problem}
</p>
</div>
<Pill
tone={
selectedRun.priority === 'critical' ||
selectedRun.priority === 'high'
? 'orange'
: 'green'
}
>
Prioritas {selectedRun.priority}
</Pill>
</div>
<div className='grid gap-4 md:grid-cols-3'>
<div className='rounded-3xl bg-[#08111F] p-5 text-white'>
<p className='text-sm text-cyan-100'>Priority Score</p>
<p className='mt-2 text-4xl font-black'>
{selectedRun.priorityScore}
</p>
<p className='text-xs text-slate-300'>Skala 1100</p>
</div>
<div className='rounded-3xl bg-emerald-50 p-5 text-emerald-900 dark:bg-emerald-950 dark:text-emerald-100'>
<p className='text-sm'>Solusi terbaik</p>
<p className='mt-2 text-4xl font-black'>
{selectedRun.selectedSolution.score}
</p>
<p className='text-xs'>Decision Score</p>
</div>
<div className='rounded-3xl bg-orange-50 p-5 text-orange-900 dark:bg-orange-950 dark:text-orange-100'>
<p className='text-sm'>Success Rate</p>
<p className='mt-2 text-4xl font-black'>
{selectedRun.selectedSolution.success}%
</p>
<p className='text-xs'>Simulasi awal</p>
</div>
</div>
</CardBox>
<CardBox className='shadow-sm' cardBoxClassName='p-6'>
<div className='mb-5 flex flex-wrap items-center justify-between gap-3'>
<div className='flex items-center gap-3'>
<BaseIcon
path={mdiChartTimelineVariant}
className='text-cyan-600'
size={24}
/>
<h3 className='text-xl font-bold text-slate-900 dark:text-white'>
Sumber Data Analisis
</h3>
</div>
<Pill tone='slate'>Maks. {MAX_DATA_SOURCES} data</Pill>
</div>
<div className='grid gap-4 md:grid-cols-3'>
{selectedDataSources.map((source, index) => (
<div
key={source.id}
className='rounded-3xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-900'
>
<div className='mb-3 flex items-center justify-between gap-3'>
<Pill
tone={
source.category === 'metric'
? 'green'
: source.category === 'pattern'
? 'orange'
: 'blue'
}
>
Data {index + 1}
</Pill>
<span className='text-xs font-semibold text-slate-500 dark:text-slate-400'>
Confidence {source.confidence}%
</span>
</div>
<h4 className='font-bold text-slate-900 dark:text-white'>
{source.title}
</h4>
<p className='mt-2 text-xs leading-5 text-slate-500 dark:text-slate-400'>
{source.summary}
</p>
</div>
))}
</div>
</CardBox>
<CardBox className='shadow-sm' cardBoxClassName='p-6'>
<div className='mb-5 flex items-center gap-3'>
<BaseIcon
path={mdiBrain}
className='text-cyan-600'
size={24}
/>
<h3 className='text-xl font-bold text-slate-900 dark:text-white'>
Root Cause Map
</h3>
</div>
<div className='space-y-4'>
{selectedRun.causes.map((cause) => (
<div key={cause.id}>
<div className='mb-2 flex items-center justify-between gap-4 text-sm'>
<span className='font-semibold text-slate-700 dark:text-slate-200'>
{cause.label}
</span>
<span className='font-bold text-cyan-700'>
{cause.contribution}%
</span>
</div>
<div className='h-3 overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800'>
<div
className='h-full rounded-full bg-gradient-to-r from-cyan-500 to-emerald-400'
style={{ width: `${cause.contribution}%` }}
/>
</div>
<p className='mt-2 text-xs text-slate-500 dark:text-slate-400'>
{cause.description}
</p>
</div>
))}
</div>
</CardBox>
<CardBox className='shadow-sm' cardBoxClassName='p-6'>
<div className='mb-5 flex items-center gap-3'>
<BaseIcon
path={mdiLightbulbOnOutline}
className='text-orange-500'
size={24}
/>
<h3 className='text-xl font-bold text-slate-900 dark:text-white'>
Solution & Decision Scoring
</h3>
</div>
<div className='overflow-hidden rounded-3xl border border-slate-200 dark:border-slate-700'>
<table className='w-full min-w-[720px] text-left text-sm'>
<thead className='bg-slate-50 text-xs uppercase text-slate-500 dark:bg-slate-800 dark:text-slate-300'>
<tr>
<th className='px-4 py-3'>Solusi</th>
<th className='px-4 py-3'>I</th>
<th className='px-4 py-3'>E</th>
<th className='px-4 py-3'>S</th>
<th className='px-4 py-3'>Low Risk</th>
<th className='px-4 py-3'>Score</th>
</tr>
</thead>
<tbody className='divide-y divide-slate-100 dark:divide-slate-800'>
{selectedRun.solutions.map((solution) => (
<tr
key={solution.id}
className={
solution.id === selectedRun.selectedSolution.id
? 'bg-cyan-50/70 dark:bg-cyan-950/30'
: ''
}
>
<td className='px-4 py-4'>
<p className='font-semibold text-slate-900 dark:text-white'>
{solution.title}
</p>
<p className='mt-1 text-xs text-slate-500'>
{solution.description}
</p>
</td>
<td className='px-4 py-4 font-semibold'>
{solution.impact}
</td>
<td className='px-4 py-4 font-semibold'>
{solution.efficiency}
</td>
<td className='px-4 py-4 font-semibold'>
{solution.speed}
</td>
<td className='px-4 py-4 font-semibold'>
{solution.lowRisk}
</td>
<td className='px-4 py-4'>
<Pill tone='green'>{solution.score}</Pill>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className='mt-6 space-y-4'>
{selectedRun.solutions.map((solution, index) => (
<div
key={`${solution.id}-details`}
className='rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-700 dark:bg-slate-900'
>
<div className='flex flex-wrap items-start justify-between gap-3'>
<div>
<p className='text-xs font-semibold uppercase tracking-wide text-cyan-600'>
Detail Solusi {index + 1}
</p>
<h4 className='mt-1 text-lg font-bold text-slate-900 dark:text-white'>
{solution.title}
</h4>
</div>
<Pill
tone={
solution.id === selectedRun.selectedSolution.id
? 'green'
: 'slate'
}
>
{solution.id === selectedRun.selectedSolution.id
? 'Rekomendasi utama'
: `Score ${solution.score}`}
</Pill>
</div>
<p className='mt-3 text-sm leading-6 text-slate-600 dark:text-slate-300'>
{solution.rationale || solution.description}
</p>
<div className='mt-5 grid gap-4 lg:grid-cols-3'>
<div className='rounded-2xl bg-cyan-50 p-4 dark:bg-cyan-950/30'>
<h5 className='font-semibold text-slate-900 dark:text-white'>
Langkah implementasi
</h5>
<ol className='mt-3 list-decimal space-y-2 pl-4 text-xs leading-5 text-slate-600 dark:text-slate-300'>
{(
solution.executionSteps || [
solution.description,
]
).map((step) => (
<li key={step}>{step}</li>
))}
</ol>
</div>
<div className='rounded-2xl bg-emerald-50 p-4 dark:bg-emerald-950/30'>
<h5 className='font-semibold text-slate-900 dark:text-white'>
KPI kontrol
</h5>
<ul className='mt-3 list-disc space-y-2 pl-4 text-xs leading-5 text-slate-600 dark:text-slate-300'>
{(
solution.kpis || [
'Decision Score',
'Progress action plan',
]
).map((kpi) => (
<li key={kpi}>{kpi}</li>
))}
</ul>
</div>
<div className='rounded-2xl bg-orange-50 p-4 dark:bg-orange-950/30'>
<h5 className='font-semibold text-slate-900 dark:text-white'>
Risiko, mitigasi & resource
</h5>
<ul className='mt-3 list-disc space-y-2 pl-4 text-xs leading-5 text-slate-600 dark:text-slate-300'>
{(
solution.risks || [
'Validasi hasil harian agar risiko eksekusi cepat terlihat.',
]
).map((risk) => (
<li key={risk}>{risk}</li>
))}
</ul>
<p className='mt-3 text-xs font-semibold text-slate-500 dark:text-slate-400'>
Resource:{' '}
{(
solution.resources || [
'Owner/PIC',
'Data performa',
'Budget eksperimen',
]
).join(', ')}
</p>
</div>
</div>
</div>
))}
</div>
</CardBox>
<CardBox className='shadow-sm' cardBoxClassName='p-6'>
<div className='mb-5 flex items-center gap-3'>
<BaseIcon
path={mdiClipboardTextClockOutline}
className='text-emerald-600'
size={24}
/>
<h3 className='text-xl font-bold text-slate-900 dark:text-white'>
Automated Action Plan
</h3>
</div>
<div className='grid gap-3 md:grid-cols-5'>
{selectedRun.tasks.map((task) => (
<div
key={task.id}
className='rounded-3xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-700 dark:bg-slate-900'
>
<Pill tone={task.day === 1 ? 'orange' : 'blue'}>
Hari {task.day}
</Pill>
<h4 className='mt-3 font-bold text-slate-900 dark:text-white'>
{task.title}
</h4>
<p className='mt-2 text-xs leading-5 text-slate-500 dark:text-slate-400'>
{task.description}
</p>
</div>
))}
</div>
</CardBox>
<CardBox
className='border-0 bg-gradient-to-br from-[#08111F] via-[#102A43] to-[#13B8A6] text-white shadow-xl'
cardBoxClassName='p-6'
>
<div className='mb-4 flex flex-wrap items-center justify-between gap-3'>
<div className='flex items-center gap-3'>
<div className='rounded-2xl bg-white/15 p-3 ring-1 ring-white/20'>
<BaseIcon path={mdiLightbulbOnOutline} size={24} />
</div>
<div>
<p className='text-sm font-semibold text-cyan-100'>
Executive Summary
</p>
<h3 className='text-xl font-bold'>
Kesimpulan OPTEMA AI
</h3>
</div>
</div>
<Pill tone='orange'>Next: validasi 5 hari</Pill>
</div>
<p className='text-sm leading-7 text-cyan-50'>
{selectedConclusion}
</p>
</CardBox>
</>
)}
</div>
</div>
</SectionMain>
</>
);
};
OptemaAiPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default OptemaAiPage;