feat: add Materi section + AI agent RAG to GMW business flow

Backend:
- New materi module: schema (materi_documents table), repository, service
- ragClient: semantic + keyword search over materi docs, plus Discord
  archive via Qdrant, then LLM answer generation (RAG)
- Wire materiRouter into appRouter (list/detail/create/update/delete/chat)

Frontend:
- New types (MateriDocument, CreateMateriInput, RAG chat shapes)
- API client (SSR HTTP RPCLink + browser WS RPCLink)
- Routes: /materi list, /materi/[id] detail, /materi/new form,
  /materi/chat RAG chat UI
- Sidebar nav item 'Materi'

Migration: scripts/add-materi-documents.sql (CREATE TABLE IF NOT EXISTS)
This commit is contained in:
asepharyana
2026-08-20 15:57:04 +07:00
parent 6f20b0f146
commit 0aa893ab7d
16 changed files with 1228 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
// Materi API client — talks to backend /trpc materi router
import { createORPCClient } from "@orpc/client";
import { RPCLink } from "@orpc/client/fetch";
import { orpc } from "@/lib/orpc/client";
import type { MateriDocument, CreateMateriInput, MateriRagChatResult, MateriRagChatMessage } from "@/lib/types/materi";
import type { ORPCClient } from "@/lib/orpc/types";
const BACKEND_URL =
process.env.GMW_BACKEND_URL?.replace(/\/+$/, "") || "http://127.0.0.1:4001";
let _serverClient: ORPCClient | null = null;
function serverOrpc(): ORPCClient {
if (!_serverClient) {
const link = new RPCLink({
url: BACKEND_URL + "/trpc",
fetch(url, init) {
return fetch(url, { ...init, cache: "no-store" });
},
});
_serverClient = createORPCClient(link) as unknown as ORPCClient;
}
return _serverClient;
}
// Server-side (SSR seed) — uses the HTTP RPCLink via oRPC
export async function listMateriSSR(limit = 50, search?: string): Promise<MateriDocument[]> {
return (serverOrpc() as any).materi.list({
limit,
search,
}) as unknown as Promise<MateriDocument[]>;
}
export async function getMateriSSR(id: string): Promise<MateriDocument | null> {
return (serverOrpc() as any).materi.detail({
id,
}) as unknown as Promise<MateriDocument | null>;
}
// Client-side — browser WebSocket RPCLink (orpc is "use client")
export async function createMateri(input: CreateMateriInput): Promise<MateriDocument> {
return orpc.materi.create(input) as unknown as Promise<MateriDocument>;
}
export async function updateMateri(
id: string,
input: Partial<CreateMateriInput>,
): Promise<MateriDocument | null> {
return orpc.materi.update({ id, ...input }) as unknown as Promise<MateriDocument | null>;
}
export async function deleteMateri(id: string): Promise<boolean> {
return orpc.materi.delete({ id }) as unknown as Promise<boolean>;
}
export async function searchMateri(
query: string,
history: MateriRagChatMessage[] = [],
materiId?: string,
): Promise<MateriRagChatResult> {
return orpc.materi.chat({
message: query,
history,
materiId,
}) as unknown as Promise<MateriRagChatResult>;
}