162 lines
4.2 KiB
TypeScript
162 lines
4.2 KiB
TypeScript
'use client'
|
|
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 {
|
|
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[]
|
|
}
|
|
|
|
function toUIMessages(dbMessages: DBMessage[]): UIMessage[] {
|
|
return dbMessages.map((m) => ({
|
|
id: m.id,
|
|
role: m.role,
|
|
parts: [{ type: 'text' as const, text: m.content }],
|
|
}))
|
|
}
|
|
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(() => {
|
|
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 || 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">
|
|
{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">
|
|
<span className="flex-1">
|
|
{error.message.includes('quota') || error.message.includes('429')
|
|
? 'OpenAI quota exceeded. Please try again later.'
|
|
: `Error: ${error.message}`}
|
|
</span>
|
|
<Button
|
|
type="button"
|
|
onClick={clearError}
|
|
className="shrink-0 opacity-60 hover:opacity-100"
|
|
variant='destructive'
|
|
>
|
|
<DeleteIcon />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<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…"
|
|
className="resize-none"
|
|
rows={2}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault()
|
|
handleSend()
|
|
}
|
|
}}
|
|
/>
|
|
<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>
|
|
)
|
|
}
|