-
-
Notifications
You must be signed in to change notification settings - Fork 193
feat(ai-client,ai-react): add fetcher option to ChatClient/useChat
#512
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
31c0058
bb488ea
8efd867
8f455c5
6a7d2f7
ebec591
ced9669
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| --- | ||
| '@tanstack/ai-client': minor | ||
| '@tanstack/ai-react': minor | ||
| '@tanstack/ai-preact': patch | ||
| '@tanstack/ai-solid': patch | ||
| '@tanstack/ai-svelte': patch | ||
| '@tanstack/ai-vue': patch | ||
| --- | ||
|
|
||
| Add a `fetcher` option to `ChatClient` and the framework chat hooks | ||
| (`useChat` / `createChat`), mirroring the `fetcher` option on the | ||
| generation hooks. Pass either `connection` or `fetcher` β the XOR is | ||
| enforced at the type level via `ChatTransport`. | ||
|
|
||
| ```ts | ||
| useChat({ | ||
| fetcher: ({ messages }, { signal }) => chatFn({ data: { messages }, signal }), | ||
| }) | ||
| ``` | ||
|
|
||
| The fetcher may return either a `Response` (parsed as SSE) or an | ||
| `AsyncIterable<StreamChunk>` (yielded directly). `stream()`, | ||
| `fetchServerSentEvents`, `fetchHttpStream`, and `rpcStream` are unchanged. |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,101 @@ | ||||||||||||||
| import { useState } from 'react' | ||||||||||||||
| import { createFileRoute } from '@tanstack/react-router' | ||||||||||||||
| import { 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, | ||||||||||||||
| }) | ||||||||||||||
|
|
||||||||||||||
| function ServerFnChat() { | ||||||||||||||
| const { messages, sendMessage, isLoading, error, stop } = useChat({ | ||||||||||||||
| fetcher: ({ messages }, { signal }) => | ||||||||||||||
| chatFn({ data: { messages }, signal }), | ||||||||||||||
| }) | ||||||||||||||
| const [input, setInput] = useState('') | ||||||||||||||
|
|
||||||||||||||
| const handleSubmit = (e: React.FormEvent) => { | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§© Analysis chainπ Script executed: #!/bin/bash
set -euo pipefail
echo "Checking TS config for UMD global access + JSX mode..."
fd -i 'tsconfig*.json' | while read -r f; do
echo "### $f"
cat "$f" | jq '.compilerOptions | {jsx, allowUmdGlobalAccess, types}'
done
echo
echo "Searching for React namespace type usage in TSX files..."
rg -nP --type=tsx '\bReact\.[A-Za-z_]+' -C1Repository: TanStack/ai Length of output: 1771 π Script executed: # Attempt to examine the file and TypeScript config
# First, let's check if the repository is available and explore the file structure
# Check for the file in question
git ls-files | grep -E "(server-fn-chat|tsconfig)" | head -20Repository: TanStack/ai Length of output: 931 π Script executed: # If the file exists, read its contents
if git ls-files | grep -q "examples/ts-react-chat/src/routes/server-fn-chat.tsx"; then
echo "=== File: server-fn-chat.tsx (lines 1-50) ==="
head -50 "examples/ts-react-chat/src/routes/server-fn-chat.tsx"
echo ""
echo "=== Checking imports and line 18 context ==="
sed -n '1,30p' "examples/ts-react-chat/src/routes/server-fn-chat.tsx" | cat -n
fiRepository: TanStack/ai Length of output: 3156 π Script executed: # Check tsconfig files
echo "=== Looking for tsconfig files ==="
find . -name "tsconfig*.json" -type f | head -10 | while read f; do
echo "### $f"
head -30 "$f"
echo ""
doneRepository: TanStack/ai Length of output: 5905
Line 18 uses π€ Prompt for AI Agents |
||||||||||||||
| 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"> | ||||||||||||||
| useChat({ fetcher: ({'{'}messages{'}'}, {'{'}signal{'}'}) => | ||||||||||||||
| chatFn({'{'} data, signal {'}'}) }) | ||||||||||||||
| </code>{' '} | ||||||||||||||
|
Comment on lines
+31
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Displayed fetcher snippet uses the wrong The rendered example shows βοΈ Suggested text fix- chatFn({'{'} data, signal {'}'}) })
+ chatFn({'{'} data: {'{'} messages {'}'}, signal {'}'}) })π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||
| β the server function returns an SSE{' '} | ||||||||||||||
| <code className="text-cyan-400">Response</code>; the chat client | ||||||||||||||
| 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> | ||||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restore runtime validation for
chatFn.This handler currently accepts unchecked input: the identity
inputValidatorlets malformed payloads through, anddata.messages as anyhides that at the call site. Please switch this to a real schema so bad requests fail before reachingchat().Suggested fix
export const chatFn = createServerFn({ method: 'POST' }) .inputValidator( - (data: { messages: Array<UIMessage>; data?: Record<string, any> }) => data, + z.object({ + messages: z.array(z.any()), + data: z.record(z.unknown()).optional(), + }), )π€ Prompt for AI Agents