ToothFairyAI TS Integration
Simple TypeScript guide to upload files and chat with AI agents using documents.
Prerequisites
const API_KEY = "your-api-key";
const WORKSPACE_ID = "your-workspace-id";
const AGENT_ID = "your-agent-id";
File Path Structure
All uploaded files follow this format:
{folder}/{workspace-id}/{timestamp}{filename}
Folder prefixes by type:
- Documents & Images:
imported_doc_files/ - Videos:
imported_video_files/ - Audios:
imported_audio_files/
Example:
imported_doc_files/6586b7e6-683e-4ee6-a6cf-24c19729b5ff/1760886623830contract.pdf
Step 1: Upload a File
TypeScript Function
interface UploadResult {
uploadURL: string;
filePath: string;
}
async function uploadFile(
file: File,
workspaceId: string,
apiKey: string
): Promise<string> {
// Generate timestamp
const timestamp = Date.now();
// Determine folder and import type based on file type
const fileType = file.type;
let folder: string;
let importType: string;
if (fileType.startsWith('image/')) {
folder = 'imported_doc_files';
importType = 'image';
} else if (fileType.startsWith('video/')) {
folder = 'imported_video_files';
importType = 'document';
} else if (fileType.startsWith('audio/')) {
folder = 'imported_audio_files';
importType = 'document';
} else if (fileType === 'application/pdf') {
folder = 'imported_doc_files';
importType = 'pdf';
} else if (fileType.includes('spreadsheet') || fileType.includes('excel')) {
folder = 'imported_doc_files';
importType = 'spreadsheet';
} else {
folder = 'imported_doc_files';
importType = 'document';
}
// Build filename with folder prefix
const filename = `${folder}/${workspaceId}/${timestamp}${file.name}`;
// Step 1: Get pre-signed URL
const params = new URLSearchParams({
filename,
importType,
contentType: fileType
});
const urlResponse = await fetch(
`https://api.toothfairyai.com/documents/requestPreSignedURL?${params}`,
{
method: 'GET',
headers: {
'x-api-key': apiKey,
'Content-Type': 'application/json'
}
}
);
const { uploadURL, filePath }: UploadResult = await urlResponse.json();
// Step 2: Upload file to S3
const fileBuffer = await file.arrayBuffer();
await fetch(uploadURL, {
method: 'PUT',
headers: {
'Content-Type': fileType
},
body: fileBuffer
});
// Step 3: Extract filename for chat
const chatFilename = filePath.replace(/^s3:\/\/[^/]+\//, '');
return chatFilename;
}
Usage Example
// Upload a PDF
const pdfFile = document.querySelector('input[type="file"]').files[0];
const uploadedPath = await uploadFile(pdfFile, WORKSPACE_ID, API_KEY);
console.log('Uploaded:', uploadedPath);
// Output: "imported_doc_files/6586b7e6-683e-4ee6-a6cf-24c19729b5ff/1760886623830contract.pdf"
Step 2: Chat with Uploaded Files
TypeScript Function
interface ChatMessage {
text: string;
role: 'user' | 'assistant';
files?: string[];
images?: string[];
videos?: string[];
audios?: string[];
}
interface ChatRequest {
workspaceid: string;
agentid: string;
chatid: string | null;
messages: ChatMessage[];
}
interface ChatResponse {
message?: string;
text?: string;
chatid?: string;
}
async function chatWithFiles(
message: string,
filePaths: string[],
workspaceId: string,
agentId: string,
apiKey: string,
chatId: string | null = null
): Promise<ChatResponse> {
// Categorize files by type
const files: string[] = [];
const images: string[] = [];
const videos: string[] = [];
const audios: string[] = [];
filePaths.forEach(path => {
if (path.startsWith('imported_video_files/')) {
videos.push(path);
} else if (path.startsWith('imported_audio_files/')) {
audios.push(path);
} else if (/\.(jpg|jpeg|png|gif|webp)$/i.test(path)) {
images.push(path);
} else {
files.push(path);
}
});
// Build message object
const chatMessage: ChatMessage = {
text: message,
role: 'user'
};
if (files.length > 0) chatMessage.files = files;
if (images.length > 0) chatMessage.images = images;
if (videos.length > 0) chatMessage.videos = videos;
if (audios.length > 0) chatMessage.audios = audios;
// Build request payload
const payload: ChatRequest = {
workspaceid: workspaceId,
agentid: agentId,
chatid: chatId,
messages: [chatMessage]
};
// Send request
const response = await fetch('https://ais.toothfairyai.com/agent', {
method: 'POST',
headers: {
'x-api-key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const result: ChatResponse = await response.json();
return result;
}
Usage Examples
Example 1: Chat with a PDF
const uploadedPdf = await uploadFile(pdfFile, WORKSPACE_ID, API_KEY);
const response = await chatWithFiles(
"Summarize this contract",
[uploadedPdf],
WORKSPACE_ID,
AGENT_ID,
API_KEY
);
console.log('AI Response:', response.message || response.text);
console.log('Chat ID:', response.chatid); // Save for follow-up questions
Example 2: Chat with an Image
const uploadedImage = await uploadFile(imageFile, WORKSPACE_ID, API_KEY);
const response = await chatWithFiles(
"What's in this image?",
[uploadedImage],
WORKSPACE_ID,
AGENT_ID,
API_KEY
);
console.log('AI Response:', response.message);
Example 3: Chat with Multiple Files
const pdfPath = await uploadFile(pdfFile, WORKSPACE_ID, API_KEY);
const imagePath = await uploadFile(imageFile, WORKSPACE_ID, API_KEY);
const response = await chatWithFiles(
"Compare the data in the PDF with the chart in the image",
[pdfPath, imagePath],
WORKSPACE_ID,
AGENT_ID,
API_KEY
);
console.log('AI Response:', response.message);
Example 4: Follow-up Questions (Multi-turn Conversation)
// First question
const response1 = await chatWithFiles(
"What is this document about?",
[uploadedPdf],
WORKSPACE_ID,
AGENT_ID,
API_KEY
);
const chatId = response1.chatid;
console.log('Answer 1:', response1.message);
// Follow-up question (use the chatId)
const response2 = await chatWithFiles(
"Can you elaborate on the payment terms?",
[uploadedPdf],
WORKSPACE_ID,
AGENT_ID,
API_KEY,
chatId // Pass chat ID to maintain context
);
console.log('Answer 2:', response2.message);
Complete Example: Upload and Chat
async function processDocument(file: File) {
try {
// Step 1: Upload file
console.log('Uploading file...');
const filePath = await uploadFile(file, WORKSPACE_ID, API_KEY);
console.log('✅ Uploaded:', filePath);
// Step 2: Ask first question
console.log('Asking first question...');
const response1 = await chatWithFiles(
"What is the main topic of this document?",
[filePath],
WORKSPACE_ID,
AGENT_ID,
API_KEY
);
console.log('Answer:', response1.message);
// Step 3: Ask follow-up question
console.log('Asking follow-up...');
const response2 = await chatWithFiles(
"Can you provide more details?",
[filePath],
WORKSPACE_ID,
AGENT_ID,
API_KEY,
response1.chatid // Use chat ID from first response
);
console.log('Follow-up Answer:', response2.message);
} catch (error) {
console.error('Error:', error);
}
}
// Usage
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
fileInput.addEventListener('change', () => {
if (fileInput.files && fileInput.files[0]) {
processDocument(fileInput.files[0]);
}
});
Quick Reference
File Path Format
// Format: {folder}/{workspace-id}/{timestamp}{filename}
// Documents/Images
`imported_doc_files/${workspaceId}/${Date.now()}${filename}`
// Videos
`imported_video_files/${workspaceId}/${Date.now()}${filename}`
// Audios
`imported_audio_files/${workspaceId}/${Date.now()}${filename}`
Message Fields by File Type
| File Type | Message Field | Example |
|---|---|---|
| PDF, Excel, Word, Text | files | { files: ["imported_doc_files/..."] } |
| PNG, JPG, GIF, WebP | images | { images: ["imported_doc_files/..."] } |
| MP4, MOV, AVI | videos | { videos: ["imported_video_files/..."] } |
| MP3, WAV, M4A | audios | { audios: ["imported_audio_files/..."] } |
Import Types
| File Type | importType Value |
|---|---|
"pdf" | |
| Images | "image" |
| Excel/Spreadsheets | "spreadsheet" |
| Other Documents | "document" |
Error Handling
async function safeUploadAndChat(file: File, question: string) {
try {
const filePath = await uploadFile(file, WORKSPACE_ID, API_KEY);
const response = await chatWithFiles(
question,
[filePath],
WORKSPACE_ID,
AGENT_ID,
API_KEY
);
return response.message || response.text;
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('401')) {
console.error('Invalid API key');
} else if (error.message.includes('404')) {
console.error('File not found - upload may have failed');
} else {
console.error('Error:', error.message);
}
}
throw error;
}
}
Tips
- Always save the
chatidfrom responses for follow-up questions - Use correct folder prefixes: Check file type before building the path
- Timestamp format: Always use
Date.now()for millisecond precision - File size limit: Maximum 15 MB per file
- Multiple files: Pass array of file paths to
chatWithFiles()
Last Updated: 2025-10-19
API Method 3: Stream Messages (Real-time)
createStreamingSession(message, agentId, options)
Creates a streaming session that receives AI responses in real-time using Server-Sent Events (SSE).
Parameters
Same as sendToAgent():
createStreamingSession(
message: string,
agentId: string,
options?: {
attachments?: {
files?: string[];
images?: string[];
};
showProgress?: boolean;
chatId?: string;
}
)
Returns
An event emitter object with these methods:
interface StreamingSession {
on(event: string, callback: Function): void;
emit(event: string, data?: any): void;
}
Available Events
| Event | Description | Payload |
|---|---|---|
sse_event | Raw Server-Sent Event data | { type, text, chatid, chat_created, ... } |
data | Message text chunk | { text: string } |
progress | Progress update | { status: string } |
status | Status update | { status: string } |
chat_created | New chat created | { chatid: string, chat_created: true } |
complete | Response complete | { text: string } |
end | Stream ended | void |
error | Error occurred | Error |
unknown | Unknown event type | any |
Example: Basic Streaming
const session = client.createStreamingSession(
"Tell me a story",
"your-agent-id"
);
let fullResponse = "";
// Listen for data chunks
session.on("data", (chunk) => {
fullResponse = chunk.text;
console.log("Current response:", chunk.text);
});
// Listen for completion
session.on("end", () => {
console.log("Final response:", fullResponse);
});
// Listen for errors
session.on("error", (error) => {
console.error("Stream error:", error);
});
Example: Streaming with Document Attachment
// Upload document first
const uploadResult = await client.uploadFromBase64(base64Data, {
filename: "report.pdf",
contentType: "application/pdf"
});
// Create streaming session with document
const session = client.createStreamingSession(
"Analyze this report and provide key insights",
"your-agent-id",
{
attachments: {
files: [uploadResult.filename]
},
showProgress: true
}
);
let fullResponse = "";
// Handle SSE events (most detailed)
session.on("sse_event", (eventData) => {
console.log("SSE Event:", eventData);
// Extract text from message events
if (eventData.type === "message" && eventData.text) {
fullResponse = eventData.text;
// Update UI in real-time
updateChatUI(fullResponse);
}
// Capture chat ID for follow-up messages
if (eventData.chat_created && eventData.chatid) {
saveChatId(eventData.chatid);
}
});
// Handle completion
session.on("complete", (data) => {
console.log("Complete response:", data.text);
});
// Handle stream end
session.on("end", () => {
console.log("Stream finished");
});
// Handle errors
session.on("error", (error) => {
console.error("Error:", error);
});
Example: Multi-turn Conversation with Streaming
let currentChatId = null;
// First message
const session1 = client.createStreamingSession(
"What's in this document?",
"your-agent-id",
{
attachments: {
files: ["workspace-id/document.pdf"]
}
}
);
session1.on("sse_event", (eventData) => {
// Capture chat ID
if (eventData.chat_created && eventData.chatid) {
currentChatId = eventData.chatid;
console.log("Chat created:", currentChatId);
}
if (eventData.type === "message" && eventData.text) {
console.log("Response:", eventData.text);
}
});
session1.on("end", () => {
// After first response, send follow-up
sendFollowUp();
});
function sendFollowUp() {
const session2 = client.createStreamingSession(
"Can you elaborate on the first point?",
"your-agent-id",
{
chatId: currentChatId, // Include chat ID for context
attachments: {
files: ["workspace-id/document.pdf"]
}
}
);
session2.on("sse_event", (eventData) => {
if (eventData.type === "message" && eventData.text) {
console.log("Follow-up response:", eventData.text);
}
});
}
Complete Workflow Example
Upload Document → Ask Questions → Follow-up
import { ToothFairyAPIClient } from "@/utils/toothfairyApi";
class DocumentChat {
private client: ToothFairyAPIClient;
private agentId: string;
private currentChatId: string | null = null;
constructor(apiKey: string, workspaceId: string, agentId: string) {
this.client = new ToothFairyAPIClient(apiKey, workspaceId);
this.agentId = agentId;
}
// Step 1: Upload document
async uploadDocument(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async () => {
try {
const base64 = (reader.result as string).split(',')[1];
const result = await this.client.uploadFromBase64(base64, {
filename: file.name,
contentType: file.type,
onProgress: (percent) => {
console.log(`Upload: ${percent}%`);
}
});
console.log('✅ Uploaded:', result.filename);
resolve(result.filename);
} catch (error) {
reject(error);
}
};
reader.readAsDataURL(file);
});
}
// Step 2: Ask question with streaming
async askQuestion(
message: string,
documentPath: string,
onUpdate: (text: string) => void
): Promise<string> {
return new Promise((resolve, reject) => {
// Determine if image or file
const isImage = /\.(jpg|jpeg|png|gif|webp)$/i.test(documentPath);
const session = this.client.createStreamingSession(
message,
this.agentId,
{
attachments: isImage
? { images: [documentPath] }
: { files: [documentPath] },
showProgress: true,
...(this.currentChatId && { chatId: this.currentChatId })
}
);
let fullResponse = "";
session.on("sse_event", (data) => {
// Capture chat ID
if (data.chat_created && data.chatid) {
this.currentChatId = data.chatid;
}
// Update with latest text
if (data.type === "message" && data.text) {
fullResponse = data.text;
onUpdate(fullResponse);
}
});
session.on("end", () => {
resolve(fullResponse);
});
session.on("error", (error) => {
reject(error);
});
});
}
// Step 3: Ask follow-up question
async followUp(
message: string,
documentPath: string,
onUpdate: (text: string) => void
): Promise<string> {
if (!this.currentChatId) {
throw new Error("No active chat. Start with askQuestion() first.");
}
// Same as askQuestion, but chatId is already set
return this.askQuestion(message, documentPath, onUpdate);
}
}
// Usage
const chat = new DocumentChat(
"your-api-key",
"your-workspace-id",
"your-agent-id"
);
// Upload
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];
const documentPath = await chat.uploadDocument(file);
// First question
await chat.askQuestion(
"What is this document about?",
documentPath,
(text) => console.log("Streaming:", text)
);
// Follow-up question (maintains context)
await chat.followUp(
"Can you summarize the key points?",
documentPath,
(text) => console.log("Streaming:", text)
);
Attachment Types: Files vs Images
When to Use files vs images
The API distinguishes between document files and images:
// For PDFs, Excel, Word, etc.
attachments: {
files: ["workspace-id/document.pdf"]
}
// For images (JPG, PNG, GIF, WebP)
attachments: {
images: ["workspace-id/photo.jpg"]
}
// You can attach both simultaneously
attachments: {
files: ["workspace-id/report.pdf"],
images: ["workspace-id/chart.png"]
}
Automatic Detection Helper
function getAttachmentType(filename: string) {
const extension = filename.split('.').pop()?.toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
return imageExtensions.includes(extension || '')
? { images: [filename] }
: { files: [filename] };
}
// Usage
const attachments = getAttachmentType(uploadResult.filename);
const session = client.createStreamingSession(message, agentId, { attachments });
Error Handling
Upload Errors
try {
const result = await client.uploadFromBase64(base64Data, options);
} catch (error) {
if (error.message.includes("exceeds 15MB limit")) {
console.error("File too large");
} else if (error.message.includes("Invalid base64")) {
console.error("Invalid file encoding");
} else if (error.message.includes("Failed to get upload URL")) {
console.error("Server error - check API key and workspace ID");
} else {
console.error("Upload failed:", error.message);
}
}
Streaming Errors
const session = client.createStreamingSession(message, agentId, options);
session.on("error", (error) => {
if (error.message.includes("HTTP error")) {
console.error("Network error - check internet connection");
} else if (error.message.includes("No response body")) {
console.error("Invalid response from server");
} else {
console.error("Stream error:", error.message);
}
});
Best Practices
1. Cache Upload Results
const uploadCache = new Map<string, string>();
async function getCachedUpload(file: File): Promise<string> {
const cacheKey = `${file.name}-${file.size}-${file.lastModified}`;
if (uploadCache.has(cacheKey)) {
return uploadCache.get(cacheKey)!;
}
const result = await uploadDocument(file);
uploadCache.set(cacheKey, result.filename);
return result.filename;
}
2. Always Store Chat IDs
let chatHistory = new Map<string, string>(); // documentId → chatId
session.on("sse_event", (data) => {
if (data.chat_created && data.chatid) {
chatHistory.set(documentId, data.chatid);
}
});
3. Use Progress Callbacks
await client.uploadFromBase64(base64Data, {
filename: file.name,
contentType: file.type,
onProgress: (percent, loaded, total) => {
updateProgressBar(percent);
console.log(`${loaded} / ${total} bytes`);
}
});
4. Handle All Stream Events
const session = client.createStreamingSession(message, agentId, options);
session.on("sse_event", handleSSE);
session.on("data", handleData);
session.on("progress", handleProgress);
session.on("chat_created", handleChatCreated);
session.on("complete", handleComplete);
session.on("end", handleEnd);
session.on("error", handleError);
API Limits
| Limit | Value |
|---|---|
| Max file size | 15 MB |
| Supported formats | PDF, Excel, Images (JPG, PNG, GIF, WebP), Documents |
| Base URL (documents) | https://api.toothfairyai.com |
| Base URL (agents) | https://ais.toothfairyai.com |
Troubleshooting
Issue: Upload fails with "Invalid base64"
Solution: Ensure you remove the data URL prefix:
// ❌ Wrong
const base64 = "data:application/pdf;base64,JVBERi0xLj..."
// ✅ Correct
const base64 = "JVBERi0xLj..."
// OR
const base64 = dataUrl.split(',')[1];
Issue: AI doesn't reference the document
Solution: Verify you're using the correct attachment type (files vs images) and the correct filename from upload result:
const result = await client.uploadFromBase64(...);
// Use result.filename, NOT result.originalFilename
const attachments = { files: [result.filename] };
Issue: Follow-up questions lose context
Solution: Make sure to capture and reuse the chat ID:
let chatId = null;
session.on("sse_event", (data) => {
if (data.chat_created && data.chatid) {
chatId = data.chatid; // Store this!
}
});
// Use in next message
const nextSession = client.createStreamingSession(message, agentId, {
chatId: chatId, // Include for context
attachments: { files: [filename] }
});
TypeScript Type Definitions
// Client
class ToothFairyAPIClient {
constructor(apiKey: string, workspaceId: string);
uploadFromBase64(
base64Data: string,
options: Base64FileUploadOptions
): Promise<FileUploadResult>;
sendToAgent(
message: string,
agentId: string,
options?: AgentOptions
): Promise<string>;
createStreamingSession(
message: string,
agentId: string,
options?: AgentOptions
): StreamingSession;
}
// Options
interface Base64FileUploadOptions {
filename: string;
contentType: string;
importType?: string;
onProgress?: (percent: number, loaded: number, total: number) => void;
}
interface AgentOptions {
attachments?: {
files?: string[];
images?: string[];
};
showProgress?: boolean;
chatId?: string;
}
// Results
interface FileUploadResult {
success: boolean;
originalFilename: string;
sanitizedFilename: string;
filename: string;
importType: string;
contentType: string;
size: number;
sizeInMB: number;
}
// Streaming
interface StreamingSession {
on(event: string, callback: Function): void;
emit(event: string, data?: any): void;
}
Last Updated: 2025-10-19