Open
Conversation
…quirements; Voice and tone - Changed "After the user visits" to "After you visit" to maintain consistent use of "you" for the reader
…t paragraph to follow 10/20/70 format; Terminology - Changed "a MCP Server" to "an MCP server" and "MCP Server" to "MCP server" in headings and body text; Structure - Changed "seamless integration" to "integration" to remove marketing language
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
app/en/guides/tool-calling/custom-apps/get-tool-definitions/page.mdx
Outdated
Show resolved
Hide resolved
Comment on lines
-914
to
-1110
| import { createFileRoute } from "@tanstack/react-router"; | ||
| import { useChat, fetchServerSentEvents } from "@tanstack/ai-react"; | ||
| import { useState, useRef, useEffect } from "react"; | ||
| import ReactMarkdown from "react-markdown"; | ||
| import { checkAuthStatus } from "../functions/auth"; | ||
|
|
||
| function AuthPendingUI({ | ||
| authUrl, | ||
| toolName, | ||
| onAuthComplete, | ||
| }: { | ||
| authUrl: string; | ||
| toolName: string; | ||
| onAuthComplete: () => void; | ||
| }) { | ||
| const [status, setStatus] = useState<"initial" | "waiting" | "completed">( | ||
| "initial" | ||
| ); | ||
| const pollingRef = useRef<NodeJS.Timeout | null>(null); | ||
| const hasCompletedRef = useRef(false); | ||
| const onAuthCompleteRef = useRef(onAuthComplete); | ||
|
|
||
| useEffect(() => { | ||
| onAuthCompleteRef.current = onAuthComplete; | ||
| }, [onAuthComplete]); | ||
|
|
||
| useEffect(() => { | ||
| if (status !== "waiting" || !toolName || hasCompletedRef.current) return; | ||
|
|
||
| const pollStatus = async () => { | ||
| try { | ||
| const result = await checkAuthStatus({ data: { toolName } }); | ||
|
|
||
| if (result.status === "completed" && !hasCompletedRef.current) { | ||
| hasCompletedRef.current = true; | ||
| if (pollingRef.current) clearInterval(pollingRef.current); | ||
| setStatus("completed"); | ||
| setTimeout(() => onAuthCompleteRef.current(), 1500); | ||
| } | ||
| } catch (error) { | ||
| console.error("Polling error:", error); | ||
| } | ||
| }; | ||
|
|
||
| pollingRef.current = setInterval(pollStatus, 2000); | ||
| return () => { | ||
| if (pollingRef.current) clearInterval(pollingRef.current); | ||
| }; | ||
| }, [status, toolName]); | ||
|
|
||
| const displayName = toolName.split("_")[0] || toolName; | ||
|
|
||
| const handleAuthClick = () => { | ||
| if (!authUrl) return; | ||
| window.open(authUrl, "_blank"); | ||
| setStatus("waiting"); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg"> | ||
| {status === "completed" ? ( | ||
| <p className="text-green-600 font-medium"> | ||
| {displayName} authorized successfully | ||
| </p> | ||
| ) : !authUrl ? ( | ||
| <p className="text-red-600">Authorization URL not available</p> | ||
| ) : ( | ||
| <div className="flex items-center gap-3"> | ||
| <span>Authorize access to {displayName}?</span> | ||
| <button | ||
| onClick={handleAuthClick} | ||
| className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium" | ||
| > | ||
| {status === "waiting" ? "Retry" : "Authorize"} | ||
| </button> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function Chat() { | ||
| const [input, setInput] = useState(""); | ||
| const inputRef = useRef<HTMLInputElement>(null); | ||
|
|
||
| const { messages, sendMessage, reload, isLoading } = useChat({ | ||
| connection: fetchServerSentEvents("/api/chat"), | ||
| }); | ||
|
|
||
| const handleSubmit = (e: React.FormEvent) => { | ||
| e.preventDefault(); | ||
| if (input.trim() && !isLoading) { | ||
| sendMessage(input); | ||
| setInput(""); | ||
| } | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| if (!isLoading && inputRef.current) { | ||
| inputRef.current.focus(); | ||
| } | ||
| }, [isLoading]); | ||
|
|
||
| return ( | ||
| <div className="flex flex-col h-screen max-w-2xl mx-auto p-4"> | ||
| <h1 className="text-2xl font-bold mb-4">Arcade + TanStack AI Chat</h1> | ||
|
|
||
| <div className="flex-1 overflow-y-auto space-y-4 mb-4"> | ||
| {messages.map((message) => { | ||
| // Extract text content and tool results from parts | ||
| const textParts = (message.parts || []).filter( | ||
| (p: { type: string }) => p.type === "text" | ||
| ); | ||
| const toolResultParts = (message.parts || []).filter( | ||
| (p: { type: string }) => p.type === "tool-result" | ||
| ); | ||
| const textContent = textParts | ||
| .map((p: { content?: string }) => p.content || "") | ||
| .join(""); | ||
| const authRequired = toolResultParts.find( | ||
| (result: { output?: { authorization_required?: boolean } }) => | ||
| result.output?.authorization_required | ||
| ); | ||
|
|
||
| return ( | ||
| <div | ||
| key={message.id} | ||
| className={`p-4 rounded-lg ${ | ||
| message.role === "assistant" | ||
| ? "bg-gray-100" | ||
| : "bg-blue-100 ml-8" | ||
| }`} | ||
| > | ||
| <div className="font-semibold text-sm text-gray-500 mb-1"> | ||
| {message.role === "assistant" ? "Assistant" : "You"} | ||
| </div> | ||
|
|
||
| {authRequired ? ( | ||
| <AuthPendingUI | ||
| authUrl={ | ||
| ( | ||
| authRequired as { | ||
| output?: { authorization_response?: { url?: string } }; | ||
| } | ||
| )?.output?.authorization_response?.url || "" | ||
| } | ||
| toolName={ | ||
| (authRequired as { output?: { tool_name?: string } }) | ||
| ?.output?.tool_name || "" | ||
| } | ||
| onAuthComplete={() => reload()} | ||
| /> | ||
| ) : ( | ||
| <div className="whitespace-pre-wrap">{textContent}</div> | ||
| )} | ||
| </div> | ||
| ); | ||
| })} | ||
|
|
||
| {isLoading && ( | ||
| <div className="p-4 rounded-lg bg-gray-100"> | ||
| <div className="font-semibold text-sm text-gray-500 mb-1"> | ||
| Assistant | ||
| </div> | ||
| <div className="animate-pulse">Thinking...</div> | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
| <form onSubmit={handleSubmit} className="flex gap-2"> | ||
| <input | ||
| ref={inputRef} | ||
| type="text" | ||
| value={input} | ||
| onChange={(e) => setInput(e.target.value)} | ||
| placeholder="Ask about your emails or Slack..." | ||
| disabled={isLoading} | ||
| className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" | ||
| /> | ||
| <button | ||
| type="submit" | ||
| disabled={isLoading || !input.trim()} | ||
| className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" | ||
| > | ||
| Send | ||
| </button> | ||
| </form> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export const Route = createFileRoute("/")({ | ||
| component: Chat, | ||
| }); | ||
| ``` | ||
|
|
||
| </details> |
Contributor
There was a problem hiding this comment.
import { createFileRoute } from "@tanstack/react-router";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import { useState, useRef, useEffect } from "react";
import ReactMarkdown from "react-markdown";
import { checkAuthStatus } from "../functions/auth";
function AuthPendingUI({
authUrl,
toolName,
onAuthComplete,
}: {
authUrl: string;
toolName: string;
onAuthComplete: () => void;
}) {
const [status, setStatus] = useState<"initial" | "waiting" | "completed">(
"initial"
);
const pollingRef = useRef<NodeJS.Timeout | null>(null);
const hasCompletedRef = useRef(false);
const onAuthCompleteRef = useRef(onAuthComplete);
useEffect(() => {
onAuthCompleteRef.current = onAuthComplete;
}, [onAuthComplete]);
useEffect(() => {
if (status !== "waiting" || !toolName || hasCompletedRef.current) return;
const pollStatus = async () => {
try {
const result = await checkAuthStatus({ data: { toolName } });
if (result.status === "completed" && !hasCompletedRef.current) {
hasCompletedRef.current = true;
if (pollingRef.current) clearInterval(pollingRef.current);
setStatus("completed");
setTimeout(() => onAuthCompleteRef.current(), 1500);
}
} catch (error) {
console.error("Polling error:", error);
}
};
pollingRef.current = setInterval(pollStatus, 2000);
return () => {
if (pollingRef.current) clearInterval(pollingRef.current);
};
}, [status, toolName]);
const displayName = toolName.split("_")[0] || toolName;
const handleAuthClick = () => {
if (!authUrl) return;
window.open(authUrl, "_blank");
setStatus("waiting");
};
return (
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
{status === "completed" ? (
<p className="text-green-600 font-medium">
{displayName} authorized successfully
</p>
) : !authUrl ? (
<p className="text-red-600">Authorization URL not available</p>
) : (
<div className="flex items-center gap-3">
<span>Authorize access to {displayName}?</span>
<button
onClick={handleAuthClick}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium"
>
{status === "waiting" ? "Retry" : "Authorize"}
</button>
</div>
)}
</div>
);
}
function Chat() {
const [input, setInput] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const { messages, sendMessage, reload, isLoading } = useChat({
connection: fetchServerSentEvents("/api/chat"),
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (input.trim() && !isLoading) {
sendMessage(input);
setInput("");
}
};
useEffect(() => {
if (!isLoading && inputRef.current) {
inputRef.current.focus();
}
}, [isLoading]);
return (
<div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Arcade + TanStack AI Chat</h1>
<div className="flex-1 overflow-y-auto space-y-4 mb-4">
{messages.map((message) => {
// Extract text content and tool results from parts
const textParts = (message.parts || []).filter(
(p: { type: string }) => p.type === "text"
);
const toolResultParts = (message.parts || []).filter(
(p: { type: string }) => p.type === "tool-result"
);
const textContent = textParts
.map((p: { content?: string }) => p.content || "")
.join("");
const authRequired = toolResultParts.find(
(result: { output?: { authorization_required?: boolean } }) =>
result.output?.authorization_required
);
return (
<div
key={message.id}
className={`p-4 rounded-lg ${
message.role === "assistant"
? "bg-gray-100"
: "bg-blue-100 ml-8"
}`}
>
<div className="font-semibold text-sm text-gray-500 mb-1">
{message.role === "assistant" ? "Assistant" : "You"}
</div>
{authRequired ? (
<AuthPendingUI
authUrl={
(
authRequired as {
output?: { authorization_response?: { url?: string } };
}
)?.output?.authorization_response?.url || ""
}
toolName={
(authRequired as { output?: { tool_name?: string } })
?.output?.tool_name || ""
}
onAuthComplete={() => reload()}
/>
) : (
<div className="whitespace-pre-wrap">{textContent}</div>
)}
</div>
);
})}
{isLoading && (
<div className="p-4 rounded-lg bg-gray-100">
<div className="font-semibold text-sm text-gray-500 mb-1">
Assistant
</div>
<div className="animate-pulse">Thinking...</div>
</div>
)}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
ref={inputRef}
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask about your emails or Slack..."
disabled={isLoading}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Send
</button>
</form>
</div>
);
}
export const Route = createFileRoute("/")({
component: Chat,
});
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Editorial Review
This PR contains structural improvements to documentation changed in #810.
Changes
Context
These suggestions are based on our style guide and focus on document structure, not word-level fixes (which are handled by Vale).
Review each change carefully - accept what improves the docs, modify what needs adjustment, or close this PR if the changes aren't helpful.
Generated by editorial review using Claude