Threads and Input
Manages conversations, suggestions, voice input, and image attachments.
Quick Start
import { useTambo, useTamboThreadInput } from "@tambo-ai/react";
const { thread, messages, isIdle } = useTambo(); const { value, setValue, submit } = useTamboThreadInput();
await submit(); // sends current input value
Thread Management
Access and manage the current thread using useTambo() and useTamboThreadInput() :
import { useTambo, useTamboThreadInput, ComponentRenderer, } from "@tambo-ai/react";
function Chat() { const { thread, // Current thread state messages, // Messages with computed properties isIdle, // True when not generating isStreaming, // True when streaming response isWaiting, // True when waiting for server currentThreadId, // Active thread ID switchThread, // Switch to different thread startNewThread, // Create new thread, returns ID cancelRun, // Cancel active generation } = useTambo();
const { value, // Current input value setValue, // Update input submit, // Send message isPending, // Submission in progress images, // Staged image files addImage, // Add single image removeImage, // Remove image by ID } = useTamboThreadInput();
const handleSend = async () => { await submit(); };
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>
{msg.content.map((block) => {
switch (block.type) {
case "text":
return <p key={${msg.id}:text}>{block.text}</p>;
case "component":
return (
<ComponentRenderer
key={block.id}
content={block}
threadId={currentThreadId}
messageId={msg.id}
/>
);
case "tool_use":
return (
<div key={block.id}>
{block.statusMessage ?? Running ${block.name}...}
</div>
);
default:
return null;
}
})}
</div>
))}
<input value={value} onChange={(e) => setValue(e.target.value)} />
<button onClick={handleSend} disabled={!isIdle || isPending}>
Send
</button>
</div>
);
}
Streaming State
Property Type Description
isIdle
boolean
Not generating
isWaiting
boolean
Waiting for server response
isStreaming
boolean
Actively streaming response
The streamingState object provides additional detail:
const { streamingState } = useTambo(); // streamingState.status: "idle" | "waiting" | "streaming" // streamingState.runId: current run ID // streamingState.error: { message, code } if error occurred
Content Block Types
Messages contain an array of content blocks. Handle each type:
Type Description Key Fields
text
Plain text text
component
AI-generated component id , name , props
tool_use
Tool invocation id , name , input
tool_result
Tool response toolUseId , content
resource
MCP resource uri , name , text
Submit Options
const { submit } = useTamboThreadInput();
await submit({ toolChoice: "auto", // "auto" | "required" | "none" | { name: "toolName" } debug: true, // Enable debug logging for the stream });
Fetching a Thread by ID
To fetch a specific thread (e.g., for a detail view), use useTamboThread(threadId) :
import { useTamboThread } from "@tambo-ai/react";
function ThreadView({ threadId }: { threadId: string }) { const { data: thread, isLoading, isError } = useTamboThread(threadId);
if (isLoading) return <Skeleton />; if (isError) return <div>Failed to load thread</div>;
return <div>{thread.name}</div>; }
This is a React Query hook - use it for read-only thread fetching, not for the active conversation.
Thread List
Manage multiple conversations:
import { useTambo, useTamboThreadList } from "@tambo-ai/react";
function ThreadSidebar() { const { data, isLoading } = useTamboThreadList(); const { currentThreadId, switchThread, startNewThread } = useTambo();
if (isLoading) return <Skeleton />;
return ( <div> <button onClick={() => startNewThread()}>New Thread</button> <ul> {data?.threads.map((t) => ( <li key={t.id}> <button onClick={() => switchThread(t.id)} className={currentThreadId === t.id ? "active" : ""} > {t.name || "Untitled"} </button> </li> ))} </ul> </div> ); }
Thread List Options
const { data } = useTamboThreadList({ userKey: "user_123", // Filter by user (defaults to provider's userKey) limit: 20, // Max results cursor: nextCursor, // Pagination cursor });
// data.threads: TamboThread[] // data.hasMore: boolean // data.nextCursor: string
Suggestions
AI-generated follow-up suggestions after each assistant message:
import { useTamboSuggestions } from "@tambo-ai/react";
function Suggestions() { const { suggestions, isLoading, accept, isAccepting } = useTamboSuggestions({ maxSuggestions: 3, // 1-10, default 3 autoGenerate: true, // Auto-generate after assistant message });
if (isLoading) return <Skeleton />;
return ( <div className="suggestions"> {suggestions.map((s) => ( <button key={s.id} onClick={() => accept({ suggestion: s })} disabled={isAccepting} > {s.title} </button> ))} </div> ); }
Auto-Submit Suggestion
// Accept and immediately submit as a message accept({ suggestion: s, shouldSubmit: true });
Manual Generation
const { generate, isGenerating } = useTamboSuggestions({ autoGenerate: false, // Disable auto-generation });
<button onClick={() => generate()} disabled={isGenerating}> Get suggestions </button>;
Voice Input
Speech-to-text transcription:
import { useTamboVoice } from "@tambo-ai/react";
function VoiceButton() { const { startRecording, stopRecording, isRecording, isTranscribing, transcript, transcriptionError, mediaAccessError, } = useTamboVoice();
return ( <div> <button onClick={isRecording ? stopRecording : startRecording}> {isRecording ? "Stop" : "Record"} </button> {isTranscribing && <span>Transcribing...</span>} {transcript && <p>{transcript}</p>} {transcriptionError && <p className="error">{transcriptionError}</p>} </div> ); }
Voice Hook Returns
Property Type Description
startRecording
() => void
Start recording, reset transcript
stopRecording
() => void
Stop and start transcription
isRecording
boolean
Currently recording
isTranscribing
boolean
Processing audio
transcript
string | null
Transcribed text
transcriptionError
string | null
Transcription error
mediaAccessError
string | null
Mic access error
Image Attachments
Images are managed via useTamboThreadInput() :
import { useTamboThreadInput } from "@tambo-ai/react";
function ImageInput() { const { images, addImage, addImages, removeImage, clearImages } = useTamboThreadInput();
const handleFiles = async (files: FileList) => { await addImages(Array.from(files)); };
return ( <div> <input type="file" accept="image/*" multiple onChange={(e) => handleFiles(e.target.files!)} /> {images.map((img) => ( <div key={img.id}> <img src={img.dataUrl} alt={img.name} /> <button onClick={() => removeImage(img.id)}>Remove</button> </div> ))} </div> ); }
StagedImage Properties
Property Type Description
id
string
Unique image ID
name
string
File name
dataUrl
string
Base64 data URL
file
File
Original File object
size
number
File size in bytes
type
string
MIME type
User Authentication
Enable per-user thread isolation:
import { TamboProvider } from "@tambo-ai/react";
function App() { return ( <TamboProvider apiKey={apiKey} userKey="user_123" // Simple user identifier > <Chat /> </TamboProvider> ); }
For OAuth-based auth, use userToken instead:
function App() { const userToken = useUserToken(); // From your auth provider
return ( <TamboProvider apiKey={apiKey} userToken={userToken}> <Chat /> </TamboProvider> ); }
Use userKey for simple user identification or userToken for OAuth JWT tokens. Don't use both.