PrimeThink Documentation Help

Complete Live Pages Examples

Overview

This page contains complete, production-ready examples demonstrating various Live Pages features and patterns.

Advanced Todo Application

This comprehensive example demonstrates:

  • Multiple entity types (task and filter_config)

  • Per-user filter preferences using creator_user_id

  • Server-side filtering and pagination

  • Chat member integration

  • Task assignment with default assignee

  • Inline task editing

  • Enhanced UX features

Key Features

  • Multiple Entity Types: Uses both task entities (shared tasks) and filter_config entities (personal settings)

  • Per-User Configuration: Each user has their own filter preferences automatically saved

  • Task Assignment: New tasks are assigned to current user by default, with dropdown to change assignee

  • Inline Editing: Click on any task to edit its details (text, priority, due date, assignee)

  • Creator Tracking: Shows who created each task using the creator_user_id field

  • Member Integration: Displays creator names and assignees from chat members

  • Server-Side Pagination: Efficient loading with page-based pagination

Complete Implementation

<div class="container mx-auto p-6 max-w-4xl"> <h1 class="text-3xl font-bold text-gray-800 mb-8">Advanced Todo List</h1> <!-- Search and Filter Controls --> <div class="bg-white rounded-lg shadow-md p-6 mb-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4"> <!-- Text Search with partial matching --> <input type="text" id="searchInput" class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="Search tasks (partial match)..." > <!-- Priority Filter (Multi-select) --> <div class="relative"> <button id="priorityDropdown" onclick="togglePriorityDropdown()" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-left bg-white" > <span id="priorityLabel">All Priorities</span> <svg class="float-right mt-1 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> </svg> </button> <div id="priorityOptions" class="hidden absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg"> <label class="flex items-center px-3 py-2 hover:bg-gray-50"> <input type="checkbox" value="high" onchange="updatePriorityFilter()" class="mr-2"> High </label> <label class="flex items-center px-3 py-2 hover:bg-gray-50"> <input type="checkbox" value="medium" onchange="updatePriorityFilter()" class="mr-2"> Medium </label> <label class="flex items-center px-3 py-2 hover:bg-gray-50"> <input type="checkbox" value="low" onchange="updatePriorityFilter()" class="mr-2"> Low </label> </div> </div> <!-- Status Filter with default to hide completed --> <select id="statusFilter" class="px-3 py-2 border border-gray-300 rounded-md"> <option value="pending" selected>Pending Only</option> <option value="all">All Tasks</option> <option value="completed">Completed</option> <option value="overdue">Overdue</option> </select> </div> <!-- Quick Filter Buttons & Bulk Actions --> <div class="flex flex-wrap justify-between items-center gap-2"> <div class="flex flex-wrap gap-2"> <button onclick="quickFilter('urgent')" class="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm"> Urgent Tasks </button> <button onclick="quickFilter('today')" class="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm"> Due Today </button> <button onclick="clearFilters()" class="bg-gray-100 text-gray-800 px-3 py-1 rounded-full text-sm"> Clear Filters </button> </div> <button onclick="clearCompletedTasks()" class="bg-red-500 text-white px-4 py-2 rounded-md text-sm hover:bg-red-600" > Clear Completed Tasks </button> </div> </div> <!-- Add New Task (with assignment dropdown) --> <div class="bg-white rounded-lg shadow-md p-6 mb-6"> <div class="grid grid-cols-1 gap-3"> <input type="text" id="taskInput" class="w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="Enter new task..." > <div class="grid grid-cols-1 md:grid-cols-4 gap-3"> <select id="assigneeInput" class="px-3 py-2 border border-gray-300 rounded-md"> <!-- Populated dynamically --> </select> <select id="priorityInput" class="px-3 py-2 border border-gray-300 rounded-md"> <option value="low">Low</option> <option value="medium">Medium</option> <option value="high">High</option> </select> <input type="date" id="dueDateInput" class="px-3 py-2 border border-gray-300 rounded-md"> <button onclick="addTask()" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"> Add Task </button> </div> </div> </div> <!-- Edit Task Modal --> <div id="editModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div class="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 p-6"> <h2 class="text-xl font-bold mb-4">Edit Task</h2> <div class="space-y-4"> <div> <label class="block text-sm font-medium mb-1">Task Description</label> <input type="text" id="editTaskText" class="w-full px-3 py-2 border rounded-md" > </div> <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div> <label class="block text-sm font-medium mb-1">Assigned To</label> <select id="editAssigneeInput" class="w-full px-3 py-2 border rounded-md"> <!-- Populated dynamically --> </select> </div> <div> <label class="block text-sm font-medium mb-1">Priority</label> <select id="editPriorityInput" class="w-full px-3 py-2 border rounded-md"> <option value="low">Low</option> <option value="medium">Medium</option> <option value="high">High</option> </select> </div> <div> <label class="block text-sm font-medium mb-1">Due Date</label> <input type="date" id="editDueDateInput" class="w-full px-3 py-2 border rounded-md"> </div> </div> </div> <div class="flex justify-end gap-2 mt-6"> <button onclick="closeEditModal()" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300" > Cancel </button> <button onclick="saveTaskEdit()" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700" > Save Changes </button> </div> </div> </div> <!-- Tasks List --> <div id="tasksList" class="space-y-4"> <!-- Tasks will be displayed here --> </div> <!-- Pagination Controls --> <div id="paginationControls" class="mt-6 flex justify-center items-center gap-4 bg-white rounded-lg shadow-md p-4"> <button id="firstPageBtn" onclick="goToFirstPage()" class="px-3 py-2 bg-gray-200 rounded-md hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed" > First </button> <button id="prevPageBtn" onclick="goToPreviousPage()" class="px-3 py-2 bg-gray-200 rounded-md hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed" > Previous </button> <span id="pageInfo" class="text-gray-700 font-medium"> Page 1 </span> <button id="nextPageBtn" onclick="goToNextPage()" class="px-3 py-2 bg-gray-200 rounded-md hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed" > Next </button> </div> </div> <script> let allMembers = []; let currentUserId = null; let selectedPriorities = []; let searchTimeout; let currentPage = 1; let pageSize = 20; let hasMorePages = false; let editingTaskId = null; // Initialize the application document.addEventListener('DOMContentLoaded', async () => { // Load chat members first allMembers = await pt.getChatMembers(); // Identify current user (simplified - you may need different logic) const currentUser = allMembers.find(m => m.type === 'user' && m.is_owner); currentUserId = currentUser?.id; // Populate assignee dropdowns populateAssigneeDropdowns(); // Load user's saved filter configuration await loadFilterConfig(); // Apply filters and load tasks await applyFilters(); setupEventListeners(); }); // Populate assignee dropdowns with chat members function populateAssigneeDropdowns() { const users = allMembers.filter(m => m.type === 'user'); const options = users.map(user => { const selected = user.id === currentUserId ? 'selected' : ''; return `<option value="${user.id}" ${selected}>${user.name}</option>`; }).join(''); // Populate new task dropdown (current user selected by default) document.getElementById('assigneeInput').innerHTML = options; // Populate edit modal dropdown document.getElementById('editAssigneeInput').innerHTML = options; } // Load user's saved filter preferences from filter_config entity async function loadFilterConfig() { if (!currentUserId) return; try { const configs = await pt.list({ entityNames: ['filter_config'], filters: { creator_user_id: currentUserId } }); if (configs.length > 0) { const config = configs[0].data; if (config.searchTerm) { document.getElementById('searchInput').value = config.searchTerm; } if (config.statusFilter) { document.getElementById('statusFilter').value = config.statusFilter; } if (config.selectedPriorities && Array.isArray(config.selectedPriorities)) { selectedPriorities = config.selectedPriorities; const checkboxes = document.querySelectorAll('#priorityOptions input[type="checkbox"]'); checkboxes.forEach(cb => { cb.checked = selectedPriorities.includes(cb.value); }); updatePriorityLabel(); } if (config.pageSize) { pageSize = config.pageSize; } } } catch (error) { console.error('Error loading filter config:', error); } } // Save current filter preferences to filter_config entity async function saveFilterConfig() { if (!currentUserId) return; try { const searchTerm = document.getElementById('searchInput').value.trim(); const statusFilter = document.getElementById('statusFilter').value; const configs = await pt.list({ entityNames: ['filter_config'], filters: { creator_user_id: currentUserId } }); const configData = { searchTerm: searchTerm, statusFilter: statusFilter, selectedPriorities: selectedPriorities, pageSize: pageSize }; if (configs.length > 0) { await pt.edit(configs[0].id, configData); } else { await pt.add('filter_config', configData); } } catch (error) { console.error('Error saving filter config:', error); } } function setupEventListeners() { const searchInput = document.getElementById('searchInput'); searchInput.addEventListener('input', () => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => applyFilters(), 300); }); const statusFilter = document.getElementById('statusFilter'); statusFilter.addEventListener('change', () => applyFilters()); document.addEventListener('click', (e) => { const dropdown = document.getElementById('priorityOptions'); const button = document.getElementById('priorityDropdown'); if (!dropdown.contains(e.target) && !button.contains(e.target)) { dropdown.classList.add('hidden'); } }); } function togglePriorityDropdown() { document.getElementById('priorityOptions').classList.toggle('hidden'); } function updatePriorityLabel() { const label = document.getElementById('priorityLabel'); if (selectedPriorities.length === 0 || selectedPriorities.length === 3) { label.textContent = 'All Priorities'; } else { label.textContent = selectedPriorities.map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(', '); } } function updatePriorityFilter() { const checkboxes = document.querySelectorAll('#priorityOptions input[type="checkbox"]'); selectedPriorities = Array.from(checkboxes) .filter(cb => cb.checked) .map(cb => cb.value); updatePriorityLabel(); applyFilters(); } async function applyFilters(resetPage = true) { if (resetPage) { currentPage = 1; } const searchTerm = document.getElementById('searchInput').value.trim(); const statusFilter = document.getElementById('statusFilter').value; await saveFilterConfig(); let serverFilters = {}; if (searchTerm) { serverFilters.text = { $contains: searchTerm }; } if (selectedPriorities.length > 0 && selectedPriorities.length < 3) { serverFilters.priority = { $in: selectedPriorities }; } if (statusFilter === 'pending') { serverFilters.completed = { $ne: "true" }; } else if (statusFilter === 'completed') { serverFilters.completed = "true"; } try { const result = await pt.list({ entityNames: ['task'], filters: serverFilters, page: currentPage, pageSize: pageSize, returnMetadata: true }); let filteredTasks = result.entities.filter(entity => entity.entity_name === 'task'); if (statusFilter === 'overdue') { const today = new Date().toISOString().split('T')[0]; filteredTasks = filteredTasks.filter(task => task.data.due_date && task.data.due_date < today && task.data.completed !== "true" ); } hasMorePages = result.pagination.has_more; updatePaginationControls(); displayTasks(filteredTasks); } catch (error) { console.error('Error filtering tasks:', error); } } function updatePaginationControls() { document.getElementById('pageInfo').textContent = `Page ${currentPage}`; document.getElementById('firstPageBtn').disabled = currentPage === 1; document.getElementById('prevPageBtn').disabled = currentPage === 1; document.getElementById('nextPageBtn').disabled = !hasMorePages; } async function goToFirstPage() { currentPage = 1; await applyFilters(false); } async function goToPreviousPage() { if (currentPage > 1) { currentPage--; await applyFilters(false); } } async function goToNextPage() { if (hasMorePages) { currentPage++; await applyFilters(false); } } function displayTasks(tasks) { const tasksList = document.getElementById('tasksList'); if (tasks.length === 0) { tasksList.innerHTML = '<div class="text-center py-8 text-gray-500">No tasks found</div>'; return; } tasksList.innerHTML = tasks.map(task => { const taskData = task.data; const isCompleted = taskData.completed === "true"; const today = new Date().toISOString().split('T')[0]; const isOverdue = taskData.due_date && taskData.due_date < today && !isCompleted; const creator = allMembers.find(m => m.id === task.creator_user_id); const creatorName = creator ? creator.name : 'Unknown'; const assignee = allMembers.find(m => m.id === taskData.assignee_id); const assigneeName = assignee ? assignee.name : 'Unassigned'; let dueDateDisplay = ''; if (taskData.due_date) { const formattedDate = formatDate(taskData.due_date); dueDateDisplay = isOverdue ? `Due: ${formattedDate} (Overdue)` : `Due: ${formattedDate}`; } return ` <div class="bg-white rounded-lg shadow-md p-4 ${isCompleted ? 'opacity-75' : ''} cursor-pointer hover:shadow-lg transition-shadow"> <div class="flex items-center justify-between"> <div class="flex items-center space-x-3 flex-1" onclick="openEditModal(${task.id})"> <input type="checkbox" ${isCompleted ? 'checked' : ''} onchange="toggleTask(${task.id})" onclick="event.stopPropagation()" class="h-4 w-4 text-blue-600" > <span class="${isCompleted ? 'line-through text-gray-500' : 'text-gray-800'} flex-1"> ${escapeHtml(taskData.text)} </span> <span class="px-2 py-1 text-xs rounded-full ${getPriorityColor(taskData.priority)}"> ${taskData.priority} </span> ${dueDateDisplay ? `<span class="text-sm ${isOverdue ? 'text-red-500' : 'text-gray-500'}">${dueDateDisplay}</span>` : ''} </div> <div class="flex items-center gap-3 ml-3"> <div class="text-xs text-gray-500"> <div>Assigned: <span class="font-medium">${escapeHtml(assigneeName)}</span></div> <div class="text-gray-400 italic">By: ${escapeHtml(creatorName)}</div> </div> <button onclick="deleteTask(${task.id}); event.stopPropagation()" class="text-red-500 hover:text-red-700" > Delete </button> </div> </div> </div> `; }).join(''); } // Open edit modal with task data async function openEditModal(taskId) { editingTaskId = taskId; const task = await pt.get(taskId); document.getElementById('editTaskText').value = task.data.text || ''; document.getElementById('editAssigneeInput').value = task.data.assignee_id || ''; document.getElementById('editPriorityInput').value = task.data.priority || 'low'; document.getElementById('editDueDateInput').value = task.data.due_date || ''; document.getElementById('editModal').classList.remove('hidden'); } // Close edit modal function closeEditModal() { editingTaskId = null; document.getElementById('editModal').classList.add('hidden'); } // Save edited task async function saveTaskEdit() { if (!editingTaskId) return; try { const task = await pt.get(editingTaskId); const updatedData = { ...task.data, text: document.getElementById('editTaskText').value.trim(), assignee_id: parseInt(document.getElementById('editAssigneeInput').value), priority: document.getElementById('editPriorityInput').value, due_date: document.getElementById('editDueDateInput').value || null }; await pt.edit(editingTaskId, updatedData); closeEditModal(); await applyFilters(false); } catch (error) { console.error('Error saving task:', error); alert('Failed to save changes'); } } function quickFilter(type) { if (type === 'urgent') { document.getElementById('searchInput').value = 'urgent'; } else if (type === 'today') { const today = new Date().toISOString().split('T')[0]; document.getElementById('dueDateInput').value = today; } applyFilters(); } function clearFilters() { document.getElementById('searchInput').value = ''; document.getElementById('statusFilter').value = 'pending'; selectedPriorities = []; const checkboxes = document.querySelectorAll('#priorityOptions input[type="checkbox"]'); checkboxes.forEach(cb => cb.checked = false); document.getElementById('priorityLabel').textContent = 'All Priorities'; applyFilters(); } async function clearCompletedTasks() { const completedEntities = await pt.list({ entityNames: ['task'], filters: { completed: "true" }, limit: 1000 }); const completedTasks = completedEntities.filter(entity => entity.entity_name === 'task'); if (completedTasks.length === 0) { alert('No completed tasks to clear.'); return; } const confirmMessage = `Delete ${completedTasks.length} completed task${completedTasks.length > 1 ? 's' : ''}? This cannot be undone.`; if (confirm(confirmMessage)) { const deletePromises = completedTasks.map(task => pt.delete(task.id)); await Promise.all(deletePromises); await applyFilters(); alert(`Deleted ${completedTasks.length} completed task${completedTasks.length > 1 ? 's' : ''}.`); } } async function addTask() { const text = document.getElementById('taskInput').value.trim(); const assigneeId = document.getElementById('assigneeInput').value; const priority = document.getElementById('priorityInput').value; const dueDate = document.getElementById('dueDateInput').value; if (!text) return; try { await pt.add('task', { text: text, completed: "false", assignee_id: parseInt(assigneeId), priority: priority, due_date: dueDate || null }); // Only clear task text - preserve other fields for easier batch entry document.getElementById('taskInput').value = ''; await applyFilters(); } catch (error) { console.error('Error adding task:', error); } } async function toggleTask(taskId) { try { const task = await pt.get(taskId); await pt.edit(taskId, { ...task.data, completed: task.data.completed === "true" ? "false" : "true" }); await applyFilters(); } catch (error) { console.error('Error toggling task:', error); } } async function deleteTask(taskId) { if (confirm('Delete this task? This cannot be undone.')) { try { await pt.delete(taskId); await applyFilters(); } catch (error) { console.error('Error deleting task:', error); } } } function formatDate(dateString) { const date = new Date(dateString); const today = new Date(); if (date.toDateString() === today.toDateString()) { return 'Today'; } else if (date.toDateString() === new Date(today.getTime() + 86400000).toDateString()) { return 'Tomorrow'; } else { return date.toLocaleDateString(); } } function getPriorityColor(priority) { const colors = { high: 'bg-red-100 text-red-800', medium: 'bg-yellow-100 text-yellow-800', low: 'bg-green-100 text-green-800' }; return colors[priority] || colors.low; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } </script>

