diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx index cb78b1e..b235f88 100644 --- a/frontend/src/components/AsideMenuLayer.tsx +++ b/frontend/src/components/AsideMenuLayer.tsx @@ -3,10 +3,9 @@ import { mdiLogout, mdiClose } from '@mdi/js' import BaseIcon from './BaseIcon' import AsideMenuList from './AsideMenuList' import { MenuAsideItem } from '../interfaces' -import { useAppSelector } from '../stores/hooks' +import { useAppDispatch, useAppSelector } from '../stores/hooks' import Link from 'next/link'; -import { useAppDispatch } from '../stores/hooks'; import { createAsyncThunk } from '@reduxjs/toolkit'; import axios from 'axios'; diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..fcbd9b9 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, { useEffect, useRef, useState } from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..73d8391 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index fda03bc..3f8b370 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -2,6 +2,13 @@ import * as icon from '@mdi/js'; import { MenuAsideItem } from './interfaces' const menuAside: MenuAsideItem[] = [ + + { + href: '/optema-ai', + icon: icon.mdiBrain, + label: 'OPTEMA AI Lab', + permissions: 'READ_CASES' + }, { href: '/dashboard', icon: icon.mdiViewDashboardOutline, diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 33bf56b..46291ce 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,159 @@ - -import React, { useEffect, useState } from 'react'; -import type { ReactElement } from 'react'; +import { mdiArrowRight, mdiBrain, mdiChartTimelineVariant, mdiClipboardTextClockOutline, mdiLightbulbOnOutline, mdiLogin } from '@mdi/js'; import Head from 'next/head'; import Link from 'next/link'; -import BaseButton from '../components/BaseButton'; -import CardBox from '../components/CardBox'; -import SectionFullScreen from '../components/SectionFullScreen'; +import React, { ReactElement } from 'react'; +import BaseIcon from '../components/BaseIcon'; import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; -import BaseButtons from '../components/BaseButtons'; import { getPageTitle } from '../config'; -import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; +const pillars = [ + { + icon: mdiBrain, + title: 'Problem Detection', + text: 'Tangkap masalah bisnis berbahasa bebas, lalu ubah menjadi prioritas, dampak, dan sinyal keputusan yang lebih terstruktur.', + }, + { + icon: mdiChartTimelineVariant, + title: 'Root Cause Map', + text: 'Simulasikan peta akar masalah ala 5-Why/Fishbone agar tim tidak terburu-buru memilih solusi permukaan.', + }, + { + icon: mdiLightbulbOnOutline, + title: 'Decision Scoring', + text: 'Ranking solusi memakai Impact, Efficiency, Speed, dan Low Risk sehingga opsi terbaik terlihat jelas.', + }, + { + icon: mdiClipboardTextClockOutline, + title: 'Action Plan', + text: 'Ubah keputusan terpilih menjadi rencana kerja 5 hari yang bisa langsung dieksekusi dan dilacak.', + }, +]; -export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('video'); - const [contentPosition, setContentPosition] = useState('left'); - const textColor = useAppSelector((state) => state.style.linkColor); +const Landing = () => ( + <> + + {getPageTitle('OPTEMA AI')} + + - const title = 'OPTEMA AI' +
+
+
+ - // Fetch Pexels image/video - useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); - - const imageBlock = (image) => ( -
-
- - Photo by {image?.photographer} on Pexels - +
+
+
+ Decision Intelligence Platform untuk UMKM & tim bisnis
-
- ); +

+ Dari masalah menjadi keputusan, dari keputusan menjadi hasil. +

+

+ OPTEMA AI membantu Anda memasukkan masalah bisnis yang masih messy, lalu menghasilkan prioritas, akar masalah, ranking solusi, dan rencana aksi praktis dalam satu alur kerja. +

+
+ + Mulai di Admin + + + Login ke Dashboard + +
+
+
+

4

+

Kriteria skor

+
+
+

5

+

Hari action plan

+
+
+

1

+

Alur keputusan

+
+
+
- const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- -
- - 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} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

+ ))} +
+
+
+

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. +

+
+
+ + +