diff options
author | sharkiecodes <lanyi_stroud@brown.edu> | 2025-07-23 14:45:59 -0400 |
---|---|---|
committer | sharkiecodes <lanyi_stroud@brown.edu> | 2025-07-23 14:45:59 -0400 |
commit | 7f49356b9460d46a06e7b7d67c369c4bb1d4bbe5 (patch) | |
tree | b9efa7d22ab520c171dd783509b4fbb8cbe146d7 /src | |
parent | 968cbfa1fd18bba2b2857d756a88f8a036bd45fa (diff) | |
parent | eea5881bddaa66ebe544bdfc94ce80fd0fbf8860 (diff) |
Merge branch 'lanyi-expanded-agent-paper-main' of https://github.com/brown-dash/Dash-Web into lanyi-expanded-agent-paper-main
Diffstat (limited to 'src')
4 files changed, 405 insertions, 436 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()}'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..18e7481f5 100644 --- a/src/client/views/nodes/chatbot/tools/FilterDocsTool.ts +++ b/src/client/views/nodes/chatbot/tools/FilterDocsTool.ts @@ -3,343 +3,312 @@ 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[]; + + 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; + await this._documentDescriptions; + + 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, + }; + } +} diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index 6e0d58932..65be98c83 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -92,8 +92,12 @@ export class GPTPopup extends ObservableReactComponent<object> { 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}`) + Doc.getDescription(doc).then(text => text.replace(/\n/g, ' ').trim()) + .then(text => this._textToDocMap.set(text, doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`) )).then(docDescriptions => docDescriptions.join()); // prettier-ignore + this._documentDescriptions.then(descs => { + console.log(descs); + }); } }, { fireImmediately: true } |