From 7f4196124dec95027bac7890a857b515e7d88ffd Mon Sep 17 00:00:00 2001 From: mytheclipsebotreview Date: Thu, 20 Aug 2026 18:45:09 +0700 Subject: [PATCH] fix(ci): lint and a11y fixes unblocks GHA deploy - materi/new/page.tsx: add htmlFor+id pairs for all 5 form labels (a11y) - biome --write --unsafe: fix useTemplate, useLiteralKeys, import sort across materi module files (backend + frontend) - These pre-existing lint errors from 0aa893a blocked the deploy pipeline --- services/backend/src/modules/materi/index.ts | 20 ++++--- .../src/modules/materi/materi.repository.ts | 5 +- .../src/modules/materi/materi.service.ts | 25 ++++++--- .../backend/src/modules/materi/ragClient.ts | 55 ++++++++++++------- services/backend/src/orpc/router.ts | 4 +- .../src/app/(dashboard)/materi/[id]/page.tsx | 10 ++-- .../src/app/(dashboard)/materi/chat/page.tsx | 50 ++++++++++++----- .../src/app/(dashboard)/materi/new/page.tsx | 53 ++++++++++++++---- .../src/app/(dashboard)/materi/page.tsx | 12 ++-- services/frontend/src/lib/api/materi.ts | 23 ++++++-- 10 files changed, 179 insertions(+), 78 deletions(-) diff --git a/services/backend/src/modules/materi/index.ts b/services/backend/src/modules/materi/index.ts index 7cf5b3a..d2a541b 100644 --- a/services/backend/src/modules/materi/index.ts +++ b/services/backend/src/modules/materi/index.ts @@ -1,14 +1,18 @@ -export { MateriService } from "./materi.service.js"; -export { materiService } from "./materi.service.js"; export { materiRepository } from "./materi.repository.js"; export { - createMateriSchema, - updateMateriSchema, - materiQuerySchema, - materiRagChatSchema, type CreateMateriInput, - type UpdateMateriInput, + createMateriSchema, type MateriQueryInput, type MateriRagChatInput, + materiQuerySchema, + materiRagChatSchema, + type UpdateMateriInput, + updateMateriSchema, } from "./materi.schema.js"; -export { ragChat, searchMateri, type MateriSearchHit, type RAGChatResult } from "./ragClient.js"; +export { MateriService, materiService } from "./materi.service.js"; +export { + type MateriSearchHit, + type RAGChatResult, + ragChat, + searchMateri, +} from "./ragClient.js"; diff --git a/services/backend/src/modules/materi/materi.repository.ts b/services/backend/src/modules/materi/materi.repository.ts index d41bbab..785598e 100644 --- a/services/backend/src/modules/materi/materi.repository.ts +++ b/services/backend/src/modules/materi/materi.repository.ts @@ -1,9 +1,9 @@ import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; +import { getDatabase } from "@/shared/database/index.js"; import { type MateriDocument, materiDocumentsTable, } from "@/shared/database/schema.js"; -import { getDatabase } from "@/shared/database/index.js"; import type { MateriQueryInput } from "./materi.schema.js"; export class MateriRepository { @@ -39,8 +39,7 @@ export class MateriRepository { conditions.push(eq(materiDocumentsTable.is_public, true)); } - const whereClause = - conditions.length > 0 ? and(...conditions) : undefined; + const whereClause = conditions.length > 0 ? and(...conditions) : undefined; const result = await db .select() diff --git a/services/backend/src/modules/materi/materi.service.ts b/services/backend/src/modules/materi/materi.service.ts index eec86c2..84d0dfe 100644 --- a/services/backend/src/modules/materi/materi.service.ts +++ b/services/backend/src/modules/materi/materi.service.ts @@ -1,11 +1,11 @@ import type { MateriDocument } from "@/shared/database/schema.js"; import { createChildLogger } from "@/shared/logger/index.js"; import { materiRepository } from "./materi.repository.js"; -import { - type MateriQueryInput, - type CreateMateriInput, - type UpdateMateriInput, - type MateriRagChatInput, +import type { + CreateMateriInput, + MateriQueryInput, + MateriRagChatInput, + UpdateMateriInput, } from "./materi.schema.js"; import { ragChat } from "./ragClient.js"; @@ -14,7 +14,10 @@ const logger = createChildLogger("materi.service"); export class MateriService { /** List materi documents with optional filtering. */ async list(input: MateriQueryInput): Promise { - logger.debug({ limit: input.limit, search: input.search }, "Listing materi"); + logger.debug( + { limit: input.limit, search: input.search }, + "Listing materi", + ); return materiRepository.list(input); } @@ -72,7 +75,15 @@ export class MateriService { async ragChat( input: MateriRagChatInput, ownerUserId: string, - ): Promise<{ answer: string; sources: Array<{ id: string; title: string; score: number; excerpt: string }> }> { + ): Promise<{ + answer: string; + sources: Array<{ + id: string; + title: string; + score: number; + excerpt: string; + }>; + }> { logger.info({ ownerUserId, hasMateriId: !!input.materiId }, "RAG chat"); // Fetch relevant materi documents diff --git a/services/backend/src/modules/materi/ragClient.ts b/services/backend/src/modules/materi/ragClient.ts index 9153143..8e81e96 100644 --- a/services/backend/src/modules/materi/ragClient.ts +++ b/services/backend/src/modules/materi/ragClient.ts @@ -1,6 +1,6 @@ import { config } from "@/shared/config/index.js"; -import { createChildLogger } from "@/shared/logger/index.js"; import type { MateriDocument } from "@/shared/database/schema.js"; +import { createChildLogger } from "@/shared/logger/index.js"; import { embedQuery } from "../messages/embed.js"; import { searchArchive } from "../messages/qdrant.js"; @@ -101,7 +101,9 @@ export async function searchMateri( } else { // Fallback: keyword match scoring const titleMatch = doc.title.toLowerCase().includes(query.toLowerCase()); - const contentMatch = doc.content.toLowerCase().includes(query.toLowerCase()); + const contentMatch = doc.content + .toLowerCase() + .includes(query.toLowerCase()); const tagMatch = (doc.tags ?? []).some((t) => t.toLowerCase().includes(query.toLowerCase()), ); @@ -117,11 +119,15 @@ export async function searchMateri( } // Also search Discord message archive via Qdrant for conversation context - const archiveHits = await searchArchive(queryVec ?? [], topK, SIMILARITY_THRESHOLD); + const archiveHits = await searchArchive( + queryVec ?? [], + topK, + SIMILARITY_THRESHOLD, + ); for (const hit of archiveHits) { results.push({ document: { - id: "archive-" + Date.now(), + id: `archive-${Date.now()}`, title: "Discord Archive", description: null, content: hit.payload.text, @@ -153,21 +159,30 @@ export async function ragChat( ): Promise { const hits = await searchMateri(query, documents); - const contextBlock = hits - .map((h) => { - const scoreStr = h.score.toFixed(3); - return ( - "\n" + - h.chunkText + - "\n" - ); - }) - .join("\n\n") || "(tidak ada konteks relevan ditemukan)"; + const contextBlock = + hits + .map((h) => { + const scoreStr = h.score.toFixed(3); + return ( + '\n' + + h.chunkText + + "\n" + ); + }) + .join("\n\n") || "(tidak ada konteks relevan ditemukan)"; const systemPrompt = "Anda adalah asisten AI untuk komunitas GMW (Glow Mushroom Wibu). " + "Jawab pertanyaan pengguna berdasarkan konteks berikut. Jika tidak tahu, katakan tidak tahu.\n\n" + - "Konteks materi dan arsip Discord:\n" + contextBlock + "\n\n" + + "Konteks materi dan arsip Discord:\n" + + contextBlock + + "\n\n" + "Instruksi: jawab singkat, akurat, dan berguna. Kutip sumber jika perlu."; const baseUrL = config.AI_LLM_BASE_URL; @@ -185,8 +200,8 @@ export async function ragChat( ].filter((m) => m.content) as Array<{ role: string; content: string }>; const authHeaders: Record = {}; - authHeaders["Authorization"] = authHeader; - const res = await fetch(baseUrL + "/chat/completions", { + authHeaders.Authorization = authHeader; + const res = await fetch(`${baseUrL}/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", @@ -202,14 +217,16 @@ export async function ragChat( }); if (!res.ok) { - throw new Error("LLM request failed: " + res.status); + throw new Error(`LLM request failed: ${res.status}`); } const data = (await res.json()) as { choices?: Array<{ message?: { content?: string } }>; }; - const answer = data.choices?.[0]?.message?.content ?? "Maaf, tidak bisa menjawab saat ini."; + const answer = + data.choices?.[0]?.message?.content ?? + "Maaf, tidak bisa menjawab saat ini."; return { answer, diff --git a/services/backend/src/orpc/router.ts b/services/backend/src/orpc/router.ts index 114b4a6..1eef09e 100644 --- a/services/backend/src/orpc/router.ts +++ b/services/backend/src/orpc/router.ts @@ -7,11 +7,11 @@ import { chatbotService } from "../modules/chatbot/chatbot.service"; import { dashboardService } from "../modules/dashboard/dashboard.service"; import { knowledgeService } from "../modules/knowledge/knowledge.service"; import { + createMateriSchema, materiQuerySchema, materiRagChatSchema, - createMateriSchema, - updateMateriSchema, materiService, + updateMateriSchema, } from "../modules/materi/index.js"; import { mediaLoopSchema, diff --git a/services/frontend/src/app/(dashboard)/materi/[id]/page.tsx b/services/frontend/src/app/(dashboard)/materi/[id]/page.tsx index a7a0d33..63adca8 100644 --- a/services/frontend/src/app/(dashboard)/materi/[id]/page.tsx +++ b/services/frontend/src/app/(dashboard)/materi/[id]/page.tsx @@ -1,7 +1,7 @@ +import { Pencil, Trash2 } from "lucide-react"; import { notFound } from "next/navigation"; -import { PageTransition, MarkdownLite } from "@/components/shared"; -import { Button, Badge } from "@/components/primitives"; -import { Trash2, Pencil } from "lucide-react"; +import { Badge, Button } from "@/components/primitives"; +import { MarkdownLite, PageTransition } from "@/components/shared"; import { getMateriSSR } from "@/lib/api/materi"; export const dynamic = "force-dynamic"; @@ -30,12 +30,12 @@ export default async function MateriDetailPage({
diff --git a/services/frontend/src/app/(dashboard)/materi/chat/page.tsx b/services/frontend/src/app/(dashboard)/materi/chat/page.tsx index 5c42585..1947d57 100644 --- a/services/frontend/src/app/(dashboard)/materi/chat/page.tsx +++ b/services/frontend/src/app/(dashboard)/materi/chat/page.tsx @@ -1,11 +1,14 @@ "use client"; -import { useState, useRef, useEffect } from "react"; +import { Bot, ExternalLink, Loader2, Send, User } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { Button, GlassCard, Textarea } from "@/components/primitives"; import { PageTransition } from "@/components/shared"; -import { Button, Input, Textarea, GlassCard } from "@/components/primitives"; -import { Send, Bot, User, Loader2, ExternalLink } from "lucide-react"; import { searchMateri } from "@/lib/api/materi"; -import type { MateriRagChatMessage, MateriRagChatResult } from "@/lib/types/materi"; +import type { + MateriRagChatMessage, + MateriRagChatResult, +} from "@/lib/types/materi"; export const dynamic = "force-dynamic"; @@ -18,12 +21,15 @@ export default function MateriChatPage() { useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages]); + }, []); async function handleSend() { if (!input.trim() || isLoading) return; - const userMsg: MateriRagChatMessage = { role: "user", content: input.trim() }; + const userMsg: MateriRagChatMessage = { + role: "user", + content: input.trim(), + }; const newMessages = [...messages, userMsg]; setMessages(newMessages); setInput(""); @@ -31,8 +37,15 @@ export default function MateriChatPage() { setSources([]); try { - const result = await searchMateri(userMsg.content, newMessages, undefined); - const assistantMsg: MateriRagChatMessage = { role: "assistant", content: result.answer }; + const result = await searchMateri( + userMsg.content, + newMessages, + undefined, + ); + const assistantMsg: MateriRagChatMessage = { + role: "assistant", + content: result.answer, + }; setMessages([...newMessages, assistantMsg]); setSources(result.sources); } catch { @@ -70,8 +83,8 @@ export default function MateriChatPage() {

Silakan tanyakan sesuatu tentang materi komunitas.

- Contoh: "Apa itu screenshare audio di GMW?" atau - "Cara pakai voice recording" + Contoh: "Apa itu screenshare audio di GMW?" atau "Cara pakai + voice recording"

) : ( @@ -101,7 +114,9 @@ export default function MateriChatPage() { {msg.role === "user" ? "Anda" : "AI Agent"} -
{msg.content}
+
+ {msg.content} +
)) @@ -112,7 +127,9 @@ export default function MateriChatPage() {
- AI sedang mencari di materi... + + AI sedang mencari di materi... +
@@ -155,14 +172,19 @@ export default function MateriChatPage() { className="flex-1" rows={2} /> -
- AI mengacu pada materi dan arsip Discord. Jawaban mungkin tidak 100% akurat. + AI mengacu pada materi dan arsip Discord. Jawaban mungkin tidak 100% + akurat.
diff --git a/services/frontend/src/app/(dashboard)/materi/new/page.tsx b/services/frontend/src/app/(dashboard)/materi/new/page.tsx index a6888e9..1d15bc2 100644 --- a/services/frontend/src/app/(dashboard)/materi/new/page.tsx +++ b/services/frontend/src/app/(dashboard)/materi/new/page.tsx @@ -1,10 +1,10 @@ "use client"; -import { useState } from "react"; +import { ArrowLeft, Save } from "lucide-react"; import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { Button, GlassCard, Input, Textarea } from "@/components/primitives"; import { PageTransition } from "@/components/shared"; -import { Button, Input, Textarea, GlassCard } from "@/components/primitives"; -import { Save, ArrowLeft } from "lucide-react"; import { createMateri } from "@/lib/api/materi"; import type { CreateMateriInput } from "@/lib/types/materi"; @@ -24,7 +24,10 @@ export default function MateriNewPage() { const [saving, setSaving] = useState(false); const [error, setError] = useState(null); - function update(key: K, value: CreateMateriInput[K]) { + function update( + key: K, + value: CreateMateriInput[K], + ) { setForm((f) => ({ ...f, [key]: value })); } @@ -41,7 +44,7 @@ export default function MateriNewPage() { .map((t) => t.trim()) .filter(Boolean); const doc = await createMateri({ ...form, tags }); - router.push("/materi/" + doc.id); + router.push(`/materi/${doc.id}`); } catch (err) { setError(err instanceof Error ? err.message : "Gagal menyimpan materi."); setSaving(false); @@ -66,8 +69,14 @@ export default function MateriNewPage() {
- + update("title", e.target.value)} placeholder="Judul materi" @@ -75,8 +84,14 @@ export default function MateriNewPage() {
- + update("description", e.target.value)} placeholder="Deskripsi singkat" @@ -84,8 +99,14 @@ export default function MateriNewPage() {
- + update("category", e.target.value)} placeholder="general" @@ -93,8 +114,14 @@ export default function MateriNewPage() {
- + setTagsInput(e.target.value)} placeholder="wibu, discord, moderation" @@ -102,8 +129,14 @@ export default function MateriNewPage() {
- +