Skip to content

Commit 793475d

Browse files
committed
fix: upgrading tanchat to AI SDK v5
1 parent 1121c2c commit 793475d

File tree

8 files changed

+169
-164
lines changed

8 files changed

+169
-164
lines changed

frameworks/react-cra/add-ons/db/assets/src/components/demo.chat-area.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
1-
import { useEffect, useRef, useState } from 'react'
1+
import { useState } from 'react'
22

33
import { useChat, useMessages } from '@/hooks/demo.useChat'
44

55
import Messages from './demo.messages'
66

77
export default function ChatArea() {
8-
const messagesEndRef = useRef<HTMLDivElement>(null)
9-
108
const { sendMessage } = useChat()
119

1210
const messages = useMessages()
1311

1412
const [message, setMessage] = useState('')
1513
const [user, setUser] = useState('Alice')
1614

17-
useEffect(() => {
18-
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
19-
}, [messages])
20-
2115
const postMessage = () => {
2216
if (message.trim().length) {
2317
sendMessage(message, user)
@@ -35,7 +29,6 @@ export default function ChatArea() {
3529
<>
3630
<div className="px-4 py-6 space-y-4">
3731
<Messages messages={messages} user={user} />
38-
<div ref={messagesEndRef} />
3932
</div>
4033

4134
<div className="bg-white border-t border-gray-200 px-4 py-4">

frameworks/react-cra/examples/tanchat/assets/src/components/example-AIAssistant.tsx

Lines changed: 59 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef } from 'react'
1+
import { useEffect, useRef, useState } from 'react'
22
import { useStore } from '@tanstack/react-store'
33
import { Send, X } from 'lucide-react'
44
import ReactMarkdown from 'react-markdown'
@@ -7,7 +7,7 @@ import rehypeSanitize from 'rehype-sanitize'
77
import rehypeHighlight from 'rehype-highlight'
88
import remarkGfm from 'remark-gfm'
99
import { useChat } from '@ai-sdk/react'
10-
import { genAIResponse } from '../utils/demo.ai'
10+
import { DefaultChatTransport } from 'ai'
1111

1212
import { showAIAssistant } from '../store/example-assistant'
1313
import GuitarRecommendation from './example-GuitarRecommendation'
@@ -34,7 +34,7 @@ function Messages({ messages }: { messages: Array<UIMessage> }) {
3434

3535
return (
3636
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto">
37-
{messages.map(({ id, role, content, parts }) => (
37+
{messages.map(({ id, role, parts }) => (
3838
<div
3939
key={id}
4040
className={`py-3 ${
@@ -43,45 +43,49 @@ function Messages({ messages }: { messages: Array<UIMessage> }) {
4343
: 'bg-transparent'
4444
}`}
4545
>
46-
{content.length > 0 && (
47-
<div className="flex items-start gap-2 px-4">
48-
{role === 'assistant' ? (
49-
<div className="w-6 h-6 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 flex items-center justify-center text-xs font-medium text-white flex-shrink-0">
50-
AI
46+
{parts.map((part) => {
47+
if (part.type === 'text') {
48+
return (
49+
<div className="flex items-start gap-2 px-4">
50+
{role === 'assistant' ? (
51+
<div className="w-6 h-6 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 flex items-center justify-center text-xs font-medium text-white flex-shrink-0">
52+
AI
53+
</div>
54+
) : (
55+
<div className="w-6 h-6 rounded-lg bg-gray-700 flex items-center justify-center text-xs font-medium text-white flex-shrink-0">
56+
Y
57+
</div>
58+
)}
59+
<div className="flex-1 min-w-0">
60+
<ReactMarkdown
61+
className="prose dark:prose-invert max-w-none prose-sm"
62+
rehypePlugins={[
63+
rehypeRaw,
64+
rehypeSanitize,
65+
rehypeHighlight,
66+
remarkGfm,
67+
]}
68+
>
69+
{part.text}
70+
</ReactMarkdown>
71+
</div>
5172
</div>
52-
) : (
53-
<div className="w-6 h-6 rounded-lg bg-gray-700 flex items-center justify-center text-xs font-medium text-white flex-shrink-0">
54-
Y
73+
)
74+
}
75+
if (
76+
part.type === 'tool-recommendGuitar' &&
77+
part.state === 'output-available' &&
78+
(part.output as { id: string })?.id
79+
) {
80+
return (
81+
<div key={id} className="max-w-[80%] mx-auto">
82+
<GuitarRecommendation
83+
id={(part.output as { id: string })?.id}
84+
/>
5585
</div>
56-
)}
57-
<div className="flex-1 min-w-0">
58-
<ReactMarkdown
59-
className="prose dark:prose-invert max-w-none prose-sm"
60-
rehypePlugins={[
61-
rehypeRaw,
62-
rehypeSanitize,
63-
rehypeHighlight,
64-
remarkGfm,
65-
]}
66-
>
67-
{content}
68-
</ReactMarkdown>
69-
</div>
70-
</div>
71-
)}
72-
{parts
73-
.filter((part) => part.type === 'tool-invocation')
74-
.filter(
75-
(part) => part.toolInvocation.toolName === 'recommendGuitar',
76-
)
77-
.map((toolCall) => (
78-
<div
79-
key={toolCall.toolInvocation.toolName}
80-
className="max-w-[80%] mx-auto"
81-
>
82-
<GuitarRecommendation id={toolCall.toolInvocation.args.id} />
83-
</div>
84-
))}
86+
)
87+
}
88+
})}
8589
</div>
8690
))}
8791
</div>
@@ -90,22 +94,12 @@ function Messages({ messages }: { messages: Array<UIMessage> }) {
9094

9195
export default function AIAssistant() {
9296
const isOpen = useStore(showAIAssistant)
93-
const { messages, input, handleInputChange, handleSubmit } = useChat({
94-
initialMessages: [],
95-
fetch: (_url, options) => {
96-
const { messages } = JSON.parse(options!.body! as string)
97-
return genAIResponse({
98-
data: {
99-
messages,
100-
},
101-
})
102-
},
103-
onToolCall: (call) => {
104-
if (call.toolCall.toolName === 'recommendGuitar') {
105-
return 'Handled by the UI'
106-
}
107-
},
97+
const { messages, sendMessage } = useChat({
98+
transport: new DefaultChatTransport({
99+
api: '/api/demo-chat',
100+
}),
108101
})
102+
const [input, setInput] = useState('')
109103

110104
return (
111105
<div className="relative z-50">
@@ -134,11 +128,17 @@ export default function AIAssistant() {
134128
<Messages messages={messages} />
135129

136130
<div className="p-3 border-t border-orange-500/20">
137-
<form onSubmit={handleSubmit}>
131+
<form
132+
onSubmit={(e) => {
133+
e.preventDefault()
134+
sendMessage({ text: input })
135+
setInput('')
136+
}}
137+
>
138138
<div className="relative">
139139
<textarea
140140
value={input}
141-
onChange={handleInputChange}
141+
onChange={(e) => setInput(e.target.value)}
142142
placeholder="Type your message..."
143143
className="w-full rounded-lg border border-orange-500/20 bg-gray-800/50 pl-3 pr-10 py-2 text-sm text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500/50 focus:border-transparent resize-none overflow-hidden"
144144
rows={1}
@@ -152,7 +152,8 @@ export default function AIAssistant() {
152152
onKeyDown={(e) => {
153153
if (e.key === 'Enter' && !e.shiftKey) {
154154
e.preventDefault()
155-
handleSubmit(e)
155+
sendMessage({ text: input })
156+
setInput('')
156157
}
157158
}}
158159
/>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { createServerFileRoute } from '@tanstack/react-start/server'
2+
import { anthropic } from '@ai-sdk/anthropic'
3+
import { convertToModelMessages, stepCountIs, streamText } from 'ai'
4+
5+
import getTools from '@/utils/demo.tools'
6+
7+
const SYSTEM_PROMPT = `You are a helpful assistant for a store that sells guitars.
8+
9+
You can use the following tools to help the user:
10+
11+
- getGuitars: Get all guitars from the database
12+
- recommendGuitar: Recommend a guitar to the user
13+
`
14+
15+
export const ServerRoute = createServerFileRoute('/api/demo-chat').methods({
16+
POST: async ({ request }) => {
17+
try {
18+
const { messages } = await request.json()
19+
20+
const tools = await getTools()
21+
22+
const result = await streamText({
23+
model: anthropic('claude-3-5-sonnet-latest'),
24+
messages: convertToModelMessages(messages),
25+
temperature: 0.7,
26+
stopWhen: stepCountIs(5),
27+
system: SYSTEM_PROMPT,
28+
tools,
29+
})
30+
31+
return result.toUIMessageStreamResponse()
32+
} catch (error) {
33+
console.error('Chat API error:', error)
34+
return new Response(
35+
JSON.stringify({ error: 'Failed to process chat request' }),
36+
{
37+
status: 500,
38+
headers: { 'Content-Type': 'application/json' },
39+
},
40+
)
41+
}
42+
},
43+
})

frameworks/react-cra/examples/tanchat/assets/src/routes/example.chat.tsx

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1+
import { useEffect, useRef, useState } from 'react'
12
import { createFileRoute } from '@tanstack/react-router'
2-
import { useEffect, useRef } from 'react'
33
import { Send } from 'lucide-react'
44
import ReactMarkdown from 'react-markdown'
55
import rehypeRaw from 'rehype-raw'
66
import rehypeSanitize from 'rehype-sanitize'
77
import rehypeHighlight from 'rehype-highlight'
88
import remarkGfm from 'remark-gfm'
99
import { useChat } from '@ai-sdk/react'
10-
11-
import { genAIResponse } from '../utils/demo.ai'
10+
import { DefaultChatTransport } from 'ai'
1211

1312
import type { UIMessage } from 'ai'
1413

14+
import GuitarRecommendation from '@/components/example-GuitarRecommendation'
15+
1516
import '../demo.index.css'
1617

1718
function InitalLayout({ children }: { children: React.ReactNode }) {
@@ -56,10 +57,10 @@ function Messages({ messages }: { messages: Array<UIMessage> }) {
5657
return (
5758
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto pb-24">
5859
<div className="max-w-3xl mx-auto w-full px-4">
59-
{messages.map(({ id, role, content }) => (
60+
{messages.map(({ id, role, parts }) => (
6061
<div
6162
key={id}
62-
className={`py-6 ${
63+
className={`p-4 ${
6364
role === 'assistant'
6465
? 'bg-gradient-to-r from-orange-500/5 to-red-600/5'
6566
: 'bg-transparent'
@@ -75,18 +76,39 @@ function Messages({ messages }: { messages: Array<UIMessage> }) {
7576
Y
7677
</div>
7778
)}
78-
<div className="flex-1 min-w-0">
79-
<ReactMarkdown
80-
className="prose dark:prose-invert max-w-none"
81-
rehypePlugins={[
82-
rehypeRaw,
83-
rehypeSanitize,
84-
rehypeHighlight,
85-
remarkGfm,
86-
]}
87-
>
88-
{content}
89-
</ReactMarkdown>
79+
<div className="flex-1">
80+
{parts.map((part, index) => {
81+
if (part.type === 'text') {
82+
return (
83+
<div className="flex-1 min-w-0" key={index}>
84+
<ReactMarkdown
85+
className="prose dark:prose-invert max-w-none"
86+
rehypePlugins={[
87+
rehypeRaw,
88+
rehypeSanitize,
89+
rehypeHighlight,
90+
remarkGfm,
91+
]}
92+
>
93+
{part.text}
94+
</ReactMarkdown>
95+
</div>
96+
)
97+
}
98+
if (
99+
part.type === 'tool-recommendGuitar' &&
100+
part.state === 'output-available' &&
101+
(part.output as { id: string })?.id
102+
) {
103+
return (
104+
<div key={index} className="max-w-[80%] mx-auto">
105+
<GuitarRecommendation
106+
id={(part.output as { id: string })?.id}
107+
/>
108+
</div>
109+
)
110+
}
111+
})}
90112
</div>
91113
</div>
92114
</div>
@@ -97,17 +119,12 @@ function Messages({ messages }: { messages: Array<UIMessage> }) {
97119
}
98120

99121
function ChatPage() {
100-
const { messages, input, handleInputChange, handleSubmit } = useChat({
101-
initialMessages: [],
102-
fetch: (_url, options) => {
103-
const { messages } = JSON.parse(options!.body! as string)
104-
return genAIResponse({
105-
data: {
106-
messages,
107-
},
108-
})
109-
},
122+
const { messages, sendMessage } = useChat({
123+
transport: new DefaultChatTransport({
124+
api: '/api/demo-chat',
125+
}),
110126
})
127+
const [input, setInput] = useState('')
111128

112129
const Layout = messages.length ? ChattingLayout : InitalLayout
113130

@@ -117,11 +134,17 @@ function ChatPage() {
117134
<Messages messages={messages} />
118135

119136
<Layout>
120-
<form onSubmit={handleSubmit}>
137+
<form
138+
onSubmit={(e) => {
139+
e.preventDefault()
140+
sendMessage({ text: input })
141+
setInput('')
142+
}}
143+
>
121144
<div className="relative max-w-xl mx-auto">
122145
<textarea
123146
value={input}
124-
onChange={handleInputChange}
147+
onChange={(e) => setInput(e.target.value)}
125148
placeholder="Type something clever (or don't, we won't judge)..."
126149
className="w-full rounded-lg border border-orange-500/20 bg-gray-800/50 pl-4 pr-12 py-3 text-sm text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500/50 focus:border-transparent resize-none overflow-hidden shadow-lg"
127150
rows={1}
@@ -135,7 +158,8 @@ function ChatPage() {
135158
onKeyDown={(e) => {
136159
if (e.key === 'Enter' && !e.shiftKey) {
137160
e.preventDefault()
138-
handleSubmit(e)
161+
sendMessage({ text: input })
162+
setInput('')
139163
}
140164
}}
141165
/>

0 commit comments

Comments
 (0)