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
@@ -596,3 +596,44 @@ export type DbRetentionPolicyInsert =
// Chatbot Messages
export type ChatbotMessage = typeof chatbotMessagesTable.$inferSelect;
export type ChatbotMessageInsert = typeof chatbotMessagesTable.$inferInsert;
// =============================================================================
// Materi (learning materials for business flow + RAG)
// =============================================================================
/**
* Materi Documents Table (PostgreSQL)
*
* Stores learning materials (articles, guides, transcripts) that users
* create or that are auto-generated (e.g. AI conversation summaries).
* Used by the RAG chat agent to ground answers in authoritative content.
*/
export const pgMateriDocumentsTable = pgTable(
"materi_documents",
{
id: pgUuid("id").primaryKey().defaultRandom(),
title: pgText("title").notNull(),
description: pgText("description"),
content: pgText("content").notNull(),
category: pgText("category").notNull().default("general"),
tags: pgText("tags").array(),
owner_user_id: pgText("owner_user_id").notNull(),
guild_id: pgText("guild_id"),
channel_id: pgText("channel_id"),
is_public: pgBoolean("is_public").notNull().default(true),
view_count: pgInteger("view_count").notNull().default(0),
created_at: pgBigint("created_at", { mode: "number" }).notNull(),
updated_at: pgBigint("updated_at", { mode: "number" }).notNull(),
},
(table) => ({
categoryIdx: pgIndex("idx_materi_category").on(table.category),
ownerIdx: pgIndex("idx_materi_owner").on(table.owner_user_id),
guildIdx: pgIndex("idx_materi_guild").on(table.guild_id),
searchIdx: pgIndex("idx_materi_search").on(table.title, table.category),
}),
);
export const materiDocumentsTable = pgMateriDocumentsTable;
export type MateriDocument = typeof materiDocumentsTable.$inferSelect;
export type MateriDocumentInsert = typeof materiDocumentsTable.$inferInsert;