aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/views/nodes/chatbot/agentsystem/Agent.ts17
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx28
-rw-r--r--src/client/views/nodes/chatbot/tools/CreateLinksTool.ts68
-rw-r--r--src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts884
-rw-r--r--src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts923
5 files changed, 1032 insertions, 888 deletions
diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
index 43faf5bf4..5af021dbf 100644
--- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts
+++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
@@ -20,10 +20,12 @@ import { getReactPrompt } from './prompts';
//import { DictionaryTool } from '../tools/DictionaryTool';
import { ChatCompletionMessageParam } from 'openai/resources';
import { Doc } from '../../../../../fields/Doc';
-import { parsedDoc } from '../chatboxcomponents/ChatBox';
+import { ChatBox, parsedDoc } from '../chatboxcomponents/ChatBox';
import { WebsiteInfoScraperTool } from '../tools/WebsiteInfoScraperTool';
import { Upload } from '../../../../../server/SharedMediaTypes';
import { RAGTool } from '../tools/RAGTool';
+import { AgentDocumentManager } from '../utils/AgentDocumentManager';
+import { CreateLinksTool } from '../tools/CreateLinksTool';
//import { CreateTextDocTool } from '../tools/CreateTextDocumentTool';
dotenv.config();
@@ -47,6 +49,7 @@ export class Agent {
private processingInfo: ProcessingInfo[] = [];
private streamedAnswerParser: StreamedAnswerParser = new StreamedAnswerParser();
private tools: Record<string, BaseTool<ReadonlyArray<Parameter>>>;
+ private _docManager: AgentDocumentManager;
/**
* The constructor initializes the agent with the vector store and toolset, and sets up the OpenAI client.
@@ -64,9 +67,9 @@ export class Agent {
csvData: () => { filename: string; id: string; text: string }[],
addLinkedUrlDoc: (url: string, id: string) => void,
createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void,
- addLinkedDoc: (doc: parsedDoc) => Doc | undefined,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- createCSVInDash: (url: string, title: string, id: string, data: string) => void
+ createCSVInDash: (url: string, title: string, id: string, data: string) => void,
+ chatBox: ChatBox
) {
// Initialize OpenAI client with API key from environment
this.client = new OpenAI({ apiKey: process.env.OPENAI_KEY, dangerouslyAllowBrowser: true });
@@ -74,6 +77,7 @@ export class Agent {
this._history = history;
this._summaries = summaries;
this._csvData = csvData;
+ this._docManager = new AgentDocumentManager(chatBox);
// Define available tools for the assistant
this.tools = {
@@ -84,7 +88,8 @@ export class Agent {
searchTool: new SearchTool(addLinkedUrlDoc),
noTool: new NoTool(),
//imageCreationTool: new ImageCreationTool(createImage),
- documentMetadata: new DocumentMetadataTool(this),
+ documentMetadata: new DocumentMetadataTool(this._docManager),
+ createLinks: new CreateLinksTool(this._docManager),
};
}
@@ -541,9 +546,9 @@ export class Agent {
*
* @param chatBox The ChatBox instance to pass to the DocumentMetadataTool
*/
- public reinitializeDocumentMetadataTool(chatBox: any): void {
+ public reinitializeDocumentMetadataTool(): void {
if (this.tools && this.tools.documentMetadata) {
- this.tools.documentMetadata = new DocumentMetadataTool(chatBox);
+ this.tools.documentMetadata = new DocumentMetadataTool(this._docManager);
console.log('Agent: Reinitialized DocumentMetadataTool with ChatBox instance');
} else {
console.warn('Agent: Could not reinitialize DocumentMetadataTool - tool not found');
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index 33419e559..e09b4313f 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -107,11 +107,11 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.vectorstore_id = StrCast(this.dataDoc.vectorstore_id);
}
this.vectorstore = new Vectorstore(this.vectorstore_id, this.retrieveDocIds);
- this.agent = new Agent(this.vectorstore, this.retrieveSummaries, this.retrieveFormattedHistory, this.retrieveCSVData, this.addLinkedUrlDoc, this.createImageInDash, this.createDocInDash, this.createCSVInDash);
+ this.agent = new Agent(this.vectorstore, this.retrieveSummaries, this.retrieveFormattedHistory, this.retrieveCSVData, this.addLinkedUrlDoc, this.createImageInDash, this.createCSVInDash, this);
// Reinitialize the DocumentMetadataTool with a direct reference to this ChatBox instance
// This ensures the tool can properly access documents in the same Freeform view
- this.agent.reinitializeDocumentMetadataTool(this);
+ this.agent.reinitializeDocumentMetadataTool();
this.messagesRef = React.createRef<HTMLDivElement>();
@@ -446,7 +446,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
private createCollectionWithChildren = (data: parsedDoc[], insideCol: boolean): Opt<Doc>[] => data.map(doc => this.whichDoc(doc, insideCol));
@action
- whichDoc = (doc: parsedDoc, insideCol: boolean): Opt<Doc> => {
+ public whichDoc = (doc: parsedDoc, insideCol: boolean): Opt<Doc> => {
const options = OmitKeys(doc, ['doct_type', 'data']).omit as DocumentOptions;
const data = (doc as parsedDocData).data;
const ndoc = (() => {
@@ -516,28 +516,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
/**
- * Creates a document in the dashboard.
- *
- * @param {string} doc_type - The type of document to create.
- * @param {string} data - The data used to generate the document.
- * @param {DocumentOptions} options - Configuration options for the document.
- * @returns {Promise<void>} A promise that resolves once the document is created and displayed.
- */
- @action
- createDocInDash = (pdoc: parsedDoc) => {
- const linkAndShowDoc = (doc: Opt<Doc>) => {
- if (doc) {
- LinkManager.Instance.addLink(Docs.Create.LinkDocument(this.Document, doc));
- this._props.addDocument?.(doc);
- DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
- }
- };
- const doc = this.whichDoc(pdoc, false);
- if (doc) linkAndShowDoc(doc);
- return doc;
- };
-
- /**
* Creates a deck of flashcards.
*
* @param {any} data - The data used to generate the flashcards. Can be a string or an object.
diff --git a/src/client/views/nodes/chatbot/tools/CreateLinksTool.ts b/src/client/views/nodes/chatbot/tools/CreateLinksTool.ts
new file mode 100644
index 000000000..c2850a8ce
--- /dev/null
+++ b/src/client/views/nodes/chatbot/tools/CreateLinksTool.ts
@@ -0,0 +1,68 @@
+import { Observation } from '../types/types';
+import { ParametersType, ToolInfo } from '../types/tool_types';
+import { BaseTool } from './BaseTool';
+import { AgentDocumentManager } from '../utils/AgentDocumentManager';
+
+const createLinksToolParams = [
+ {
+ name: 'document_ids',
+ type: 'string[]',
+ description: 'List of document IDs to create links between. All documents will be linked to each other.',
+ required: true,
+ },
+] as const;
+
+type CreateLinksToolParamsType = typeof createLinksToolParams;
+
+const createLinksToolInfo: ToolInfo<CreateLinksToolParamsType> = {
+ name: 'createLinks',
+ description: 'Creates visual links between multiple documents in the dashboard. This allows related documents to be connected visually with lines that users can see.',
+ citationRules: 'No citation needed.',
+ parameterRules: createLinksToolParams,
+};
+
+export class CreateLinksTool extends BaseTool<CreateLinksToolParamsType> {
+ private _documentManager: AgentDocumentManager;
+
+ constructor(documentManager: AgentDocumentManager) {
+ super(createLinksToolInfo);
+ this._documentManager = documentManager;
+ }
+
+ async execute(args: ParametersType<CreateLinksToolParamsType>): Promise<Observation[]> {
+ try {
+ // Validate that we have at least 2 documents to link
+ if (args.document_ids.length < 2) {
+ return [{ type: 'text', text: 'Error: At least 2 document IDs are required to create links.' }];
+ }
+
+ // Validate that all documents exist
+ const missingDocIds = args.document_ids.filter(id => !this._documentManager.has(id));
+ if (missingDocIds.length > 0) {
+ return [
+ {
+ type: 'text',
+ text: `Error: The following document IDs were not found: ${missingDocIds.join(', ')}`,
+ },
+ ];
+ }
+
+ // Create links between all documents with the specified relationship
+ const createdLinks = this._documentManager.addLinks(args.document_ids);
+
+ return [
+ {
+ type: 'text',
+ text: `Successfully created ${createdLinks.length} visual links between ${args.document_ids.length}.`,
+ },
+ ];
+ } catch (error) {
+ return [
+ {
+ type: 'text',
+ text: `Error creating links: ${error instanceof Error ? error.message : String(error)}`,
+ },
+ ];
+ }
+ }
+}
diff --git a/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts b/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts
index 08351143b..4b751acc0 100644
--- a/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts
+++ b/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts
@@ -10,6 +10,7 @@ import { LinkManager } from '../../../../util/LinkManager';
import { DocCast, StrCast } from '../../../../../fields/Types';
import { supportedDocTypes } from '../types/tool_types';
import { parsedDoc } from '../chatboxcomponents/ChatBox';
+import { AgentDocumentManager } from '../utils/AgentDocumentManager';
// Define the parameters for the DocumentMetadataTool
const parameterDefinitions: ReadonlyArray<Parameter> = [
@@ -199,746 +200,12 @@ const documentMetadataToolInfo: ToolInfo<DocumentMetadataToolParamsType> = {
* and allows for editing document fields in the correct location.
*/
export class DocumentMetadataTool extends BaseTool<DocumentMetadataToolParamsType> {
- private freeformView: any;
- private chatBox: any;
- private chatBoxDocument: Doc | null = null;
- private documentsById: Map<string, Doc> = new Map();
- private layoutDocsById: Map<string, Doc> = new Map();
- private dataDocsById: Map<string, Doc> = new Map();
- private fieldMetadata: Record<string, any> = {};
- private readonly DOCUMENT_ID_FIELD = '_dash_document_id';
-
- constructor(chatBox: any) {
- super(documentMetadataToolInfo);
- this.chatBox = chatBox;
-
- // Store a direct reference to the ChatBox document
- if (chatBox && chatBox.Document) {
- this.chatBoxDocument = chatBox.Document;
- if (this.chatBoxDocument && this.chatBoxDocument.id) {
- console.log('DocumentMetadataTool initialized with ChatBox Document:', this.chatBoxDocument.id);
- } else {
- console.log('DocumentMetadataTool initialized with ChatBox Document (no ID)');
- }
- } else if (chatBox && chatBox.props && chatBox.props.Document) {
- this.chatBoxDocument = chatBox.props.Document;
- if (this.chatBoxDocument && this.chatBoxDocument.id) {
- console.log('DocumentMetadataTool initialized with ChatBox props.Document:', this.chatBoxDocument.id);
- } else {
- console.log('DocumentMetadataTool initialized with ChatBox props.Document (no ID)');
- }
- } else {
- console.warn('DocumentMetadataTool initialized without valid ChatBox Document reference');
- }
-
- 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<string, any> = {
- 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;
- }
- }
-
- /**
- * 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
- */
- private findDocumentsInFreeformView() {
- // Reset collections
- this.documentsById.clear();
- this.layoutDocsById.clear();
- this.dataDocsById.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((doc: Doc) => {
- if (doc) {
- this.processDocument(doc);
- }
- });
-
- // Include the ChatBox document itself
- this.processDocument(this.chatBoxDocument);
-
- // If we have access to the Document's parent, try to find sibling documents
- if (this.chatBoxDocument.parent) {
- const parent = this.chatBoxDocument.parent;
- console.log('Found parent document, checking for siblings');
-
- // Check if parent is a Doc type and has a childDocs function
- if (parent && typeof parent === 'object' && 'childDocs' in parent && typeof parent.childDocs === 'function') {
- try {
- const siblingDocs = parent.childDocs();
- if (Array.isArray(siblingDocs)) {
- console.log(`Found ${siblingDocs.length} sibling documents via parent.childDocs()`);
- siblingDocs.forEach((doc: Doc) => {
- if (doc) {
- this.processDocument(doc);
- }
- });
- }
- } catch (e) {
- console.warn('Error accessing parent.childDocs:', e);
- }
- }
- }
- } else if (this.chatBox && this.chatBox.linkedDocs) {
- // If we have direct access to the linkedDocs computed property from ChatBox
- console.log('Using ChatBox.linkedDocs directly');
- const linkedDocs = this.chatBox.linkedDocs;
- if (Array.isArray(linkedDocs)) {
- console.log(`Found ${linkedDocs.length} documents via ChatBox.linkedDocs`);
- linkedDocs.forEach((doc: Doc) => {
- if (doc) {
- this.processDocument(doc);
- }
- });
- }
-
- // Process the ChatBox document if available
- if (this.chatBox.Document) {
- this.processDocument(this.chatBox.Document);
- }
- } else {
- console.warn('No ChatBox document reference available for finding linked documents');
- }
-
- console.log(`DocumentMetadataTool found ${this.documentsById.size} total documents`);
-
- // If we didn't find any documents, try a fallback method
- if (this.documentsById.size === 0 && this.chatBox) {
- console.log('No documents found, trying fallback method');
-
- // Try to access any field that might contain documents
- if (this.chatBox.props && this.chatBox.props.documents) {
- const documents = this.chatBox.props.documents;
- if (Array.isArray(documents)) {
- console.log(`Found ${documents.length} documents via ChatBox.props.documents`);
- documents.forEach((doc: Doc) => {
- if (doc) {
- this.processDocument(doc);
- }
- });
- }
- }
- }
- } catch (error) {
- console.error('Error finding documents in Freeform view:', error);
- }
- }
-
- /**
- * Process a document by ensuring it has an ID and adding it to the appropriate collections
- * @param doc The document to process
- */
- private processDocument(doc: Doc) {
- // Ensure document has a persistent ID
- const docId = this.ensureDocumentId(doc);
-
- // Only add if we haven't already processed this document
- if (!this.documentsById.has(docId)) {
- this.documentsById.set(docId, doc);
-
- // Get layout doc (the document itself or its layout)
- // TODO: Check if this works.
- const layoutDoc = doc;
- if (layoutDoc) {
- this.layoutDocsById.set(docId, layoutDoc);
- }
-
- // Get data doc
- const dataDoc = doc[DocData];
- if (dataDoc) {
- this.dataDocsById.set(docId, dataDoc);
- }
- }
- }
-
- /**
- * 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;
-
- // First try to get the ID from our custom field
- if (doc[this.DOCUMENT_ID_FIELD]) {
- docId = String(doc[this.DOCUMENT_ID_FIELD]);
- return docId;
- }
-
- // Try different ways to get a document ID
-
- // 1. Try the direct id property if it exists
- if (doc.id && typeof doc.id === 'string') {
- docId = doc.id;
- }
- // 2. Try doc._id if it exists
- else if (doc._id && typeof doc._id === 'string') {
- docId = doc._id;
- }
- // 3. Try doc.data?.id if it exists
- else if (doc.data && typeof doc.data === 'object' && 'id' in doc.data && typeof doc.data.id === 'string') {
- docId = doc.data.id;
- }
- // 4. If none of the above work, generate a UUID
- else {
- docId = uuidv4();
- console.log(`Generated new UUID for document with title: ${doc.title || 'Untitled'}`);
- }
-
- // Store the ID in the document's metadata so it persists
- try {
- doc[this.DOCUMENT_ID_FIELD] = docId;
- } catch (e) {
- console.warn(`Could not assign ID to document property`, e);
- }
-
- 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
- */
- private extractDocumentMetadata(doc?: Doc) {
- if (!doc) return null;
- const layoutDoc = doc;
- const dataDoc = doc[DocData];
-
- const metadata: Record<string, any> = {
- id: doc.dash_document_id || doc.id || '',
- title: doc.title || '',
- type: doc.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;
- }
+ private _docManager: AgentDocumentManager;
- /**
- * 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
- */
- private editDocumentField(
- docId: string,
- fieldName: string,
- fieldValue: string | number | boolean
- ): {
- success: boolean;
- message: string;
- fieldName?: string;
- originalFieldName?: string;
- newValue?: any;
- 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 = this.layoutDocsById.get(docId);
- const dataDoc = this.dataDocsById.get(docId);
-
- 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;
-
- 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)}`,
- };
- }
- }
-
- /**
- * 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, fieldValue: any): any {
- // If fieldValue is already a number or boolean, we don't need to convert it from string
- if (typeof fieldValue === 'number' || typeof fieldValue === 'boolean') {
- return fieldValue;
- }
-
- // If fieldValue is a string "true" or "false", convert to boolean
- if (typeof fieldValue === 'string') {
- if (fieldValue.toLowerCase() === 'true') {
- return true;
- }
- if (fieldValue.toLowerCase() === 'false') {
- return false;
- }
- }
-
- // If fieldValue is not a string (and not a number or boolean), convert it to string
- if (typeof fieldValue !== 'string') {
- fieldValue = String(fieldValue);
- }
-
- // 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 (e) {
- // 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 (e) {
- return fieldValue;
- }
- } else if (fieldType.includes('list') || fieldType.includes('array')) {
- // Try to parse as JSON array
- try {
- return JSON.parse(fieldValue);
- } catch (e) {
- return fieldValue;
- }
- } else if (fieldType === 'json' || fieldType === 'object') {
- // Try to parse as JSON object
- try {
- return JSON.parse(fieldValue);
- } catch (e) {
- return fieldValue;
- }
- }
-
- // Default to string
- return fieldValue;
- }
-
- /**
- * 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: any): any {
- 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: any) => {
- if (node.text) {
- plainText += node.text;
- }
- if (node.content && Array.isArray(node.content)) {
- node.content.forEach((child: any) => extractText(child));
- }
- };
-
- 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 (e) {
- // 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 (e) {
- return '[Complex Object]';
- }
- }
-
- // Return primitive values as is
- return value;
- }
-
- /**
- * Extracts all field metadata from DocumentOptions
- * @returns A structured object containing metadata about all available document fields
- */
- private getAllFieldMetadata() {
- // Start with our already populated fieldMetadata from the DocumentOptions class
- const result: Record<string, any> = {
- 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<string, any> = {
- 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;
+ constructor(docManager: AgentDocumentManager) {
+ super(documentMetadataToolInfo);
+ this._docManager = docManager;
+ this._docManager.initializeFindDocsFreeform();
}
/**
@@ -950,7 +217,7 @@ export class DocumentMetadataTool extends BaseTool<DocumentMetadataToolParamsTyp
console.log('DocumentMetadataTool: Executing with args:', args);
// Find all documents in the Freeform view
- this.findDocumentsInFreeformView();
+ this._docManager.initializeFindDocsFreeform();
try {
// Validate required input parameters based on action
@@ -981,7 +248,7 @@ export class DocumentMetadataTool extends BaseTool<DocumentMetadataToolParamsTyp
switch (action) {
case 'get': {
// Get metadata for a specific document or all documents
- const result = this.getDocumentMetadata(documentId);
+ const result = this._docManager.getDocumentMetadata(documentId);
console.log('DocumentMetadataTool: Get metadata result:', result);
return [
{
@@ -1003,7 +270,7 @@ export class DocumentMetadataTool extends BaseTool<DocumentMetadataToolParamsTyp
}
// Ensure document exists
- if (!this.documentsById.has(documentId)) {
+ if (!this._docManager.has(documentId)) {
return [
{
type: 'text',
@@ -1059,7 +326,7 @@ export class DocumentMetadataTool extends BaseTool<DocumentMetadataToolParamsTyp
const fieldName = String(edit.fieldName);
// Edit the field
- const result = this.editDocumentField(documentId, fieldName, fieldValue);
+ const result = this._docManager.editDocumentField(documentId, fieldName, fieldValue);
console.log(`DocumentMetadataTool: Edit field result for ${fieldName}:`, result);
@@ -1101,7 +368,7 @@ export class DocumentMetadataTool extends BaseTool<DocumentMetadataToolParamsTyp
}
// Get the updated metadata to return
- const updatedMetadata = this.getDocumentMetadata(documentId);
+ const updatedMetadata = this._docManager.getDocumentMetadata(documentId);
return [
{
@@ -1120,33 +387,12 @@ export class DocumentMetadataTool extends BaseTool<DocumentMetadataToolParamsTyp
}
case 'list': {
- // List all available documents in simple format
- const docs = Array.from(this.documentsById.entries()).map(([id, doc]) => ({
- id,
- title: doc.title || 'Untitled Document',
- type: doc.type || 'Unknown Type',
- }));
-
- if (docs.length === 0) {
- return [
- {
- type: 'text',
- text: 'No documents found in the current view.',
- },
- ];
- }
-
- return [
- {
- type: 'text',
- text: `Found ${docs.length} document(s) in the current view:\n${JSON.stringify(docs, null, 2)}`,
- },
- ];
+ this._docManager.listDocs();
}
case 'getFieldOptions': {
// Get all available field options with metadata
- const fieldOptions = this.getAllFieldMetadata();
+ const fieldOptions = this._docManager.getAllFieldMetadata();
return [
{
@@ -1171,61 +417,27 @@ export class DocumentMetadataTool extends BaseTool<DocumentMetadataToolParamsTyp
const title = String(args.title);
const data = String(args.data);
- // Validate doc_type
- if (!this.isValidDocType(docType)) {
+ const createdDoc = this._docManager.createDocInDash(docType, title, data);
+
+ if (!createdDoc) {
return [
{
type: 'text',
- text: `Error: Invalid doc_type. Valid options are: ${Object.keys(supportedDocTypes).join(',')}`,
+ text: 'Error: Failed to create document.',
},
];
}
- try {
- // Create simple document with just title and data
- const simpleDoc: parsedDoc = {
- doc_type: docType,
- title: title,
- data: data,
- x: 0,
- y: 0,
- _width: 300,
- _height: 300,
- _layout_fitWidth: false,
- _layout_autoHeight: true,
- };
-
- // Use the chatBox's createDocInDash method to create and link the document
- if (!this.chatBox || !this.chatBox.createDocInDash) {
- return [
- {
- type: 'text',
- text: 'Error: Could not access document creation functionality.',
- },
- ];
- }
-
- const createdDoc = this.chatBox.createDocInDash(simpleDoc);
-
- if (!createdDoc) {
- return [
- {
- type: 'text',
- text: 'Error: Failed to create document.',
- },
- ];
- }
+ // Update our local document maps with the new document
+ this._docManager.processDocument(createdDoc);
- // Update our local document maps with the new document
- this.processDocument(createdDoc);
+ // Get the created document's metadata
+ const createdMetadata = this._docManager.extractDocumentMetadata(this._docManager.createAgentDoc(createdDoc));
- // Get the created document's metadata
- const createdMetadata = this.extractDocumentMetadata(createdDoc);
-
- return [
- {
- type: 'text',
- text: `Document created successfully.
+ return [
+ {
+ type: 'text',
+ text: `Document created successfully.
Document ID: ${createdDoc.id}
Type: ${docType}
Title: "${title}"
@@ -1241,16 +453,8 @@ Next steps:
Full metadata for the created document:
${JSON.stringify(createdMetadata, null, 2)}`,
- },
- ];
- } catch (error) {
- return [
- {
- type: 'text',
- text: `Error creating document: ${error instanceof Error ? error.message : String(error)}`,
- },
- ];
- }
+ },
+ ];
}
default:
@@ -1370,38 +574,4 @@ ${JSON.stringify(createdMetadata, null, 2)}`,
return `Unknown action "${action}". Valid actions are "get", "edit", "list", "getFieldOptions", or "create".`;
}
}
-
- /**
- * 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
- */
- private getDocumentMetadata(documentId?: string): any {
- if (documentId) {
- const doc = this.documentsById.get(documentId);
- // Get metadata for a specific document
- return this.extractDocumentMetadata(doc);
- } else {
- // Get metadata for all documents
- const documentsMetadata: Record<string, any> = {};
- for (const doc of this.documentsById.values()) {
- documentsMetadata.add(this.extractDocumentMetadata(doc));
- }
-
- return {
- documentCount: this.documentsById.size,
- documents: documentsMetadata,
- fieldDefinitions: this.fieldMetadata,
- };
- }
- }
-
- /**
- * 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);
- }
}
diff --git a/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts b/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts
new file mode 100644
index 000000000..c954226e4
--- /dev/null
+++ b/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts
@@ -0,0 +1,923 @@
+import { ChatBox } from '../chatboxcomponents/ChatBox';
+import { Doc, FieldType, Opt } from '../../../../../fields/Doc';
+import { DocData } from '../../../../../fields/DocSymbols';
+import { Observation } from '../types/types';
+import { ParametersType, ToolInfo, Parameter } from '../types/tool_types';
+import { BaseTool } from '../tools/BaseTool';
+import { Docs, DocumentOptions } from '../../../../documents/Documents';
+import { CollectionFreeFormDocumentView } from '../../CollectionFreeFormDocumentView';
+import { v4 as uuidv4 } from 'uuid';
+import { LinkManager, UPDATE_SERVER_CACHE } from '../../../../util/LinkManager';
+import { DocCast, StrCast } from '../../../../../fields/Types';
+import { supportedDocTypes } from '../types/tool_types';
+import { parsedDoc } from '../chatboxcomponents/ChatBox';
+import { faThumbTackSlash } from '@fortawesome/free-solid-svg-icons';
+import { DocumentManager } from '../../../../util/DocumentManager';
+import { DocumentView } from '../../DocumentView';
+
+/**
+ * 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 {
+ private documentsById: Map<string, AgentDocument>;
+ private chatBox: ChatBox;
+ private chatBoxDocument: Doc | null = null;
+ private fieldMetadata: Record<string, any> = {};
+ private readonly DOCUMENT_ID_FIELD = '_dash_document_id';
+
+ /**
+ * Creates a new DocumentManager
+ * @param templateDocument The document that serves as a template for new documents
+ */
+ constructor(chatBox: ChatBox) {
+ this.documentsById = new Map<string, AgentDocument>();
+ this.chatBox = chatBox;
+ this.chatBoxDocument = chatBox.Document;
+ 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<string, any> = {
+ 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;
+ }
+ }
+
+ /**
+ * 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((doc: Doc) => {
+ if (doc) {
+ this.processDocument(doc);
+ }
+ });
+
+ // Include the ChatBox document itself
+ this.processDocument(this.chatBoxDocument);
+
+ // If we have access to the Document's parent, try to find sibling documents
+ if (this.chatBoxDocument.parent) {
+ const parent = this.chatBoxDocument.parent;
+ console.log('Found parent document, checking for siblings');
+
+ // Check if parent is a Doc type and has a childDocs function
+ if (parent && typeof parent === 'object' && 'childDocs' in parent && typeof parent.childDocs === 'function') {
+ try {
+ const siblingDocs = parent.childDocs();
+ if (Array.isArray(siblingDocs)) {
+ console.log(`Found ${siblingDocs.length} sibling documents via parent.childDocs()`);
+ siblingDocs.forEach((doc: Doc) => {
+ if (doc) {
+ this.processDocument(doc);
+ }
+ });
+ }
+ } catch (e) {
+ console.warn('Error accessing parent.childDocs:', e);
+ }
+ }
+ }
+ } else if (this.chatBox && this.chatBox.linkedDocs) {
+ // If we have direct access to the linkedDocs computed property from ChatBox
+ console.log('Using ChatBox.linkedDocs directly');
+ const linkedDocs = this.chatBox.linkedDocs;
+ if (Array.isArray(linkedDocs)) {
+ console.log(`Found ${linkedDocs.length} documents via ChatBox.linkedDocs`);
+ linkedDocs.forEach((doc: Doc) => {
+ if (doc) {
+ this.processDocument(doc);
+ }
+ });
+ }
+
+ // Process the ChatBox document if available
+ if (this.chatBox.Document) {
+ this.processDocument(this.chatBox.Document);
+ }
+ } else {
+ console.warn('No ChatBox document reference available for finding linked documents');
+ }
+
+ console.log(`DocumentMetadataTool found ${this.documentsById.size} total documents`);
+ } catch (error) {
+ console.error('Error finding documents in Freeform view:', error);
+ }
+ }
+
+ /**
+ * Process a document by ensuring it has an ID and adding it to the appropriate collections
+ * @param doc The document to process
+ */
+ public processDocument(doc: Doc) {
+ // Ensure document has a persistent ID
+ const docId = this.ensureDocumentId(doc);
+ // Only add if we haven't already processed this document
+ if (!this.documentsById.has(docId)) {
+ this.documentsById.set(docId, { layoutDoc: doc, dataDoc: doc[DocData] });
+ }
+ }
+
+ /**
+ * 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;
+
+ // First try to get the ID from our custom field
+ if (doc[this.DOCUMENT_ID_FIELD]) {
+ docId = String(doc[this.DOCUMENT_ID_FIELD]);
+ return docId;
+ }
+
+ // Try different ways to get a document ID
+
+ // 1. Try the direct id property if it exists
+ if (doc.id && typeof doc.id === 'string') {
+ docId = doc.id;
+ }
+ // 2. Try doc._id if it exists
+ else if (doc._id && typeof doc._id === 'string') {
+ docId = doc._id;
+ }
+ // 3. Try doc.data?.id if it exists
+ else if (doc.data && typeof doc.data === 'object' && 'id' in doc.data && typeof doc.data.id === 'string') {
+ docId = doc.data.id;
+ }
+ // 4. If none of the above work, generate a UUID
+ else {
+ docId = uuidv4();
+ console.log(`Generated new UUID for document with title: ${doc.title || 'Untitled'}`);
+ }
+
+ // Store the ID in the document's metadata so it persists
+ try {
+ doc[this.DOCUMENT_ID_FIELD] = docId;
+ } catch (e) {
+ console.warn(`Could not assign ID to document property`, e);
+ }
+
+ 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(doc?: AgentDocument) {
+ if (!doc) return null;
+ const layoutDoc = doc.layoutDoc;
+ const dataDoc = doc.dataDoc;
+
+ const metadata: Record<string, any> = {
+ id: layoutDoc.dash_document_id || layoutDoc.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: any): any {
+ 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: any) => {
+ if (node.text) {
+ plainText += node.text;
+ }
+ if (node.content && Array.isArray(node.content)) {
+ node.content.forEach((child: any) => extractText(child));
+ }
+ };
+
+ 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 (e) {
+ // 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 (e) {
+ 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, fieldValue: any): any {
+ // If fieldValue is already a number or boolean, we don't need to convert it from string
+ if (typeof fieldValue === 'number' || typeof fieldValue === 'boolean') {
+ return fieldValue;
+ }
+
+ // If fieldValue is a string "true" or "false", convert to boolean
+ if (typeof fieldValue === 'string') {
+ if (fieldValue.toLowerCase() === 'true') {
+ return true;
+ }
+ if (fieldValue.toLowerCase() === 'false') {
+ return false;
+ }
+ }
+
+ // If fieldValue is not a string (and not a number or boolean), convert it to string
+ if (typeof fieldValue !== 'string') {
+ fieldValue = String(fieldValue);
+ }
+
+ // 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 (e) {
+ // 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 (e) {
+ return fieldValue;
+ }
+ } else if (fieldType.includes('list') || fieldType.includes('array')) {
+ // Try to parse as JSON array
+ try {
+ return JSON.parse(fieldValue);
+ } catch (e) {
+ return fieldValue;
+ }
+ } else if (fieldType === 'json' || fieldType === 'object') {
+ // Try to parse as JSON object
+ try {
+ return JSON.parse(fieldValue);
+ } catch (e) {
+ 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<string, any> = {
+ 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<string, any> = {
+ 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?: any;
+ 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;
+
+ 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): any {
+ if (documentId) {
+ const doc = this.documentsById.get(documentId);
+ // Get metadata for a specific document
+ return this.extractDocumentMetadata(doc);
+ } else {
+ // Get metadata for all documents
+ const documentsMetadata: Record<string, any> = {};
+ for (const doc of this.documentsById.values()) {
+ documentsMetadata.add(this.extractDocumentMetadata(doc));
+ }
+
+ return {
+ documentCount: this.documentsById.size,
+ documents: documentsMetadata,
+ fieldDefinitions: this.fieldMetadata,
+ };
+ }
+ }
+
+ /**
+ * 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<string>();
+
+ // 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.
+ *
+ * @param {string} doc_type - The type of document to create.
+ * @param {string} data - The data used to generate the document.
+ * @param {DocumentOptions} options - Configuration options for the document.
+ * @returns {Promise<void>} A promise that resolves once the document is created and displayed.
+ */
+ createDocInDash = (docType: string, title: string, data: string) => {
+ // 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 = {
+ doc_type: docType,
+ title: title,
+ data: data,
+ x: 0,
+ y: 0,
+ _width: 300,
+ _height: 300,
+ _layout_fitWidth: false,
+ _layout_autoHeight: true,
+ };
+
+ // Use the chatBox's createDocInDash method to create and link the document
+ if (!this.chatBox) {
+ throw new Error('ChatBox instance not available for creating document');
+ }
+ const linkAndShowDoc = (doc: Opt<Doc>) => {
+ if (doc) {
+ LinkManager.Instance.addLink(Docs.Create.LinkDocument(this.chatBoxDocument!, doc));
+ this.chatBox._props.addDocument?.(doc);
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
+ }
+ };
+ const doc = this.chatBox.whichDoc(simpleDoc, false);
+ if (doc) linkAndShowDoc(doc);
+ return doc;
+ } catch (error) {
+ throw new Error(`Error creating document: ${error}`);
+ }
+ };
+
+ public has(docId: string) {
+ return this.documentsById.has(docId);
+ }
+
+ public listDocs() {
+ // List all available documents in simple format
+ const docs = Array.from(this.documentsById.entries()).map(([id, doc]) => ({
+ id,
+ title: doc.layoutDoc.title || 'Untitled Document',
+ type: doc.layoutDoc.type || doc.dataDoc.type || 'Unknown Type',
+ }));
+
+ if (docs.length === 0) {
+ return [
+ {
+ type: 'text',
+ text: 'No documents found in the current view.',
+ },
+ ];
+ }
+
+ return [
+ {
+ type: 'text',
+ text: `Found ${docs.length} document(s) in the current view:\n${JSON.stringify(docs, null, 2)}`,
+ },
+ ];
+ }
+
+ public createAgentDoc(doc: Doc) {
+ // Ideally check if Doc is already in there.
+ const agentDoc = { layoutDoc: doc, dataDoc: doc[DocData] };
+ this.documentsById.set(this.ensureDocumentId(doc), agentDoc);
+ return agentDoc;
+ }
+}