diff --git a/apps/web/src/app/(app)/@admin/documents/table.tsx b/apps/web/src/app/(app)/@admin/documents/table.tsx index 37413d1..5e8e7f3 100644 --- a/apps/web/src/app/(app)/@admin/documents/table.tsx +++ b/apps/web/src/app/(app)/@admin/documents/table.tsx @@ -184,7 +184,7 @@ export function DocumentsTable() { (id ? id : crypto.randomUUID()), [id]); + + const { + messages, + isLoading, + append: mutate, + } = useChat({ + streamMode: "text", + onError: (error) => toast.error(error.message), + sendExtraMessageFields: true, + onFinish, + initialMessages, + id: chatId, + }); + const chatContainerRef = useChatScroll(messages); + + //#region //*=========== Form =========== + const methods = useForm({ + mode: "onSubmit", + values: { prompt: value }, + schema: z.object({ + prompt: z.string().min(1, { message: "Please enter a message" }), + topic: z.string().optional(), + }), + }); + const { handleSubmit, control, watch, setValue } = methods; + const onSubmit = handleSubmit(async (data) => { + setValue("prompt", ""); + setValue("topic", ""); + await mutate( + { content: data.prompt, role: "user" }, + { options: { body: { topic: data.topic, chatId } } }, + ); + }); + //#endregion //*======== Form =========== + + const [dropdownOpen, setDropdownOpen] = React.useState(false); + const [suggestionsOpen, setSuggestionsOpen] = React.useState(false); + const [filteredTopics, setFilteredTopics] = React.useState(TOPICS); + + const [debouncedPrompt] = useDebounceValue(watch("prompt"), 500); + const suggestions = api.chat.getSuggestion.useQuery(debouncedPrompt, { + enabled: !debouncedPrompt.startsWith("/") && debouncedPrompt.length > 0, + placeholderData: keepPreviousData, + }); + const isAnySuggestion = suggestions.data && suggestions.data.length > 0; + + return ( +
0} + > + +
+ {messages.map((item) => ( +
+
+ {item.content.includes("") ? ( +
+ ) : ( + item.content + )} +
+
+ ))} +
+ + +
+ + { + if ( + (e.key === "Backspace" || e.key === "Delete") && + !watch("prompt") && + watch("topic") + ) + setValue("topic", ""); + + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + await onSubmit(); + } + }} + onValueChange={(value) => { + if (value.startsWith("/") && !watch("topic")) { + setFilteredTopics( + TOPICS.filter((topic) => + topic.toLowerCase().includes(value.slice(1).toLowerCase()), + ), + ); + setDropdownOpen(true); + return; + } + + console.log("value", value); + if (isAnySuggestion && value.length > 0) { + setSuggestionsOpen(true); + return; + } + + setDropdownOpen(false); + setSuggestionsOpen(false); + }} + startContent={ + <> + setSuggestionsOpen(false)} + classNames={{ content: "min-w-[24.8rem]" }} + > + +
+ + { + const selectedId = Array.from(selected).at(0)?.toString(); + + setSuggestionsOpen(false); + setValue( + "prompt", + suggestions.data?.find((s) => s.id === selectedId) + ?.content ?? "", + ); + }} + > + {suggestions.data?.map((suggestion) => ( + + {suggestion.content} + + )) ?? No suggestion} + + + + setDropdownOpen(false)} + > + {watch("topic") ? ( + + + /{watch("topic")} + + + ) : ( + + + + )} + { + setDropdownOpen(false); + setValue("prompt", ""); + setValue("topic", Array.from(selected).at(0)?.toString()); + }} + > + {filteredTopics.map((topic) => ( + + {topic} + + ))} + + + + } + endContent={ + + } + /> + + +
+ ); +} diff --git a/apps/web/src/app/(app)/layout.tsx b/apps/web/src/app/(app)/layout.tsx index 31cdb2c..4967518 100644 --- a/apps/web/src/app/(app)/layout.tsx +++ b/apps/web/src/app/(app)/layout.tsx @@ -42,7 +42,7 @@ export default async function AuthLayout(props: {
{isAdmin ? props.admin : props.user} diff --git a/apps/web/src/server/api/routers/chat/chat.procedure.ts b/apps/web/src/server/api/routers/chat/chat.procedure.ts index c762598..183624e 100644 --- a/apps/web/src/server/api/routers/chat/chat.procedure.ts +++ b/apps/web/src/server/api/routers/chat/chat.procedure.ts @@ -1,9 +1,42 @@ import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { db } from "@/server/db"; import { chats, messages } from "@/server/db/schema"; import { and, desc, eq, ilike, notExists, or } from "drizzle-orm"; import { alias } from "drizzle-orm/pg-core"; import { z } from "zod"; +function getQuestion(input?: string) { + const m = alias(messages, "m"); + return db + .select() + .from(m) + .where( + and( + eq(m.role, "user"), + ilike(m.content, `%${input}%`).if(input && input.length > 0), + notExists( + db + .select() + .from(messages) + .where( + and( + eq(messages.chatId, m.chatId), + eq(messages.role, "assistant"), + or( + ilike(messages.content, "%saya tidak tahu%"), + ilike(messages.content, "%hi %"), + ilike(messages.content, "%halo%"), + ), + ), + ) + .orderBy(desc(messages.createdAt)), + ), + ), + ) + .orderBy(desc(m.createdAt)) + .limit(3); +} + export const chatRouter = createTRPCRouter({ get: protectedProcedure .input( @@ -29,35 +62,14 @@ export const chatRouter = createTRPCRouter({ }); }), - getRecentQuestion: protectedProcedure.query(async ({ ctx }) => { - const m = alias(messages, "m"); - return await ctx.db - .select() - .from(m) - .where( - and( - eq(m.role, "user"), - notExists( - ctx.db - .select() - .from(messages) - .where( - and( - eq(messages.chatId, m.chatId), - eq(messages.role, "assistant"), - or( - ilike(messages.content, "%saya tidak tahu%"), - ilike(messages.content, "%hi %"), - ilike(messages.content, "%halo%"), - ), - ), - ) - .orderBy(desc(messages.createdAt)), - ), - ), - ) - .orderBy(desc(m.createdAt)) - .limit(3); + getSuggestion: protectedProcedure + .input(z.string()) + .query(async ({ input }) => { + return await getQuestion(input); + }), + + getRecentQuestion: protectedProcedure.query(async () => { + return await getQuestion(); }), show: protectedProcedure diff --git a/packages/ui/src/chat.tsx b/packages/ui/src/chat.tsx index 05994b2..f7412e4 100644 --- a/packages/ui/src/chat.tsx +++ b/packages/ui/src/chat.tsx @@ -21,7 +21,7 @@ import { Form, FormTextArea, useForm } from "./form"; const TOPICS = ["template", "MBKM", "SKEM", "UKT", "TA", "wisuda", "silabus"]; -function useChatScroll(dep: T) { +export function useChatScroll(dep: T) { const ref = React.useRef(null); React.useEffect(() => { if (ref.current) {