Key UX Improvements

  1. Efficient Batch Entry: Only the description field is cleared after adding a task

  2. Smart Date Display: Tasks due today show "Due: Today" without confusing suffixes

  3. Multi-Select Priority Filter: Select multiple priorities with efficient server-side filtering

  4. Default Pending Filter: Interface defaults to showing only pending tasks

  5. Bulk Operations: Clear all completed tasks at once with confirmation

  6. Performance Optimization: Server-side filtering for text search, status, and priorities

  7. Server-Side Pagination: Loads only 20 tasks per page for faster performance

Simple Todo Application

A minimal todo app for quick implementation:

<div class="container mx-auto p-6 max-w-2xl"> <h1 class="text-2xl font-bold mb-4">My Tasks</h1> <div class="flex gap-2 mb-4"> <input type="text" id="taskInput" class="flex-1 px-3 py-2 border rounded" placeholder="New task..." > <button onclick="addTask()" class="bg-blue-500 text-white px-4 py-2 rounded" > Add </button> </div> <div id="tasksList"></div> </div> <script> async function loadTasks() { const entities = await pt.list({ entityNames: ['task'], filters: { completed: false } }); const tasks = entities.filter(e => e.entity_name === 'task'); document.getElementById('tasksList').innerHTML = tasks.map(task => ` <div class="bg-white p-4 rounded shadow mb-2 flex justify-between items-center"> <div class="flex items-center gap-3"> <input type="checkbox" onchange="toggleTask(${task.id})" class="h-4 w-4" > <span>${task.data.text}</span> </div> <button onclick="deleteTask(${task.id})" class="text-red-500"> Delete </button> </div> `).join(''); } async function addTask() { const text = document.getElementById('taskInput').value.trim(); if (!text) return; await pt.add('task', { text: text, completed: false }); document.getElementById('taskInput').value = ''; await loadTasks(); } async function toggleTask(taskId) { const task = await pt.get(taskId); await pt.edit(taskId, { ...task.data, completed: !task.data.completed }); await loadTasks(); } async function deleteTask(taskId) { await pt.delete(taskId); await loadTasks(); } document.addEventListener('DOMContentLoaded', loadTasks); </script>

Note-Taking Application

A simple note-taking app with markdown support:

<div class="container mx-auto p-6"> <h1 class="text-2xl font-bold mb-4">My Notes</h1> <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <!-- Notes List --> <div class="md:col-span-1"> <button onclick="createNewNote()" class="w-full bg-blue-500 text-white px-4 py-2 rounded mb-4"> New Note </button> <div id="notesList" class="space-y-2"></div> </div> <!-- Note Editor --> <div class="md:col-span-2"> <div id="noteEditor" class="bg-white rounded-lg shadow p-6"> <input type="text" id="noteTitle" placeholder="Note title..." class="w-full text-xl font-bold mb-4 px-3 py-2 border rounded" > <textarea id="noteContent" rows="20" placeholder="Start typing..." class="w-full px-3 py-2 border rounded" ></textarea> <div class="flex gap-2 mt-4"> <button onclick="saveNote()" class="bg-green-500 text-white px-4 py-2 rounded"> Save </button> <button onclick="deleteCurrentNote()" class="bg-red-500 text-white px-4 py-2 rounded"> Delete </button> </div> </div> </div> </div> </div> <script> let currentNoteId = null; async function loadNotes() { const entities = await pt.list({ entityNames: ['note'], limit: 100 }); const notes = entities.filter(e => e.entity_name === 'note'); document.getElementById('notesList').innerHTML = notes.map(note => ` <div onclick="selectNote(${note.id})" class="bg-white p-3 rounded shadow cursor-pointer hover:bg-gray-50 ${currentNoteId === note.id ? 'border-2 border-blue-500' : ''}" > <div class="font-semibold">${note.data.title || 'Untitled'}</div> <div class="text-xs text-gray-500">${new Date(note.updated_at).toLocaleDateString()}</div> </div> `).join(''); } async function selectNote(noteId) { const note = await pt.get(noteId); currentNoteId = noteId; document.getElementById('noteTitle').value = note.data.title || ''; document.getElementById('noteContent').value = note.data.content || ''; await loadNotes(); } async function createNewNote() { const result = await pt.add('note', { title: 'New Note', content: '' }); currentNoteId = result.id; await loadNotes(); await selectNote(result.id); } async function saveNote() { if (!currentNoteId) { await createNewNote(); return; } const title = document.getElementById('noteTitle').value.trim(); const content = document.getElementById('noteContent').value; const note = await pt.get(currentNoteId); await pt.edit(currentNoteId, { ...note.data, title: title, content: content }); await loadNotes(); } async function deleteCurrentNote() { if (!currentNoteId) return; if (confirm('Delete this note?')) { await pt.delete(currentNoteId); currentNoteId = null; document.getElementById('noteTitle').value = ''; document.getElementById('noteContent').value = ''; await loadNotes(); } } document.addEventListener('DOMContentLoaded', loadNotes); // Auto-save every 30 seconds setInterval(() => { if (currentNoteId) { saveNote(); } }, 30000); </script>

