@tanstack/cta-framework-react-cra 0.17.4 → 0.17.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,10 @@
1
- import { useEffect, useRef, useState } from 'react'
1
+ import { useState } from 'react'
2
2
 
3
3
  import { useChat, useMessages } from '@/hooks/demo.useChat'
4
4
 
5
5
  import Messages from './demo.messages'
6
6
 
7
7
  export default function ChatArea() {
8
- const messagesEndRef = useRef<HTMLDivElement>(null)
9
-
10
8
  const { sendMessage } = useChat()
11
9
 
12
10
  const messages = useMessages()
@@ -14,10 +12,6 @@ export default function ChatArea() {
14
12
  const [message, setMessage] = useState('')
15
13
  const [user, setUser] = useState('Alice')
16
14
 
17
- useEffect(() => {
18
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
19
- }, [messages])
20
-
21
15
  const postMessage = () => {
22
16
  if (message.trim().length) {
23
17
  sendMessage(message, user)
@@ -35,7 +29,6 @@ export default function ChatArea() {
35
29
  <>
36
30
  <div className="px-4 py-6 space-y-4">
37
31
  <Messages messages={messages} user={user} />
38
- <div ref={messagesEndRef} />
39
32
  </div>
40
33
 
41
34
  <div className="bg-white border-t border-gray-200 px-4 py-4">
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef } from 'react'
1
+ import { useEffect, useRef, useState } from 'react'
2
2
  import { useStore } from '@tanstack/react-store'
3
3
  import { Send, X } from 'lucide-react'
4
4
  import ReactMarkdown from 'react-markdown'
@@ -7,7 +7,7 @@ import rehypeSanitize from 'rehype-sanitize'
7
7
  import rehypeHighlight from 'rehype-highlight'
8
8
  import remarkGfm from 'remark-gfm'
9
9
  import { useChat } from '@ai-sdk/react'
10
- import { genAIResponse } from '../utils/demo.ai'
10
+ import { DefaultChatTransport } from 'ai'
11
11
 
12
12
  import { showAIAssistant } from '../store/example-assistant'
13
13
  import GuitarRecommendation from './example-GuitarRecommendation'
@@ -34,7 +34,7 @@ function Messages({ messages }: { messages: Array<UIMessage> }) {
34
34
 
35
35
  return (
36
36
  <div ref={messagesContainerRef} className="flex-1 overflow-y-auto">
37
- {messages.map(({ id, role, content, parts }) => (
37
+ {messages.map(({ id, role, parts }) => (
38
38
  <div
39
39
  key={id}
40
40
  className={`py-3 ${
@@ -43,45 +43,49 @@ function Messages({ messages }: { messages: Array<UIMessage> }) {
43
43
  : 'bg-transparent'
44
44
  }`}
45
45
  >
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>
51
72
  </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
+ />
55
85
  </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
+ })}
85
89
  </div>
86
90
  ))}
87
91
  </div>
@@ -90,22 +94,12 @@ function Messages({ messages }: { messages: Array<UIMessage> }) {
90
94
 
91
95
  export default function AIAssistant() {
92
96
  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
+ }),
108
101
  })
102
+ const [input, setInput] = useState('')
109
103
 
110
104
  return (
111
105
  <div className="relative z-50">
@@ -134,11 +128,17 @@ export default function AIAssistant() {
134
128
  <Messages messages={messages} />
135
129
 
136
130
  <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
+ >
138
138
  <div className="relative">
139
139
  <textarea
140
140
  value={input}
141
- onChange={handleInputChange}
141
+ onChange={(e) => setInput(e.target.value)}
142
142
  placeholder="Type your message..."
143
143
  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"
144
144
  rows={1}
@@ -152,7 +152,8 @@ export default function AIAssistant() {
152
152
  onKeyDown={(e) => {
153
153
  if (e.key === 'Enter' && !e.shiftKey) {
154
154
  e.preventDefault()
155
- handleSubmit(e)
155
+ sendMessage({ text: input })
156
+ setInput('')
156
157
  }
157
158
  }}
158
159
  />
@@ -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
+ })
@@ -1,5 +1,5 @@
1
+ import { useEffect, useRef, useState } from 'react'
1
2
  import { createFileRoute } from '@tanstack/react-router'
2
- import { useEffect, useRef } from 'react'
3
3
  import { Send } from 'lucide-react'
4
4
  import ReactMarkdown from 'react-markdown'
5
5
  import rehypeRaw from 'rehype-raw'
@@ -7,11 +7,12 @@ import rehypeSanitize from 'rehype-sanitize'
7
7
  import rehypeHighlight from 'rehype-highlight'
8
8
  import remarkGfm from 'remark-gfm'
9
9
  import { useChat } from '@ai-sdk/react'
10
-
11
- import { genAIResponse } from '../utils/demo.ai'
10
+ import { DefaultChatTransport } from 'ai'
12
11
 
13
12
  import type { UIMessage } from 'ai'
14
13
 
14
+ import GuitarRecommendation from '@/components/example-GuitarRecommendation'
15
+
15
16
  import '../demo.index.css'
16
17
 
17
18
  function InitalLayout({ children }: { children: React.ReactNode }) {
@@ -56,10 +57,10 @@ function Messages({ messages }: { messages: Array<UIMessage> }) {
56
57
  return (
57
58
  <div ref={messagesContainerRef} className="flex-1 overflow-y-auto pb-24">
58
59
  <div className="max-w-3xl mx-auto w-full px-4">
59
- {messages.map(({ id, role, content }) => (
60
+ {messages.map(({ id, role, parts }) => (
60
61
  <div
61
62
  key={id}
62
- className={`py-6 ${
63
+ className={`p-4 ${
63
64
  role === 'assistant'
64
65
  ? 'bg-gradient-to-r from-orange-500/5 to-red-600/5'
65
66
  : 'bg-transparent'
@@ -75,18 +76,39 @@ function Messages({ messages }: { messages: Array<UIMessage> }) {
75
76
  Y
76
77
  </div>
77
78
  )}
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
+ })}
90
112
  </div>
91
113
  </div>
92
114
  </div>
@@ -97,17 +119,12 @@ function Messages({ messages }: { messages: Array<UIMessage> }) {
97
119
  }
98
120
 
99
121
  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
+ }),
110
126
  })
127
+ const [input, setInput] = useState('')
111
128
 
112
129
  const Layout = messages.length ? ChattingLayout : InitalLayout
113
130
 
@@ -117,11 +134,17 @@ function ChatPage() {
117
134
  <Messages messages={messages} />
118
135
 
119
136
  <Layout>
120
- <form onSubmit={handleSubmit}>
137
+ <form
138
+ onSubmit={(e) => {
139
+ e.preventDefault()
140
+ sendMessage({ text: input })
141
+ setInput('')
142
+ }}
143
+ >
121
144
  <div className="relative max-w-xl mx-auto">
122
145
  <textarea
123
146
  value={input}
124
- onChange={handleInputChange}
147
+ onChange={(e) => setInput(e.target.value)}
125
148
  placeholder="Type something clever (or don't, we won't judge)..."
126
149
  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"
127
150
  rows={1}
@@ -135,7 +158,8 @@ function ChatPage() {
135
158
  onKeyDown={(e) => {
136
159
  if (e.key === 'Enter' && !e.shiftKey) {
137
160
  e.preventDefault()
138
- handleSubmit(e)
161
+ sendMessage({ text: input })
162
+ setInput('')
139
163
  }
140
164
  }}
141
165
  />
@@ -1,6 +1,7 @@
1
1
  import { experimental_createMCPClient, tool } from 'ai'
2
2
  //import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
3
  import { z } from 'zod'
4
+
4
5
  import guitars from '../data/example-guitars'
5
6
 
6
7
  // Example of using an SSE MCP server
@@ -24,7 +25,7 @@ import guitars from '../data/example-guitars'
24
25
 
25
26
  const getGuitars = tool({
26
27
  description: 'Get all products from the database',
27
- parameters: z.object({}),
28
+ inputSchema: z.object({}),
28
29
  execute: async () => {
29
30
  return Promise.resolve(guitars)
30
31
  },
@@ -32,9 +33,14 @@ const getGuitars = tool({
32
33
 
33
34
  const recommendGuitar = tool({
34
35
  description: 'Use this tool to recommend a guitar to the user',
35
- parameters: z.object({
36
+ inputSchema: z.object({
36
37
  id: z.string().describe('The id of the guitar to recommend'),
37
38
  }),
39
+ execute: async ({ id }) => {
40
+ return {
41
+ id,
42
+ }
43
+ },
38
44
  })
39
45
 
40
46
  export default async function getTools() {
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "dependencies": {
3
- "@ai-sdk/anthropic": "^1.1.17",
4
- "@ai-sdk/react": "^1.1.23",
3
+ "@ai-sdk/anthropic": "^2.0.1",
4
+ "@ai-sdk/react": "^2.0.8",
5
5
  "@modelcontextprotocol/sdk": "^1.8.0",
6
- "ai": "^4.1.65",
6
+ "ai": "^5.0.8",
7
7
  "highlight.js": "^11.11.1",
8
8
  "react-markdown": "^9.0.1",
9
9
  "rehype-highlight": "^7.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cta-framework-react-cra",
3
- "version": "0.17.4",
3
+ "version": "0.17.5",
4
4
  "description": "CTA Framework for React (Create React App)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "author": "Jack Herrington <jherr@pobox.com>",
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
- "@tanstack/cta-engine": "0.17.4"
26
+ "@tanstack/cta-engine": "0.17.5"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.13.4",
@@ -1,62 +0,0 @@
1
- import { createServerFn } from '@tanstack/react-start'
2
- import { anthropic } from '@ai-sdk/anthropic'
3
- import { streamText } from 'ai'
4
-
5
- import getTools from './demo.tools'
6
-
7
- export interface Message {
8
- id: string
9
- role: 'user' | 'assistant'
10
- content: string
11
- }
12
-
13
- const SYSTEM_PROMPT = `You are a helpful assistant for a store that sells guitars.
14
-
15
- You can use the following tools to help the user:
16
-
17
- - getGuitars: Get all guitars from the database
18
- - recommendGuitar: Recommend a guitar to the user
19
- `
20
-
21
- export const genAIResponse = createServerFn({ method: 'POST', response: 'raw' })
22
- .validator(
23
- (d: {
24
- messages: Array<Message>
25
- systemPrompt?: { value: string; enabled: boolean }
26
- }) => d,
27
- )
28
- .handler(async ({ data }) => {
29
- const messages = data.messages
30
- .filter(
31
- (msg) =>
32
- msg.content.trim() !== '' &&
33
- !msg.content.startsWith('Sorry, I encountered an error'),
34
- )
35
- .map((msg) => ({
36
- role: msg.role,
37
- content: msg.content.trim(),
38
- }))
39
-
40
- const tools = await getTools()
41
-
42
- try {
43
- const result = streamText({
44
- model: anthropic('claude-3-5-sonnet-latest'),
45
- messages,
46
- system: SYSTEM_PROMPT,
47
- maxSteps: 10,
48
- tools,
49
- })
50
-
51
- return result.toDataStreamResponse()
52
- } catch (error) {
53
- console.error('Error in genAIResponse:', error)
54
- if (error instanceof Error && error.message.includes('rate limit')) {
55
- return { error: 'Rate limit exceeded. Please try again in a moment.' }
56
- }
57
- return {
58
- error:
59
- error instanceof Error ? error.message : 'Failed to get AI response',
60
- }
61
- }
62
- })