Skip to main content

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 TypeMessage FieldExample
PDF, Excel, Word, Textfiles{ files: ["imported_doc_files/..."] }
PNG, JPG, GIF, WebPimages{ images: ["imported_doc_files/..."] }
MP4, MOV, AVIvideos{ videos: ["imported_video_files/..."] }
MP3, WAV, M4Aaudios{ audios: ["imported_audio_files/..."] }

Import Types

File TypeimportType Value
PDF"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

  1. Always save the chatid from responses for follow-up questions
  2. Use correct folder prefixes: Check file type before building the path
  3. Timestamp format: Always use Date.now() for millisecond precision
  4. File size limit: Maximum 15 MB per file
  5. 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

EventDescriptionPayload
sse_eventRaw Server-Sent Event data{ type, text, chatid, chat_created, ... }
dataMessage text chunk{ text: string }
progressProgress update{ status: string }
statusStatus update{ status: string }
chat_createdNew chat created{ chatid: string, chat_created: true }
completeResponse complete{ text: string }
endStream endedvoid
errorError occurredError
unknownUnknown event typeany

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

LimitValue
Max file size15 MB
Supported formatsPDF, 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