aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-07-23 13:49:26 -0400
committerbobzel <zzzman@gmail.com>2025-07-23 13:49:26 -0400
commit5a76ed69d488cc5798161fb2a528baabc690928d (patch)
tree8ab6eb33c09ddc068b687b7469843e50c1b4376d
parent16e7cfcac3d41bd86ef953f131bb0fecba11f299 (diff)
fixes for chatbox that already exists when a collection is opened to still have access to the collection's docs.
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx69
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx197
-rw-r--r--src/client/views/nodes/chatbot/tools/FilterDocsTool.ts568
3 files changed, 399 insertions, 435 deletions
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3e6aa777f..fe5910671 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,5 +1,4 @@
import { Button, Colors, NumberDropdown, Size, Type } from '@dash/components';
-import { Slider } from '@mui/material';
import { Bezier } from 'bezier-js';
import { Property } from 'csstype';
import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
@@ -94,7 +93,7 @@ export interface collectionFreeformViewProps {
@observer
export class CollectionFreeFormView extends CollectionSubView<Partial<collectionFreeformViewProps>>() {
private static _infoUIInstance: CollectionFreeFormInfoUI | null = null;
-
+
public get displayName() {
return 'CollectionFreeFormView(' + (this.Document.title?.toString() ?? '') + ')';
} // this makes mobx trace() statements more descriptive
@@ -1756,47 +1755,46 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
CollectionFreeFormView._infoUI = func;
}
/**
- * Called from TutorialTool in Agent system
- */
- public static showTutorial(kind: 'links' | 'pins' | 'presentation') {
+ * Called from TutorialTool in Agent system
+ */
+ public static showTutorial(kind: 'links' | 'pins' | 'presentation') {
const ui = CollectionFreeFormView._infoUIInstance;
if (!ui) return;
switch (kind) {
- case 'links':
- ui.skipToState((ui).tutorialStates.multipleDocs);
- ui._nextState
- break;
- case 'pins':
- ui.skipToState((ui).tutorialStates.presentDocs);
- ui._nextState
- break;
- case 'presentation':
- ui.skipToState((ui).tutorialStates.makePresentation);
- ui._nextState
- break;
+ case 'links':
+ ui.skipToState(ui.tutorialStates.multipleDocs);
+ ui._nextState;
+ break;
+ case 'pins':
+ ui.skipToState(ui.tutorialStates.presentDocs);
+ ui._nextState;
+ break;
+ case 'presentation':
+ ui.skipToState(ui.tutorialStates.makePresentation);
+ ui._nextState;
+ break;
}
- }
+ }
infoUI = () => {
Doc.IsInfoUIDisabled || this.Document.annotationOn || this._props.renderDepth
? null //
: CollectionFreeFormView._infoUI?.(this.Document, this.layoutDoc, this.childDocsFunc, this.closeInfo) || null;
- if (Doc.IsInfoUIDisabled || this.Document.annotationOn || this._props.renderDepth) {
- return null;
- }
- const creator = CollectionFreeFormView._infoUI;
- if (!creator) return null;
- const element = creator(this.Document, this.layoutDoc, this.childDocsFunc, this.closeInfo);
- // attach ref so we can call skipToState(...) later
- return React.isValidElement(element)
- ? React.cloneElement(element, {
- ref: (r: CollectionFreeFormInfoUI) => {
- CollectionFreeFormView._infoUIInstance = r;
- }
- })
- : element;
-
+ if (Doc.IsInfoUIDisabled || this.Document.annotationOn || this._props.renderDepth) {
+ return null;
+ }
+ const creator = CollectionFreeFormView._infoUI;
+ if (!creator) return null;
+ const element = creator(this.Document, this.layoutDoc, this.childDocsFunc, this.closeInfo);
+ // attach ref so we can call skipToState(...) later
+ return React.isValidElement(element)
+ ? React.cloneElement(element, {
+ ref: (r: CollectionFreeFormInfoUI) => {
+ CollectionFreeFormView._infoUIInstance = r;
+ },
+ })
+ : element;
};
componentDidMount() {
this._props.setContentViewBox?.(this);
@@ -2248,7 +2246,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
max={100}
number={this._fireflyRefStrength}
size={Size.XXSMALL}
- setNumber={undoable(action(val => (this._fireflyRefStrength = val as number)),`${this.Document.title} button set from list` )}
+ setNumber={undoable(
+ action(val => (this._fireflyRefStrength = val as number)),
+ `${this.Document.title} button set from list`
+ )}
fillWidth
/>
</div>
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index 195e85412..c09df166d 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -15,7 +15,7 @@ import * as React from 'react';
import { v4 as uuidv4 } from 'uuid';
import { ClientUtils, OmitKeys } from '../../../../../ClientUtils';
import { Doc, DocListCast, Opt } from '../../../../../fields/Doc';
-import { DocData, DocLayout, DocViews } from '../../../../../fields/DocSymbols';
+import { DocData, DocViews } from '../../../../../fields/DocSymbols';
import { Id } from '../../../../../fields/FieldSymbols';
import { RichTextField } from '../../../../../fields/RichTextField';
import { ScriptField } from '../../../../../fields/ScriptField';
@@ -44,7 +44,6 @@ import './ChatBox.scss';
import MessageComponentBox from './MessageComponent';
import { OpenWhere } from '../../OpenWhere';
import { Upload } from '../../../../../server/SharedMediaTypes';
-import { DocumentMetadataTool } from '../tools/DocumentMetadataTool';
import { AgentDocumentManager } from '../utils/AgentDocumentManager';
import { AiOutlineSend } from 'react-icons/ai';
import { SnappingManager } from '../../../../util/SnappingManager';
@@ -88,12 +87,12 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// Private properties for managing OpenAI API, vector store, agent, and UI elements
private openai!: OpenAI; // Using definite assignment assertion
- private vectorstore_id: string;
- private vectorstore: Vectorstore;
- private agent: Agent;
- private messagesRef: React.RefObject<HTMLDivElement>;
+ private vectorstore_id: string | undefined;
+ private vectorstore: Vectorstore | undefined;
+ private agent: Agent | undefined;
+ private messagesRef: React.RefObject<HTMLDivElement> = React.createRef();
private _textInputRef: HTMLInputElement | undefined | null;
- private docManager: AgentDocumentManager;
+ private docManager: AgentDocumentManager | undefined;
/**
* Static method that returns the layout string for the field.
@@ -109,8 +108,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@action
toggleCanvasMode = () => {
- const newMode = !this.docManager.getCanvasMode();
- this.docManager.setCanvasMode(newMode);
+ const newMode = !this.docManager?.getCanvasMode();
+ this.docManager?.setCanvasMode(newMode);
};
/**
@@ -121,69 +120,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
constructor(props: FieldViewProps) {
super(props);
makeObservable(this);
-
- // At mount time, find the DocumentView whose .Document is the collection container.
- const parentView = DocumentView.Selected().lastElement();
- if (!parentView) {
- console.warn('GPT ChatBox not inside a DocumentView – cannot sort.');
- }
-
- this.messagesRef = React.createRef();
- this.docManager = new AgentDocumentManager(this, parentView);
-
- // Initialize OpenAI client
- this.initializeOpenAI();
-
- // Create a unique vectorstore ID for this ChatBox
- this.vectorstore_id = uuidv4();
-
- // Initialize vectorstore with the document manager
- this.vectorstore = new Vectorstore(this.vectorstore_id, this.docManager);
-
- // Create an agent with the vectorstore
- this.agent = new Agent(this.vectorstore, this.retrieveFormattedHistory.bind(this), this.retrieveCSVData.bind(this), this.createImageInDash.bind(this), this.createCSVInDash.bind(this), this.docManager);
-
- // Set up the tool created callback
- this.agent.setToolCreatedCallback(this.handleToolCreated);
-
- // Add event listeners
- this.addScrollListener();
-
- // Reaction to update dataDoc when chat history changes
- reaction(
- () =>
- this._history.map((msg: AssistantMessage) => ({
- role: msg.role,
- content: msg.content,
- follow_up_questions: msg.follow_up_questions,
- citations: msg.citations,
- })),
- serializableHistory => {
- this.dataDoc.data = JSON.stringify(serializableHistory);
- }
- );
-
- /*
- reaction(
- () => ({ selDoc: DocumentView.Selected().lastElement(), visible: SnappingManager.ChatVisible }),
- ({ selDoc, visible }) => {
- const hasChildDocs = visible && selDoc?.ComponentView?.hasChildDocs;
- if (hasChildDocs) {
- this._textToDocMap.clear();
- this.setCollectionContext(selDoc.Document);
- this.onGptResponse = (sortResult: string, questionType: GPTDocCommand) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType);
- this.onQuizRandom = () => this.randomlyChooseDoc(selDoc.Document, hasChildDocs());
- this._documentDescriptions = Promise.all(hasChildDocs().map(doc =>
- Doc.getDescription(doc).then(text => this._textToDocMap.set(text.replace(/\n/g, ' ').trim(), doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`)
- )).then(docDescriptions => docDescriptions.join()); // prettier-ignore
- }
- },
- { fireImmediately: true }
- );
- }*/
-
- // Initialize font size from saved preference
- this.initFontSize();
}
/**
@@ -203,6 +139,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._currentStep = isAudioOrVideo ? 'Preparing media file...' : 'Processing document...';
});
+ if (!this.docManager || !this.vectorstore) throw new Error('Document manager or vectorstore is not initialized');
// Process the document first to ensure it has a valid ID
await this.docManager.processDocument(newLinkedDoc);
@@ -337,7 +274,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
* Adds a scroll event listener to detect user scrolling and handle passive wheel events.
*/
addScrollListener = () => {
- if (this.messagesRef.current) {
+ if (this.messagesRef?.current) {
this.messagesRef.current.addEventListener('wheel', this.onPassiveWheel, { passive: false });
}
};
@@ -346,7 +283,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
* Removes the scroll event listener from the chat messages container.
*/
removeScrollListener = () => {
- if (this.messagesRef.current) {
+ if (this.messagesRef?.current) {
this.messagesRef.current.removeEventListener('wheel', this.onPassiveWheel);
}
};
@@ -428,6 +365,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
});
};
+ if (!this.agent) throw new Error('Agent is not initialized');
// Send the user's question to the assistant and get the final message
const finalMessage = await this.agent.askAgent(trimmedText, onProcessingUpdate, onAnswerUpdate);
@@ -537,7 +475,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
case supportedDocTypes.image: return Docs.Create.ImageDocument(data as string, options);
case supportedDocTypes.equation: return Docs.Create.EquationDocument(data as string, options);
case supportedDocTypes.notetaking: return Docs.Create.NoteTakingDocument([], options);
- case supportedDocTypes.web:
+ case supportedDocTypes.web: {
// Create web document with enhanced safety options
const webOptions = {
...options,
@@ -546,10 +484,11 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// If iframe_sandbox was passed from AgentDocumentManager, add it to the options
if ('_iframe_sandbox' in options) {
- (webOptions as any)._iframe_sandbox = options._iframe_sandbox;
+ webOptions._iframe_sandbox = options._iframe_sandbox;
}
return Docs.Create.WebDocument(data as string, webOptions);
+ }
case supportedDocTypes.dataviz: case supportedDocTypes.table: return Docs.Create.DataVizDocument('/Users/ajshul/Dash-Web/src/server/public/files/csv/0d237e7c-98c9-44d0-aa61-5285fdbcf96c-random_sample.csv.csv', options);
case supportedDocTypes.pdf: return Docs.Create.PdfDocument(data as string, options);
case supportedDocTypes.video: return Docs.Create.VideoDocument(data as string, options);
@@ -688,6 +627,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
citation: JSON.stringify(citation, null, 2),
});
+ if (!this.docManager) throw new Error('Document manager is not initialized');
+
// Get the simplified chunk using the document manager
const { foundChunk, doc, dataDoc } = this.docManager.getSimplifiedChunkById(chunkId);
console.log('doc: ', doc);
@@ -722,7 +663,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
this.handleOtherChunkTypes(foundChunk, citation, doc, dataDoc);
// Show the chunk text in citation popup
- let chunkText = citation.direct_text || 'Text content not available';
+ const chunkText = citation.direct_text || 'Text content not available';
this.showCitationPopup(chunkText);
// Also navigate to the document
@@ -741,7 +682,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
* @returns The starting timestamp of the matching segment, or -1 if not found
*/
getDirectMatchingSegmentStart = (doc: Doc, citationText: string, indexesOfSegments: string[]): number => {
- if (!doc || !citationText) return -1;
+ if (!doc || !citationText || !this.docManager) return -1;
// Get original segments using document manager
const original_segments = this.docManager.getOriginalSegments(doc);
@@ -754,7 +695,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// If specific indexes are provided, filter segments by those indexes
if (indexesOfSegments && indexesOfSegments.length > 0) {
- segments = original_segments.filter((segment: any) => indexesOfSegments.includes(segment.index));
+ segments = original_segments.filter(segment => indexesOfSegments.includes(segment.index));
}
// If no segments match the indexes, use all segments
@@ -763,7 +704,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
// First try to find an exact match
- const exactMatch = segments.find((segment: any) => segment.text && segment.text.includes(citationText));
+ const exactMatch = segments.find(segment => segment.text && segment.text.includes(citationText));
if (exactMatch) {
return exactMatch.start;
@@ -880,14 +821,16 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
break;
case CHUNK_TYPE.TEXT:
- this._citationPopup = { text: citation.direct_text ?? 'No text available', visible: true };
- this.startCitationPopupTimer();
+ {
+ this._citationPopup = { text: citation.direct_text ?? 'No text available', visible: true };
+ this.startCitationPopupTimer();
- // Check if the document is a PDF (has a PDF viewer component)
- const isPDF = PDFCast(dataDoc!.data) !== null || dataDoc!.type === DocumentType.PDF;
+ // Check if the document is a PDF (has a PDF viewer component)
+ const isPDF = PDFCast(dataDoc!.data) !== null || dataDoc!.type === DocumentType.PDF;
- // First ensure document is fully visible before trying to access its views
- this.ensureDocumentIsVisible(dataDoc!, isPDF, citation, foundChunk, doc);
+ // First ensure document is fully visible before trying to access its views
+ this.ensureDocumentIsVisible(dataDoc!, isPDF, citation, foundChunk, doc);
+ }
break;
case CHUNK_TYPE.CSV:
case CHUNK_TYPE.URL:
@@ -1079,6 +1022,65 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
* Initializes scroll listeners, sets up document reactions, and loads chat history from dataDoc if available.
*/
componentDidMount() {
+ // At mount time, find the DocumentView whose .Document is the collection container.
+ const parentView = this.DocumentView?.()._props.containerViewPath?.().lastElement();
+ if (!parentView) {
+ console.warn('GPT ChatBox not inside a DocumentView – cannot perform operations on Documents.');
+ return;
+ }
+
+ this.docManager = new AgentDocumentManager(this, parentView);
+
+ // Initialize OpenAI client
+ this.initializeOpenAI();
+
+ // Create a unique vectorstore ID for this ChatBox
+ this.vectorstore_id = uuidv4();
+
+ // Initialize vectorstore with the document manager
+ this.vectorstore = new Vectorstore(this.vectorstore_id, this.docManager);
+
+ /*
+ reaction(
+ () => ({ selDoc: DocumentView.Selected().lastElement(), visible: SnappingManager.ChatVisible }),
+ ({ selDoc, visible }) => {
+ const hasChildDocs = visible && selDoc?.ComponentView?.hasChildDocs;
+ if (hasChildDocs) {
+ this._textToDocMap.clear();
+ this.setCollectionContext(selDoc.Document);
+ this.onGptResponse = (sortResult: string, questionType: GPTDocCommand) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType);
+ this.onQuizRandom = () => this.randomlyChooseDoc(selDoc.Document, hasChildDocs());
+ this._documentDescriptions = Promise.all(hasChildDocs().map(doc =>
+ Doc.getDescription(doc).then(text => this._textToDocMap.set(text.replace(/\n/g, ' ').trim(), doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`)
+ )).then(docDescriptions => docDescriptions.join()); // prettier-ignore
+ }
+ },
+ { fireImmediately: true }
+ );
+ }*/
+
+ // Initialize font size from saved preference
+ this.initFontSize();
+ // Create an agent with the vectorstore
+ this.agent = new Agent(this.vectorstore, this.retrieveFormattedHistory.bind(this), this.retrieveCSVData.bind(this), this.createImageInDash.bind(this), this.createCSVInDash.bind(this), this.docManager);
+
+ // Set up the tool created callback
+ this.agent.setToolCreatedCallback(this.handleToolCreated);
+
+ // Reaction to update dataDoc when chat history changes
+ reaction(
+ () =>
+ this._history.map((msg: AssistantMessage) => ({
+ role: msg.role,
+ content: msg.content,
+ follow_up_questions: msg.follow_up_questions,
+ citations: msg.citations,
+ })),
+ serializableHistory => {
+ this.dataDoc.data = JSON.stringify(serializableHistory);
+ }
+ );
+
this._props.setContentViewBox?.(this);
if (this.dataDoc.data) {
try {
@@ -1146,9 +1148,12 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// If there are stored doc IDs in our list of docs to add, process them
if (this._linked_docs_to_add.size > 0) {
this._linked_docs_to_add.forEach(async doc => {
- await this.docManager.processDocument(doc);
+ await this.docManager!.processDocument(doc);
});
}
+
+ // Add event listeners
+ this.addScrollListener();
}
/**
@@ -1235,7 +1240,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
try {
// Perform the deferred tool save operation
- const saveSuccess = await this.agent.performDeferredToolSave();
+ const saveSuccess = await this.agent?.performDeferredToolSave();
if (saveSuccess) {
console.log('Tool saved successfully, proceeding with reload...');
@@ -1447,19 +1452,9 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
<div className="chat-header">
<h2>{this.userName()}&apos;s AI Assistant</h2>
<div className="header-controls">
- <Tooltip title={
- <div className="dash-tooltip">
- {this.docManager.getCanvasMode()
- ? "Click to limit scope to linked documents"
- : "Click to expand scope to all documents on canvas"
- }
- </div>
- } placement="bottom">
- <div
- className={`canvas-mode-toggle ${this.docManager.getCanvasMode() ? 'canvas-active' : ''}`}
- onClick={this.toggleCanvasMode}
- >
- {this.docManager.getCanvasMode() ? <MdViewModule /> : <MdLink />}
+ <Tooltip title={<div className="dash-tooltip">{this.docManager?.getCanvasMode() ? 'Click to limit scope to linked documents' : 'Click to expand scope to all documents on canvas'}</div>} placement="bottom">
+ <div className={`canvas-mode-toggle ${this.docManager?.getCanvasMode() ? 'canvas-active' : ''}`} onClick={this.toggleCanvasMode}>
+ {this.docManager?.getCanvasMode() ? <MdViewModule /> : <MdLink />}
</div>
</Tooltip>
<div className="font-size-control" onClick={this.toggleFontSizeModal}>
diff --git a/src/client/views/nodes/chatbot/tools/FilterDocsTool.ts b/src/client/views/nodes/chatbot/tools/FilterDocsTool.ts
index b160badde..a921f6058 100644
--- a/src/client/views/nodes/chatbot/tools/FilterDocsTool.ts
+++ b/src/client/views/nodes/chatbot/tools/FilterDocsTool.ts
@@ -3,343 +3,311 @@ import { BaseTool } from './BaseTool';
import { Observation } from '../types/types';
import { ParametersType, ToolInfo } from '../types/tool_types';
import { AgentDocumentManager } from '../utils/AgentDocumentManager';
-import {
- gptAPICall,
- GPTCallType,
- GPTDocCommand,
- DescriptionSeperator,
- DataSeperator,
- DocSeperator,
-} from '../../../../apis/gpt/GPT';
+import { gptAPICall, GPTCallType, GPTDocCommand, DescriptionSeperator, DataSeperator, DocSeperator } from '../../../../apis/gpt/GPT';
import { v4 as uuidv4 } from 'uuid';
import { TagItem } from '../../../TagsView';
import { DocumentView } from '../../DocumentView';
import { Doc } from '../../../../../fields/Doc';
+import { computed } from 'mobx';
const parameterRules = [
- {
- name: 'filterCriteria',
- type: 'string',
- description: 'Natural-language criteria for choosing a subset of documents (e.g., "show me only research papers about AI", "filter for documents created last week")',
- required: true,
- },
+ {
+ name: 'filterCriteria',
+ type: 'string',
+ description: 'Natural-language criteria for choosing a subset of documents (e.g., "show me only research papers about AI", "filter for documents created last week")',
+ required: true,
+ },
] as const;
const toolInfo: ToolInfo<typeof parameterRules> = {
- name: 'filterDocs',
- description: 'Filters documents in a collection based on user-specified natural-language criteria. Replaces GPTPopup filtering functionality with enhanced error handling and management features.',
- parameterRules,
- citationRules: 'No citation needed for filtering operations.',
+ name: 'filterDocs',
+ description: 'Filters documents in a collection based on user-specified natural-language criteria. Replaces GPTPopup filtering functionality with enhanced error handling and management features.',
+ parameterRules,
+ citationRules: 'No citation needed for filtering operations.',
};
export class FilterDocsTool extends BaseTool<typeof parameterRules> {
- private _docManager: AgentDocumentManager;
- static ChatTag = '#chat'; // tag used to mark filtered documents
- private _collectionView: DocumentView;
- private _documentDescriptions: Promise<string> | undefined;
- private _textToDocMap = new Map<string, Doc>();
- private _lastFilterCriteria: string | undefined;
-
- constructor(docManager: AgentDocumentManager, collectionView: DocumentView) {
- super(toolInfo);
- this._docManager = docManager;
- this._docManager.initializeDocuments();
- this._collectionView = collectionView;
- this._initializeDocumentContext();
- }
-
- /**
- * Initialize document context similar to GPTPopup's componentDidMount behavior
- */
- private _initializeDocumentContext() {
- const selectedDoc = this._collectionView?.Document;
- // Use any type to avoid complex type checking while maintaining runtime safety
- const componentView = selectedDoc?.ComponentView as any;
- const hasChildDocs = componentView?.hasChildDocs;
-
- if (hasChildDocs && typeof hasChildDocs === 'function') {
- this._textToDocMap.clear();
- try {
- const childDocs = hasChildDocs() as Doc[];
- this._documentDescriptions = Promise.all(
- childDocs.map((doc: Doc) =>
- Doc.getDescription(doc).then(text => {
- const cleanText = text.replace(/\n/g, ' ').trim();
- this._textToDocMap.set(cleanText, doc);
- return `${DescriptionSeperator}${cleanText}${DescriptionSeperator}`;
- })
- )
- ).then(docDescriptions => docDescriptions.join(''));
- } catch (error) {
- console.warn('[FilterDocsTool] Error initializing document context:', error);
- }
+ private _docManager: AgentDocumentManager;
+ static ChatTag = '#chat'; // tag used to mark filtered documents
+ private _collectionView: DocumentView;
+ private _documentDescriptions: Promise<string> | undefined;
+ private _lastFilterCriteria: string | undefined;
+
+ constructor(docManager: AgentDocumentManager, collectionView: DocumentView) {
+ super(toolInfo);
+ this._docManager = docManager;
+ this._docManager.initializeDocuments();
+ this._collectionView = collectionView;
}
- }
-
- /**
- * Check if a document filter is currently active
- */
- static hasActiveFilter(parentDoc: Doc | undefined): boolean {
- if (!parentDoc) return false;
- const result = Doc.hasDocFilter(parentDoc, 'tags', FilterDocsTool.ChatTag);
- return Boolean(result);
- }
-
- /**
- * Clear all chat filters from a collection (equivalent to GPTPopup's clear filter toggle)
- */
- static clearChatFilter(parentDoc: Doc | undefined, childDocs?: Doc[]): boolean {
- if (!parentDoc) return false;
-
- try {
- // Remove filter from parent document
- Doc.setDocFilter(parentDoc, 'tags', FilterDocsTool.ChatTag, 'remove');
-
- // Remove tags from child documents
- let docsToProcess: Doc[] = [];
- if (childDocs) {
- docsToProcess = childDocs;
- } else {
- // Try to get child docs from ComponentView
- const componentView = parentDoc.ComponentView as any;
- const hasChildDocs = componentView?.hasChildDocs;
- if (hasChildDocs && typeof hasChildDocs === 'function') {
- docsToProcess = hasChildDocs() as Doc[];
+
+ @computed get TextToDocMap() {
+ // Use any type to avoid complex type checking while maintaining runtime safety
+ const childDocs = this._collectionView?.ComponentView?.hasChildDocs?.();
+ if (childDocs) {
+ const textToDocMap = new Map<string, Doc>();
+ try {
+ this._documentDescriptions = Promise.all(
+ childDocs.map((doc: Doc) =>
+ Doc.getDescription(doc).then(text => {
+ const cleanText = text.replace(/\n/g, ' ').trim();
+ textToDocMap.set(cleanText, doc);
+ return `${DescriptionSeperator}${cleanText}${DescriptionSeperator}`;
+ })
+ )
+ ).then(docDescriptions => docDescriptions.join(''));
+ return textToDocMap;
+ } catch (error) {
+ console.warn('[FilterDocsTool] Error initializing document context:', error);
+ }
}
- }
-
- docsToProcess.forEach((doc: Doc) => TagItem.removeTagFromDoc(doc, FilterDocsTool.ChatTag));
-
- return true;
- } catch (error) {
- console.error('[FilterDocsTool] Error clearing chat filter:', error);
- return false;
+ return undefined;
}
- }
-
- /**
- * Determine if user input is a filter command using GPT
- */
- async isFilterCommand(userPrompt: string): Promise<boolean> {
- try {
- const commandType = await gptAPICall(userPrompt, GPTCallType.COMMANDTYPE, undefined, true);
- const commandNumber = parseInt(commandType.split(' ')[0][0]);
- return commandNumber === GPTDocCommand.Filter;
- } catch (error) {
- console.error('[FilterDocsTool] Error determining command type:', error);
- return false;
+
+ /**
+ * Check if a document filter is currently active
+ */
+ static hasActiveFilter(parentDoc: Doc | undefined): boolean {
+ if (!parentDoc) return false;
+ const result = Doc.hasDocFilter(parentDoc, 'tags', FilterDocsTool.ChatTag);
+ return Boolean(result);
}
- }
-
- async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[]> {
- const chunkId = uuidv4();
- this._lastFilterCriteria = args.filterCriteria;
- console.log('[FilterDocsTool] Executing filter with criteria:', args.filterCriteria);
-
- // Validate parent view and document
- const parentView = this._collectionView;
- const parentDoc = parentView?.Document;
- if (!parentDoc) {
- console.error('[FilterDocsTool] Missing parent DocumentView/Document!');
- return [
- {
- type: 'text',
- text: `<chunk chunk_id="${chunkId}" chunk_type="error">
-FilterDocsTool: No parent collection document found. Please ensure you're working within a document collection.
-</chunk>`,
- },
- ];
+
+ /**
+ * Clear all chat filters from a collection (equivalent to GPTPopup's clear filter toggle)
+ */
+ static clearChatFilter(parentDoc: Doc | undefined, childDocs?: Doc[]): boolean {
+ if (!parentDoc) return false;
+
+ try {
+ // Remove filter from parent document
+ Doc.setDocFilter(parentDoc, 'tags', FilterDocsTool.ChatTag, 'remove');
+
+ // Remove tags from child documents
+ let docsToProcess: Doc[] = [];
+ if (childDocs) {
+ docsToProcess = childDocs;
+ } else {
+ // Try to get child docs from ComponentView
+ const componentView = parentDoc.ComponentView as any;
+ const hasChildDocs = componentView?.hasChildDocs;
+ if (hasChildDocs && typeof hasChildDocs === 'function') {
+ docsToProcess = hasChildDocs() as Doc[];
+ }
+ }
+
+ docsToProcess.forEach((doc: Doc) => TagItem.removeTagFromDoc(doc, FilterDocsTool.ChatTag));
+
+ return true;
+ } catch (error) {
+ console.error('[FilterDocsTool] Error clearing chat filter:', error);
+ return false;
+ }
}
- try {
- // Method 1: Use pre-computed document descriptions if available (from componentDidMount-like behavior)
- let prompt: string;
- let textToDocMap: Map<string, Doc>;
-
- if (this._documentDescriptions && this._textToDocMap.size > 0) {
- console.log('[FilterDocsTool] Using pre-computed document descriptions');
- prompt = await this._documentDescriptions;
- textToDocMap = this._textToDocMap;
- } else {
- // Method 2: Build descriptions from scratch using docManager
- console.log('[FilterDocsTool] Building document descriptions from docManager');
- textToDocMap = new Map<string, Doc>();
- const blocks: string[] = [];
-
- for (const id of this._docManager.docIds) {
- const descRaw = await this._docManager.getDocDescription(id);
- const desc = descRaw.replace(/\n/g, ' ').trim();
-
- if (!desc) {
- console.warn(`[FilterDocsTool] Skipping document ${id} with empty description`);
- continue;
- }
-
- const doc = this._docManager.getDocument(id);
- if (doc) {
- textToDocMap.set(desc, doc);
- blocks.push(`${DescriptionSeperator}${desc}${DescriptionSeperator}`);
- }
+ /**
+ * Determine if user input is a filter command using GPT
+ */
+ async isFilterCommand(userPrompt: string): Promise<boolean> {
+ try {
+ const commandType = await gptAPICall(userPrompt, GPTCallType.COMMANDTYPE, undefined, true);
+ const commandNumber = parseInt(commandType.split(' ')[0][0]);
+ return commandNumber === GPTDocCommand.Filter;
+ } catch (error) {
+ console.error('[FilterDocsTool] Error determining command type:', error);
+ return false;
}
+ }
- prompt = blocks.join('');
- }
+ async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[]> {
+ const chunkId = uuidv4();
+ this._lastFilterCriteria = args.filterCriteria;
+ console.log('[FilterDocsTool] Executing filter with criteria:', args.filterCriteria);
+
+ // Validate parent view and document
+ const parentView = this._collectionView;
+ const parentDoc = parentView?.Document;
+ if (!parentDoc) {
+ console.error('[FilterDocsTool] Missing parent DocumentView/Document!');
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="error">
+FilterDocsTool: No parent collection document found. Please ensure you're working within a document collection.
+</chunk>`,
+ },
+ ];
+ }
- if (!prompt || textToDocMap.size === 0) {
- return [
- {
- type: 'text',
- text: `<chunk chunk_id="${chunkId}" chunk_type="error">
+ try {
+ // Method 1: Use pre-computed document descriptions if available (from componentDidMount-like behavior)
+ let prompt: string;
+ let textToDocMap = await this.TextToDocMap;
+
+ if (textToDocMap && textToDocMap.size > 0 && this._documentDescriptions) {
+ console.log('[FilterDocsTool] Using pre-computed document descriptions');
+ prompt = await this._documentDescriptions;
+ } else {
+ // Method 2: Build descriptions from scratch using docManager
+ console.log('[FilterDocsTool] Building document descriptions from docManager');
+ textToDocMap = new Map<string, Doc>();
+ const blocks: string[] = [];
+
+ for (const id of this._docManager.docIds) {
+ const descRaw = await this._docManager.getDocDescription(id);
+ const desc = descRaw.replace(/\n/g, ' ').trim();
+
+ if (!desc) {
+ console.warn(`[FilterDocsTool] Skipping document ${id} with empty description`);
+ continue;
+ }
+
+ const doc = this._docManager.getDocument(id);
+ if (doc) {
+ textToDocMap.set(desc, doc);
+ blocks.push(`${DescriptionSeperator}${desc}${DescriptionSeperator}`);
+ }
+ }
+
+ prompt = blocks.join('');
+ }
+
+ if (!prompt || textToDocMap?.size === 0) {
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="error">
No documents found to filter. The collection appears to be empty or documents lack descriptions.
</chunk>`,
- },
- ];
- }
-
- console.log(`[FilterDocsTool] Processing ${textToDocMap.size} documents with filter criteria`);
-
- // Call GPT for document subset selection
- const gptResponse = await gptAPICall(args.filterCriteria, GPTCallType.SUBSETDOCS, prompt);
- console.log('[FilterDocsTool] GPT response received:', gptResponse.substring(0, 200) + '...');
-
- // Clear existing filters using the enhanced clear method
- const childDocs = Array.from(textToDocMap.values());
- FilterDocsTool.clearChatFilter(parentDoc, childDocs);
-
- // Parse GPT response and apply new filters
- const filteredResults = this._parseGptResponseAndApplyFilters(
- gptResponse,
- textToDocMap,
- parentDoc,
- chunkId
- );
-
- return filteredResults;
-
- } catch (error) {
- console.error('[FilterDocsTool] Execution error:', error);
- return [
- {
- type: 'text',
- text: `<chunk chunk_id="${chunkId}" chunk_type="error">
+ },
+ ];
+ }
+
+ console.log(`[FilterDocsTool] Processing ${textToDocMap?.size} documents with filter criteria`);
+
+ // Call GPT for document subset selection
+ const gptResponse = await gptAPICall(args.filterCriteria, GPTCallType.SUBSETDOCS, prompt);
+ console.log('[FilterDocsTool] GPT response received:', gptResponse.substring(0, 200) + '...');
+
+ // Clear existing filters using the enhanced clear method
+ const childDocs = Array.from(textToDocMap?.values() ?? []);
+ FilterDocsTool.clearChatFilter(parentDoc, childDocs);
+
+ // Parse GPT response and apply new filters
+ const filteredResults = this._parseGptResponseAndApplyFilters(gptResponse, textToDocMap, parentDoc, chunkId);
+
+ return filteredResults;
+ } catch (error) {
+ console.error('[FilterDocsTool] Execution error:', error);
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="error">
Filtering failed: ${error instanceof Error ? error.message : String(error)}
Please try rephrasing your filter criteria or check if the collection contains valid documents.
</chunk>`,
- },
- ];
- }
- }
-
- /**
- * Parse GPT response and apply filters with enhanced error handling
- */
- private _parseGptResponseAndApplyFilters(
- gptResponse: string,
- textToDocMap: Map<string, Doc>,
- parentDoc: Doc,
- chunkId: string
- ): Observation[] {
- const filteredDocIds: string[] = [];
- const errors: string[] = [];
- let explanation = '';
-
- try {
- // Extract explanation if present (surrounded by DocSeperator)
- const explanationMatch = gptResponse.match(new RegExp(`${DocSeperator}\\s*([\\s\\S]*?)\\s*(?:${DocSeperator}|$)`));
- explanation = explanationMatch?.[1]?.trim() || '';
-
- // Parse document descriptions from GPT response
- const docBlocks = gptResponse
- .split(DescriptionSeperator)
- .filter(block => block.trim() !== '')
- .map(block => block.replace(/\n/g, ' ').trim());
-
- console.log(`[FilterDocsTool] Found ${docBlocks.length} document blocks in GPT response`);
-
- docBlocks.forEach((block, index) => {
- // Split by DataSeperator if present (for future extensibility)
- const [descText = ''] = block.split(DataSeperator).map(s => s.trim());
-
- if (!descText) {
- console.warn(`[FilterDocsTool] Skipping empty description block ${index}`);
- return;
- }
-
- const doc = textToDocMap.get(descText);
- if (!doc) {
- console.warn(`[FilterDocsTool] No matching document found for: "${descText.substring(0, 50)}..."`);
- errors.push(`No match found for: "${descText.substring(0, 30)}..."`);
- return;
+ },
+ ];
}
+ }
- // Apply the chat tag to mark as filtered
- try {
- TagItem.addTagToDoc(doc, FilterDocsTool.ChatTag);
- const docId = String(doc.id || `doc_${index}`);
- filteredDocIds.push(docId);
- console.log(`[FilterDocsTool] Tagged document: ${docId}`);
- } catch (tagError) {
- console.error(`[FilterDocsTool] Error tagging document:`, tagError);
- errors.push(`Failed to tag document: ${doc.id || 'unknown'}`);
- }
- });
+ /**
+ * Parse GPT response and apply filters with enhanced error handling
+ */
+ private _parseGptResponseAndApplyFilters(gptResponse: string, textToDocMap: Map<string, Doc>, parentDoc: Doc, chunkId: string): Observation[] {
+ const filteredDocIds: string[] = [];
+ const errors: string[] = [];
+ let explanation = '';
- // Apply the document filter to show only tagged documents
- if (filteredDocIds.length > 0) {
try {
- Doc.setDocFilter(parentDoc, 'tags', FilterDocsTool.ChatTag, 'check');
- const parentDocId = String(parentDoc.id || 'unknown');
- console.log(`[FilterDocsTool] Applied filter to parent document: ${parentDocId}`);
- } catch (filterError) {
- console.error('[FilterDocsTool] Error setting document filter:', filterError);
- errors.push('Failed to apply collection filter');
+ // Extract explanation if present (surrounded by DocSeperator)
+ const explanationMatch = gptResponse.match(new RegExp(`${DocSeperator}\\s*([\\s\\S]*?)\\s*(?:${DocSeperator}|$)`));
+ explanation = explanationMatch?.[1]?.trim() || '';
+
+ // Parse document descriptions from GPT response
+ const docBlocks = gptResponse
+ .split(DescriptionSeperator)
+ .filter(block => block.trim() !== '')
+ .map(block => block.replace(/\n/g, ' ').trim());
+
+ console.log(`[FilterDocsTool] Found ${docBlocks.length} document blocks in GPT response`);
+
+ docBlocks.forEach((block, index) => {
+ // Split by DataSeperator if present (for future extensibility)
+ const [descText = ''] = block.split(DataSeperator).map(s => s.trim());
+
+ if (!descText) {
+ console.warn(`[FilterDocsTool] Skipping empty description block ${index}`);
+ return;
+ }
+
+ const doc = textToDocMap.get(descText);
+ if (!doc) {
+ console.warn(`[FilterDocsTool] No matching document found for: "${descText.substring(0, 50)}..."`);
+ errors.push(`No match found for: "${descText.substring(0, 30)}..."`);
+ return;
+ }
+
+ // Apply the chat tag to mark as filtered
+ try {
+ TagItem.addTagToDoc(doc, FilterDocsTool.ChatTag);
+ const docId = String(doc.id || `doc_${index}`);
+ filteredDocIds.push(docId);
+ console.log(`[FilterDocsTool] Tagged document: ${docId}`);
+ } catch (tagError) {
+ console.error(`[FilterDocsTool] Error tagging document:`, tagError);
+ errors.push(`Failed to tag document: ${doc.id || 'unknown'}`);
+ }
+ });
+
+ // Apply the document filter to show only tagged documents
+ if (filteredDocIds.length > 0) {
+ try {
+ Doc.setDocFilter(parentDoc, 'tags', FilterDocsTool.ChatTag, 'check');
+ const parentDocId = String(parentDoc.id || 'unknown');
+ console.log(`[FilterDocsTool] Applied filter to parent document: ${parentDocId}`);
+ } catch (filterError) {
+ console.error('[FilterDocsTool] Error setting document filter:', filterError);
+ errors.push('Failed to apply collection filter');
+ }
+ }
+ } catch (parseError) {
+ console.error('[FilterDocsTool] Error parsing GPT response:', parseError);
+ errors.push(`Response parsing failed: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
}
- }
- } catch (parseError) {
- console.error('[FilterDocsTool] Error parsing GPT response:', parseError);
- errors.push(`Response parsing failed: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
- }
-
- // Build result message
- const filterCriteria = this._lastFilterCriteria || 'the specified criteria';
- const successMessage = filteredDocIds.length > 0
- ? `Successfully filtered ${filteredDocIds.length} document(s) matching "${filterCriteria}".`
- : 'No documents matched the specified filter criteria.';
+ // Build result message
+ const filterCriteria = this._lastFilterCriteria || 'the specified criteria';
+ const successMessage = filteredDocIds.length > 0 ? `Successfully filtered ${filteredDocIds.length} document(s) matching "${filterCriteria}".` : 'No documents matched the specified filter criteria.';
- const explanationMessage = explanation
- ? `\n\nGPT Explanation: ${explanation}`
- : '';
+ const explanationMessage = explanation ? `\n\nGPT Explanation: ${explanation}` : '';
- const errorMessage = errors.length > 0
- ? `\n\nWarnings: ${errors.join('; ')}`
- : '';
+ const errorMessage = errors.length > 0 ? `\n\nWarnings: ${errors.join('; ')}` : '';
- return [
- {
- type: 'text',
- text: `<chunk chunk_id="${chunkId}" chunk_type="filter_result">
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="filter_result">
${successMessage}${explanationMessage}${errorMessage}
Filtered document IDs: ${filteredDocIds.length > 0 ? filteredDocIds.join(', ') : '(none)'}
Filter tag applied: ${FilterDocsTool.ChatTag}
</chunk>`,
- },
- ];
- }
-
- /**
- * Get current filter status for UI integration
- */
- getFilterStatus(): { isActive: boolean; documentCount: number; filterTag: string } {
- const parentDoc = this._collectionView?.Document;
- const isActive = FilterDocsTool.hasActiveFilter(parentDoc);
- const documentCount = this._textToDocMap.size;
-
- return {
- isActive,
- documentCount,
- filterTag: FilterDocsTool.ChatTag,
- };
- }
-} \ No newline at end of file
+ },
+ ];
+ }
+
+ /**
+ * Get current filter status for UI integration
+ */
+ getFilterStatus(): { isActive: boolean; documentCount: number; filterTag: string } {
+ const parentDoc = this._collectionView?.Document;
+ const isActive = FilterDocsTool.hasActiveFilter(parentDoc);
+ const documentCount = this.TextToDocMap?.size ?? 0;
+
+ return {
+ isActive,
+ documentCount,
+ filterTag: FilterDocsTool.ChatTag,
+ };
+ }
+}