Contact Management

A simple CRM for managing contacts:

<div class="container mx-auto p-6"> <h1 class="text-2xl font-bold mb-4">Contacts</h1> <!-- Add Contact Form --> <div class="bg-white rounded-lg shadow p-6 mb-6"> <h2 class="text-lg font-semibold mb-4">Add Contact</h2> <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <input type="text" id="contactName" placeholder="Name" class="px-3 py-2 border rounded"> <input type="email" id="contactEmail" placeholder="Email" class="px-3 py-2 border rounded"> <input type="tel" id="contactPhone" placeholder="Phone" class="px-3 py-2 border rounded"> <input type="text" id="contactCompany" placeholder="Company" class="px-3 py-2 border rounded"> </div> <button onclick="addContact()" class="mt-4 bg-blue-500 text-white px-4 py-2 rounded"> Add Contact </button> </div> <!-- Search --> <input type="text" id="searchInput" placeholder="Search contacts..." class="w-full px-3 py-2 border rounded mb-4" > <!-- Contacts Grid --> <div id="contactsGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"></div> </div> <script> let searchTimeout; async function loadContacts() { const searchTerm = document.getElementById('searchInput').value.trim(); const filters = searchTerm ? { $or: [ { name: { $contains: searchTerm } }, { email: { $contains: searchTerm } }, { company: { $contains: searchTerm } } ] } : {}; const entities = await pt.list({ entityNames: ['contact'], filters: filters, limit: 100 }); const contacts = entities.filter(e => e.entity_name === 'contact'); document.getElementById('contactsGrid').innerHTML = contacts.map(contact => ` <div class="bg-white rounded-lg shadow p-4"> <h3 class="font-semibold text-lg">${contact.data.name}</h3> <p class="text-sm text-gray-600">${contact.data.email || ''}</p> <p class="text-sm text-gray-600">${contact.data.phone || ''}</p> <p class="text-sm text-gray-500">${contact.data.company || ''}</p> <button onclick="deleteContact(${contact.id})" class="mt-2 text-red-500 text-sm"> Delete </button> </div> `).join(''); } async function addContact() { const name = document.getElementById('contactName').value.trim(); const email = document.getElementById('contactEmail').value.trim(); const phone = document.getElementById('contactPhone').value.trim(); const company = document.getElementById('contactCompany').value.trim(); if (!name) return; await pt.add('contact', { name: name, email: email, phone: phone, company: company }); document.getElementById('contactName').value = ''; document.getElementById('contactEmail').value = ''; document.getElementById('contactPhone').value = ''; document.getElementById('contactCompany').value = ''; await loadContacts(); } async function deleteContact(contactId) { if (confirm('Delete this contact?')) { await pt.delete(contactId); await loadContacts(); } } document.getElementById('searchInput').addEventListener('input', () => { clearTimeout(searchTimeout); searchTimeout = setTimeout(loadContacts, 300); }); document.addEventListener('DOMContentLoaded', loadContacts); </script>

Chat Integration - Messages and File Upload

This example demonstrates how to send messages to the chat and upload files from your Live Page.

Features

  • Send text messages to the chat from your Live Page

  • Upload files with optional messages

  • Drag and drop file upload support

  • File preview and validation

  • Status notifications

Complete Implementation

<div class="container mx-auto p-6 max-w-4xl"> <h1 class="text-3xl font-bold text-gray-800 mb-8">Chat Integration Demo</h1> <!-- Send Message Section --> <div class="bg-white rounded-lg shadow-md p-6 mb-6"> <h2 class="text-xl font-semibold mb-4">Send Message to Chat</h2> <div class="flex gap-2 mb-4"> <input type="text" id="chatInput" placeholder="Type a message..." class="flex-1 border rounded px-3 py-2" onkeypress="if(event.key==='Enter') sendChatMessage()" > <button onclick="sendChatMessage()" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" > Send </button> </div> <!-- Quick Message Buttons --> <div class="flex flex-wrap gap-2"> <button onclick="pt.addMessage('Task completed!')" class="bg-green-100 text-green-800 px-3 py-1 rounded text-sm hover:bg-green-200" > Quick: Task Completed </button> <button onclick="pt.addMessage('Need assistance with this task')" class="bg-yellow-100 text-yellow-800 px-3 py-1 rounded text-sm hover:bg-yellow-200" > Quick: Need Help </button> <button onclick="pt.addMessage('Review requested')" class="bg-purple-100 text-purple-800 px-3 py-1 rounded text-sm hover:bg-purple-200" > Quick: Review Requested </button> </div> </div> <!-- File Upload Section --> <div class="bg-white rounded-lg shadow-md p-6 mb-6"> <h2 class="text-xl font-semibold mb-4">Upload Files to Chat</h2> <!-- Standard Form Upload --> <form id="uploadForm" onsubmit="handleUpload(event)" class="mb-6"> <div class="mb-4"> <label class="block text-sm font-medium mb-2">Select Files</label> <input type="file" name="files" multiple class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100" onchange="previewFiles(this)" > </div> <div class="mb-4"> <label class="block text-sm font-medium mb-2">Message (optional)</label> <input type="text" name="message" placeholder="Add a message with your files..." class="border rounded px-3 py-2 w-full" > </div> <div id="filePreview" class="mb-4"></div> <button type="submit" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600" > Upload Files </button> </form> <!-- Drag and Drop Zone --> <div id="dropZone" ondrop="handleDrop(event)" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center transition-colors" > <svg class="mx-auto h-12 w-12 text-gray-400 mb-3" stroke="currentColor" fill="none" viewBox="0 0 48 48"> <path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> </svg> <p class="text-gray-600 font-medium mb-1">Drop files here to upload</p> <p class="text-gray-400 text-sm">or use the file selector above</p> </div> </div> <!-- Status Messages --> <div id="statusMessage" class="hidden rounded-lg p-4 mb-4"></div> <!-- Recent Actions Log --> <div class="bg-white rounded-lg shadow-md p-6"> <h2 class="text-xl font-semibold mb-4">Recent Actions</h2> <div id="actionsLog" class="space-y-2 max-h-64 overflow-y-auto"></div> </div> </div> <script> const actionsLog = []; // Send message from input async function sendChatMessage() { const input = document.getElementById('chatInput'); const message = input.value.trim(); if (!message) { showStatus('Please enter a message', 'error'); return; } try { const result = await pt.addMessage(message); input.value = ''; showStatus('Message sent successfully!', 'success'); logAction('Message sent', message); } catch (error) { showStatus('Failed to send message: ' + error.message, 'error'); console.error('Error:', error); } } // Handle form upload async function handleUpload(event) { event.preventDefault(); const form = event.target; const fileInput = form.querySelector('input[type="file"]'); if (fileInput.files.length === 0) { showStatus('Please select at least one file', 'error'); return; } try { const result = await pt.uploadFiles(form); showStatus(`Successfully uploaded ${result.files_count} file(s)!`, 'success'); logAction('File upload', `${result.files_count} file(s) uploaded`); form.reset(); document.getElementById('filePreview').innerHTML = ''; } catch (error) { showStatus('Upload failed: ' + error.message, 'error'); console.error('Upload error:', error); } } // Handle drag and drop async function handleDrop(event) { event.preventDefault(); const dropZone = document.getElementById('dropZone'); dropZone.classList.remove('border-blue-500', 'bg-blue-50'); const files = event.dataTransfer.files; if (files.length === 0) return; const formData = new FormData(); for (const file of files) { formData.append('files', file); } try { const result = await pt.uploadFiles(formData, 'Files uploaded via drag & drop'); showStatus(`Uploaded ${result.files_count} file(s) via drag & drop`, 'success'); logAction('Drag & drop upload', `${result.files_count} file(s)`); } catch (error) { showStatus('Upload failed: ' + error.message, 'error'); console.error('Upload error:', error); } } function handleDragOver(event) { event.preventDefault(); const dropZone = document.getElementById('dropZone'); dropZone.classList.add('border-blue-500', 'bg-blue-50'); } function handleDragLeave(event) { const dropZone = document.getElementById('dropZone'); dropZone.classList.remove('border-blue-500', 'bg-blue-50'); } // Preview selected files function previewFiles(input) { const preview = document.getElementById('filePreview'); const files = input.files; if (files.length === 0) { preview.innerHTML = ''; return; } const fileList = Array.from(files).map(file => { const sizeKB = (file.size / 1024).toFixed(2); return ` <div class="flex items-center gap-2 text-sm text-gray-600 bg-gray-50 p-2 rounded"> <svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd" /> </svg> <span class="flex-1">${file.name}</span> <span class="text-gray-400">${sizeKB} KB</span> </div> `; }).join(''); preview.innerHTML = ` <div class="space-y-1"> <p class="text-sm font-medium text-gray-700 mb-2">${files.length} file(s) selected:</p> ${fileList} </div> `; } // Show status message function showStatus(message, type = 'success') { const statusDiv = document.getElementById('statusMessage'); const bgColor = type === 'success' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'; statusDiv.className = `rounded-lg p-4 mb-4 ${bgColor}`; statusDiv.textContent = message; statusDiv.classList.remove('hidden'); setTimeout(() => { statusDiv.classList.add('hidden'); }, 5000); } // Log action function logAction(action, details) { const timestamp = new Date().toLocaleTimeString(); actionsLog.unshift({ action, details, timestamp }); // Keep only last 10 actions if (actionsLog.length > 10) { actionsLog.pop(); } updateActionsLog(); } function updateActionsLog() { const logDiv = document.getElementById('actionsLog'); if (actionsLog.length === 0) { logDiv.innerHTML = '<p class="text-gray-400 text-sm">No actions yet</p>'; return; } logDiv.innerHTML = actionsLog.map(log => ` <div class="flex items-start gap-3 p-3 bg-gray-50 rounded"> <div class="flex-1"> <div class="font-medium text-sm">${log.action}</div> <div class="text-sm text-gray-600">${log.details}</div> </div> <div class="text-xs text-gray-400">${log.timestamp}</div> </div> `).join(''); } // Initialize document.addEventListener('DOMContentLoaded', () => { updateActionsLog(); }); </script>

