From 7294040fd1f272aa460dd7c92bd1e1e7757bda86 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 5 Aug 2026 17:26:27 +0700 Subject: [PATCH] feat(dimentorin): halaman Materi + AI Agent RAG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /mentoring/materi: list materi (kategori chips, cards) + detail konten per slug - /mentoring/ai-agent: chatbox RAG — tanya materi, jawaban dari Qdrant context + sumber relevansi % - service: getMaterialsList/getMaterialBySlug/getMaterialById/getMaterialCategories + postChat + hooks - header: menu Materi & AI Agent - e2e verified: list 3 materi, detail tampil, chat 'ownership' jawab dari materi Rust + 3 sources (84/64/61%) --- .../src/app/(public)/_components/header.tsx | 2 + .../app/(public)/mentoring/ai-agent/page.tsx | 154 ++++++++++++++++++ .../(public)/mentoring/materi/[slug]/page.tsx | 53 ++++++ .../app/(public)/mentoring/materi/page.tsx | 92 +++++++++++ libs/service/src/api/dimentorin/index.ts | 49 ++++++ libs/service/src/hooks/dimentorin/index.ts | 63 ++++++- libs/service/src/types/dimentorin/index.ts | 47 ++++++ 7 files changed, 459 insertions(+), 1 deletion(-) create mode 100644 apps/dimentorin/src/app/(public)/mentoring/ai-agent/page.tsx create mode 100644 apps/dimentorin/src/app/(public)/mentoring/materi/[slug]/page.tsx create mode 100644 apps/dimentorin/src/app/(public)/mentoring/materi/page.tsx diff --git a/apps/dimentorin/src/app/(public)/_components/header.tsx b/apps/dimentorin/src/app/(public)/_components/header.tsx index 93a418d..b148bf9 100644 --- a/apps/dimentorin/src/app/(public)/_components/header.tsx +++ b/apps/dimentorin/src/app/(public)/_components/header.tsx @@ -8,6 +8,8 @@ import { Link, NavLink, useLocation } from "react-router-dom"; const MENUS: { label: string; href: string }[] = [ { label: "Home", href: "/" }, { label: 'Mentoring', href: '/mentoring' }, + { label: 'Materi', href: '/mentoring/materi' }, + { label: 'AI Agent', href: '/mentoring/ai-agent' }, { label: 'Sesi Saya', href: '/mentoring/my-sessions' }, { label: 'Resources', href: '/resources' }, { label: 'Articles', href: '/articles' }, diff --git a/apps/dimentorin/src/app/(public)/mentoring/ai-agent/page.tsx b/apps/dimentorin/src/app/(public)/mentoring/ai-agent/page.tsx new file mode 100644 index 0000000..5c16021 --- /dev/null +++ b/apps/dimentorin/src/app/(public)/mentoring/ai-agent/page.tsx @@ -0,0 +1,154 @@ +import { FC, ReactElement, useRef, useState } from 'react'; +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { SendOutlined } from '@ant-design/icons'; +import { usePostChat } from '@imphnen-frontend-service/service'; +import type { TChatResponse } from '@imphnen-frontend-service/service'; + +type Message = { + role: 'user' | 'assistant'; + content: string; + sources?: TChatResponse['sources']; +}; + +export const Components: FC = (): ReactElement => { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const chatMutation = usePostChat(); + const inputRef = useRef(null); + + const handleSend = async () => { + const question = input.trim(); + if (!question || loading) return; + setInput(''); + setMessages((m) => [...m, { role: 'user', content: question }]); + setLoading(true); + try { + const res = await chatMutation.mutateAsync({ question }); + setMessages((m) => [ + ...m, + { role: 'assistant', content: res.answer, sources: res.sources }, + ]); + } catch { + setMessages((m) => [ + ...m, + { + role: 'assistant', + content: 'Maaf, terjadi kesalahan saat menjawab. Coba lagi ya.', + }, + ]); + } finally { + setLoading(false); + inputRef.current?.focus(); + } + }; + + return ( +
+
+

+ AI Agent Mentoring +

+

+ Tanya apa saja tentang materi mentoring — jawaban diambil langsung + dari konten materi yang tersedia (RAG). +

+
+ +
+ {/* Chat body */} +
+ {messages.length === 0 ? ( +
+
+ +
+

+ Coba tanya: “Apa itu ownership di Rust?”{' '} + atau “Gimana cara buat route di Axum?” +

+
+ ) : ( + messages.map((msg, idx) => ( +
+
+ {msg.content} + {msg.sources && msg.sources.length > 0 && ( +
+

+ Sumber materi +

+ {msg.sources.map((s, i) => ( +
+ + 📄 {s.title} + + + {Math.round(s.score * 100)}% + +
+ ))} +
+ )} +
+
+ )) + )} + {loading && ( +
+
+ + + + ● + + + ● + + +
+
+ )} +
+ + {/* Input */} +
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSend()} + placeholder="Tanya tentang materi mentoring..." + className="flex-1 px-4 py-2.5 rounded-xl border border-neutral-200 focus:border-primary-400 focus:outline-none text-sm" + /> + +
+
+
+ ); +}; + +export default Components; \ No newline at end of file diff --git a/apps/dimentorin/src/app/(public)/mentoring/materi/[slug]/page.tsx b/apps/dimentorin/src/app/(public)/mentoring/materi/[slug]/page.tsx new file mode 100644 index 0000000..f355ad0 --- /dev/null +++ b/apps/dimentorin/src/app/(public)/mentoring/materi/[slug]/page.tsx @@ -0,0 +1,53 @@ +import { FC, ReactElement } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { ArrowLeftOutlined } from '@ant-design/icons'; +import { useGetMaterialBySlug } from '@imphnen-frontend-service/service'; + +export const Components: FC = (): ReactElement => { + const { slug } = useParams<{ slug: string }>(); + const { data: material, isLoading } = useGetMaterialBySlug(slug ?? ''); + + return ( +
+ + + + + {isLoading || !material ? ( +
+ ) : ( +
+ + {material.category} + +

+ {material.title} +

+

{material.description}

+
+ {/* SAFE: all '<' escaped to < below, no raw HTML passes through */} +
p.trim()) + .filter(Boolean) + .map( + (p) => + `

${p.replace(/` + ) + .join(''), + }} + /> +

+
+ )} +
+ ); +}; + +export default Components; \ No newline at end of file diff --git a/apps/dimentorin/src/app/(public)/mentoring/materi/page.tsx b/apps/dimentorin/src/app/(public)/mentoring/materi/page.tsx new file mode 100644 index 0000000..91908f3 --- /dev/null +++ b/apps/dimentorin/src/app/(public)/mentoring/materi/page.tsx @@ -0,0 +1,92 @@ +import { FC, ReactElement } from 'react'; +import { Link } from 'react-router-dom'; +import { Show } from '@imphnen-frontend-service/utils'; +import { + useGetMaterialCategories, + useGetMaterialsList, +} from '@imphnen-frontend-service/service'; + +const CATEGORY_COLORS: Record = { + Backend: 'bg-primary-100 text-primary-700', + Frontend: 'bg-info-100 text-info-800', + Data: 'bg-success-100 text-success-700', + Mobile: 'bg-warning-100 text-warning-800', + Cybersecurity: 'bg-danger-100 text-danger-700', + Default: 'bg-neutral-100 text-neutral-600', +}; + +export const Components: FC = (): ReactElement => { + const { data: materials, isLoading } = useGetMaterialsList(); + const { data: categories } = useGetMaterialCategories(); + + return ( +
+
+

+ Materi Mentoring +

+

+ Kumpulan materi dari para mentor IMPHNEN untuk memperdalam + keterampilan kamu. +

+
+ + {/* Category filter chips */} + +
+ {categories?.map((cat) => ( + + {cat} + + ))} +
+
+ + {isLoading ? ( +
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+ ) : !materials?.data?.length ? ( +

Belum ada materi tersedia.

+ ) : ( +
+ {materials.data.map((m) => { + const color = + CATEGORY_COLORS[m.category] ?? CATEGORY_COLORS.Default; + return ( + + + {m.category} + +

+ {m.title} +

+

+ {m.description} +

+ + ); + })} +
+ )} +
+ ); +}; + +export default Components; \ No newline at end of file diff --git a/libs/service/src/api/dimentorin/index.ts b/libs/service/src/api/dimentorin/index.ts index a8281b6..ed624f7 100644 --- a/libs/service/src/api/dimentorin/index.ts +++ b/libs/service/src/api/dimentorin/index.ts @@ -3,6 +3,11 @@ import { TArticleDetail, TArticleListItem, TArticleListParams, + TChatRequest, + TChatResponse, + TMaterialDetail, + TMaterialItem, + TMaterialListResponse, TBookSessionRequest, TCreatePaymentRequest, TMentorAvailability, @@ -214,3 +219,47 @@ export const postRefreshPayment = async ( }); return data.data; }; + + +// ---- Materials ---- +export const getMaterialsList = async (): Promise => { + const { data } = await api({ method: 'GET', url: '/dimentorin/materials' }); + return data.data; +}; + +export const getMaterialBySlug = async ( + slug: string +): Promise => { + const { data } = await api({ + method: 'GET', + url: `/dimentorin/materials/slug/${slug}`, + }); + return data.data; +}; + +export const getMaterialById = async ( + id: string +): Promise => { + const { data } = await api({ method: 'GET', url: `/dimentorin/materials/${id}` }); + return data.data; +}; + +export const getMaterialCategories = async (): Promise => { + const { data } = await api({ + method: 'GET', + url: '/dimentorin/materials/categories', + }); + return data.data; +}; + +// ---- AI Agent (RAG) ---- +export const postChat = async ( + request: TChatRequest +): Promise => { + const { data } = await api({ + method: 'POST', + url: '/dimentorin/ai/chat', + data: request, + }); + return data.data; +}; diff --git a/libs/service/src/hooks/dimentorin/index.ts b/libs/service/src/hooks/dimentorin/index.ts index c701bd3..c52222e 100644 --- a/libs/service/src/hooks/dimentorin/index.ts +++ b/libs/service/src/hooks/dimentorin/index.ts @@ -12,7 +12,12 @@ import { getMentorStats, getMentors, getMyPayments, + getMaterialById, + getMaterialBySlug, + getMaterialCategories, + getMaterialsList, getMySessions, + postChat, getPaymentById, getSessionPayments, postBookSession, @@ -26,7 +31,12 @@ import { TArticleDetail, TArticleListItem, TArticleListParams, - TBookSessionRequest, + TBookSessionRequest, + TChatRequest, + TChatResponse, + TMaterialDetail, + TMaterialItem, + TMaterialListResponse, TCreatePaymentRequest, TMentorAvailability, TMentorDetail, @@ -245,4 +255,55 @@ export const useGetArticleById = ( }); }; + + +// ---- Materials ---- +export const useGetMaterialsList = (): UseQueryResult< + TMaterialListResponse, + TResponseError +> => { + return useQuery({ + queryKey: ['get-materials-list'], + queryFn: async () => await getMaterialsList(), + }); +}; + +export const useGetMaterialBySlug = ( + slug: string +): UseQueryResult => { + return useQuery({ + queryKey: ['get-material-by-slug', slug], + queryFn: async () => await getMaterialBySlug(slug), + enabled: !!slug, + }); +}; + +export const useGetMaterialById = ( + id: string +): UseQueryResult => { + return useQuery({ + queryKey: ['get-material-by-id', id], + queryFn: async () => await getMaterialById(id), + enabled: !!id, + }); +}; + +export const useGetMaterialCategories = (): UseQueryResult< + string[], + TResponseError +> => { + return useQuery({ + queryKey: ['get-material-categories'], + queryFn: async () => await getMaterialCategories(), + }); +}; + +// ---- AI Agent ---- +export const usePostChat = () => { + return useMutation({ + mutationKey: ['post-chat'], + mutationFn: async (request: TChatRequest) => await postChat(request), + }); +}; + export type { TResponseMessage }; diff --git a/libs/service/src/types/dimentorin/index.ts b/libs/service/src/types/dimentorin/index.ts index 61a9857..ee031d0 100644 --- a/libs/service/src/types/dimentorin/index.ts +++ b/libs/service/src/types/dimentorin/index.ts @@ -182,3 +182,50 @@ export type TPayment = { export type TCreatePaymentRequest = { method: string; // 'va' | 'qris' | 'manual' }; + + +export type TMaterialItem = { + id: string; + mentorId: string; + title: string; + slug: string; + category: string; + description: string; + coverUrl?: string | null; + isPublished: boolean; + createdAt: string; + updatedAt: string; +}; + +export type TMaterialDetail = TMaterialItem & { + content: string; +}; + +export type TMaterialListResponse = { + data: TMaterialItem[]; + meta: { + page: number; + per_page: number; + total?: number; + total_pages?: number; + has_next: boolean; + has_prev: boolean; + }; +}; + +export type TChatRequest = { + question: string; + materialId?: string; +}; + +export type TChatSource = { + materialId: string; + title: string; + score: number; + snippet: string; +}; + +export type TChatResponse = { + answer: string; + sources: TChatSource[]; +};