Performance and Best Practices¶
Overview¶
This guide covers best practices for building efficient, maintainable Live Pages applications.
Performance Optimization¶
1. Use pt.get() for Single Entities¶
When you know the entity ID, always use pt.get() for the fastest retrieval:
// ✅ GOOD: Fast primary key lookup
const task = await pt.get(123);
// ❌ AVOID: Slower filtering when ID is known
const tasks = await pt.list({
entityNames: ['task'],
filters: { id: 123 }
});
const task = tasks[0];
2. Implement Server-Side Filtering¶
Always filter on the server rather than loading all data and filtering client-side:
// ✅ GOOD: Server-side filtering
const activeTasks = await pt.list({
entityNames: ['task'],
filters: { status: 'active' },
limit: 50
});
// ❌ AVOID: Client-side filtering of large datasets
const allTasks = await pt.list({
entityNames: ['task'],
limit: 10000
});
const activeTasks = allTasks.filter(t => t.data.status === 'active');
3. Use Appropriate Operators¶
Choose the most efficient operator for your use case:
// ✅ GOOD: Use exact match when possible (fastest)
const task = await pt.list({
entityNames: ['task'],
filters: { status: 'active' }
});
// ✅ GOOD: Use $in for multiple values
const tasks = await pt.list({
entityNames: ['task'],
filters: { priority: { $in: ['high', 'medium'] } }
});
// ❌ AVOID: Unnecessary $or for same field
const tasks = await pt.list({
entityNames: ['task'],
filters: {
$or: [
{ priority: 'high' },
{ priority: 'medium' }
]
}
});
4. Cache Static Data¶
Cache data that doesn't change frequently:
// ✅ GOOD: Cache chat members at app initialization
let allMembers = [];
async function initApp() {
allMembers = await pt.getChatMembers();
await loadTasks();
}
function getMemberName(userId) {
const member = allMembers.find(m => m.id === userId);
return member ? member.name : 'Unknown';
}
// ❌ AVOID: Calling getChatMembers() repeatedly
async function displayTask(task) {
const members = await pt.getChatMembers(); // Called for every task!
const creator = members.find(m => m.id === task.creator_user_id);
return creator.name;
}
5. Use Pagination¶
For large datasets, always implement pagination:
// ✅ GOOD: Load data in pages
const result = await pt.list({
entityNames: ['task'],
filters: { status: 'active' },
page: 1,
pageSize: 20,
returnMetadata: true
});
// ❌ AVOID: Loading thousands of records at once
const allTasks = await pt.list({
entityNames: ['task'],
limit: 10000
});
6. Batch Operations¶
Use Promise.all() for parallel operations:
// ✅ GOOD: Parallel operations
async function batchUpdate(taskIds, updates) {
const promises = taskIds.map(async id => {
const task = await pt.get(id);
return pt.edit(id, { ...task.data, ...updates });
});
await Promise.all(promises);
}
// ❌ AVOID: Sequential operations
async function slowBatchUpdate(taskIds, updates) {
for (const id of taskIds) {
const task = await pt.get(id);
await pt.edit(id, { ...task.data, ...updates });
}
}
7. Provide Immediate Feedback with Processing Status¶
For file uploads or long-running operations, create database rows immediately with a PROCESSING status to provide instant user feedback, then update them when processing completes.
Pattern: Add-Then-Update¶
// ✅ GOOD: Create PROCESSING row immediately, update when done
async function uploadFileWithFeedback(file) {
// 1. Create row immediately - user sees it right away
const created = await pt.add('document', {
filename: file.name,
status: 'PROCESSING',
topic: null,
document_id: null
});
try {
// 2. Upload and process
const formData = new FormData();
formData.append('files', file);
const msg = `Process this file and use 'chatdb_edit' with entity_id: ${created.id} to update the row with results.`;
await pt.addMessage(formData, msg);
// User will see PROCESSING status immediately, then SUCCESS after AI updates
} catch (error) {
// 3. Update to ERROR if something fails
await pt.edit(created.id, {
filename: file.name,
status: 'ERROR',
error_message: error.message
});
}
}
// ❌ AVOID: User waits with no feedback
async function uploadFileNoFeedback(file) {
const formData = new FormData();
formData.append('files', file);
// User sees nothing until AI finishes processing
const msg = `Process this file and use 'chatdb_add' to create a row.`;
await pt.addMessage(formData, msg);
}
When to Use Each Approach¶
Upload-Then-Add (chatdb_add): - Goals and automation (chat, email) - Background tasks - No user waiting for feedback - Simpler with fewer failure modes
Add-Then-Update (pt.add + chatdb_edit): - Interactive Live Page uploads - User is actively waiting - Immediate feedback is critical - Dashboard/real-time applications
Comparison:
| Aspect | Upload-Then-Add | Add-Then-Update |
|---|---|---|
| User Feedback | Delayed | Immediate |
| Operations | 1 (add only) | 2 (add + edit) |
| Complexity | Simple | More complex |
| Orphaned Rows | None | Possible |
| Best For | Automation | Interactive UIs |
Best Practices for Add-Then-Update¶
1. Handle Cleanup on Failure:
try {
const created = await pt.add('document', { status: 'PROCESSING' });
await processDocument(created.id);
} catch (error) {
// Option 1: Update to ERROR status
await pt.edit(created.id, { status: 'ERROR', error: error.message });
// Option 2: Delete orphaned row
// await pt.delete(created.id);
}
2. Use Clear Status Values:
const STATUS = {
PROCESSING: 'PROCESSING', // Yellow badge, spinner
SUCCESS: 'SUCCESS', // Green badge, checkmark
ERROR: 'ERROR' // Red badge, x mark
};
3. Add Auto-Refresh:
4. Show Processing Indicators:
function renderStatus(status) {
if (status === 'PROCESSING') {
return `<span class="text-yellow-600">⏳ Processing...</span>`;
}
if (status === 'SUCCESS') {
return `<span class="text-green-600">✓ Complete</span>`;
}
return `<span class="text-red-600">✗ Error</span>`;
}
5. Use waitForMessageReceived for Real-Time Updates:
Instead of polling with setInterval, use pt.waitForMessageReceived() to get notified when AI processing completes:
async function uploadFileWithRealTimeUpdate(file) {
// 1. Create PROCESSING row immediately
const created = await pt.add('document', {
filename: file.name,
status: 'PROCESSING',
topic: null,
document_id: null
});
// 2. Refresh UI to show the new row
await loadData();
try {
// 3. Upload and process
const formData = new FormData();
formData.append('files', file);
const msg = `Process this file and use 'chatdb_edit' with entity_id: ${created.id} to update the row with results.`;
const result = await pt.addMessage(formData, msg);
// 4. Wait for AI to finish processing
const response = await pt.waitForMessageReceived(result.task_id);
console.log('AI finished processing:', response.id);
// Refresh to show updated status
await loadData();
} catch (error) {
await pt.edit(created.id, {
filename: file.name,
status: 'ERROR',
error_message: error.message
});
await loadData();
}
}
This approach is more efficient than polling because: - Updates appear immediately when AI completes (no 10-second delay) - No unnecessary API calls when nothing has changed - Works well with multiple concurrent uploads - Clean async/await syntax with proper error handling
8. Implement Debouncing¶
Debounce search inputs to reduce API calls:
let searchTimeout;
document.getElementById('searchInput').addEventListener('input', (e) => {
clearTimeout(searchTimeout);
const query = e.target.value.trim();
if (query.length < 2) {
clearResults();
return;
}
searchTimeout = setTimeout(async () => {
const results = await pt.list({
entityNames: ['task'],
filters: { text: { $contains: query } },
limit: 20
});
displayResults(results);
}, 300); // Wait 300ms after user stops typing
});
9. Cache Page Results¶
Implement caching for pagination:
const pageCache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
async function loadPageWithCache(page, filters) {
const cacheKey = `${page}-${JSON.stringify(filters)}`;
const cached = pageCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
return cached.data;
}
const result = await pt.list({
entityNames: ['task'],
filters: filters,
page: page,
pageSize: 20,
returnMetadata: true
});
pageCache.set(cacheKey, {
data: result,
timestamp: Date.now()
});
return result;
}
Error Handling¶
1. Always Handle Errors¶
Wrap data operations in try-catch blocks:
async function robustOperation() {
try {
const entity = await pt.get(123);
return entity;
} catch (error) {
console.error('Operation failed:', error);
return null;
}
}
2. Provide User Feedback¶
Show meaningful error messages to users:
async function addTask() {
const text = document.getElementById('taskInput').value.trim();
if (!text) {
showError('Please enter a task description');
return;
}
try {
await pt.add('task', {
text: text,
completed: false
});
showSuccess('Task added successfully');
await loadTasks();
} catch (error) {
console.error('Error adding task:', error);
showError('Failed to add task. Please try again.');
}
}
function showError(message) {
const alert = document.createElement('div');
alert.className = 'bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4';
alert.textContent = message;
document.getElementById('alerts').appendChild(alert);
setTimeout(() => alert.remove(), 5000);
}
function showSuccess(message) {
const alert = document.createElement('div');
alert.className = 'bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4';
alert.textContent = message;
document.getElementById('alerts').appendChild(alert);
setTimeout(() => alert.remove(), 3000);
}
3. Implement Fallback Strategies¶
Provide fallbacks when operations fail:
async function robustDataOperation() {
try {
const result = await pt.list({
entityNames: ['task'],
filters: { text: { $contains: 'important' } },
limit: 50
});
return result;
} catch (error) {
console.error('Primary operation failed:', error);
// Fallback to simpler query
try {
return await pt.list({
entityNames: ['task'],
limit: 20
});
} catch (fallbackError) {
console.error('Fallback also failed:', fallbackError);
return [];
}
}
}
Code Organization¶
1. Separate Concerns¶
Organize code into logical functions:
// ✅ GOOD: Separate concerns
async function loadTasks() {
const entities = await fetchTasks();
const tasks = filterTaskEntities(entities);
displayTasks(tasks);
}
async function fetchTasks() {
return await pt.list({
entityNames: ['task'],
filters: { completed: false }
});
}
function filterTaskEntities(entities) {
return entities.filter(e => e.entity_name === 'task');
}
function displayTasks(tasks) {
document.getElementById('tasksList').innerHTML = tasks.map(renderTask).join('');
}
function renderTask(task) {
return `
<div class="task-card">
<span>${task.data.text}</span>
<button onclick="deleteTask(${task.id})">Delete</button>
</div>
`;
}
// ❌ AVOID: Everything in one function
async function doEverything() {
const entities = await pt.list({ entityNames: ['task'] });
const tasks = entities.filter(e => e.entity_name === 'task');
document.getElementById('tasksList').innerHTML = tasks.map(t =>
`<div><span>${t.data.text}</span><button onclick="deleteTask(${t.id})">Delete</button></div>`
).join('');
}
2. Use Meaningful Names¶
// ✅ GOOD: Clear variable names
const chatMembers = await pt.getChatMembers();
const humanUsers = chatMembers.filter(m => m.type === 'user');
const aiAgents = chatMembers.filter(m => m.type === 'agent');
const chatOwner = chatMembers.find(m => m.is_chat_owner);
const currentUser = chatMembers.find(m => m.is_logged_user);
// ❌ AVOID: Unclear names
const m = await pt.getChatMembers();
const u = m.filter(x => x.type === 'user');
3. Create Reusable Components¶
// Reusable task card renderer
function createTaskCard(task) {
const card = document.createElement('div');
card.className = 'bg-white rounded-lg shadow p-4 mb-2';
const isCompleted = task.data.completed === "true";
card.innerHTML = `
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<input
type="checkbox"
${isCompleted ? 'checked' : ''}
onchange="toggleTask(${task.id})"
class="h-4 w-4"
>
<span class="${isCompleted ? 'line-through text-gray-500' : ''}">
${escapeHtml(task.data.text)}
</span>
</div>
<button onclick="deleteTask(${task.id})" class="text-red-500">
Delete
</button>
</div>
`;
return card;
}
// Usage
function displayTasks(tasks) {
const container = document.getElementById('tasksList');
container.innerHTML = '';
tasks.forEach(task => {
container.appendChild(createTaskCard(task));
});
}
Data Management Best Practices¶
1. Always Merge When Editing¶
// ✅ GOOD: Preserve existing fields
const task = await pt.get(taskId);
await pt.edit(taskId, {
...task.data,
completed: true
});
// ❌ BAD: Lose all other fields
await pt.edit(taskId, { completed: true });
2. Validate Before Saving¶
async function addTask() {
const text = document.getElementById('taskInput').value.trim();
// Validate
if (!text) {
showError('Task description is required');
return;
}
if (text.length > 500) {
showError('Task description is too long (max 500 characters)');
return;
}
// Save
await pt.add('task', {
text: text,
completed: false
});
}
3. Handle Missing Members Gracefully¶
function getMemberName(userId) {
const member = allMembers.find(m => m.id === userId);
return member ? member.name : 'Unknown User';
}
function displayTaskCreator(task) {
const creator = allMembers.find(m => m.id === task.creator_user_id);
if (!creator) {
return '<span class="text-gray-400">Unknown</span>';
}
const icon = creator.type === 'user' ? '👤' : '🤖';
return `${icon} ${creator.name}`;
}
4. Use Appropriate Limits¶
// ✅ GOOD: Reasonable limits
const recentTasks = await pt.list({
entityNames: ['task'],
limit: 50
});
// ❌ AVOID: Requesting too much data
const allTasks = await pt.list({
entityNames: ['task'],
limit: 10000
});
Using Goals with Live Pages¶
What are Goals?¶
Goals are automatic AI instructions that execute when specific conditions are met in a chat. When combined with Live Pages, Goals enable powerful automation workflows where data can be processed and stored in your database without manual intervention.
How Goals Work with Live Pages¶
When you set up a Goal in your chat settings, the AI automatically: 1. Detects when the goal condition is triggered (e.g., file upload, specific keywords) 2. Executes the instructions you've defined in the Goal 3. Can use chatdb tools to create/update/query database entities 4. Stores results that your Live Page can display and interact with
This creates a seamless integration between: - Direct chat interactions (uploading files, sending messages) - Email forwarding (files sent to chat email address) - API uploads (files uploaded programmatically via PrimeThink API) - Chat mentions (files uploaded when the chat is mentioned in other conversations) - AI processing (extraction, categorization, validation) - Database storage (structured data in entities) - Live Page display (visualization and interaction)
Common Use Cases for Goals with Live Pages¶
1. Document Processing
Goal Trigger: User uploads a PDF file
Goal Action: Extract key information, categorize, store in database
Live Page: Display categorized documents with search/filter
2. Email Automation
Goal Trigger: Email with attachments forwarded to chat
Goal Action: Extract data, create database records
Live Page: Show processed emails in dashboard
3. Data Entry Shortcuts
Goal Trigger: User sends message with specific format
Goal Action: Parse message, validate, store as entity
Live Page: Display and manage all entries
4. File Analysis
Goal Trigger: Invoice/receipt uploaded
Goal Action: Extract line items, amounts, vendors
Live Page: Financial dashboard showing all invoices
5. Content Categorization
Goal Trigger: Document uploaded
Goal Action: Analyze content, assign categories/tags
Live Page: Browse and filter by categories
6. API Integration
Goal Trigger: File uploaded via API
Goal Action: Process file, extract metadata, store in database
Live Page: Monitor API uploads with status tracking
7. Chat Mention Processing
Goal Trigger: Chat mentioned in another conversation with file attachment
Goal Action: Process file in context of mention, create record
Live Page: Show all processed mentions and their results
8. Multi-Channel Document Inbox
Goal Trigger: File uploaded via any channel (chat, email, API, chat mentions)
Goal Action: Unified processing regardless of source
Live Page: Single dashboard showing all documents from all channels
Best Practices for Writing Goal Prompts¶
1. Be Explicit About Tool Usage
Always specify which chatdb tool to use:
✅ GOOD: Use the tool 'chatdb_add' to create a new invoice record
❌ AVOID: Store the invoice information
2. Demand Actual Tool Execution (Not Simulation)
The AI might summarize or simulate tool calls instead of actually executing them if your instructions are too high-level. Be explicit that you want actual execution:
❌ BAD: High-level workflow that AI might just simulate
"Analyze the uploaded files and extract IP tasks. Use chatdb_list to check duplicates
by unique_key. If not found, use chatdb_add to insert the task."
Result: AI returns JSON summary but doesn't actually call the tools
✅ GOOD: Explicit tool execution instructions
"Analyze the uploaded files and extract IP tasks. For EACH task:
1. ACTUALLY CALL the tool 'chatdb_list' to check for duplicates by unique_key
2. If not found, ACTUALLY CALL the tool 'chatdb_add' to insert the task
3. Show all tool call results
4. At the end, return a JSON summary"
Result: AI actually executes chatdb_list and chatdb_add for each task
Key Phrases for Actual Execution: - "ACTUALLY CALL the tool 'chatdb_list'" - "ACTUALLY USE the tool 'chatdb_add'" - "For EACH task, use the tool..." - "Show all tool call results" - "Execute the following steps with tool calls"
Why This Matters: - Without explicit execution instructions, AI may treat your prompt as a conceptual workflow - AI might return a summary of what "would happen" instead of actually doing it - Your database won't be updated even though the response looks correct - This is especially critical for multi-step operations with loops
Real-World Example:
❌ AVOID: Conceptual instruction
"Extract tasks from the document. For each task, check if it exists using unique_key.
If not, add it to the database with these fields: [list]. Never modify existing tasks."
✅ PREFER: Explicit execution instruction
"Extract all tasks from the document. Then FOR EACH extracted task:
1. ACTUALLY CALL chatdb_list with filters: {unique_key: "<computed_hash>"}
2. If the list returns empty (no duplicate):
- ACTUALLY CALL chatdb_add with entity_name 'ip_task' and data: {
matter_reference: "...",
application_number: "...",
due_date: "YYYY-MM-DD",
task_description: "...",
unique_key: "<computed_hash>",
source: "auto",
source_document_id: <doc_id>
}
3. If the list returns results (duplicate exists):
- Skip this task
IMPORTANT: You must ACTUALLY EXECUTE chatdb_list and chatdb_add for each task.
Show the tool call results. Never modify existing tasks.
After processing all tasks, respond with JSON:
{
"added": <number>,
"skipped": <number>,
"items": [{"application_number": "...", "due_date": "...", "task": "..."}]
}
Testing Your Instructions: 1. Run your instruction with a test file 2. Check if chatdb tools were actually called (look for tool execution in chat) 3. Verify database entities were actually created 4. If AI only returns a summary without tool calls, add "ACTUALLY CALL" phrases
3. Specify Entity Structure Clearly
Define exactly what fields to create:
Use the tool 'chatdb_add' with:
- entity_name: "invoice"
- data: {
"invoice_number": "<extracted_number>",
"vendor": "<extracted_vendor>",
"amount": <extracted_amount>,
"date": "<extracted_date>",
"status": "pending"
}
4. Handle Edge Cases
Account for scenarios where data might not be available:
If the document has extracted text:
- Extract information and use 'chatdb_add' with status: "success"
If the document has no text or processing fails:
- Use 'chatdb_add' with status: "pending" or "error"
5. Request Structured Responses
Ask for JSON responses for easier validation:
Respond with JUST JSON in this format:
[
{
"document_id": <id>,
"extracted_field": "<value>",
"status": "<success|error>"
}
]
6. Create One Record Per Item
Be explicit about quantity:
✅ GOOD: You MUST call 'chatdb_add' exactly once per uploaded file.
❌ AVOID: Create records for each file.
7. Maintain Consistency
Use the same entity names and data structures across goals:
Always use:
- entity_name: "invoice" (not "invoices", "invoice_data", etc.)
- data.status: "pending" | "success" | "error" (consistent values)
- data.created_date: ISO format (consistent format)
Goal Example for Live Pages¶
Here's a complete example of a Goal that works with a Live Page:
Goal Trigger: If a user uploads a PDF or DOCX file
Goal Instructions:
AI Processing Request
You are given a file uploaded as an attachment to THIS message. Follow EXACTLY:
1) If the document has extracted text:
- Analyze the content and extract key information
- Use the tool 'chatdb_add' to create a database record:
- entity_name: "document"
- data: {
"filename": "<actual filename>",
"category": "<derived category>",
"summary": "<brief summary, max 200 chars>",
"document_id": <ID from message attachments>,
"processing_status": "success"
}
2) If the document has no text or processing fails:
- Use the tool 'chatdb_add' to create a database record:
- entity_name: "document"
- data: {
"filename": "<actual filename>",
"category": "Uncategorized",
"summary": "Processing pending",
"document_id": <ID from message attachments>,
"processing_status": "pending"
}
3) Respond with JSON:
{
"status": "<success|pending|error>",
"filename": "<filename>",
"category": "<category>"
}
Corresponding Live Page: - Displays all documents using pt.list({ entityNames: ['document'] }) - Shows category, summary, processing status - Allows filtering by category or status - Provides "View" button to see document text with pt.getDocumentText() - Shows "Re-process" button for pending items using pt.addMessage()
Benefits of Goals with Live Pages¶
Unified Experience: - Users can upload files via Live Page, chat message, email, API, or chat mentions - All uploads are processed consistently regardless of source - All results appear in the same Live Page interface - Single Goal handles all four upload channels
Automation: - No manual data entry required - AI handles extraction and categorization - Reduces human error - Zero-touch processing for API and automated uploads
Flexibility: - Live Page: Visual interface for interactive uploads - Chat: Conversational interface for manual uploads - Email: Integration with existing email workflows - API: Programmatic uploads for system integrations - Chat Mentions: Context-aware processing when the chat is mentioned in other conversations
Scalability: - Process single files or batch uploads - Same Goal handles all sources (chat, email, API, chat mentions) - Live Page adapts to any data volume - Supports high-throughput API integrations
Testing Goals with Live Pages¶
1. Test with Live Page Upload First: - Use pt.addMessage(formData, instructions) for uploads with AI processing - Or use pt.uploadFiles(formData, folder) for silent document uploads - Verify AI creates correct database entities (if using pt.addMessage) - Check that Live Page displays data correctly
2. Test Chat Upload: - Upload file directly in chat message - Verify Goal triggers and runs - Confirm same entity structure is created
3. Test Email Upload: - Forward email with attachment to chat email address - Verify Goal triggers automatically - Confirm consistent entity structure
4. Test API Upload: - Upload file programmatically via PrimeThink API - Verify Goal triggers for API uploads - Confirm API uploads create same entity structure
5. Test Chat Mention Upload: - Mention the chat in another conversation with file attachment - Verify Goal triggers when chat is mentioned - Confirm entity structure matches other channels
6. Test Edge Cases: - Upload file without text - Upload unsupported format - Upload very large file - Upload multiple files at once - Test each channel with edge cases
7. Verify Multi-Channel Consistency: - Compare entities from all four channels (Live Page, chat, email, API, chat mentions) - Ensure entity_name matches exactly across all sources - Confirm data structure is identical regardless of upload method - Verify all uploads appear correctly in Live Page
Goal + Live Page Checklist¶
- Goal uses explicit chatdb tool names
- Entity structure is clearly defined
- Edge cases are handled (no text, errors)
- Response format is specified (JSON recommended)
- Entity names are consistent with Live Page queries
- Data field names match what Live Page expects
- Status values are well-defined and consistent
- Goal tested with Live Page uploads
- Goal tested with chat uploads
- Goal tested with email uploads
- Goal tested with API uploads
- Goal tested with chat mention uploads
- Live Page can display all possible status values
- Error cases have retry mechanisms
- All channels create identical entity structures
Security Best Practices¶
1. Escape User Input¶
Always escape HTML when displaying user-generated content:
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Usage
function renderTask(task) {
return `
<div class="task-card">
<span>${escapeHtml(task.data.text)}</span>
</div>
`;
}
2. Validate Input Length¶
function validateTaskInput(text) {
if (!text || text.trim().length === 0) {
return { valid: false, error: 'Task description is required' };
}
if (text.length > 500) {
return { valid: false, error: 'Task description is too long (max 500 characters)' };
}
return { valid: true };
}
async function addTask() {
const text = document.getElementById('taskInput').value;
const validation = validateTaskInput(text);
if (!validation.valid) {
showError(validation.error);
return;
}
await pt.add('task', {
text: text.trim(),
completed: false
});
}
3. Confirm Destructive Actions¶
async function deleteTask(taskId) {
if (!confirm('Delete this task? This cannot be undone.')) {
return;
}
try {
await pt.delete(taskId);
await loadTasks();
} catch (error) {
console.error('Error deleting task:', error);
showError('Failed to delete task');
}
}
UI/UX Best Practices¶
1. Show Loading States¶
async function loadTasks() {
const loading = document.getElementById('loading');
const tasksList = document.getElementById('tasksList');
loading.style.display = 'block';
tasksList.style.display = 'none';
try {
const entities = await pt.list({
entityNames: ['task'],
filters: { completed: false }
});
const tasks = entities.filter(e => e.entity_name === 'task');
displayTasks(tasks);
} finally {
loading.style.display = 'none';
tasksList.style.display = 'block';
}
}
2. Provide Visual Feedback¶
async function addTask() {
const button = document.getElementById('addButton');
const originalText = button.textContent;
// Show loading state
button.disabled = true;
button.textContent = 'Adding...';
try {
const text = document.getElementById('taskInput').value.trim();
await pt.add('task', { text: text, completed: false });
// Show success
button.textContent = '✓ Added';
document.getElementById('taskInput').value = '';
await loadTasks();
// Reset button after delay
setTimeout(() => {
button.textContent = originalText;
button.disabled = false;
}, 1000);
} catch (error) {
button.textContent = 'Error';
button.disabled = false;
setTimeout(() => {
button.textContent = originalText;
}, 2000);
}
}
3. Handle Empty States¶
function displayTasks(tasks) {
const container = document.getElementById('tasksList');
if (tasks.length === 0) {
container.innerHTML = `
<div class="text-center py-12 text-gray-500">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
</svg>
<p class="mt-2 text-sm">No tasks found</p>
<button onclick="clearFilters()" class="mt-4 text-blue-500 text-sm">
Clear filters
</button>
</div>
`;
return;
}
container.innerHTML = tasks.map(renderTask).join('');
}
Troubleshooting¶
Common Issues¶
Issue: pt.list() returns empty array
Solutions: - Check that entity name is correct - Verify filters are valid - Try without filters to see if entities exist - Check browser console for errors
// Debug filters
async function debugFilters() {
// Start simple
console.log('All tasks:', await pt.list({ entityNames: ['task'] }));
// Add filters incrementally
console.log('Active tasks:', await pt.list({
entityNames: ['task'],
filters: { status: 'active' }
}));
}
Issue: Data not updating after edit
Solutions: - Ensure you're merging with existing data - Check that you're calling loadTasks() after edit - Verify the edit was successful
// ✅ GOOD: Proper edit
const task = await pt.get(taskId);
await pt.edit(taskId, {
...task.data,
completed: true
});
await loadTasks(); // Refresh display
Issue: Performance is slow
Solutions: - Implement pagination - Use server-side filtering - Cache static data - Reduce data transfer with appropriate limits
// ✅ GOOD: Optimized loading
const result = await pt.list({
entityNames: ['task'],
filters: { status: 'active' }, // Server-side filter
page: 1,
pageSize: 20, // Pagination
returnMetadata: true
});
Testing Tips¶
1. Test with Empty Data¶
function displayTasks(tasks) {
// Handle empty state
if (!tasks || tasks.length === 0) {
showEmptyState();
return;
}
renderTasks(tasks);
}
2. Test with Large Datasets¶
// Test pagination with many items
async function testWithLargeDataset() {
const tasks = await pt.list({
entityNames: ['task'],
page: 1,
pageSize: 20,
returnMetadata: true
});
console.log('Has more pages:', tasks.pagination.has_more);
console.log('Count:', tasks.count);
}
3. Test Error Scenarios¶
async function testErrorHandling() {
try {
await pt.get(999999); // Non-existent ID
} catch (error) {
console.log('Error handled correctly:', error);
}
}
Performance Checklist¶
- Use
pt.get()for single entity lookups - Implement server-side filtering
- Use pagination for large datasets
- Cache chat members and other static data
- Implement debouncing for search inputs
- Use batch operations with
Promise.all() - Provide immediate feedback with PROCESSING status for uploads
- Choose appropriate filter operators
- Set reasonable limits on queries
- Implement caching where appropriate
- Show loading states
- Handle errors gracefully
- Validate user input
- Escape HTML content
- Test with empty and large datasets
- Use Goals for unified upload experience (Live Page + Chat + Email + API + Chat Mentions)
- Ensure Goal entity structure matches Live Page queries
- Make Goal prompts explicit about chatdb tool usage
- Use "ACTUALLY CALL" phrases to ensure tool execution (not simulation)
- Verify in chat that tools were actually executed (not just summarized)
- For multi-step operations, explicitly state "FOR EACH item, ACTUALLY CALL..."
- Choose appropriate pattern: Upload-Then-Add for automation, Add-Then-Update for interactive UIs
- Use
waitForMessageReceivedfor clean async/await AI response handling - Use
waitForDocumentReadyonly for silent uploads (uploadFiles) when you need extracted text - Remember:
addMessagewith attachments processes files immediately - nowaitForDocumentReadyneeded - Use
onDocumentChangedfor batch upload progress tracking - Cache generated documents (PDF/DOCX) by entity ID to avoid regenerating on repeated requests
Document Processing Patterns¶
When to Use waitForDocumentReady¶
Understanding when you need waitForDocumentReady() depends on your upload method:
| Upload Method | AI Processes Immediately | Need waitForDocumentReady |
|---|---|---|
pt.addMessage(formData, message) | ✅ Yes | ❌ No |
pt.uploadFiles(form) | ❌ No | ✅ Yes (for text extraction) |
Key insight: When using addMessage() with attachments, files are sent directly to the AI with your message. The AI processes them immediately, so you only need waitForMessageReceived() - not waitForDocumentReady().
Upload with AI Analysis (No waitForDocumentReady Needed)¶
When you want AI to analyze uploaded files immediately:
async function uploadAndAnalyze(file, instructions) {
const formData = new FormData();
formData.append('files', file);
// Files are sent with the message - AI processes them immediately
const result = await pt.addMessage(formData, instructions);
// Only wait for AI response - no waitForDocumentReady needed!
const response = await pt.waitForMessageReceived(result.task_id, {
timeout: 120000 // 2 minutes for complex analysis
});
return response.message;
}
// Usage
const analysis = await uploadAndAnalyze(
invoiceFile,
'Extract line items, total, and vendor from this invoice'
);
Silent Upload with Text Extraction (waitForDocumentReady Required)¶
When uploading files silently (without AI) and you need the extracted text:
async function uploadAndExtractText(file) {
const formData = new FormData();
formData.append('files', file);
// Silent upload - no AI processing
const result = await pt.uploadFiles(formData);
const docId = result.documents[0].id;
// Must wait for document processing to complete
try {
const doc = await pt.waitForDocumentReady(docId, {
timeout: 60000 // 1 minute
});
// Now safe to get extracted text
const text = await pt.getDocumentText(docId);
return { success: true, text: text.text };
} catch (error) {
return { success: false, error: error.message };
}
}
Track Batch Upload Progress¶
For multiple file uploads with progress tracking (silent uploads):
async function uploadWithProgress(files) {
const formData = new FormData();
files.forEach(f => formData.append('files', f));
const result = await pt.uploadFiles(formData);
const docIds = result.documents.map(d => d.id);
const total = docIds.length;
return new Promise((resolve) => {
const processed = new Set();
const unsubscribe = pt.onDocumentChanged((doc) => {
if (doc.status === 'Ready' || doc.status === 'Error') {
processed.add(doc.id);
updateProgressBar(processed.size, total);
if (processed.size === total) {
unsubscribe();
resolve({ processed: processed.size });
}
}
}, { documentIds: docIds });
});
}
Cache Generated Documents by Entity ID¶
When generating PDFs, DOCX, or other files from entity data, save them with a predictable filename based on the entity ID. This allows you to check for cached versions before regenerating, saving processing time and API calls.
Pattern: 1. Save generated documents to a dedicated folder (e.g., /translations/) 2. Use a consistent naming convention: {type}_{entityId}.{format} 3. Before generating, check if a cached version exists using pt.getDocumentInfo() 4. If cached, download directly from the existing URL 5. If not cached, generate and save to the folder
// Configuration
const CACHE_FOLDER = '/translations';
/**
* Get or generate a document for an entity
* Returns cached version if available, otherwise generates new one
*/
async function getOrGenerateDocument(entityId, format = 'pdf') {
const filename = `translation_${entityId}.${format}`;
const filepath = `${CACHE_FOLDER}/${filename}`;
// Check if cached version exists
try {
const docInfo = await pt.getDocumentInfo(filepath);
if (docInfo && docInfo.document && docInfo.document.download_url) {
console.log('Using cached document:', filename);
return {
cached: true,
url: docInfo.document.download_url,
documentId: docInfo.document.id
};
}
} catch (error) {
// Document doesn't exist, will generate new one
console.log('No cached version found, generating new document');
}
// Generate new document
const entity = await pt.get(entityId);
const generatedDoc = await generateDocument(entity, format);
// Save to cache folder
const formData = new FormData();
formData.append('files', generatedDoc.blob, filename);
const result = await pt.uploadFiles(formData, CACHE_FOLDER);
const savedDoc = result.documents[0];
return {
cached: false,
url: savedDoc.download_url,
documentId: savedDoc.id
};
}
/**
* Download button handler with caching
*/
async function handleDownload(entityId, format) {
const downloadBtn = document.getElementById('downloadBtn');
downloadBtn.disabled = true;
downloadBtn.textContent = 'Preparing...';
try {
const result = await getOrGenerateDocument(entityId, format);
// Trigger download
const link = document.createElement('a');
link.href = result.url;
link.download = `translation_${entityId}.${format}`;
link.click();
if (result.cached) {
showSuccess('Downloaded from cache');
} else {
showSuccess('Document generated and downloaded');
}
} catch (error) {
showError('Failed to generate document: ' + error.message);
} finally {
downloadBtn.disabled = false;
downloadBtn.textContent = 'Download';
}
}
Benefits: - Faster downloads for repeated requests (no regeneration needed) - Reduced server load and API usage - Consistent file organization - Easy to find and manage generated documents
When to Use: - Translation exports - Report generation - Invoice/receipt PDFs - Any document generated from entity data that doesn't change frequently
Cache Invalidation: If entity data changes and you need to regenerate, delete the cached document first:
async function regenerateDocument(entityId, format = 'pdf') {
const filename = `translation_${entityId}.${format}`;
const filepath = `${CACHE_FOLDER}/${filename}`;
// Delete cached version if exists
try {
const docInfo = await pt.getDocumentInfo(filepath);
if (docInfo && docInfo.document) {
await pt.deleteDocument(docInfo.document.id);
}
} catch (error) {
// No cached version to delete
}
// Generate fresh document
return await getOrGenerateDocument(entityId, format);
}
Downloading Documents Generated with pt.saveDocument()¶
When using pt.saveDocument() to generate PDF, DOCX, or other document formats, the function saves the document to PrimeThink's storage but doesn't automatically trigger a browser download. To enable automatic downloads, extract the download_url from the response and trigger the download manually.
Helper Functions:
// Extract download URL from various response structures
function extractDownloadUrl(result) {
if (result?.result?.documents?.[0]?.download_url) return result.result.documents[0].download_url;
if (result?.documents?.[0]?.download_url) return result.documents[0].download_url;
if (result?.download_url) return result.download_url;
// Fallback: construct relative URL from UUID (works within PrimeThink platform)
if (result?.uuid) return `/api/v1/documents/uuid/${result.uuid}/download`;
return null;
}
// Trigger browser download from URL
function triggerDownload(url, filename) {
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
Usage Example:
async function downloadAsPdf() {
try {
showToast('Generating PDF...', 'info');
const filename = 'my-document.pdf';
// Save document to PrimeThink storage
const result = await pt.saveDocument(
filename,
'PDF',
'application/pdf',
content,
'exports'
);
// Extract and trigger download
const downloadUrl = extractDownloadUrl(result);
if (downloadUrl) {
triggerDownload(downloadUrl, filename);
showToast('PDF downloaded', 'success');
} else {
showToast('PDF saved to documents', 'success');
}
} catch (error) {
console.error('PDF generation failed:', error);
showToast('Failed to generate PDF', 'error');
}
}
Key Points: - pt.saveDocument() returns a result object containing the document's download_url - The response structure can vary, so extractDownloadUrl() checks multiple paths - Relative URLs work since Live Pages run within the PrimeThink platform - Use triggerDownload() to programmatically click a download link - Always provide a fallback message if the download URL isn't available
Next Steps¶
- Creating Live Pages - Back to main guide
- Data Management API Reference - Learn about all pt API methods
- Message Response Handling - Handle AI responses with waitForMessageReceived
- Document Events - Track document processing with waitForDocumentReady
- Filtering and Querying - Advanced filtering techniques
- Complete Examples - See best practices in action