aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/chatbot/tools/CanvasDocsTool.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/chatbot/tools/CanvasDocsTool.ts')
-rw-r--r--src/client/views/nodes/chatbot/tools/CanvasDocsTool.ts428
1 files changed, 428 insertions, 0 deletions
diff --git a/src/client/views/nodes/chatbot/tools/CanvasDocsTool.ts b/src/client/views/nodes/chatbot/tools/CanvasDocsTool.ts
new file mode 100644
index 000000000..daf6ed941
--- /dev/null
+++ b/src/client/views/nodes/chatbot/tools/CanvasDocsTool.ts
@@ -0,0 +1,428 @@
+// CanvasDocsTool.ts - Provides access to all documents on the canvas
+import { BaseTool } from './BaseTool';
+import { Observation } from '../types/types';
+import { ParametersType, ToolInfo } from '../types/tool_types';
+import { DocumentView } from '../../DocumentView';
+import { Doc } from '../../../../../fields/Doc';
+import { v4 as uuidv4 } from 'uuid';
+import { Id } from '../../../../../fields/FieldSymbols';
+import { DocumentType } from '../../../../documents/DocumentTypes';
+
+const parameterRules = [
+ {
+ name: 'action',
+ type: 'string',
+ description: 'The action to perform: "list" to get all canvas documents, "getById" to get a specific document, "getByType" to filter by document type',
+ required: true,
+ },
+ {
+ name: 'documentId',
+ type: 'string',
+ description: 'The ID of a specific document to retrieve (required for "getById" action)',
+ required: false,
+ },
+ {
+ name: 'documentType',
+ type: 'string',
+ description: 'Filter documents by type: text, image, pdf, video, audio, web, diagram, dataviz, collection, etc. (required for "getByType" action)',
+ required: false,
+ },
+ {
+ name: 'includeSystemDocs',
+ type: 'boolean',
+ description: 'Whether to include system documents in the results (default: false)',
+ required: false,
+ },
+] as const;
+
+const toolInfo: ToolInfo<typeof parameterRules> = {
+ name: 'canvasDocs',
+ description: 'Access and analyze all documents currently visible on the canvas/workspace. This tool provides broader document recognition beyond just linked documents, allowing the agent to work with any document in the current view. Useful for comprehensive document analysis, finding related content, and understanding the full context of the workspace.',
+ parameterRules,
+ citationRules: 'Cite documents by their title and type when referencing canvas documents.',
+};
+
+export class CanvasDocsTool extends BaseTool<typeof parameterRules> {
+
+ constructor() {
+ super(toolInfo);
+ }
+
+ /**
+ * Get all documents currently visible on the canvas using parent view approach
+ */
+ private getAllCanvasDocuments(includeSystemDocs: boolean = false): Doc[] {
+ try {
+ console.log('[CanvasDocsTool] Starting canvas document discovery...');
+
+ const canvasDocs: Doc[] = [];
+ const seenIds = new Set<string>();
+ const sources = { parentView: 0, allViews: 0, lightbox: 0 };
+
+ // Method 1: Try to get documents from parent view if it's a collection
+ const parentViewDocs = this.getParentViewDocuments();
+ if (parentViewDocs.length > 0) {
+ console.log(`[CanvasDocsTool] Found ${parentViewDocs.length} documents from parent view`);
+ const parentDocsAdded = parentViewDocs.reduce((count, doc) => {
+ return this.addDocumentToResults(doc, canvasDocs, seenIds, includeSystemDocs, 'parentView') ? count + 1 : count;
+ }, 0);
+ sources.parentView = parentDocsAdded;
+ }
+
+ // Method 2: Get documents from all rendered views (fallback)
+ const viewDocs = this.getDocumentsFromViews();
+ console.log(`[CanvasDocsTool] Found ${viewDocs.length} documents from document views`);
+ const viewDocsAdded = viewDocs.reduce((count, doc) => {
+ return this.addDocumentToResults(doc, canvasDocs, seenIds, includeSystemDocs, 'allViews') ? count + 1 : count;
+ }, 0);
+ sources.allViews = viewDocsAdded;
+
+ // Method 3: Include lightbox document if active
+ const lightboxDoc = this.getLightboxDocument();
+ if (lightboxDoc) {
+ console.log('[CanvasDocsTool] Found lightbox document');
+ sources.lightbox = this.addDocumentToResults(lightboxDoc, canvasDocs, seenIds, includeSystemDocs, 'lightbox') ? 1 : 0;
+ }
+
+ console.log(`[CanvasDocsTool] Total: ${canvasDocs.length} documents found from sources:`, sources);
+ return canvasDocs;
+ } catch (error) {
+ console.error('[CanvasDocsTool] Error getting canvas documents:', error);
+ return [];
+ }
+ }
+
+ /**
+ * Get documents from the parent view if it's a collection
+ */
+ private getParentViewDocuments(): Doc[] {
+ try {
+ // Get all DocumentViews and look for collections with child documents
+ const allViews = DocumentView.allViews();
+ const parentDocs: Doc[] = [];
+
+ for (const view of allViews) {
+ // Check if this view has a ComponentView with child documents
+ const componentView = (view as any).ComponentView || (view as any)._componentView;
+
+ if (componentView) {
+ // Method 1: Try hasChildDocs() which returns layout documents
+ if (typeof componentView.hasChildDocs === 'function') {
+ try {
+ const childDocs = componentView.hasChildDocs();
+ if (Array.isArray(childDocs) && childDocs.length > 0) {
+ console.log(`[CanvasDocsTool] Found collection with ${childDocs.length} child documents from hasChildDocs`);
+ childDocs.forEach((doc: any, index: number) => {
+ console.log(`[CanvasDocsTool] Child doc ${index}: id=${doc?.[Id]}, title="${doc?.title}", type=${typeof doc}`);
+ if (doc && typeof doc === 'object' && doc[Id]) {
+ parentDocs.push(doc);
+ }
+ });
+ }
+ } catch (e) {
+ console.log('[CanvasDocsTool] Error calling hasChildDocs:', e);
+ }
+ }
+
+ // Method 2: Try childLayoutPairs property
+ if (componentView.childLayoutPairs) {
+ try {
+ const childPairs = componentView.childLayoutPairs;
+ if (Array.isArray(childPairs)) {
+ console.log(`[CanvasDocsTool] Found ${childPairs.length} childLayoutPairs`);
+ childPairs.forEach((pair: any, index: number) => {
+ const layoutDoc = pair.layout;
+ console.log(`[CanvasDocsTool] Pair ${index}: layout id=${layoutDoc?.[Id]}, title="${layoutDoc?.title}"`);
+ if (layoutDoc && layoutDoc[Id]) {
+ parentDocs.push(layoutDoc);
+ }
+ });
+ }
+ } catch (e) {
+ console.log('[CanvasDocsTool] Error accessing childLayoutPairs:', e);
+ }
+ }
+
+ // Method 3: Try childDocs property (source data)
+ if (componentView.childDocs) {
+ try {
+ const childDocs = componentView.childDocs;
+ if (Array.isArray(childDocs)) {
+ console.log(`[CanvasDocsTool] Found ${childDocs.length} raw childDocs`);
+ childDocs.forEach((doc: any, index: number) => {
+ console.log(`[CanvasDocsTool] Raw doc ${index}: id=${doc?.[Id]}, title="${doc?.title}"`);
+ if (doc && doc[Id]) {
+ parentDocs.push(doc);
+ }
+ });
+ }
+ } catch (e) {
+ console.log('[CanvasDocsTool] Error accessing childDocs:', e);
+ }
+ }
+ }
+ }
+
+ console.log(`[CanvasDocsTool] Total parent docs collected: ${parentDocs.length}`);
+ return parentDocs;
+ } catch (error) {
+ console.error('[CanvasDocsTool] Error getting parent view documents:', error);
+ return [];
+ }
+ }
+
+ /**
+ * Get documents from rendered DocumentViews
+ */
+ private getDocumentsFromViews(): Doc[] {
+ try {
+ const allViews = DocumentView.allViews();
+ const viewDocs: Doc[] = [];
+
+ for (const view of allViews) {
+ const doc = view.Document;
+ if (doc && doc[Id]) {
+ viewDocs.push(doc);
+ }
+ }
+
+ return viewDocs;
+ } catch (error) {
+ console.error('[CanvasDocsTool] Error getting view documents:', error);
+ return [];
+ }
+ }
+
+ /**
+ * Get the current lightbox document if any
+ */
+ private getLightboxDocument(): Doc | null {
+ try {
+ // Try to get lightbox document using DocumentView static method
+ if (typeof DocumentView.LightboxDoc === 'function') {
+ const lightboxDoc = DocumentView.LightboxDoc();
+ return lightboxDoc || null;
+ }
+ return null;
+ } catch (error) {
+ console.log('[CanvasDocsTool] No lightbox document found');
+ return null;
+ }
+ }
+
+ /**
+ * Add a document to results if it passes filters
+ */
+ private addDocumentToResults(doc: any, canvasDocs: Doc[], seenIds: Set<string>, includeSystemDocs: boolean, source: string): boolean {
+ if (!doc || !doc[Id]) {
+ console.log(`[CanvasDocsTool] Skipped document from ${source}: no doc or no ID`);
+ return false;
+ }
+
+ const docId = String(doc[Id]);
+ if (seenIds.has(docId)) {
+ console.log(`[CanvasDocsTool] Skipped document from ${source}: duplicate ID ${docId}`);
+ return false;
+ }
+ seenIds.add(docId);
+
+ // Skip system documents unless explicitly requested
+ if (!includeSystemDocs && Doc.IsSystem(doc)) {
+ console.log(`[CanvasDocsTool] Skipped document from ${source}: system document ${doc.title || docId}`);
+ return false;
+ }
+
+ // Skip hidden documents
+ if (doc.hidden) {
+ console.log(`[CanvasDocsTool] Skipped document from ${source}: hidden document ${doc.title || docId}`);
+ return false;
+ }
+
+ canvasDocs.push(doc);
+ console.log(`[CanvasDocsTool] Added document from ${source}: ${doc.title || 'Untitled'} (${doc.type || 'unknown'})`);
+ return true;
+ }
+
+ /**
+ * Get document summary for display
+ */
+ private getDocumentSummary(doc: Doc): any {
+ try {
+ return {
+ id: String(doc[Id] || 'unknown'),
+ title: String(doc.title || 'Untitled'),
+ type: doc.type || 'unknown',
+ x: doc.x || 0,
+ y: doc.y || 0,
+ width: doc._width || 'auto',
+ height: doc._height || 'auto',
+ tags: doc.tags ? Array.from(doc.tags as any) : [],
+ hasData: !!doc.data,
+ dataType: typeof doc.data,
+ lastModified: doc.lastModified || 'unknown',
+ layout: {
+ fitWidth: doc._layout_fitWidth || false,
+ autoHeight: doc._layout_autoHeight || false,
+ showTitle: doc._layout_showTitle || false,
+ }
+ };
+ } catch (error) {
+ console.warn('[CanvasDocsTool] Error getting document summary for doc:', doc[Id], error);
+ return {
+ id: String(doc[Id] || 'unknown'),
+ title: 'Error reading document',
+ type: 'unknown',
+ error: String(error)
+ };
+ }
+ }
+
+ async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[]> {
+ const chunkId = uuidv4();
+ console.log('[CanvasDocsTool] Executing action:', args.action);
+
+ try {
+ const includeSystemDocs = args.includeSystemDocs || false;
+ const canvasDocs = this.getAllCanvasDocuments(includeSystemDocs);
+
+ switch (args.action) {
+ case 'list': {
+ const documentSummaries = canvasDocs.map(doc => this.getDocumentSummary(doc));
+
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="canvas_documents">
+Found ${canvasDocs.length} documents on the canvas:
+
+${documentSummaries.map(doc =>
+ `• ${doc.title} (${doc.type}) - ID: ${doc.id} - Position: (${doc.x}, ${doc.y}) - Size: ${doc.width}x${doc.height}`
+).join('\n')}
+
+Document Details:
+${JSON.stringify(documentSummaries, null, 2)}
+</chunk>`,
+ },
+ ];
+ }
+
+ case 'getById': {
+ if (!args.documentId) {
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="error">
+documentId parameter is required for getById action.
+</chunk>`,
+ },
+ ];
+ }
+
+ const targetDoc = canvasDocs.find(doc => String(doc[Id]) === args.documentId);
+ if (!targetDoc) {
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="error">
+Document with ID "${args.documentId}" not found on canvas.
+Available document IDs: ${canvasDocs.map(d => d[Id]).join(', ')}
+</chunk>`,
+ },
+ ];
+ }
+
+ const docSummary = this.getDocumentSummary(targetDoc);
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="canvas_document">
+Document found: ${docSummary.title} (${docSummary.type})
+
+${JSON.stringify(docSummary, null, 2)}
+</chunk>`,
+ },
+ ];
+ }
+
+ case 'getByType': {
+ if (!args.documentType) {
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="error">
+documentType parameter is required for getByType action.
+Available types: ${Array.from(new Set(canvasDocs.map(d => d.type))).join(', ')}
+</chunk>`,
+ },
+ ];
+ }
+
+ const filteredDocs = canvasDocs.filter(doc =>
+ doc.type === args.documentType ||
+ (doc.type && typeof doc.type === 'string' && doc.type.toLowerCase() === args.documentType.toLowerCase())
+ );
+
+ if (filteredDocs.length === 0) {
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="canvas_documents">
+No documents of type "${args.documentType}" found on canvas.
+Available types: ${Array.from(new Set(canvasDocs.map(d => d.type))).join(', ')}
+</chunk>`,
+ },
+ ];
+ }
+
+ const documentSummaries = filteredDocs.map(doc => this.getDocumentSummary(doc));
+
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="canvas_documents">
+Found ${filteredDocs.length} documents of type "${args.documentType}" on canvas:
+
+${documentSummaries.map(doc =>
+ `• ${doc.title} - ID: ${doc.id} - Position: (${doc.x}, ${doc.y})`
+).join('\n')}
+
+Document Details:
+${JSON.stringify(documentSummaries, null, 2)}
+</chunk>`,
+ },
+ ];
+ }
+
+ default:
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="error">
+Unknown action: "${args.action}". Available actions: list, getById, getByType
+</chunk>`,
+ },
+ ];
+ }
+
+ } catch (error) {
+ console.error('[CanvasDocsTool] Execution error:', error);
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="error">
+Canvas documents access failed: ${error instanceof Error ? error.message : String(error)}
+</chunk>`,
+ },
+ ];
+ }
+ }
+
+ /**
+ * Get all canvas documents as a simple array (for external use)
+ */
+ static getAllCanvasDocuments(includeSystemDocs: boolean = false): Doc[] {
+ // Create a temporary instance to use the private methods
+ const tool = new CanvasDocsTool();
+ return tool.getAllCanvasDocuments(includeSystemDocs);
+ }
+} \ No newline at end of file