import { action, computed, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { Doc, FieldResult, StrListCast } from '../../../../../fields/Doc'; import { DocData } from '../../../../../fields/DocSymbols'; import { Id } from '../../../../../fields/FieldSymbols'; import { List } from '../../../../../fields/List'; import { DocCast, StrCast } from '../../../../../fields/Types'; import { DocServer } from '../../../../DocServer'; import { Docs, DocumentOptions } from '../../../../documents/Documents'; import { DocumentManager } from '../../../../util/DocumentManager'; import { LinkManager, UPDATE_SERVER_CACHE } from '../../../../util/LinkManager'; import { DocumentView } from '../../DocumentView'; import { ChatBox, parsedDoc } from '../chatboxcomponents/ChatBox'; import { CanvasDocsTool } from '../tools/CanvasDocsTool'; import { supportedDocTypes } from '../types/tool_types'; import { CHUNK_TYPE, RAGChunk, SimplifiedChunk } from '../types/types'; /** * Interface representing a document in the freeform view */ interface AgentDocument { layoutDoc: Doc; dataDoc: Doc; } /** * Class to manage documents in a freeform view */ export class AgentDocumentManager { @observable private documentsById: ObservableMap; private chatBox: ChatBox; private parentView: DocumentView; private chatBoxDocument: Doc | null = null; @observable private _useCanvasMode: boolean = false; private fieldMetadata: Record = {}; // bcz: CHANGE any to a proper type! @observable private simplifiedChunks: ObservableMap; /** * Creates a new DocumentManager * @param templateDocument The document that serves as a template for new documents */ constructor(chatBox: ChatBox, parentView: DocumentView) { makeObservable(this); this.parentView = parentView; const agentDoc = DocCast(chatBox.Document.agentDocument) ?? new Doc(); const chunk_simpl = DocCast(agentDoc.chunk_simpl) ?? new Doc(); agentDoc.title = chatBox.Document.title + '_agentDocument'; chunk_simpl.title = '_chunk_simpl'; chatBox.Document.agentDocument = agentDoc; DocCast(chatBox.Document.agentDocument)!.chunk_simpl = chunk_simpl; this.simplifiedChunks = StrListCast(chunk_simpl.mapping).reduce((mapping, chunks) => { StrListCast(chunks).forEach(chunk => { const parsed = JSON.parse(StrCast(chunk)); mapping.set(parsed.chunkId, parsed); }); return mapping; }, new ObservableMap()); this.documentsById = StrListCast(agentDoc.mapping).reduce((mapping, content) => { const [id, layoutId, docId] = content.split(':'); const layoutDoc = DocServer.GetCachedRefField(layoutId); const dataDoc = DocServer.GetCachedRefField(docId); if (!layoutDoc || !dataDoc) { console.warn(`Document with ID ${id} not found in mapping`); } else { mapping.set(id, { layoutDoc, dataDoc }); } return mapping; }, new ObservableMap()); console.log(`AgentDocumentManager initialized with ${this.documentsById.size} documents`); this.chatBox = chatBox; this.chatBoxDocument = chatBox.Document; reaction( () => this.documentsById.values(), () => { if (this.chatBoxDocument && DocCast(this.chatBoxDocument.agentDocument)) { DocCast(this.chatBoxDocument.agentDocument)!.mapping = new List(Array.from(this.documentsById.entries()).map(([id, agent]) => `${id}:${agent.dataDoc[Id]}:${agent.layoutDoc[Id]}`)); } } //{ fireImmediately: true } ); reaction( () => this.simplifiedChunks.values(), () => { if (this.chatBoxDocument && DocCast(this.chatBoxDocument.agentDocument)) { DocCast(DocCast(this.chatBoxDocument.agentDocument)!.chunk_simpl)!.mapping = new List(Array.from(this.simplifiedChunks.values()).map(chunk => JSON.stringify(chunk))); } } //{ fireImmediately: true } ); this.processDocument(this.chatBoxDocument); this.initializeFieldMetadata(); } /** * Extracts field metadata from DocumentOptions class */ private initializeFieldMetadata() { // Parse DocumentOptions to extract field definitions const documentOptionsInstance = new DocumentOptions(); const documentOptionsEntries = Object.entries(documentOptionsInstance); for (const [fieldName, fieldInfo] of documentOptionsEntries) { // Extract field information const fieldData: Record = { // bcz: CHANGE any to a proper type! name: fieldName, withoutUnderscore: fieldName.startsWith('_') ? fieldName.substring(1) : fieldName, description: '', type: 'unknown', required: false, defaultValue: undefined, possibleValues: [], }; // Check if fieldInfo has description property (it's likely a FInfo instance) if (fieldInfo && typeof fieldInfo === 'object' && 'description' in fieldInfo) { fieldData.description = fieldInfo.description; // Extract field type if available if ('fieldType' in fieldInfo) { fieldData.type = fieldInfo.fieldType; } // Extract possible values if available if ('values' in fieldInfo && Array.isArray(fieldInfo.values)) { fieldData.possibleValues = fieldInfo.values; } } this.fieldMetadata[fieldName] = fieldData; } } /** * Toggle between linked documents mode and canvas mode */ @action public setCanvasMode(useCanvas: boolean) { this._useCanvasMode = useCanvas; console.log(`[AgentDocumentManager] Canvas mode ${useCanvas ? 'enabled' : 'disabled'}`); // Reinitialize documents based on new mode if (useCanvas) { this.initializeCanvasDocuments(); } else { this.initializeFindDocsFreeform(); } } /** * Get current canvas mode status */ public get useCanvasMode(): boolean { return this._useCanvasMode; } /** * Get current canvas mode status (for external access) */ public getCanvasMode(): boolean { return this._useCanvasMode; } /** * Initialize documents from the entire canvas */ @action public initializeCanvasDocuments() { try { console.log('[AgentDocumentManager] Finding all documents on canvas...'); console.log('[AgentDocumentManager] Canvas mode enabled, looking for all documents...'); // Get all canvas documents using CanvasDocsTool const canvasDocs = CanvasDocsTool.getAllCanvasDocuments(false); console.log(`[AgentDocumentManager] Found ${canvasDocs.length} documents on canvas`); if (canvasDocs.length === 0) { console.warn('[AgentDocumentManager] No documents found on canvas. This might indicate:'); console.warn(' 1. No documents are currently rendered/visible'); console.warn(' 2. All documents are considered "system" documents'); console.warn(' 3. DocumentView.allViews() is not returning expected results'); // Let's also try including system docs to see if that's the issue const canvasDocsWithSystem = CanvasDocsTool.getAllCanvasDocuments(true); console.log(`[AgentDocumentManager] With system docs included: ${canvasDocsWithSystem.length} documents`); } // Process each canvas document canvasDocs.forEach(async (doc: Doc) => { if (doc && doc !== this.chatBoxDocument) { // Don't process the chatbox itself console.log('[AgentDocumentManager] Processing canvas document:', doc.id, doc.title, doc.type); await this.processDocument(doc); console.log('[AgentDocumentManager] Completed processing document:', doc.id); } }); } catch (error) { console.error('[AgentDocumentManager] Error finding documents on canvas:', error); } } /** * Gets all documents in the same Freeform view as the ChatBox * Uses the LinkManager to get all linked documents, similar to how ChatBox does it */ public initializeFindDocsFreeform() { // Reset collections //this.documentsById.clear(); try { // Use the LinkManager approach which is proven to work in ChatBox if (this.chatBoxDocument) { console.log('Finding documents linked to ChatBox document with ID:', this.chatBoxDocument[Id]); // Get directly linked documents via LinkManager const linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.chatBoxDocument) .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.chatBoxDocument!))) .map(d => DocCast(d?.annotationOn, d)) .filter(d => d); console.log(`Found ${linkedDocs.length} linked documents via LinkManager`); // Process the linked documents linkedDocs.forEach(async (doc: Doc | undefined) => { if (doc) { await this.processDocument(doc); console.log('Processed linked document:', doc[Id], doc.title, doc.type); } }); } } catch (error) { console.error('Error finding documents in Freeform view:', error); } } public get parentViewDocument(): DocumentView { return this.parentView; } /** * Process a document by ensuring it has an ID and adding it to the appropriate collections * @param doc The document to process */ @action public async processDocument(doc: Doc): Promise { // Ensure document has a persistent ID const docId = this.ensureDocumentId(doc); if (doc.chunk_simplified) { const newChunks: SimplifiedChunk[] = []; for (const chunk of JSON.parse(StrCast(doc.chunk_simplified))) { console.log('chunk', chunk); newChunks.push(chunk as SimplifiedChunk); } console.log('Added simplified chunks to simplifiedChunks:', docId, newChunks); this.addSimplifiedChunks(newChunks); //DocCast(DocCast(this.chatBoxDocument!.agentDocument)!.chunk_simpl)!.mapping = new List(Array.from(this.simplifiedChunks.values()).map(chunk => JSON.stringify(chunk))); } // Only add if we haven't already processed this document if (!this.documentsById.has(docId)) { this.documentsById.set(docId, { layoutDoc: doc, dataDoc: doc[DocData] }); console.log('Added document to documentsById:', doc[Id], docId, doc[Id], doc[DocData][Id]); } return docId; } /** * Ensures a document has a persistent ID stored in its metadata * @param doc The document to ensure has an ID * @returns The document's ID */ private ensureDocumentId(doc: Doc): string { let docId: string | undefined; // 1. Try the direct id property if it exists if (doc[Id]) { console.log('Found document ID (normal):', doc[Id]); docId = doc[Id]; } else { throw new Error('No document ID found'); } return docId; } /** * Extracts metadata from a specific document * @param docId The ID of the document to extract metadata from * @returns An object containing the document's metadata */ public extractDocumentMetadata(id: string) { if (!id) return null; const agentDoc = this.documentsById.get(id); if (!agentDoc) return null; const layoutDoc = agentDoc.layoutDoc; const dataDoc = agentDoc.dataDoc; const metadata: Record = { // bcz: CHANGE any to a proper type! id: layoutDoc[Id] || dataDoc[Id] || '', title: layoutDoc.title || '', type: layoutDoc.type || '', fields: { layout: {}, data: {}, }, fieldLocationMap: {}, }; // Process all known field definitions Object.keys(this.fieldMetadata).forEach(fieldName => { // const fieldDef = this.fieldMetadata[fieldName]; const strippedName = fieldName.startsWith('_') ? fieldName.substring(1) : fieldName; // Check if field exists on layout document let layoutValue = undefined; if (layoutDoc) { layoutValue = layoutDoc[fieldName]; if (layoutValue !== undefined) { // Field exists on layout document metadata.fields.layout[fieldName] = this.formatFieldValue(layoutValue); metadata.fieldLocationMap[strippedName] = 'layout'; } } // Check if field exists on data document let dataValue = undefined; if (dataDoc) { dataValue = dataDoc[fieldName]; if (dataValue !== undefined) { // Field exists on data document metadata.fields.data[fieldName] = this.formatFieldValue(dataValue); if (!metadata.fieldLocationMap[strippedName]) { metadata.fieldLocationMap[strippedName] = 'data'; } } } // For fields with stripped names (without leading underscore), // also check if they exist on documents without the underscore if (fieldName.startsWith('_')) { const nonUnderscoreFieldName = fieldName.substring(1); if (layoutDoc) { const nonUnderscoreLayoutValue = layoutDoc[nonUnderscoreFieldName]; if (nonUnderscoreLayoutValue !== undefined) { metadata.fields.layout[nonUnderscoreFieldName] = this.formatFieldValue(nonUnderscoreLayoutValue); metadata.fieldLocationMap[nonUnderscoreFieldName] = 'layout'; } } if (dataDoc) { const nonUnderscoreDataValue = dataDoc[nonUnderscoreFieldName]; if (nonUnderscoreDataValue !== undefined) { metadata.fields.data[nonUnderscoreFieldName] = this.formatFieldValue(nonUnderscoreDataValue); if (!metadata.fieldLocationMap[nonUnderscoreFieldName]) { metadata.fieldLocationMap[nonUnderscoreFieldName] = 'data'; } } } } }); // Add common field aliases for easier discovery // This helps users understand both width and _width refer to the same property if (metadata.fields.layout._width !== undefined && metadata.fields.layout.width === undefined) { metadata.fields.layout.width = metadata.fields.layout._width; metadata.fieldLocationMap.width = 'layout'; } if (metadata.fields.layout._height !== undefined && metadata.fields.layout.height === undefined) { metadata.fields.layout.height = metadata.fields.layout._height; metadata.fieldLocationMap.height = 'layout'; } return metadata; } /** * Formats a field value for JSON output * @param value The field value to format * @returns A JSON-friendly representation of the field value */ private formatFieldValue(value: FieldResult | undefined) { if (value === undefined || value === null) { return null; } // Handle Doc objects if (value instanceof Doc) { return { type: 'Doc', id: value[Id] || this.ensureDocumentId(value), title: value.title || '', docType: value.type || '', }; } // Handle RichTextField (try to extract plain text) if (typeof value === 'string' && value.includes('"type":"doc"') && value.includes('"content":')) { try { const rtfObj = JSON.parse(value); // If this looks like a rich text field structure if (rtfObj.doc && rtfObj.doc.content) { // Recursively extract text from the content let plainText = ''; const extractText = (node: { text: string; content?: unknown[] }) => { if (node.text) { plainText += node.text; } if (node.content && Array.isArray(node.content)) { node.content.forEach(child => extractText(child as { text: string; content?: unknown[] })); } }; extractText(rtfObj.doc); // If we successfully extracted text, show it, but also preserve the original value if (plainText) { return { type: 'RichText', text: plainText, length: plainText.length, // Don't include the full value as it can be very large }; } } } catch { // If parsing fails, just treat as a regular string } } // Handle arrays and complex objects if (typeof value === 'object') { // If the object has a toString method, use it if (value.toString && value.toString !== Object.prototype.toString) { return value.toString(); } try { // Try to convert to JSON string return JSON.stringify(value); } catch { return '[Complex Object]'; } } // Return primitive values as is return value; } /** * Converts a string field value to the appropriate type based on field metadata * @param fieldName The name of the field * @param fieldValue The string value to convert * @returns The converted value with the appropriate type */ private convertFieldValue(fieldName: string, fieldValueIn: string | number | boolean): FieldResult | undefined { // If fieldValue is already a number or boolean, we don't need to convert it from string if (typeof fieldValueIn === 'number' || typeof fieldValueIn === 'boolean') { return fieldValueIn; } // If fieldValue is a string "true" or "false", convert to boolean if (typeof fieldValueIn === 'string') { if (fieldValueIn.toLowerCase() === 'true') { return true; } if (fieldValueIn.toLowerCase() === 'false') { return false; } } // coerce fieldvValue to a string const fieldValue = typeof fieldValueIn !== 'string' ? String(fieldValueIn) : fieldValueIn; // Special handling for text field - convert to proper RichTextField format if (fieldName === 'text') { try { // Check if it's already a valid JSON RichTextField JSON.parse(fieldValue); return fieldValue; } catch { // It's a plain text string, so convert it to RichTextField format const rtf = { doc: { type: 'doc', content: [ { type: 'paragraph', content: [ { type: 'text', text: fieldValue, }, ], }, ], }, }; return JSON.stringify(rtf); } } // Get field metadata const normalizedFieldName = fieldName.startsWith('_') ? fieldName : `_${fieldName}`; const strippedFieldName = fieldName.startsWith('_') ? fieldName.substring(1) : fieldName; // Check both versions of the field name in metadata const fieldMeta = this.fieldMetadata[normalizedFieldName] || this.fieldMetadata[strippedFieldName]; // Special handling for width and height without metadata if (!fieldMeta && (fieldName === '_width' || fieldName === '_height' || fieldName === 'width' || fieldName === 'height')) { const num = Number(fieldValue); return isNaN(num) ? fieldValue : num; } if (!fieldMeta) { // If no metadata found, just return the string value return fieldValue; } // Convert based on field type const fieldType = fieldMeta.type; if (fieldType === 'boolean') { // Convert to boolean return fieldValue.toLowerCase() === 'true'; } else if (fieldType === 'number') { // Convert to number const num = Number(fieldValue); return isNaN(num) ? fieldValue : num; } else if (fieldType === 'date') { // Try to convert to date (stored as number timestamp) try { return new Date(fieldValue).getTime(); } catch { return fieldValue; } } else if (fieldType.includes('list') || fieldType.includes('array')) { // Try to parse as JSON array try { return JSON.parse(fieldValue) as FieldResult; // bcz: this needs to be typed properly. Dash fields can't accept a generic 'objext' } catch { return fieldValue; } } else if (fieldType === 'json' || fieldType === 'object') { // Try to parse as JSON object try { return JSON.parse(fieldValue) as FieldResult; // bcz: this needs to be typed properly. Dash fields can't accept a generic 'objext' } catch { return fieldValue; } } // Default to string return fieldValue; } /** * Extracts all field metadata from DocumentOptions * @returns A structured object containing metadata about all available document fields */ public getAllFieldMetadata() { // Start with our already populated fieldMetadata from the DocumentOptions class const result: Record = { // bcz: CHANGE any to a proper type! fieldCount: Object.keys(this.fieldMetadata).length, fields: {}, fieldsByType: { string: [], number: [], boolean: [], //doc: [], //list: [], //date: [], //enumeration: [], //other: [], }, fieldNameMappings: {}, commonFields: { appearance: [], position: [], size: [], content: [], behavior: [], layout: [], }, }; // Process each field in the metadata Object.entries(this.fieldMetadata).forEach(([fieldName, fieldInfo]) => { const strippedName = fieldName.startsWith('_') ? fieldName.substring(1) : fieldName; // Add to fieldNameMappings if (fieldName.startsWith('_')) { result.fieldNameMappings[strippedName] = fieldName; } // Create structured field metadata const fieldData: Record = { // bcz: CHANGE any to a proper type! name: fieldName, displayName: strippedName, description: fieldInfo.description || '', type: fieldInfo.fieldType || 'unknown', possibleValues: fieldInfo.values || [], }; // Add field to fields collection result.fields[fieldName] = fieldData; // Categorize by field type const type = fieldInfo.fieldType?.toLowerCase() || 'unknown'; if (type === 'string') { result.fieldsByType.string.push(fieldName); } else if (type === 'number') { result.fieldsByType.number.push(fieldName); } else if (type === 'boolean') { result.fieldsByType.boolean.push(fieldName); } else if (type === 'doc') { //result.fieldsByType.doc.push(fieldName); } else if (type === 'list') { //result.fieldsByType.list.push(fieldName); } else if (type === 'date') { //result.fieldsByType.date.push(fieldName); } else if (type === 'enumeration') { //result.fieldsByType.enumeration.push(fieldName); } else { //result.fieldsByType.other.push(fieldName); } // Categorize by field purpose if (fieldName.includes('width') || fieldName.includes('height') || fieldName.includes('size')) { result.commonFields.size.push(fieldName); } else if (fieldName.includes('color') || fieldName.includes('background') || fieldName.includes('border')) { result.commonFields.appearance.push(fieldName); } else if (fieldName.includes('x') || fieldName.includes('y') || fieldName.includes('position') || fieldName.includes('pan')) { result.commonFields.position.push(fieldName); } else if (fieldName.includes('text') || fieldName.includes('title') || fieldName.includes('data')) { result.commonFields.content.push(fieldName); } else if (fieldName.includes('action') || fieldName.includes('click') || fieldName.includes('event')) { result.commonFields.behavior.push(fieldName); } else if (fieldName.includes('layout')) { result.commonFields.layout.push(fieldName); } }); // Add special section for auto-sizing related fields result.autoSizingFields = { height: { autoHeightField: '_layout_autoHeight', heightField: '_height', displayName: 'height', usage: 'To manually set height, first set layout_autoHeight to false', }, width: { autoWidthField: '_layout_autoWidth', widthField: '_width', displayName: 'width', usage: 'To manually set width, first set layout_autoWidth to false', }, }; // Add special section for text field format result.specialFields = { text: { name: 'text', description: 'Document text content', format: 'RichTextField', note: 'When setting text, provide plain text - it will be automatically converted to the correct format', example: 'For setting: "Hello world" (plain text); For getting: Will be converted to plaintext for display', }, }; return result; } /** * Edits a specific field on a document * @param docId The ID of the document to edit * @param fieldName The name of the field to edit * @param fieldValue The new value for the field (string, number, or boolean) * @returns Object with success status, message, and additional information */ public editDocumentField( docId: string, fieldName: string, fieldValue: string | number | boolean ): { success: boolean; message: string; fieldName?: string; originalFieldName?: string; newValue?: string | number | boolean | object; warning?: string; } { // Normalize field name (handle with/without underscore) let normalizedFieldName = fieldName.startsWith('_') ? fieldName : fieldName; // const strippedFieldName = fieldName.startsWith('_') ? fieldName.substring(1) : fieldName; // Handle common field name aliases (width → _width, height → _height) // Many document fields use '_' prefix for layout properties if (fieldName === 'width') { normalizedFieldName = '_width'; } else if (fieldName === 'height') { normalizedFieldName = '_height'; } // Get the documents const doc = this.documentsById.get(docId); if (!doc) { return { success: false, message: `Document with ID ${docId} not found` }; } const { layoutDoc, dataDoc } = this.documentsById.get(docId) ?? { layoutDoc: null, dataDoc: null }; if (!layoutDoc && !dataDoc) { return { success: false, message: `Could not find layout or data document for document with ID ${docId}` }; } try { // Convert the field value to the appropriate type based on field metadata const convertedValue = this.convertFieldValue(normalizedFieldName, fieldValue); let targetDoc: Doc | undefined; let targetLocation: string; // First, check if field exists on layout document using Doc.Get if (layoutDoc) { const fieldExistsOnLayout = Doc.Get(layoutDoc, normalizedFieldName, true) !== undefined; // If it exists on layout document, update it there if (fieldExistsOnLayout) { targetDoc = layoutDoc; targetLocation = 'layout'; } // If it has an underscore prefix, it's likely a layout property even if not yet set else if (normalizedFieldName.startsWith('_')) { targetDoc = layoutDoc; targetLocation = 'layout'; } // Otherwise, look for or create on data document else if (dataDoc) { targetDoc = dataDoc; targetLocation = 'data'; } // If no data document available, default to layout else { targetDoc = layoutDoc; targetLocation = 'layout'; } } // If no layout document, use data document else if (dataDoc) { targetDoc = dataDoc; targetLocation = 'data'; } else { return { success: false, message: `No valid document found for editing` }; } if (!targetDoc) { return { success: false, message: `Target document not available` }; } // Set the field value on the target document targetDoc[normalizedFieldName] = convertedValue; // bcz: converteValue needs to be typed properly. Dash fields can't accept a generic 'objext' return { success: true, message: `Successfully updated field '${normalizedFieldName}' on ${targetLocation} document (ID: ${docId})`, fieldName: normalizedFieldName, originalFieldName: fieldName, newValue: convertedValue, }; } catch (error) { console.error('Error editing document field:', error); return { success: false, message: `Error updating field: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Gets metadata for a specific document or all documents * @param documentId Optional ID of a specific document to get metadata for * @returns Document metadata or metadata for all documents */ public getDocumentMetadata(documentId?: string) { if (documentId) { console.log(`Returning document metadata for docID, ${documentId}:`, this.extractDocumentMetadata(documentId)); return this.extractDocumentMetadata(documentId); } else { // Get metadata for all documents const documentsMetadata: Record> = {}; // bcz: CHANGE any to a proper type! for (const docid of this.documentsById.keys()) { const metadata = this.extractDocumentMetadata(docid); if (metadata) { documentsMetadata[docid] = metadata; } else { console.warn(`No metadata found for document with ID: ${docid}`); } } return { documentCount: this.documentsById.size, documents: documentsMetadata, //fieldDefinitions: this.fieldMetadata, // TODO: remove this, if fieldDefinitions are not needed. }; } } /** * Adds links between documents based on their IDs * @param docIds Array of document IDs to link * @param relationship Optional relationship type for the links * @returns Array of created link documents */ public addLinks(docIds: string[]): Doc[] { const createdLinks: Doc[] = []; // Use string keys for Set instead of arrays which don't work as expected as keys const alreadyLinked = new Set(); // Iterate over the document IDs and add links docIds.forEach(docId1 => { const doc1 = this.documentsById.get(docId1); docIds.forEach(docId2 => { if (docId1 === docId2) return; // Skip self-linking // Create a consistent key regardless of document order const linkKey = [docId1, docId2].sort().join('_'); if (alreadyLinked.has(linkKey)) return; const doc2 = this.documentsById.get(docId2); if (doc1?.layoutDoc && doc2?.layoutDoc) { try { // Create a link document between doc1 and doc2 const linkDoc = Docs.Create.LinkDocument(doc1.layoutDoc, doc2.layoutDoc); // Set a default color if relationship doesn't specify one if (!linkDoc.color) { linkDoc.color = 'lightBlue'; // Default blue color } // Ensure link is visible by setting essential properties linkDoc.link_visible = true; linkDoc.link_enabled = true; linkDoc.link_autoMove = true; linkDoc.link_showDirected = true; // Set the embedContainer to ensure visibility // This is shown in the image as a key difference between visible/non-visible links if (this.chatBoxDocument && this.chatBoxDocument.parent && typeof this.chatBoxDocument.parent === 'object' && 'title' in this.chatBoxDocument.parent) { linkDoc.embedContainer = String(this.chatBoxDocument.parent.title); } else if (doc1.layoutDoc.parent && typeof doc1.layoutDoc.parent === 'object' && 'title' in doc1.layoutDoc.parent) { linkDoc.embedContainer = String(doc1.layoutDoc.parent.title); } else { // Default to a tab name if we can't find one linkDoc.embedContainer = 'Untitled Tab 1'; } // Add the link to the document system LinkManager.Instance.addLink(linkDoc); const ancestor = DocumentView.linkCommonAncestor(linkDoc); ancestor?.ComponentView?.addDocument?.(linkDoc); // Add to user document list to make it visible in the UI Doc.AddDocToList(Doc.UserDoc(), 'links', linkDoc); // Create a visual link for display if (this.chatBoxDocument) { // Make sure the docs are visible in the UI this.chatBox._props.addDocument?.(doc1.layoutDoc); this.chatBox._props.addDocument?.(doc2.layoutDoc); // Use DocumentManager to ensure documents are visible DocumentManager.Instance.showDocument(doc1.layoutDoc, { willZoomCentered: false }); DocumentManager.Instance.showDocument(doc2.layoutDoc, { willZoomCentered: false }); } createdLinks.push(linkDoc); alreadyLinked.add(linkKey); } catch (error) { console.error('Error creating link between documents:', error); } } }); }); // Force update of the UI to show new links setTimeout(() => { try { // Update server cache to ensure links are persisted UPDATE_SERVER_CACHE && typeof UPDATE_SERVER_CACHE === 'function' && UPDATE_SERVER_CACHE(); } catch (e) { console.warn('Could not update server cache after creating links:', e); } }, 100); return createdLinks; } /** * Helper method to validate a document type and ensure it's a valid supportedDocType * @param docType The document type to validate * @returns True if the document type is valid, false otherwise */ private isValidDocType(docType: string): boolean { return Object.values(supportedDocTypes).includes(docType as supportedDocTypes); } /** * Creates a document in the dashboard and returns its ID. * This is a public API used by tools like SearchTool. * * @param docType The type of document to create * @param data The data for the document * @param options Optional configuration options * @returns The ID of the created document */ public async createDocInDash(docType: string, data: string, options?: DocumentOptions): Promise { // Validate doc_type if (!this.isValidDocType(docType)) { throw new Error(`Invalid document type: ${docType}`); } try { // Create simple document with just title and data const simpleDoc: parsedDoc = { ...(options as parsedDoc), // bcz: hack .. why do we need parsedDoc and not DocumentOptions here? doc_type: docType, title: options?.title ?? `Untitled Document ${this.documentsById.size + 1}`, data: data, x: options?.x ?? 0, y: options?.y ?? 0, _width: 300, _height: 300, _layout_fitWidth: false, _layout_autoHeight: true, }; // Additional handling for web documents if (docType === 'web') { // For web documents, don't sanitize the URL here // Instead, set properties to handle content safely when loaded simpleDoc._disable_resource_loading = true; simpleDoc._sandbox_iframe = true; simpleDoc.data_useCors = true; // Specify a more permissive sandbox to allow content to render properly // but still maintain security simpleDoc._iframe_sandbox = 'allow-same-origin allow-scripts allow-popups allow-forms'; } // Use the chatBox's createDocInDash method to create the document if (!this.chatBox) { throw new Error('ChatBox instance not available for creating document'); } const doc = this.chatBox.whichDoc(simpleDoc, false); if (doc) { // Use MobX runInAction to properly modify observable state runInAction(() => { if (this.chatBoxDocument && doc) { // Create link and add it to the document system const linkDoc = Docs.Create.LinkDocument(this.chatBoxDocument, doc); LinkManager.Instance.addLink(linkDoc); if (doc.type !== 'web') { // Add document to view this.chatBox._props.addDocument?.(doc); // Show document - defer actual display to prevent immediate resource loading setTimeout(() => { DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {}); }, 100); } } }); const id = await this.processDocument(doc); return id; } else { throw new Error(`Error creating document. Created document not found.`); } } catch (error) { throw new Error(`Error creating document: ${error}`); } } /** * Sanitizes web content to prevent errors with external resources * @param content The web content to sanitize * @returns Sanitized content */ private sanitizeWebContent(content: string): string { if (!content) return content; try { // Replace problematic resource references that might cause errors const sanitized = content // Remove preload links that might cause errors .replace(/]*rel=["']preload["'][^>]*>/gi, '') // Remove map file references .replace(/\/\/# sourceMappingURL=.*\.map/gi, '') // Remove external CSS map files references .replace(/\/\*# sourceMappingURL=.*\.css\.map.*\*\//gi, '') // Add sandbox to iframes .replace(/