Key Features Explained

Send Messages:

  • Text input with Enter key support

  • Quick message buttons for common actions

  • Instant feedback on message delivery

File Upload:

  • Traditional file selector with multiple file support

  • Optional message to accompany uploads

  • File preview with size information

  • Drag and drop upload zone

  • Visual feedback during drag operations

Status Notifications:

  • Success and error messages

  • Auto-hide after 5 seconds

  • Clear visual distinction

Actions Log:

  • Tracks recent actions (messages and uploads)

  • Displays timestamps

  • Limited to last 10 actions

Use Cases

  1. Project Management: Send status updates and upload deliverables

  2. Support Systems: Submit issues with file attachments

  3. Collaboration: Share files and communicate progress

  4. Reporting: Upload reports and notify team members

  5. Feedback Collection: Submit feedback with supporting documents

Document Management and Notifications

This comprehensive example demonstrates document search, viewing, creation, and push notifications.

Features

  • Search documents and collections using semantic search

  • View document content with optional range selection

  • Create and save documents in multiple formats (TXT, MD, PDF, DOCX, CSV, XLSX)

  • Send push notifications to team members

  • Format-specific MIME type handling

  • Real-time status updates

Complete Implementation

<div class="container mx-auto p-6 max-w-6xl"> <h1 class="text-3xl font-bold text-gray-800 mb-8">Document Management System</h1> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> <!-- Search Documents --> <div class="bg-white rounded-lg shadow-md p-6"> <h2 class="text-xl font-semibold mb-4 flex items-center"> <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> Search Documents </h2> <input type="text" id="searchQuery" placeholder="What are you looking for?" class="border rounded px-3 py-2 w-full mb-3" > <select id="searchScope" class="border rounded px-3 py-2 w-full mb-3"> <option value="ALL">All (Documents & Collections)</option> <option value="DOCUMENTS_ONLY">Documents Only</option> <option value="COLLECTIONS_ONLY">Collections Only</option> </select> <button onclick="searchDocuments()" class="bg-blue-500 text-white px-4 py-2 rounded w-full hover:bg-blue-600" > Search </button> <div id="searchResults" class="mt-4 hidden"> <h3 class="font-semibold mb-2">Results:</h3> <div class="bg-gray-50 rounded p-3 max-h-80 overflow-auto"> <pre id="resultsContent" class="text-sm whitespace-pre-wrap"></pre> </div> </div> </div> <!-- View Document --> <div class="bg-white rounded-lg shadow-md p-6"> <h2 class="text-xl font-semibold mb-4 flex items-center"> <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path> </svg> View Document </h2> <input type="number" id="viewDocId" placeholder="Document ID" class="border rounded px-3 py-2 w-full mb-3" > <div class="grid grid-cols-2 gap-2 mb-3"> <input type="number" id="fromChar" placeholder="From (optional)" class="border rounded px-3 py-2" > <input type="number" id="toChar" placeholder="To (optional)" class="border rounded px-3 py-2" > </div> <button onclick="viewDocument()" class="bg-green-500 text-white px-4 py-2 rounded w-full hover:bg-green-600" > View Text </button> <div id="docView" class="mt-4 hidden"> <h3 class="font-semibold mb-2">Document Content:</h3> <div class="bg-gray-50 rounded p-3 max-h-80 overflow-auto"> <pre id="docText" class="text-sm whitespace-pre-wrap"></pre> </div> </div> </div> <!-- Create Document --> <div class="bg-white rounded-lg shadow-md p-6"> <h2 class="text-xl font-semibold mb-4 flex items-center"> <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> </svg> Create Document </h2> <input type="text" id="docFilename" placeholder="Filename (e.g., report.txt)" class="border rounded px-3 py-2 w-full mb-3" > <select id="docFormat" class="border rounded px-3 py-2 w-full mb-3" onchange="updateMimetype()"> <option value="TXT">TXT - Plain Text</option> <option value="MD">MD - Markdown</option> <option value="HTML">HTML</option> <option value="DOCX">DOCX - Word Document (use Markdown)</option> <option value="PDF">PDF (use Markdown)</option> <option value="CSV">CSV</option> <option value="XLSX">XLSX - Excel (use CSV format)</option> <option value="CUSTOM">CUSTOM</option> </select> <input type="text" id="docMimetype" value="text/plain" class="border rounded px-3 py-2 w-full mb-3 text-sm text-gray-600" readonly > <textarea id="docContent" rows="8" placeholder="Enter document content..." class="border rounded px-3 py-2 w-full mb-3 font-mono text-sm" ></textarea> <button onclick="saveDocument()" class="bg-purple-500 text-white px-4 py-2 rounded w-full hover:bg-purple-600" > Save Document </button> </div> <!-- Send Notification --> <div class="bg-white rounded-lg shadow-md p-6"> <h2 class="text-xl font-semibold mb-4 flex items-center"> <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path> </svg> Send Notification </h2> <select id="userSelect" class="border rounded px-3 py-2 w-full mb-3"> <option value="">Select a user...</option> </select> <input type="text" id="notifTitle" placeholder="Notification title" class="border rounded px-3 py-2 w-full mb-3" > <textarea id="notifText" rows="4" placeholder="Notification message" class="border rounded px-3 py-2 w-full mb-3" ></textarea> <button onclick="sendNotification()" class="bg-orange-500 text-white px-4 py-2 rounded w-full hover:bg-orange-600" > Send Notification </button> </div> </div> <!-- Status Messages --> <div id="statusMessage" class="hidden mt-6 rounded-lg p-4"></div> </div> <script> // MIME type mapping const mimeTypes = { 'TXT': 'text/plain', 'MD': 'text/markdown', 'HTML': 'text/html', 'DOCX': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'PDF': 'application/pdf', 'CSV': 'text/csv', 'XLSX': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'CUSTOM': 'text/plain' }; // Load users on page load async function loadUsers() { try { const members = await pt.getChatMembers(); const select = document.getElementById('userSelect'); members .filter(m => m.type === 'user') .forEach(member => { const option = document.createElement('option'); option.value = member.id; option.textContent = member.name; select.appendChild(option); }); } catch (error) { console.error('Error loading users:', error); } } function updateMimetype() { const format = document.getElementById('docFormat').value; document.getElementById('docMimetype').value = mimeTypes[format] || 'text/plain'; } // Search documents async function searchDocuments() { const query = document.getElementById('searchQuery').value.trim(); const scope = document.getElementById('searchScope').value; if (!query) { showStatus('Please enter a search query', 'error'); return; } try { const result = await pt.searchDocuments(query, scope); document.getElementById('resultsContent').textContent = result.results; document.getElementById('searchResults').classList.remove('hidden'); showStatus('Search completed successfully', 'success'); } catch (error) { showStatus('Search failed: ' + error.message, 'error'); console.error('Search error:', error); } } // View document async function viewDocument() { const docId = parseInt(document.getElementById('viewDocId').value); const fromChar = document.getElementById('fromChar').value; const toChar = document.getElementById('toChar').value; if (!docId) { showStatus('Please enter a document ID', 'error'); return; } try { const options = {}; if (fromChar) options.from = parseInt(fromChar); if (toChar) options.to = parseInt(toChar); const result = await pt.getDocumentText(docId, options); if (result.text) { document.getElementById('docText').textContent = result.text; document.getElementById('docView').classList.remove('hidden'); showStatus('Document loaded successfully', 'success'); } else { showStatus(result.message || 'No text available', 'error'); } } catch (error) { showStatus('Error loading document: ' + error.message, 'error'); console.error('View error:', error); } } // Save document async function saveDocument() { const filename = document.getElementById('docFilename').value.trim(); const format = document.getElementById('docFormat').value; const mimetype = document.getElementById('docMimetype').value; const content = document.getElementById('docContent').value; if (!filename || !content) { showStatus('Please provide filename and content', 'error'); return; } try { const result = await pt.saveDocument(filename, format, mimetype, content); showStatus('Document saved successfully: ' + result.filename, 'success'); // Clear form document.getElementById('docFilename').value = ''; document.getElementById('docContent').value = ''; } catch (error) { showStatus('Error saving document: ' + error.message, 'error'); console.error('Save error:', error); } } // Send notification async function sendNotification() { const userId = parseInt(document.getElementById('userSelect').value); const title = document.getElementById('notifTitle').value.trim(); const text = document.getElementById('notifText').value.trim(); if (!userId || !title || !text) { showStatus('Please fill all notification fields', 'error'); return; } try { await pt.sendNotification(userId, title, text); showStatus('Notification sent successfully!', 'success'); // Clear form document.getElementById('notifTitle').value = ''; document.getElementById('notifText').value = ''; } catch (error) { showStatus('Error sending notification: ' + error.message, 'error'); console.error('Notification error:', error); } } // Show status message function showStatus(message, type = 'success') { const statusDiv = document.getElementById('statusMessage'); const bgColor = type === 'success' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'; statusDiv.className = `mt-6 rounded-lg p-4 ${bgColor}`; statusDiv.textContent = message; statusDiv.classList.remove('hidden'); setTimeout(() => { statusDiv.classList.add('hidden'); }, 5000); } // Initialize document.addEventListener('DOMContentLoaded', () => { loadUsers(); }); </script>

Key Features Explained

Document Search:

  • Semantic search across documents and collections

  • Scope selection (all, documents only, collections only)

  • XML-formatted search results display

Document Viewer:

  • Load full document text or specific character ranges

  • Useful for previewing large documents

  • Text-based content display

Document Creator:

  • Multiple format support (TXT, MD, HTML, DOCX, PDF, CSV, XLSX)

  • Automatic MIME type selection

  • Markdown to Word/PDF conversion

  • CSV to Excel conversion

Push Notifications:

  • Send notifications to specific users

  • Custom title and message

  • User selection from chat members

Practical Use Cases

  1. Knowledge Base: Search documentation and create new articles

  2. Report Generation: Create PDF/Word reports from data

  3. Team Collaboration: Notify team members of important updates

  4. Document Library: Browse and view document contents

  5. Data Export: Export data to CSV/Excel formats

AI-Powered Database Automation

This example demonstrates how to combine Live Page actions with AI-powered database operations. The AI can process your instructions and automatically manage database entities.

Features

  • Upload files and have AI extract structured data automatically

  • Send instructions to process information and store in database

  • Combine multiple operations in a single AI request

  • Get intelligent data validation and error handling

Complete Implementation: Intelligent Document Processor

