-
-
Notifications
You must be signed in to change notification settings - Fork 219
Expand file tree
/
Copy pathserver-fn-chat.tsx
More file actions
110 lines (104 loc) · 3.67 KB
/
server-fn-chat.tsx
File metadata and controls
110 lines (104 loc) · 3.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import { useState } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { stream, useChat } from '@tanstack/ai-react'
import { Send, Square } from 'lucide-react'
import { chatFn } from '@/lib/server-fns'
export const Route = createFileRoute('/server-fn-chat')({
component: ServerFnChat,
})
/**
* Demonstrates wiring `useChat` to a TanStack Start server function.
*
* The server function (`chatFn` in `lib/server-fns.ts`) returns
* `toServerSentEventsResponse(chat({ ... }))` — an SSE Response. The
* `stream()` connection adapter awaits the server function, detects the
* Response, and parses SSE chunks into the chat client.
*
* Compare to `routes/index.tsx`, which uses `fetchServerSentEvents('/api/...')`
* against an HTTP route handler. Same wire format; different invocation style.
*/
function ServerFnChat() {
const { messages, sendMessage, isLoading, error, stop } = useChat({
connection: stream((messages) => chatFn({ data: { messages } })),
})
const [input, setInput] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!input.trim() || isLoading) return
void sendMessage(input)
setInput('')
}
return (
<div className="flex flex-col h-[calc(100vh-72px)] bg-gray-950 text-gray-100">
<div className="border-b border-gray-800 bg-gray-900/60 px-4 py-3">
<h2 className="text-lg font-semibold">Chat via server function</h2>
<p className="text-xs text-gray-400 mt-1">
<code className="text-cyan-400">
stream(() => chatFn({ data }))
</code>{' '}
— the server function returns an SSE{' '}
<code className="text-cyan-400">Response</code>; the adapter parses
it.
</p>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{messages.length === 0 && (
<p className="text-gray-500 text-sm">
Say something to start the chat.
</p>
)}
{messages.map((m) => (
<div
key={m.id}
className={`max-w-2xl rounded-lg px-3 py-2 ${
m.role === 'user'
? 'ml-auto bg-cyan-700/40 border border-cyan-600/40'
: 'mr-auto bg-gray-800 border border-gray-700'
}`}
>
{m.parts.map((part, i) =>
part.type === 'text' ? <span key={i}>{part.content}</span> : null,
)}
</div>
))}
{error && (
<div className="rounded-lg border border-red-700/60 bg-red-900/30 px-3 py-2 text-sm text-red-200">
{error.message}
</div>
)}
</div>
<form
onSubmit={handleSubmit}
className="border-t border-gray-800 bg-gray-900/80 p-3 flex gap-2"
>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Message..."
disabled={isLoading}
className="flex-1 rounded-lg bg-gray-800 border border-gray-700 px-3 py-2 text-sm focus:outline-none focus:border-cyan-500"
/>
{isLoading ? (
<button
type="button"
onClick={stop}
className="px-3 py-2 rounded-lg bg-red-600 hover:bg-red-700 text-white"
aria-label="Stop"
>
<Square size={18} />
</button>
) : (
<button
type="submit"
disabled={!input.trim()}
className="px-3 py-2 rounded-lg bg-cyan-600 hover:bg-cyan-700 disabled:opacity-50 text-white"
aria-label="Send"
>
<Send size={18} />
</button>
)}
</form>
</div>
)
}