From 30e3dbb42b334a4d3087d4f04fa4ac23dc0db8ea Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Tue, 21 Apr 2026 14:19:50 +0200 Subject: [PATCH] chat interface --- .../(.)assistant/_components/ChatModal.tsx | 21 ++-- src/app/@modal/(.)assistant/page.tsx | 2 +- src/app/_providers/MessagesProvider.tsx | 69 ++++++++++++++ src/app/chat/_components/AssistantMessage.tsx | 2 +- src/app/chat/_components/ChatInterface.tsx | 95 +++++++------------ src/app/chat/_components/Messages.tsx | 27 ++++-- src/app/chat/page.tsx | 21 ++-- src/app/layout.tsx | 17 ++-- src/components/ui/card.tsx | 5 +- src/lib/hooks.ts | 9 +- src/server/routers/chat.ts | 2 + 11 files changed, 173 insertions(+), 97 deletions(-) create mode 100644 src/app/_providers/MessagesProvider.tsx diff --git a/src/app/@modal/(.)assistant/_components/ChatModal.tsx b/src/app/@modal/(.)assistant/_components/ChatModal.tsx index 088911b..eb19864 100644 --- a/src/app/@modal/(.)assistant/_components/ChatModal.tsx +++ b/src/app/@modal/(.)assistant/_components/ChatModal.tsx @@ -2,6 +2,8 @@ import { useRouter } from 'next/navigation' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui/dialog' import ChatInterface from '~/app/chat/_components/ChatInterface' +import { useMessages } from '~/app/_providers/MessagesProvider'; +import { Spinner } from '~/components/ui/spinner'; type DBMessage = { id: string @@ -9,14 +11,9 @@ type DBMessage = { content: string } -interface ChatModalProps { - sessionId?: string - // initialMessages: DBMessage[] -} - -export default function ChatModal({ sessionId }: ChatModalProps) { +export default function ChatModal() { const router = useRouter() - + const {messages,session,clearChat,clearingChat,isLoading,error,refetchMessages} = useMessages() return ( router.back()}> @@ -24,7 +21,15 @@ export default function ChatModal({ sessionId }: ChatModalProps) { Talk To My AI-Assistant
- + {messages && session?.id && + + } + {isLoading && + <> Loading Messages... + } + {error && +
{error}
+ }
diff --git a/src/app/@modal/(.)assistant/page.tsx b/src/app/@modal/(.)assistant/page.tsx index 8fd4f7d..6dc86fe 100644 --- a/src/app/@modal/(.)assistant/page.tsx +++ b/src/app/@modal/(.)assistant/page.tsx @@ -8,7 +8,7 @@ export default function AssistantModalPage() { const { data: session, error, isLoading } = trpc.chat.getSession.useQuery(); return ( <> - + {error &&
{error.message}
} {isLoading && } diff --git a/src/app/_providers/MessagesProvider.tsx b/src/app/_providers/MessagesProvider.tsx new file mode 100644 index 0000000..e179dfe --- /dev/null +++ b/src/app/_providers/MessagesProvider.tsx @@ -0,0 +1,69 @@ +'use client' +import type { inferRouterOutputs } from '@trpc/server'; +import { createContext, useContext, useEffect, useState, type ReactNode } from 'react' +import { trpc } from '~/app/_trpc/Client' +import { type ChatRouter } from '~/server/routers/chat' +const MessageContext = createContext<{ + session?: inferRouterOutputs['getSession'] + messages?: inferRouterOutputs['getMessages'] + refetchMessages: () => void + clearChat: (callback?: () => void) => void + error: string|null + isLoading: boolean + clearingChat: boolean + clearedChat: boolean +}>({ + session: undefined, + messages: undefined, + refetchMessages: () => undefined, + clearChat: () => undefined, + error: null, + isLoading: true, + clearingChat: false, + clearedChat: false +}) +export const useMessages = () => useContext(MessageContext) +export const MessagesProvider = ({children}:{children:ReactNode}) => { + const [error,setError] = useState(null) + const [isLoading,setIsLoading] = useState(true) + const { data: session,error:sessionError,isLoading:sessionLoading} = trpc.chat.getSession.useQuery() + const { data: messages, refetch, error:messageError, isLoading:messagesLoading } = trpc.chat.getMessages.useQuery(session?.id ? session.id : "") + const { mutate ,isPending:clearingChat,isSuccess:clearedChat } = trpc.chat.clearChat.useMutation() + const utils = trpc.useUtils() + const refetchMessages = () => { + utils.chat.getMessages.invalidate() + refetch() + } + const clearChat = (callback?: () => void) => { + mutate(undefined,{onSuccess: () => { + if (callback) { + callback() + } + utils.chat.getMessages.invalidate() + }}) + } + useEffect(() => { + messageError && setError(messageError.message) + sessionError && setError(sessionError.message) + },[messageError,sessionError]) + useEffect(() => { + !sessionLoading && !messagesLoading && setIsLoading(false) + sessionLoading || messagesLoading && setIsLoading(true) + },[sessionLoading,messagesLoading]) + return ( + + {children} + + ) +} diff --git a/src/app/chat/_components/AssistantMessage.tsx b/src/app/chat/_components/AssistantMessage.tsx index 9c24c68..98d10ec 100644 --- a/src/app/chat/_components/AssistantMessage.tsx +++ b/src/app/chat/_components/AssistantMessage.tsx @@ -16,7 +16,7 @@ export const AssistantMessage = (props: { message: UIMessage }) => { {message.parts.map((part, i) => { if (part.type === 'text') { return ( - + {part.text} ) diff --git a/src/app/chat/_components/ChatInterface.tsx b/src/app/chat/_components/ChatInterface.tsx index 66736ef..b28465e 100644 --- a/src/app/chat/_components/ChatInterface.tsx +++ b/src/app/chat/_components/ChatInterface.tsx @@ -9,9 +9,8 @@ import { } from '~/app/_providers/GsapProvicer'; import Messages from './Messages' import { DeleteIcon } from 'lucide-react'; -import { trpc } from '~/app/_trpc/Client' import { Spinner } from '~/components/ui/spinner'; -import { Skeleton } from '~/components/ui/skeleton'; +import { useMessages } from '~/app/_providers/MessagesProvider'; interface DBMessage { id: string role: 'user' | 'assistant' @@ -19,8 +18,8 @@ interface DBMessage { } interface ChatInterfaceProps { - sessionId?: string - // initialMessages: DBMessage[] + sessionId: string, + dbMessages: DBMessage[], } function toUIMessages(dbMessages: DBMessage[]): UIMessage[] { @@ -30,28 +29,10 @@ function toUIMessages(dbMessages: DBMessage[]): UIMessage[] { parts: [{ type: 'text' as const, text: m.content }], })) } -export default function ChatInterface({ sessionId }: ChatInterfaceProps) { - if (!sessionId) { - return ( -
- - - -
- ) - } - const utils = trpc.useUtils(); - const { data: dbMessages, refetch: refetchMessages } = trpc.chat.getMessages.useQuery(sessionId) - const [messages, setMessages] = useState([]); - function addMessage(newMessage: UIMessage) { - setMessages(prev => [...prev, newMessage]); - } - useEffect(() => { - setMessages(toUIMessages(dbMessages ?? [])); - }, [dbMessages]); - if (messages.at(0)?.id != 'init') { - messages.unshift({ +function addInitMessage(messageArray: UIMessage[]) { + if (messageArray.at(0)?.id != 'init') { + messageArray.unshift({ id: "init", role: 'assistant', parts: [{ @@ -60,46 +41,31 @@ export default function ChatInterface({ sessionId }: ChatInterfaceProps) { }], }) } +} + +export default function ChatInterface({ dbMessages, sessionId }: ChatInterfaceProps) { const [input, setInput] = useState('') - const { sendMessage, status, error, clearError } = useChat({ + const { clearingChat, clearChat, refetchMessages } = useMessages(); + const initialMessages = toUIMessages(dbMessages) + addInitMessage(initialMessages) + const { messages, sendMessage, status, error, clearError, setMessages } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', body: { sessionId }, }), - messages: messages, + messages: initialMessages, }) - const handleSend = () => { - const text = input.trim() - if (!text || status != 'ready' || sessionId == undefined) return - setInput('') - sendMessage({ text }) - addMessage({ - id: "", role: "user", parts: [ - { type: 'text', text } - ] - }) - addMessage({ - id: "", role: "assistant", parts: [ - { type: 'text', text: "Thinking..." } - ] - }) - } - const clearChatMutation = trpc.chat.clearChat.useMutation() - const handleClear = () => { - clearChatMutation.mutate(undefined, { - onSuccess: () => { - utils.chat.getMessages.invalidate() - refetchMessages() - } - }) - } - const gsapContext = useGsapContext() useEffect(() => { - console.log(status) - if (status == 'ready') { - utils.chat.getMessages.invalidate(); + return () => { refetchMessages() } - }, [status]) + }, []) + const handleSend = () => { + const text = input.trim() + if (!text || status != 'ready' || clearingChat) return + setInput('') + sendMessage({ text }) + } + const gsapContext = useGsapContext() useEffect(() => { let scroller = gsapContext?.getScroller() if (scroller instanceof Window) { @@ -111,9 +77,8 @@ export default function ChatInterface({ sessionId }: ChatInterfaceProps) { return (
{messages && - + } - {error && (
@@ -156,10 +121,16 @@ export default function ChatInterface({ sessionId }: ChatInterfaceProps) {