<div class="container mx-auto p-6 max-w-6xl"> <h1 class="text-3xl font-bold text-gray-800 mb-8">AI-Powered Data Processor</h1> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> <!-- Upload Files for AI Processing --> <div class="bg-white rounded-lg shadow-md p-6"> <h2 class="text-xl font-semibold mb-4">Upload & Extract Data</h2> <div class="mb-4"> <label class="block text-sm font-medium mb-2">Select Processing Type</label> <select id="processingType" class="border rounded px-3 py-2 w-full" onchange="updateInstructions()"> <option value="invoice">Invoice Processing</option> <option value="resume">Resume Parsing</option> <option value="receipt">Expense Receipts</option> <option value="meeting">Meeting Notes</option> <option value="contacts">Contact Import</option> <option value="custom">Custom Instructions</option> </select> </div> <div class="mb-4"> <label class="block text-sm font-medium mb-2">Upload Files</label> <input type="file" id="fileInput" multiple class="block w-full text-sm" onchange="previewFiles()" > <div id="filePreview" class="mt-2 text-sm text-gray-600"></div> </div> <div class="mb-4"> <label class="block text-sm font-medium mb-2">AI Instructions</label> <textarea id="aiInstructions" rows="8" class="border rounded px-3 py-2 w-full font-mono text-sm" placeholder="Enter custom instructions for the AI..." ></textarea> </div> <button onclick="processFiles()" class="bg-blue-500 text-white px-6 py-2 rounded w-full hover:bg-blue-600" > Process with AI </button> </div> <!-- Direct AI Commands --> <div class="bg-white rounded-lg shadow-md p-6"> <h2 class="text-xl font-semibold mb-4">Direct AI Commands</h2> <div class="space-y-4"> <!-- Quick Actions --> <div> <h3 class="font-medium mb-2">Quick Actions</h3> <div class="grid grid-cols-2 gap-2"> <button onclick="quickAction('competitors')" class="bg-green-100 text-green-800 px-3 py-2 rounded text-sm hover:bg-green-200" > Research Competitors </button> <button onclick="quickAction('tasks')" class="bg-blue-100 text-blue-800 px-3 py-2 rounded text-sm hover:bg-blue-200" > Analyze Tasks </button> <button onclick="quickAction('summary')" class="bg-purple-100 text-purple-800 px-3 py-2 rounded text-sm hover:bg-purple-200" > Generate Summary </button> <button onclick="quickAction('export')" class="bg-orange-100 text-orange-800 px-3 py-2 rounded text-sm hover:bg-orange-200" > Export Data </button> </div> </div> <!-- Custom Command --> <div> <h3 class="font-medium mb-2">Custom AI Command</h3> <textarea id="customCommand" rows="6" placeholder="Enter your command for the AI assistant..." class="border rounded px-3 py-2 w-full text-sm" ></textarea> <button onclick="sendCustomCommand()" class="bg-indigo-500 text-white px-4 py-2 rounded w-full mt-2 hover:bg-indigo-600" > Execute Command </button> </div> </div> <div class="mt-6 p-4 bg-gray-50 rounded"> <h3 class="font-medium mb-2">Example Commands:</h3> <ul class="text-sm text-gray-700 space-y-1"> <li>• "Find all overdue tasks and create urgent reminders"</li> <li>• "Search for industry trends and save as research notes"</li> <li>• "Analyze recent expenses and categorize them"</li> <li>• "Extract key contacts from meeting notes"</li> </ul> </div> </div> </div> <!-- Status Messages --> <div id="statusMessage" class="hidden mt-6 rounded-lg p-4"></div> </div> <script> // Predefined instruction templates const instructionTemplates = { invoice: `Extract invoice information and use the tool 'chatdb_add' to create database records: - entity_name: "invoice" - data: { invoice_number: string, date: string, vendor: string, amount: number, due_date: string, line_items: array }`, resume: `Extract candidate information from resumes and use the tool 'chatdb_add' to create entries: - entity_name: "candidate" - data: { name: string, email: string, phone: string, years_experience: number, skills: array, education: string, previous_companies: array }`, receipt: `Extract expense data from receipts and use the tool 'chatdb_add' to store each: - entity_name: "expense" - data: { date: string, merchant: string, category: string, amount: number, currency: string }`, meeting: `Extract action items from meeting notes and use the tool 'chatdb_add' to create entries: - entity_name: "action_item" - data: { description: string, assigned_to: string, due_date: string, priority: string }`, contacts: `Parse contact information and use the tool 'chatdb_add' to create entries: - entity_name: "contact" - data: { name: string, email: string, phone: string, company: string, role: string }` }; function updateInstructions() { const type = document.getElementById('processingType').value; const textarea = document.getElementById('aiInstructions'); if (type !== 'custom' && instructionTemplates[type]) { textarea.value = instructionTemplates[type]; } else if (type === 'custom') { textarea.value = ''; textarea.placeholder = 'Enter your custom instructions...'; } } function previewFiles() { const input = document.getElementById('fileInput'); const preview = document.getElementById('filePreview'); if (input.files.length === 0) { preview.textContent = ''; return; } const fileList = Array.from(input.files) .map(f => f.name) .join(', '); preview.textContent = `${input.files.length} file(s) selected: ${fileList}`; } async function processFiles() { const input = document.getElementById('fileInput'); const instructions = document.getElementById('aiInstructions').value.trim(); if (input.files.length === 0) { showStatus('Please select files to process', 'error'); return; } if (!instructions) { showStatus('Please provide AI instructions', 'error'); return; } try { const formData = new FormData(); for (const file of input.files) { formData.append('files', file); } await pt.uploadFiles(formData, instructions); showStatus( `Files uploaded successfully! The AI is processing ${input.files.length} file(s) and will store the data according to your instructions.`, 'success' ); // Clear form input.value = ''; document.getElementById('filePreview').textContent = ''; } catch (error) { showStatus('Error processing files: ' + error.message, 'error'); } } async function quickAction(type) { const commands = { competitors: `Search the web for the top 5 competitors in the AI assistant market and use the tool 'chatdb_add' to create database entries: - entity_name: "competitor" - data: { name: string, website: string, key_features: array, pricing_model: string, market_position: string } Research each competitor and provide comprehensive information.`, tasks: `Use the tool 'chatdb_list' to find all tasks with status "pending" or "in_progress". For any task that is overdue or high priority, use the tool 'chatdb_add' to create: - entity_name: "urgent_action" - data: { task_id: number, title: string, days_overdue: number, priority: string, recommended_action: string }`, summary: `Use the tool 'chatdb_list' to review all activities from the past week. Then use the tool 'chatdb_add' to create a weekly summary: - entity_name: "weekly_summary" - data: { week_ending: string, tasks_completed: number, documents_created: number, key_highlights: array, upcoming_priorities: array }`, export: `Use the tool 'chatdb_list' to get all contacts, then export them to a CSV file with columns: name, email, phone, company, tags. Save it as "contacts_export_[date].csv" using the appropriate document creation tool and notify me when complete.` }; try { await pt.addMessage(commands[type]); showStatus('AI command sent successfully! Processing...', 'success'); } catch (error) { showStatus('Error: ' + error.message, 'error'); } } async function sendCustomCommand() { const command = document.getElementById('customCommand').value.trim(); if (!command) { showStatus('Please enter a command', 'error'); return; } try { await pt.addMessage(command); showStatus('Custom command sent to AI assistant!', 'success'); document.getElementById('customCommand').value = ''; } catch (error) { showStatus('Error: ' + error.message, 'error'); } } function showStatus(message, type = 'success') { const statusDiv = document.getElementById('statusMessage'); const bgColor = type === 'success' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'; statusDiv.className = `mt-6 rounded-lg p-4 ${bgColor}`; statusDiv.textContent = message; statusDiv.classList.remove('hidden'); setTimeout(() => { statusDiv.classList.add('hidden'); }, 8000); } // Initialize document.addEventListener('DOMContentLoaded', () => { updateInstructions(); }); </script>

Key Features Explained

AI-Powered File Processing:

  • Upload files with specific extraction instructions

  • AI automatically parses content and creates database records

  • Supports multiple file types and batch processing

  • Intelligent error handling and data validation

Direct AI Commands:

  • Send natural language instructions to the AI

  • AI can search, analyze, and store data autonomously

  • Quick action buttons for common workflows

  • Custom command interface for flexibility

Real-World Applications:

  1. Invoice Processing: Upload invoices, AI extracts data, stores in database

  2. Resume Screening: Batch process resumes, extract candidate info

  3. Expense Management: Upload receipts, AI categorizes and stores expenses

  4. Meeting Notes: Upload recordings/notes, AI extracts action items

  5. Data Migration: Upload spreadsheets, AI imports and validates data

  6. Competitive Research: AI searches and compiles competitor information

  7. Task Analysis: AI reviews tasks and creates urgency reports

  8. Report Generation: AI compiles data and generates formatted reports

Benefits

  • Automation: Eliminate manual data entry and processing

  • Intelligence: AI understands context and validates data

  • Flexibility: Use natural language to describe what you need

  • Batch Processing: Handle multiple files/records at once

  • Error Handling: AI catches and reports data issues

  • Structured Storage: Data automatically organized in database

Document Processing with Parallel Upload

This advanced example demonstrates how to handle multiple file uploads concurrently with AI-powered topic extraction. Files are processed in parallel for maximum performance, with the AI extracting topics and returning structured JSON responses.

Features

  • Upload multiple PDF/DOCX files concurrently (parallel processing)

  • AI extracts topics and returns structured JSON

  • Store file metadata with processing status

  • "Check" button for files still waiting for processing

  • Auto-refresh table every 10 seconds

  • Document text preview modal

  • Handles large document processing delays gracefully

  • Includes both parallel and serial upload examples

Use Case

Perfect for applications where:

  • You need to upload and process multiple documents at once

  • AI should extract information (topics, metadata, etc.) from documents

  • You want fast, concurrent processing instead of sequential

  • Documents may or may not have text immediately available

  • Users can manually re-check documents that weren't ready on first upload

Complete Implementation

