This commit is contained in:
2026-03-31 14:03:41 +02:00
parent 399d78e508
commit d567fa3e02
14 changed files with 336 additions and 153 deletions

View File

@@ -16,7 +16,7 @@ export const AssistantMessage = (props: { message: UIMessage }) => {
{message.parts.map((part, i) => {
if (part.type === 'text') {
return (
<Markdown>
<Markdown key={i}>
{part.text}
</Markdown>
)

View File

@@ -1,23 +1,25 @@
'use client'
import { useRef, useState, useEffect } from 'react'
import { useState, useEffect } from 'react'
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport, type UIMessage } from 'ai'
import { Button } from '~/components/ui/button'
import { Textarea } from '~/components/ui/textarea'
import { cn } from '~/lib/utils'
import Markdown from 'react-markdown';
import { AssistantMessage } from './AssistantMessage';
import { UserMessage } from './UserMessage';
type DBMessage = {
import {
useGsapContext,
} 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';
interface DBMessage {
id: string
role: 'user' | 'assistant'
content: string
}
interface ChatInterfaceProps {
sessionId: string
initialMessages: DBMessage[]
sessionId?: string
// initialMessages: DBMessage[]
}
function toUIMessages(dbMessages: DBMessage[]): UIMessage[] {
@@ -27,60 +29,80 @@ function toUIMessages(dbMessages: DBMessage[]): UIMessage[] {
parts: [{ type: 'text' as const, text: m.content }],
}))
}
export default function ChatInterface({ sessionId, initialMessages }: ChatInterfaceProps) {
const [input, setInput] = useState('')
const messagesEndRef = useRef<HTMLDivElement>(null)
const { messages, sendMessage, status, error, clearError } = useChat({
transport: new DefaultChatTransport({
api: '/api/chat',
body: { sessionId },
}),
messages: toUIMessages(initialMessages),
})
const isLoading = status === 'submitted' || status === 'streaming'
const hasError = status === 'error'
export default function ChatInterface({ sessionId }: ChatInterfaceProps) {
const utils = trpc.useUtils();
const { data: dbMessages, refetch: refetchMessages } = trpc.chat.getMessages.useQuery(sessionId ? sessionId : "")
const [messages, setMessages] = useState<UIMessage[]>([]);
function addMessage(newMessage: UIMessage) {
setMessages(prev => [...prev, newMessage]);
}
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
setMessages(toUIMessages(dbMessages ?? []));
}, [dbMessages]);
if (messages.at(0)?.id != 'init') {
messages.unshift({
id: "init",
role: 'assistant',
parts: [{
type: 'text',
text: "Hi im gregors ai assistant,you can ask me to provide general information or to schedule a meeting."
}],
})
}
const [input, setInput] = useState('')
const { sendMessage, status, error, clearError } = useChat({
transport: new DefaultChatTransport({
api: '/api/chat', body: { sessionId },
}),
messages: messages,
})
const handleSend = () => {
const text = input.trim()
if (!text || isLoading) return
if (!text || status != 'ready') 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();
refetchMessages()
}
}, [status])
useEffect(() => {
let scroller = gsapContext?.getScroller()
if (scroller instanceof Window) {
return;
}
console.log(scroller?.scrollHeight)
scroller?.scrollTo({ behavior: 'smooth', top: scroller.scrollHeight })
}, [messages])
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 && (
<div className="text-center text-muted-foreground py-12">
<p className="text-base font-medium mb-1">Hi! I'm Gregor's AI recruiter assistant.</p>
<p className="text-sm">Ask me about his skills and experience, or schedule a meeting!</p>
</div>
)}
{messages.map((message) => (
<>
{message.role == 'assistant' && <AssistantMessage message={message}/>}
{message.role == 'user' && <UserMessage message={message}/>}
</>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-muted rounded-lg px-4 py-2 text-sm">
<span className="animate-pulse">Thinking</span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{messages &&
<Messages messages={messages} />
}
{error && (
<div className="mx-4 mb-2 flex items-start gap-2 rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
@@ -89,19 +111,20 @@ export default function ChatInterface({ sessionId, initialMessages }: ChatInterf
? 'OpenAI quota exceeded. Please try again later.'
: `Error: ${error.message}`}
</span>
<button
<Button
type="button"
onClick={clearError}
className="shrink-0 opacity-60 hover:opacity-100"
aria-label="Dismiss"
variant='destructive'
>
</button>
<DeleteIcon />
</Button>
</div>
)}
<div className="p-4 border-t flex gap-2">
<div className="p-4 border-t flex flex-row gap-2">
<Textarea
name='message'
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask about Gregor's experience or schedule a meeting…"
@@ -114,13 +137,24 @@ export default function ChatInterface({ sessionId, initialMessages }: ChatInterf
}
}}
/>
<Button
onClick={handleSend}
disabled={isLoading || hasError || !input.trim()}
className="self-end"
>
Send
</Button>
<div className='flex flex-col gap-2'>
<Button
onClick={handleSend}
disabled={status != "ready" || !input.trim()}
>
Send
</Button>
<Button
variant='destructive'
onClick={handleClear}
disabled={status != "ready" || clearChatMutation.isPending}
>
{clearChatMutation.isPending ?
<Spinner /> :
"Clear Chat"
}
</Button>
</div>
</div>
</div>
)

View File

@@ -0,0 +1,25 @@
import { type UIMessage } from 'ai'
import * as Card from "~/components/ui/card"
import AnimateTextIn from '~/app/_components/Animated/AnimateIn';
import { UserMessage } from './UserMessage';
import { AssistantMessage } from './AssistantMessage';
import { ScrollArea } from '~/components/ui/scroll-area';
import { useTimeLine } from '~/app/_providers/GsapProvicer';
import {
memo
} from 'react';
const Messages = memo(({ messages}: { messages: UIMessage[]}) => {
return (
<ScrollArea data-scroller-priority='1' className="w-full h-[90%] max-w-4xl mx-auto">
{messages.map((message, i) => (
<Card.AnimatedCard scrollOnly={true} tlId='chat' position={i * 0.2} key={i}>
<Card.CardContent>
{message.role == 'assistant' && <AssistantMessage message={message} />}
{message.role == 'user' && <UserMessage message={message} />}
</Card.CardContent>
</Card.AnimatedCard>
))}
</ScrollArea>)
})
export default Messages;