163 lines
4.3 KiB
TypeScript
163 lines
4.3 KiB
TypeScript
'use client'
|
|
import { useRef, 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'
|
|
|
|
type 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, initialMessages }: ChatInterfaceProps) {
|
|
const [input, setInput] = useState('')
|
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
|
|
const { messages, sendMessage, status } = useChat({
|
|
transport: new DefaultChatTransport({
|
|
api: '/api/chat',
|
|
body: { sessionId },
|
|
}),
|
|
messages: toUIMessages(initialMessages),
|
|
})
|
|
|
|
const isLoading = status === 'submitted' || status === 'streaming'
|
|
|
|
useEffect(() => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
}, [messages])
|
|
|
|
const handleSend = () => {
|
|
const text = input.trim()
|
|
if (!text || isLoading) return
|
|
setInput('')
|
|
sendMessage({ text })
|
|
}
|
|
|
|
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) => (
|
|
<div
|
|
key={message.id}
|
|
className={cn('flex', message.role === 'user' ? 'justify-end' : 'justify-start')}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'max-w-[80%] rounded-lg px-4 py-2 text-sm space-y-2',
|
|
message.role === 'user' ? 'bg-primary text-primary-foreground' : 'bg-muted',
|
|
)}
|
|
>
|
|
{message.parts.map((part, i) => {
|
|
if (part.type === 'text') {
|
|
return (
|
|
<p key={i} className="whitespace-pre-wrap">
|
|
{part.text}
|
|
</p>
|
|
)
|
|
}
|
|
if (part.type === 'tool-scheduleMeeting') {
|
|
const toolPart = part as unknown as {
|
|
type: 'tool-scheduleMeeting'
|
|
state: string
|
|
input: unknown
|
|
output?: { success: boolean; message?: string; htmlLink?: string; error?: string }
|
|
}
|
|
if (toolPart.state === 'input-available' || toolPart.state === 'input-streaming') {
|
|
return (
|
|
<p key={i} className="text-xs opacity-70 italic">
|
|
Scheduling meeting…
|
|
</p>
|
|
)
|
|
}
|
|
if (toolPart.state === 'output-available' && toolPart.output) {
|
|
const result = toolPart.output
|
|
return (
|
|
<div key={i} className="text-xs mt-1 p-2 bg-background/20 rounded">
|
|
{result.success ? (
|
|
<span>
|
|
✓ {result.message}{' '}
|
|
{result.htmlLink && (
|
|
<a
|
|
href={result.htmlLink}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="underline"
|
|
>
|
|
View event
|
|
</a>
|
|
)}
|
|
</span>
|
|
) : (
|
|
<span>✗ {result.error}</span>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
return null
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{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>
|
|
|
|
<div className="p-4 border-t flex gap-2">
|
|
<Textarea
|
|
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()
|
|
}
|
|
}}
|
|
/>
|
|
<Button
|
|
onClick={handleSend}
|
|
disabled={isLoading || !input.trim()}
|
|
className="self-end"
|
|
>
|
|
Send
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|