<div class="container mx-auto p-6 max-w-4xl"> <!-- Header --> <div class="flex items-center justify-between mb-6"> <div class="flex items-center gap-3 text-gray-700"> <h1 class="text-xl md:text-2xl font-bold">Topic extractor</h1> </div> <button class="px-3 py-2 rounded bg-gray-100 hover:bg-gray-200 text-sm" id="refreshBtn">Refresh</button> </div> <!-- Uploader --> <div class="bg-white rounded-xl shadow p-4"> <p class="text-gray-600 text-sm mb-3">Upload .pdf or .docx (≤ 1MB). The AI will wait up to 30s for text extraction. If ready, it will write a Topic and add a DB row (entity: <code>file_topic</code>). If not ready in time, it will add a row with upload_status="waiting".</p> <div class="flex flex-col md:flex-row md:items-center gap-3"> <input accept=".pdf,.docx" class="block w-full text-sm text-gray-700" id="fileInput" multiple type="file" /> <button class="px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700 flex items-center gap-2" id="uploadBtn"> <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1M12 12V4m0 0l-3 3m3-3l3 3" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path> </svg> Upload </button> </div> <div class="hidden mt-3 rounded p-3 text-sm" id="uploadNotice"></div> </div> <!-- Table --> <div class="mt-6 bg-white rounded-xl shadow p-4"> <div class="flex items-center justify-between mb-3"> <div class="text-sm text-gray-500">Auto-refreshes every 10s</div> </div> <div class="overflow-x-auto"> <table class="min-w-full divide-y divide-gray-200"> <thead class="bg-gray-50"> <tr> <th class="px-4 py-2 text-left text-xs font-semibold text-gray-600">File Name</th> <th class="px-4 py-2 text-left text-xs font-semibold text-gray-600">Topic</th> <th class="px-4 py-2 text-left text-xs font-semibold text-gray-600">Uploaded At</th> <th class="px-4 py-2 text-left text-xs font-semibold text-gray-600">Upload Status</th> <th class="px-4 py-2 text-left text-xs font-semibold text-gray-600">Actions</th> </tr> </thead> <tbody class="divide-y divide-gray-100 bg-white" id="rows"></tbody> </table> </div> <div class="hidden text-center text-gray-500 text-sm py-8" id="emptyState">No uploads yet</div> </div> <!-- Viewer Modal --> <div class="hidden fixed inset-0 bg-black/50 z-50 items-center justify-center" id="viewerModal"> <div class="bg-white rounded-lg shadow-xl max-w-3xl w-full mx-4"> <div class="flex items-center justify-between p-4 border-b"> <h3 class="font-semibold text-gray-800">Document Preview</h3> <button class="text-gray-500 hover:text-gray-700" id="closeViewer">✕</button> </div> <div class="p-4 max-h-[60vh] overflow-auto"> <pre class="whitespace-pre-wrap text-sm text-gray-800" id="viewerText"></pre> </div> </div> </div> <script> (function(){ const rowsEl = document.getElementById('rows'); const emptyEl = document.getElementById('emptyState'); const refreshBtn = document.getElementById('refreshBtn'); const uploadBtn = document.getElementById('uploadBtn'); const fileInput = document.getElementById('fileInput'); const noticeEl = document.getElementById('uploadNotice'); const viewerModal = document.getElementById('viewerModal'); const closeViewer = document.getElementById('closeViewer'); const viewerText = document.getElementById('viewerText'); const MAX_SIZE = 1024 * 1024; // 1MB let autoTimer = null; function setNotice(text, type){ if(!text){ noticeEl.classList.add('hidden'); noticeEl.textContent = ''; return; } const base = 'mt-3 rounded p-3 text-sm '; if(type==='success') noticeEl.className = base + 'bg-green-50 border border-green-200 text-green-800'; else if(type==='warn') noticeEl.className = base + 'bg-yellow-50 border border-yellow-200 text-yellow-800'; else noticeEl.className = base + 'bg-red-50 border border-red-200 text-red-800'; noticeEl.textContent = text; noticeEl.classList.remove('hidden'); } function badge(status){ const map = { success: 'bg-green-100 text-green-800', waiting: 'bg-yellow-100 text-yellow-800', error: 'bg-red-100 text-red-800', pending: 'bg-gray-100 text-gray-800' }; const cls = map[status] || map.pending; const label = (status||'pending').toUpperCase(); return `<span class="px-2 py-1 text-xs rounded ${cls}">${label}</span>`; } function fmtDate(s){ try { return new Date(s).toLocaleString(); } catch(e){ return s || ''; } } function esc(str){ const d = document.createElement('div'); d.textContent = String(str ?? ''); return d.innerHTML; } function rowHTML(item){ const d = item.data || {}; const file = d.file_name || ''; const topic = (d.topic === null || d.topic === undefined || d.topic === '') ? 'Pending' : d.topic; const status = d.upload_status || 'pending'; const canView = !!d.document_id; const viewBtnCls = 'px-3 py-1.5 rounded ' + (canView ? 'bg-blue-600 hover:bg-blue-700 text-white' : 'bg-gray-200 text-gray-500 cursor-not-allowed'); const viewDisabled = canView ? '' : 'disabled'; const docId = d.document_id ? String(d.document_id) : ''; const checkBtn = status === 'waiting' ? `<button class="px-2.5 py-1.5 rounded bg-amber-600 hover:bg-amber-700 text-white text-xs" data-entity-id="${item.id}" data-document-id="${docId}" data-file-name="${esc(file)}" onclick="window.__checkWaiting(event)">Check</button>` : ''; return ` <tr> <td class="px-4 py-2 text-sm text-gray-800">${esc(file)}</td> <td class="px-4 py-2 text-sm text-gray-700">${esc(topic)}</td> <td class="px-4 py-2 text-sm text-gray-500">${fmtDate(item.created_at)}</td> <td class="px-4 py-2 text-sm">${badge(status)}</td> <td class="px-4 py-2 text-sm flex items-center gap-2"> <button class="${viewBtnCls}" ${viewDisabled} onclick="window.__openViewer(${docId || 'null'})">View</button> ${checkBtn} </td> </tr> `; } async function loadRows(){ try{ const entities = await pt.list({ entityNames: ['file_topic'], limit: 200 }); const items = (entities || []).filter(e => e.entity_name === 'file_topic'); if(items.length === 0){ rowsEl.innerHTML = ''; emptyEl.classList.remove('hidden'); return; } emptyEl.classList.add('hidden'); rowsEl.innerHTML = items.map(rowHTML).join(''); }catch(err){ setNotice('Failed to load rows. Please try Refresh.', 'error'); } } function setUploading(on){ uploadBtn.disabled = !!on; if(on){ uploadBtn.classList.add('opacity-60','cursor-not-allowed'); } else { uploadBtn.classList.remove('opacity-60','cursor-not-allowed'); } } // NEW: upload multiple files concurrently async function uploadFiles(){ const files = Array.from(fileInput.files || []); if(files.length === 0){ setNotice('Please select one or more files.', 'warn'); return; } const allowed = ['pdf','docx']; let skipped = 0; // Build parallel jobs for valid files const jobs = files.map(file => { const ext = (file.name.split('.').pop() || '').toLowerCase(); if(!allowed.includes(ext) || file.size > MAX_SIZE){ skipped++; return null; // mark as skipped } const formData = new FormData(); formData.append('files', file); // Instruction message (kept as in current flow) const msg = `AI Processing Request You are given a file uploaded from a PrimeThink Live Page as an attachment to THIS message. Follow EXACTLY: 1) If the documents attached have text: derive a concise topic (<= 6 words, noun phrase) for each one. Then create EXACTLY ONE database record for each using 'chatdb_add' with: - entity_name: "file_topic" - data: { "file_name": "${file.name}", "topic": "<PUT_TOPIC>", "document_id": <ID of the just-uploaded document in THIS message's attachments>, "upload_status": "success" } 3) If all or some of the documents have no text: create EXACTLY ONE database record for the ones with no text using 'chatdb_add' with: - entity_name: "file_topic" - data: { "file_name": "${file.name}", "topic": "Pending", "document_id": <ID of the just-uploaded document in THIS message's attachments>, "upload_status": "waiting" } 4) You MUST call 'chatdb_add' exactly once per uploaded file. 5) Respond with the JUST JSON, with the format: [ { "document_id": <document_id>, "document_filename": ""<filename>", "document_topic": "<topic>" "extraction_status": "<success|error> } ]`; return pt.uploadFiles(formData, msg); }).filter(Boolean); if(jobs.length === 0){ setNotice(`No valid files to upload. Skipped: ${skipped}.`, 'warn'); return; } setUploading(true); setNotice(`Uploading ${jobs.length} file(s) in parallel...`, 'warn'); const results = await Promise.allSettled(jobs); const ok = results.filter(r => r.status === 'fulfilled').length; const fail = results.filter(r => r.status === 'rejected').length; setUploading(false); setNotice(`Upload finished. Success: ${ok}, Error: ${fail}. Skipped: ${skipped}.`, fail ? 'error' : 'success'); // clear input and refresh table once fileInput.value = ''; await loadRows(); } // Expose a global check handler for waiting rows window.__checkWaiting = async function(e){ const btn = e.currentTarget; const entityId = parseInt(btn.getAttribute('data-entity-id')); const docId = parseInt(btn.getAttribute('data-document-id')); const fileName = btn.getAttribute('data-file-name') || ''; try{ btn.disabled = true; btn.classList.add('opacity-60','cursor-not-allowed'); const msg = `Re-check uploaded document readiness and update the existing DB row. Document info: - document_id: ${docId} - file_name: "${fileName}" - entity_id: ${entityId} Instructions: 1) try to read the document text, if the document has extracted text: - Derive a concise topic (<= 6 words). - Use 'chatdb_edit' with entity_id and set data to: { "file_name": "${fileName}", "topic": "<PUT_TOPIC>", "document_id": ${docId}, "upload_status": "success" } 2) If still not Ready or no text: make no DB changes. 3) Respond with the JUST JSON, with the format: [ { "document_id": <document_id>, "document_filename": ""<filename>", "document_topic": "<topic>" "extraction_status": "<success|error> } ]`; await pt.addMessage(msg); setNotice('Check requested. Give it a moment, then Refresh.', 'success'); await loadRows(); }catch(err){ setNotice('Check failed. Please try again.', 'error'); }finally{ btn.disabled = false; btn.classList.remove('opacity-60','cursor-not-allowed'); } } // Viewer helpers window.__openViewer = async function(docId){ if(!docId){ return; } try{ const res = await pt.getDocumentText(docId, { to: 2000 }); const txt = (res && res.text) ? res.text : (res && res.message) ? res.message : 'No text available.'; viewerText.textContent = txt; viewerModal.classList.remove('hidden'); viewerModal.classList.add('flex'); }catch(err){ viewerText.textContent = 'Failed to load document text.'; viewerModal.classList.remove('hidden'); viewerModal.classList.add('flex'); } } function closeModal(){ viewerModal.classList.add('hidden'); viewerModal.classList.remove('flex'); viewerText.textContent = ''; } closeViewer.addEventListener('click', closeModal); viewerModal.addEventListener('click', (e)=>{ if(e.target === viewerModal) closeModal(); }); // Bindings refreshBtn.addEventListener('click', loadRows); uploadBtn.addEventListener('click', uploadFiles); // Initial load + auto refresh (async function init(){ await loadRows(); if(autoTimer) clearInterval(autoTimer); autoTimer = setInterval(loadRows, 10000); })(); })(); </script> </div>

Key Features Explained

Parallel File Upload:

  • Uploads multiple files concurrently using Promise.allSettled()

  • Significantly faster when uploading multiple files

  • Each file is processed independently by the AI

  • Provides aggregate success/failure counts

  • Recommended for better user experience

JSON Response Format:

  • AI returns structured JSON with extraction results

  • Easy to parse and validate

  • Includes document_id, filename, topic, and status

  • Better for programmatic handling of responses

Status Management:

  • success - Document processed and topic extracted

  • waiting - Document still being processed

  • pending - Initial state

  • error - Processing failed

Check Later Functionality:

  • "Check" button appears for waiting documents

  • Sends AI command to re-check document readiness

  • Updates database record when document becomes ready

  • User can manually trigger checks anytime

Auto-Refresh:

  • Table refreshes every 10 seconds

  • Shows latest processing status automatically

  • No manual refresh needed for most operations

Parallel vs Serial Upload Processing

The example above uses parallel upload (concurrent processing) for better performance. However, you may want serial upload (one at a time) in certain scenarios.

When to Use Parallel Upload (Recommended):

  • Default choice for most use cases

  • Faster overall completion time

  • Better user experience

  • Files are independent of each other

  • You want to maximize throughput

When to Use Serial Upload:

  • You need to process files in specific order

  • You want to limit concurrent AI requests

  • Each file depends on previous file's results

  • You need more predictable resource usage

Serial Upload Implementation:

Replace the uploadFiles function with this serial version:

// Serial upload: process one file at a time async function uploadFiles(){ const files = Array.from(fileInput.files || []); if(files.length === 0){ setNotice('Please select one or more files.', 'warn'); return; } const allowed = ['pdf','docx']; let ok = 0, fail = 0, skipped = 0; setUploading(true); setNotice(`Uploading ${files.length} file(s) one at a time...`, 'warn'); // Process files sequentially for(const file of files){ const ext = (file.name.split('.').pop() || '').toLowerCase(); if(!allowed.includes(ext) || file.size > MAX_SIZE){ skipped++; continue; } try{ const formData = new FormData(); formData.append('files', file); const msg = `AI Processing Request You are given a file uploaded from a PrimeThink Live Page as an attachment to THIS message. Follow EXACTLY: 1) If the documents attached have text: derive a concise topic (<= 6 words, noun phrase) for each one. Then create EXACTLY ONE database record for each using 'chatdb_add' with: - entity_name: "file_topic" - data: { "file_name": "${file.name}", "topic": "<PUT_TOPIC>", "document_id": <ID of the just-uploaded document in THIS message's attachments>, "upload_status": "success" } 3) If all or some of the documents have no text: create EXACTLY ONE database record for the ones with no text using 'chatdb_add' with: - entity_name: "file_topic" - data: { "file_name": "${file.name}", "topic": "Pending", "document_id": <ID of the just-uploaded document in THIS message's attachments>, "upload_status": "waiting" } 4) You MUST call 'chatdb_add' exactly once per uploaded file. 5) Respond with the JUST JSON, with the format: [ { "document_id": <document_id>, "document_filename": ""<filename>", "document_topic": "<topic>" "extraction_status": "<success|error> } ]`; await pt.uploadFiles(formData, msg); ok++; // Update status after each file setNotice(`Uploaded ${ok} of ${files.length - skipped} file(s)...`, 'warn'); }catch(e){ fail++; } } setUploading(false); setNotice(`Upload finished. Success: ${ok}, Error: ${fail}. Skipped: ${skipped}.`, fail ? 'error' : 'success'); fileInput.value = ''; await loadRows(); }

Key Differences:

Aspect

Parallel (Recommended)

Serial

Speed

Fast - all files at once

Slower - one at a time

Syntax

Promise.allSettled()

for...of loop

Order

No guarantee

Strict order

Progress

Bulk complete

Per-file updates

Resource Usage

Higher concurrent load

Lower concurrent load

Alternative Pattern: Immediate Feedback with Processing Status

This alternative pattern provides immediate visual feedback by creating database rows with PROCESSING status before AI processes the files. The AI then updates these rows via chatdb_edit once processing completes.

Pattern Overview

How it works:

  1. User selects files

  2. App immediately creates database row per file with upload_status: 'PROCESSING'

  3. File is uploaded to chat with AI instructions

  4. AI reads the uploaded file and uses chatdb_edit to update the existing row

  5. If text is ready: AI updates to SUCCESS with topic and document_id

  6. If text isn't ready: Row remains PROCESSING (user can refresh later)

Key Advantage: Users see files appearing in the table immediately with PROCESSING status instead of waiting with no feedback.

Complete Implementation

<div class="container mx-auto p-6 max-w-4xl"> <!-- Header --> <div class="flex items-center justify-between mb-6"> <div class="flex items-center gap-3 text-gray-700"> <h1 class="text-xl md:text-2xl font-bold">Topic extractor</h1> </div> <button class="px-3 py-2 rounded bg-gray-100 hover:bg-gray-200 text-sm" id="refreshBtn" type="button">Refresh</button> </div> <!-- Uploader --> <div class="bg-white rounded-xl shadow p-4"> <p class="text-gray-600 text-sm mb-3"> When you upload files, the app immediately creates a database row per file with status <code>PROCESSING</code>. The AI will then read the uploaded file and update the same row via <code>chatdb_edit</code> to set the topic, attach the <code>document_id</code>, and switch status to <code>SUCCESS</code> once text is ready. If the text isn't ready yet, the row remains <code>PROCESSING</code>. The table auto-refreshes every 10s. </p> <div class="flex flex-col md:flex-row md:items-center gap-3"> <input accept=".pdf,.docx" class="block w-full text-sm text-gray-700" id="fileInput" multiple type="file" /> <button class="px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700 flex items-center gap-2" id="uploadBtn" type="button"> <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1M12 12V4m0 0l-3 3m3-3l3 3" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path></svg> Upload </button> </div> <div class="hidden mt-3 rounded p-3 text-sm" id="uploadNotice"></div> </div> <!-- Table --> <div class="mt-6 bg-white rounded-xl shadow p-4"> <div class="flex items-center justify-between mb-3"> <div class="text-sm text-gray-500">Auto-refreshes every 10s</div> </div> <div class="overflow-x-auto"> <table class="min-w-full divide-y divide-gray-200"> <thead class="bg-gray-50"> <tr> <th class="px-4 py-2 text-left text-xs font-semibold text-gray-600">File Name</th> <th class="px-4 py-2 text-left text-xs font-semibold text-gray-600">Topic</th> <th class="px-4 py-2 text-left text-xs font-semibold text-gray-600">Uploaded At</th> <th class="px-4 py-2 text-left text-xs font-semibold text-gray-600">Upload Status</th> <th class="px-4 py-2 text-left text-xs font-semibold text-gray-600">Actions</th> </tr> </thead> <tbody class="divide-y divide-gray-100 bg-white" id="rows"></tbody> </table> </div> <div class="hidden text-center text-gray-500 text-sm py-8" id="emptyState">No uploads yet</div> </div> <!-- Viewer Modal --> <div class="hidden fixed inset-0 bg-black/50 z-50 items-center justify-center" id="viewerModal"> <div class="bg-white rounded-lg shadow-xl max-w-3xl w-full mx-4"> <div class="flex items-center justify-between p-4 border-b"> <h3 class="font-semibold text-gray-800">Document Preview</h3> <button class="text-gray-500 hover:text-gray-700" id="closeViewer" type="button">✕</button> </div> <div class="p-4 max-h-[60vh] overflow-auto"> <pre class="whitespace-pre-wrap text-sm text-gray-800" id="viewerText"></pre> </div> </div> </div> <script> (function(){ // Elements const rowsEl = document.getElementById('rows'); const emptyEl = document.getElementById('emptyState'); const refreshBtn = document.getElementById('refreshBtn'); const uploadBtn = document.getElementById('uploadBtn'); const fileInput = document.getElementById('fileInput'); const noticeEl = document.getElementById('uploadNotice'); const viewerModal = document.getElementById('viewerModal'); const closeViewer = document.getElementById('closeViewer'); const viewerText = document.getElementById('viewerText'); const MAX_SIZE = 1024 * 1024; // 1MB let autoTimer = null; function setNotice(text, type){ if(!text){ noticeEl.classList.add('hidden'); noticeEl.textContent = ''; return; } const base = 'mt-3 rounded p-3 text-sm '; if(type==='success') noticeEl.className = base + 'bg-green-50 border border-green-200 text-green-800'; else if(type==='warn') noticeEl.className = base + 'bg-yellow-50 border border-yellow-200 text-yellow-800'; else noticeEl.className = base + 'bg-red-50 border border-red-200 text-red-800'; noticeEl.textContent = text; noticeEl.classList.remove('hidden'); } function badge(status){ const norm = String(status || 'PROCESSING').toUpperCase(); const map = { 'SUCCESS': 'bg-green-100 text-green-800', 'PROCESSING': 'bg-yellow-100 text-yellow-800', 'ERROR': 'bg-red-100 text-red-800' }; const cls = map[norm] || 'bg-gray-100 text-gray-800'; return `<span class="px-2 py-1 text-xs rounded ${cls}">${norm}</span>`; } function fmtDate(s){ try { return new Date(s).toLocaleString(); } catch(e){ return s || ''; } } function esc(str){ const d = document.createElement('div'); d.textContent = String(str ?? ''); return d.innerHTML; } function rowHTML(item){ const d = item.data || {}; const file = d.file_name || ''; const topic = (d.topic === null || d.topic === undefined || d.topic === '') ? 'Pending' : d.topic; const status = d.upload_status || 'PROCESSING'; const canView = !!d.document_id; const viewBtnCls = 'px-3 py-1.5 rounded ' + (canView ? 'bg-blue-600 hover:bg-blue-700 text-white' : 'bg-gray-200 text-gray-500 cursor-not-allowed'); const viewDisabled = canView ? '' : 'disabled'; const docId = d.document_id ? String(d.document_id) : ''; return ` <tr> <td class="px-4 py-2 text-sm text-gray-800">${esc(file)}</td> <td class="px-4 py-2 text-sm text-gray-700">${esc(topic)}</td> <td class="px-4 py-2 text-sm text-gray-500">${fmtDate(item.created_at)}</td> <td class="px-4 py-2 text-sm">${badge(status)}</td> <td class="px-4 py-2 text-sm flex items-center gap-2"> <button class="${viewBtnCls}" ${viewDisabled} onclick="window.__openViewer(${docId ? docId : 'null'})" type="button">View</button> </td> </tr> `; } async function loadRows(){ try{ const entities = await pt.list({ entityNames: ['file_topic'], limit: 200 }); const items = (entities || []).filter(e => e.entity_name === 'file_topic'); if(items.length === 0){ rowsEl.innerHTML = ''; emptyEl.classList.remove('hidden'); return; } emptyEl.classList.add('hidden'); rowsEl.innerHTML = items.map(rowHTML).join(''); }catch(err){ setNotice('Failed to load rows. Please try Refresh.', 'error'); } } function setUploading(on){ uploadBtn.disabled = !!on; if(on){ uploadBtn.classList.add('opacity-60','cursor-not-allowed'); } else { uploadBtn.classList.remove('opacity-60','cursor-not-allowed'); } } // NEW: create PROCESSING row first, then upload & let LLM chatdb_edit it to SUCCESS async function uploadFiles(){ const files = Array.from(fileInput.files || []); if(files.length === 0){ setNotice('Please select one or more files.', 'warn'); return; } const allowed = ['pdf','docx']; let skipped = 0; // Build parallel jobs for valid files const jobs = files.map(file => { const ext = (file.name.split('.').pop() || '').toLowerCase(); if(!allowed.includes(ext) || file.size > MAX_SIZE){ skipped++; return null; // skipped } return (async () => { // 1) Create PROCESSING row immediately const created = await pt.add('file_topic', { file_name: file.name, topic: null, document_id: null, upload_status: 'PROCESSING' }); const entityId = created.id; // 2) Upload file with instructions to EDIT that row via chatdb_edit const formData = new FormData(); formData.append('files', file); const msg = `AI Processing Request (Edit Existing Row) A file has been uploaded from a PrimeThink Live Page as an attachment to THIS message. A database row has ALREADY been created for this file. You MUST follow EXACTLY: 1) If the uploaded document has extracted text: - Derive a concise topic (<= 6 words, noun phrase). - Call the tool 'chatdb_edit' EXACTLY ONCE with: - entity_id: ${entityId} - data: { "file_name": "${file.name.replace(/"/g, '\\"')}", "topic": "<PUT_TOPIC>", "document_id": <ID of the just-uploaded document in THIS message's attachments>, "upload_status": "SUCCESS" } 3) If the document has NO text yet or cannot be extracted in time: - Do NOT create new rows. - Do NOT change the existing row. Leave it as "PROCESSING". 4) You MUST NOT call 'chatdb_add'. Only use 'chatdb_edit' with entity_id ${entityId}. 5) Respond with JUST JSON, in the format: [ { "document_id": <document_id>, "document_filename": "${file.name.replace(/"/g, '\\"')}", "document_topic": "<topic>", "extraction_status": "<success|error>" } ]`; await pt.uploadFiles(formData, msg); })(); }).filter(Boolean); if(jobs.length === 0){ setNotice(`No valid files to upload. Allowed: .pdf, .docx; Max size 1MB. Skipped: ${skipped}.`, 'warn'); return; } setUploading(true); setNotice(`Uploading file(s) and initializing PROCESSING rows...`, 'warn'); const results = await Promise.allSettled(jobs); const ok = results.filter(r => r.status === 'fulfilled').length; const fail = results.filter(r => r.status === 'rejected').length; setUploading(false); setNotice(`Upload initialized. Success: ${ok}, Error: ${fail}, Skipped: ${skipped}.`, fail ? 'error' : 'success'); // clear input and refresh once fileInput.value = ''; await loadRows(); } // Viewer helpers window.__openViewer = async function(docId){ if(!docId){ return; } try{ const res = await pt.getDocumentText(docId, { to: 2000 }); const txt = (res && res.text) ? res.text : (res && res.message) ? res.message : 'No text available.'; viewerText.textContent = txt; viewerModal.classList.remove('hidden'); viewerModal.classList.add('flex'); }catch(err){ viewerText.textContent = 'Failed to load document text.'; viewerModal.classList.remove('hidden'); viewerModal.classList.add('flex'); } } function closeModal(){ viewerModal.classList.add('hidden'); viewerModal.classList.remove('flex'); viewerText.textContent = ''; } closeViewer.addEventListener('click', closeModal); viewerModal.addEventListener('click', (e)=>{ if(e.target === viewerModal) closeModal(); }); // Clear notice when selecting files fileInput.addEventListener('change', () => setNotice('', '')); // Bindings refreshBtn.addEventListener('click', loadRows); uploadBtn.addEventListener('click', uploadFiles); // Initial load + auto refresh (async function init(){ await loadRows(); if(autoTimer) clearInterval(autoTimer); autoTimer = setInterval(loadRows, 10000); })(); })(); </script> </div>

Pattern Comparison: Add-Then-Update vs Upload-Then-Add

Aspect

Upload-Then-Add (Previous)

Add-Then-Update (This Pattern)

User Feedback

Delayed - waits for AI

Immediate - shows PROCESSING

Database Operations

1 operation (chatdb_add)

2 operations (pt.add + chatdb_edit)

Orphaned Rows

None

Possible if upload fails after pt.add

Complexity

Simpler

More complex

UX Quality

Good

Excellent

Error Handling

Easier

Needs cleanup logic

Best For

Background automation

Interactive user interfaces

When to Use Each Pattern

Use Upload-Then-Add (chatdb_add only):

  • Goals and automation (chat uploads, email)

  • Background processing where immediate feedback isn't needed

  • Simpler implementation with fewer failure modes

  • When you want to avoid orphaned database rows

Use Add-Then-Update (pt.add + chatdb_edit):

  • Interactive Live Page interfaces

  • When immediate visual feedback is critical

  • Dashboard-style applications

  • When users need to see upload progress in real-time

  • Applications where perceived performance matters

Key Implementation Details

1. Creating the PROCESSING Row:

const created = await pt.add('file_topic', { file_name: file.name, topic: null, document_id: null, upload_status: 'PROCESSING' }); const entityId = created.id; // Save this to pass to AI

2. AI Instructions for Editing:

const msg = `AI Processing Request (Edit Existing Row) A database row has ALREADY been created for this file. 1) If the uploaded document has extracted text: - Use 'chatdb_edit' with entity_id: ${entityId} - Set upload_status: "SUCCESS" 3) If the document has NO text: - Leave the row as "PROCESSING" 4) You MUST NOT call 'chatdb_add'. Only use 'chatdb_edit'.`;

3. Status Badge Styling:

function badge(status){ const map = { 'SUCCESS': 'bg-green-100 text-green-800', 'PROCESSING': 'bg-yellow-100 text-yellow-800', 'ERROR': 'bg-red-100 text-red-800' }; // ... }

Best Practices for This Pattern

1. Prevent Orphaned Rows:

try { const created = await pt.add('file_topic', { /* ... */ }); await pt.uploadFiles(formData, msg); } catch (error) { // If upload fails, consider deleting the row await pt.delete(created.id); throw error; }

2. Add Retry Mechanism:

  • Add "Re-process" button for PROCESSING rows that stay stuck

  • Allow users to manually trigger chatdb_edit attempts

3. Set Reasonable Timeouts:

  • Auto-refresh every 10 seconds is reasonable

  • Consider showing "stuck" indicator for rows PROCESSING > 2 minutes

4. Clear Status Indicators:

  • Use distinct colors: yellow for PROCESSING, green for SUCCESS

  • Add icons or spinners for better visual feedback

Important Note About Goals

Goals should always use chatdb_add, not this add-then-update pattern, because:

  • Goals are for automation (chat uploads, email forwarding)

  • No user is waiting for immediate visual feedback

  • Simpler implementation with fewer failure modes

  • Avoids orphaned PROCESSING rows when automation fails

The add-then-update pattern is specifically for interactive Live Page uploads where immediate user feedback is valuable.

Using Goals for Direct File Upload

For an even better user experience, you can set a Goal in the chat settings so that when users upload files through any channel, they are automatically processed without needing the Live Page interface.

File Upload Channels:

  • Direct chat messages: Users upload files in the conversation

  • Email forwarding: Files sent to the chat's email address

  • API uploads: Files uploaded programmatically via the PrimeThink API

  • Chat mentions: Files uploaded when the chat is mentioned in another conversation

When to Use Goals:

  • Users upload files through multiple channels (chat, email, API, chat mentions)

  • You want automatic processing without user interaction

  • You want consistent processing across all upload methods

  • You need unified experience regardless of upload source

Setting Up the Goal:

Navigate to your chat settings and add this goal:

If a user just uploads a pdf file, execute the following prompt: AI Processing Request You are given a file uploaded from a PrimeThink Live Page as an attachment to THIS message. Follow EXACTLY: 1) If the documents attached have text: derive a concise topic (<= 6 words, noun phrase) for each one. Then create EXACTLY ONE database record for each using 'chatdb_add' with: - entity_name: "file_topic" - data: { "file_name": "<actual filename>", "topic": "<PUT_TOPIC>", "document_id": <ID of the just-uploaded document in THIS message's attachments>, "upload_status": "success" } 3) If all or some of the documents have no text: create EXACTLY ONE database record for the ones with no text using 'chatdb_add' with: - entity_name: "file_topic" - data: { "file_name": "<actual filename>", "topic": "Pending", "document_id": <ID of the just-uploaded document in THIS message's attachments>, "upload_status": "waiting" } 4) You MUST call 'chatdb_add' exactly once per uploaded file. 5) Respond with the JUST JSON, with the format: [ { "document_id": <document_id>, "document_filename": ""<filename>", "document_topic": "<topic>" "extraction_status": "<success|error> } ]

How Goals Work:

  • Goals are instructions that the AI automatically follows when certain conditions are met

  • When a file is uploaded through any channel (chat, email, API, chat mentions), the goal triggers

  • The AI processes the file and stores results in the database using chatdb_add

  • The Live Page will show the results on next refresh

  • Users get a unified experience regardless of upload source

Goal Benefits:

  • Unified Processing: Same logic for chat, email, API, and chat mention uploads

  • No UI Required: Files can be processed without visiting the Live Page

  • Automatic Execution: Background processing with zero user intervention

  • Consistent Data: All uploads create identical database structures

  • Multi-Channel Support: Works seamlessly across all upload methods

Real-World Applications

  1. Multi-Channel Document Inbox: Automatically categorize documents from chat, email, API, and chat mentions

  2. Receipt Processing: Extract data from receipts sent via email or uploaded through API

  3. Contract Management: Process contracts uploaded via any channel and extract key terms

  4. Research Library: Organize academic papers by topic, accepting uploads from multiple sources

  5. Customer Support: Categorize support documents from email, chat mentions, or API integrations

  6. Invoice Processing: Extract invoice details from PDFs uploaded via email, API, or chat

  7. Legal Documents: Categorize and index legal filings from any upload source

  8. API-Driven Workflows: Automated document processing from third-party systems via API

  9. Mention-Based Analysis: Process documents shared when the chat is mentioned in other conversations

Best Practices

Upload Strategy:

  • Use parallel upload as default for best performance

  • Switch to serial if you need ordered processing

  • Validate file types and sizes before uploading

  • Provide immediate feedback during upload

  • Handle cases where documents don't have text yet

Error Handling:

  • Always create a database record, even if processing fails

  • Use status field to track processing state

  • Provide clear feedback to users

  • Allow retry mechanisms

Performance:

  • Use parallel upload (Promise.allSettled()) for faster processing

  • Use serial upload when order matters or to limit concurrent requests

  • Use appropriate file size limits (1MB recommended)

  • Implement auto-refresh for updated statuses

  • Cache document text when possible

  • Consider AI response format (JSON is easier to parse)

User Experience:

  • Show immediate feedback when uploading

  • Display clear status indicators

  • Provide document preview functionality

  • Allow manual re-check for waiting documents

Next Steps

09 November 2025