diff options
-rw-r--r-- | src/client/documents/Documents.ts | 1 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 2 | ||||
-rw-r--r-- | src/client/util/SearchUtil.ts | 2 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 1 | ||||
-rw-r--r-- | src/client/views/nodes/EquationBox.tsx | 4 | ||||
-rw-r--r-- | src/client/views/nodes/PDFBox.scss | 107 | ||||
-rw-r--r-- | src/client/views/nodes/PDFBox.tsx | 18 | ||||
-rw-r--r-- | src/client/views/nodes/chatbot/agentsystem/Agent.ts | 38 | ||||
-rw-r--r-- | src/client/views/nodes/chatbot/agentsystem/prompts.ts | 1 | ||||
-rw-r--r-- | src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx | 68 | ||||
-rw-r--r-- | src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts | 18 | ||||
-rw-r--r-- | src/client/views/nodes/chatbot/tools/ImageCreationTool.ts | 2 | ||||
-rw-r--r-- | src/client/views/nodes/chatbot/tools/SortDocsTool.ts | 98 | ||||
-rw-r--r-- | src/client/views/nodes/chatbot/tools/TagDocsTool.ts | 126 | ||||
-rw-r--r-- | src/client/views/nodes/chatbot/tools/TakeQuizTool.ts | 88 | ||||
-rw-r--r-- | src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts | 22 | ||||
-rw-r--r-- | src/client/views/pdf/Annotation.tsx | 4 |
17 files changed, 461 insertions, 139 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 6fd6534a4..22a771a11 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -312,7 +312,6 @@ export class DocumentOptions { title_transform?: STRt = new StrInfo('transformation to apply to title in label box (eg., uppercase)', undefined, undefined, ['uppercase', 'lowercase', 'capitalize']); text_transform?: STRt = new StrInfo('transformation to apply to text in text box (eg., uppercase)', undefined, undefined, ['uppercase', 'lowercase', 'capitalize']); text_placeholder?: BOOLt = new BoolInfo('makes the text act like a placeholder and automatically select when the text box is selected'); - fontSize?: string; _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views infoWindowOpen?: BOOLt = new BoolInfo('whether info window corresponding to pin is open (on MapDocuments)'); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index cf1c98b88..9fbc82bef 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -40,10 +40,8 @@ import { SnappingManager } from "./SnappingManager"; import { UndoManager } from "./UndoManager"; import { DocumentView } from "../views/nodes/DocumentView"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; -import { infoState } from "../views/collections/collectionFreeForm/CollectionFreeFormInfoState"; export interface Button { - targetState?: infoState; // DocumentOptions fields a button can set title?: string; toolTip?: string; diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 2f23d07dc..8077445f6 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -59,7 +59,7 @@ export namespace SearchUtil { * An array of all field names used by the Doc or its prototypes. */ export function documentKeys(doc: Doc) { - return Object.keys(Doc.GetAllPrototypes(doc).filter(proto => proto).reduce( + return Array.from(Doc.GetAllPrototypes(doc).filter(proto => proto).reduce( (keys, proto) => { Object.keys(proto).forEach(keys.add.bind(keys)); return keys; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 867a5a304..a3b2741d1 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -280,6 +280,7 @@ export class MainView extends ObservableReactComponent<object> { library.add( ...[ fa.faMinimize, + fa.faMagic, fa.faArrowsRotate, fa.faFloppyDisk, fa.faRepeat, diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index 2ce24b688..3549cc5d3 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -70,7 +70,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { y: NumCast(this.layoutDoc.y) + NumCast(this.Document._height) + 10, backgroundColor: StrCast(this.Document.backgroundColor), color: StrCast(this.Document.color), - fontSize: this.fontSize, + text_fontSize: this.fontSize, }); DocumentView.SetSelectOnLoad(nextEq); this._props.addDocument?.(nextEq); @@ -158,7 +158,7 @@ Docs.Prototypes.TemplateMap.set(DocumentType.EQUATION, { acl: '', _xMargin: 10, _yMargin: 10, - fontSize: '14px', + text_fontSize: '14px', _nativeWidth: 40, _nativeHeight: 40, _layout_reflowHorizontal: false, diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index c9edb2180..5c2da2b09 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -101,6 +101,7 @@ } } + .pdfBox-fuzzy, .pdfBox-nextIcon, .pdfBox-prevIcon { background: #121721; @@ -116,6 +117,19 @@ padding: 0px; } + .pdfBox-fuzzy { + background-color: #4a4a4a; + + &.active { + background-color: #3498db; + color: white; + } + + &:hover { + background-color: #2980b9; + } + } + .pdfBox-overlayButton:hover { background: none; } @@ -198,7 +212,7 @@ pointer-events: all; .pdfBox-searchBar { - width: calc(100% - 120px); // less size of search buttons + width: calc(100% - 140px); // less size of search buttons font-size: 14px; } } @@ -276,94 +290,3 @@ } } } - -// CSS adjusted for mobile devices -@media only screen and (max-device-width: 480px) { - .pdfBox .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsButton, - .pdfBox-interactive .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsButton { - height: 60px; - - .pdfBox-settingsButton-iconCont { - height: 60px; - width: 75px; - font-size: 30px; - } - - .pdfBox-settingsButton-arrow { - height: 60px; - border-top: 30px solid transparent; - border-bottom: 30px solid transparent; - border-right: 30px solid #121721; - } - } - - .pdfBox .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsFlyout, - .pdfBox-interactive .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsFlyout { - font-size: 30px; - } - - .pdfBox .pdfBox-ui .pdfBox-overlayCont, - .pdfBox-interactive .pdfBox-ui .pdfBox-overlayCont { - height: 60px; - - .pdfBox-searchBar { - font-size: 40px; - } - } - - .pdfBox .pdfBox-ui .pdfBox-overlayButton, - .pdfBox-interactive .pdfBox-ui .pdfBox-overlayButton { - height: 60px; - - .pdfBox-overlayButton-iconCont { - height: 60px; - width: 75px; - font-size: 30; - } - - .pdfBox-overlayButton-arrow { - border-top: 30px solid transparent; - border-bottom: 30px solid transparent; - border-right: 30px solid #121721; - } - } - - button.pdfBox-search { - font-size: 30px; - width: 50px; - height: 50px; - color: white; - } - - .pdfBox .pdfBox-ui .pdfBox-nextIcon, - .pdfBox .pdfBox-ui .pdfBox-prevIcon, - .pdfBox-interactive .pdfBox-ui .pdfBox-nextIcon, - .pdfBox-interactive .pdfBox-ui .pdfBox-prevIcon { - height: 50px; - width: 50px; - font-size: 30px; - } -} - -.pdfBox-fuzzy { - border: none; - background-color: #4a4a4a; - color: white; - padding: 0 8px; - height: 24px; - cursor: pointer; - margin-right: 4px; - border-radius: 3px; - display: flex; - align-items: center; - justify-content: center; - - &.active { - background-color: #3498db; - color: white; - } - - &:hover { - background-color: #2980b9; - } -} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 9fb1b07c4..55440b028 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,3 +1,4 @@ +import { Toggle, ToggleType, Type } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; @@ -9,15 +10,18 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; +import { List } from '../../../fields/List'; import { ComputedField } from '../../../fields/ScriptField'; import { Cast, DocCast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types'; import { ImageField, PdfField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; +import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT'; import { Docs } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { DocUtils } from '../../documents/DocUtils'; import { KeyCodes } from '../../util/KeyCodes'; +import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { CollectionFreeFormView } from '../collections/collectionFreeForm'; import { CollectionStackingView } from '../collections/CollectionStackingView'; @@ -33,9 +37,6 @@ import { ImageBox } from './ImageBox'; import { OpenWhere } from './OpenWhere'; import './PDFBox.scss'; import { CreateImage } from './WebBoxRenderer'; -import { gptAPICall } from '../../apis/gpt/GPT'; -import { List } from '../../../fields/List'; -import { GPTCallType } from '../../apis/gpt/GPT'; @observer export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @@ -456,9 +457,14 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <button type="button" className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> <FontAwesomeIcon icon="search" size="sm" /> </button> - <button type="button" className={`pdfBox-fuzzy ${this._fuzzySearchEnabled ? 'active' : ''}`} title={`${this._fuzzySearchEnabled ? 'Disable' : 'Enable'} Fuzzy Search`} onClick={this.toggleFuzzySearch}> - <FontAwesomeIcon icon="magic" size="sm" /> - </button> + <Toggle + type={Type.TERT} + toggleType={ToggleType.BUTTON} + icon={<FontAwesomeIcon icon={'magic'} onClick={this.toggleFuzzySearch} color={SnappingManager.userColor} />} + color={SnappingManager.userColor} + background={SnappingManager.userColor} + toggleStatus={this._fuzzySearchEnabled} + /> <button type="button" className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation}> <FontAwesomeIcon icon="arrow-up" size="lg" /> </button> diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts index 8516f054b..6c48820aa 100644 --- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts +++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts @@ -19,18 +19,19 @@ import { Vectorstore } from '../vectorstore/Vectorstore'; import { getReactPrompt } from './prompts'; //import { DictionaryTool } from '../tools/DictionaryTool'; import { ChatCompletionMessageParam } from 'openai/resources'; -import { Doc } from '../../../../../fields/Doc'; -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 { DocumentView } from '../../DocumentView'; import { CodebaseSummarySearchTool } from '../tools/CodebaseSummarySearchTool'; +import { CreateLinksTool } from '../tools/CreateLinksTool'; +import { CreateNewTool } from '../tools/CreateNewTool'; import { FileContentTool } from '../tools/FileContentTool'; import { FileNamesTool } from '../tools/FileNamesTool'; -import { CreateNewTool } from '../tools/CreateNewTool'; -//import { CreateTextDocTool } from '../tools/CreateTextDocumentTool'; +import { RAGTool } from '../tools/RAGTool'; +import { SortDocsTool } from '../tools/SortDocsTool'; +import { TagDocsTool } from '../tools/TagDocsTool'; +import { GPTTutorialTool } from '../tools/TutorialTool'; +import { WebsiteInfoScraperTool } from '../tools/WebsiteInfoScraperTool'; +import { AgentDocumentManager } from '../utils/AgentDocumentManager'; dotenv.config(); @@ -53,6 +54,8 @@ export class Agent { private streamedAnswerParser: StreamedAnswerParser = new StreamedAnswerParser(); private tools: Record<string, BaseTool<ReadonlyArray<Parameter>>>; private _docManager: AgentDocumentManager; + private is_dash_doc_assistant: boolean; + private parentView: DocumentView; // Dynamic tool registry for tools created at runtime private dynamicToolRegistry: Map<string, BaseTool<ReadonlyArray<Parameter>>> = new Map(); // Callback for notifying when tools are created and need reload @@ -82,6 +85,7 @@ export class Agent { // Initialize OpenAI client with API key from environment this.client = new OpenAI({ apiKey: process.env.OPENAI_KEY, dangerouslyAllowBrowser: true }); this.vectorstore = _vectorstore; + this.parentView = docManager.parentViewDocument; // Get the parent DocumentView this._history = history; this._csvData = csvData; this._docManager = docManager; @@ -97,12 +101,15 @@ export class Agent { websiteInfoScraper: new WebsiteInfoScraperTool(this._docManager), searchTool: new SearchTool(this._docManager), noTool: new NoTool(), - //imageCreationTool: new ImageCreationTool(createImage), + imageCreationTool: new ImageCreationTool(createImage), documentMetadata: new DocumentMetadataTool(this._docManager), createLinks: new CreateLinksTool(this._docManager), codebaseSummarySearch: new CodebaseSummarySearchTool(this.vectorstore), fileContent: new FileContentTool(this.vectorstore), fileNames: new FileNamesTool(this.vectorstore), + generateTutorialNode: new GPTTutorialTool(this._docManager), + sortDocs: new SortDocsTool(this._docManager, this.parentView), + tagDocs: new TagDocsTool(this._docManager), }; // Add the createNewTool after other tools are defined @@ -347,7 +354,18 @@ export class Agent { // Check both static tools and dynamic registry const tool = this.tools[currentAction] || this.dynamicToolRegistry.get(currentAction); - + if (currentAction === 'noTool') { + // Immediately ask for clarification in plain text, not as a tool prompt + this.interMessages.push({ + role: 'user', + content: `<stage number="${i + 1}" role="assistant"> + <answer> + I’m not sure what you’d like me to do. Could you clarify your request? + </answer> + </stage>`, + }); + break; + } if (tool) { // Prepare the next action based on the current tool const nextPrompt = [ diff --git a/src/client/views/nodes/chatbot/agentsystem/prompts.ts b/src/client/views/nodes/chatbot/agentsystem/prompts.ts index ab9630a6c..c18952e49 100644 --- a/src/client/views/nodes/chatbot/agentsystem/prompts.ts +++ b/src/client/views/nodes/chatbot/agentsystem/prompts.ts @@ -111,6 +111,7 @@ export function getReactPrompt(tools: BaseTool<ReadonlyArray<Parameter>>[], summ <tools> ${toolDescriptions} + <note>The tagging tool takes priority over the metadata tool for queries relating to tagging.</note> <note>If no external tool is required, use 'no_tool', but if there might be relevant external information, use the appropriate tool.</note> </tools> diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx index 093c1244c..732c4d637 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx @@ -46,6 +46,9 @@ 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'; +import { Button, Size, Type } from '@dash/components'; dotenv.config(); @@ -111,8 +114,14 @@ export class ChatBox extends ViewBoxAnnotatableComponent<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); + this.docManager = new AgentDocumentManager(this, parentView); // Initialize OpenAI client this.initializeOpenAI(); @@ -146,6 +155,25 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } ); + /* + 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(); } @@ -341,15 +369,19 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action askGPT = async (event: React.FormEvent): Promise<void> => { event.preventDefault(); + if (!this._textInputRef) { + console.log('ERROR: text input ref is undefined'); + return; + } this._inputValue = ''; // Extract the user's message - const textInput = (event.currentTarget as HTMLFormElement).elements.namedItem('messageInput') as HTMLInputElement; - const trimmedText = textInput.value.trim(); + const textInput = this._textInputRef?.value ?? ''; + const trimmedText = textInput.trim(); if (trimmedText) { + this._textInputRef.value = ''; // Clear the input field try { - textInput.value = ''; // Add the user's message to the history this._history.push({ role: ASSISTANT_ROLE.USER, @@ -485,7 +517,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action public whichDoc = (doc: parsedDoc, insideCol: boolean): Opt<Doc> => { - const options = OmitKeys(doc, ['doct_type', 'data']).omit as DocumentOptions; + const options = OmitKeys(doc, ['doc_type', 'data']).omit as DocumentOptions; const data = (doc as parsedDocData).data; const ndoc = (() => { switch (doc.doc_type) { @@ -1450,20 +1482,20 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { autoComplete="off" placeholder="Type your message here..." value={this._inputValue} - onChange={action(e => (this._inputValue = e.target.value))} + onChange={e => this.setChatInput(e.target.value)} disabled={this._isLoading} /> </div> - <button className="submit-button" onClick={() => this._dictation?.stopDictation()} type="submit" disabled={this._isLoading || !this._inputValue.trim()}> - {this._isLoading ? ( - <div className="spinner"></div> - ) : ( - <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"> - <line x1="22" y1="2" x2="11" y2="13"></line> - <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon> - </svg> - )} - </button> + <Button + // className="submit-button" + onClick={this.askGPT} + type={Type.PRIM} + tooltip="Send to AI" + color={SnappingManager.userVariantColor} + inactive={this._isLoading || !this._inputValue.trim()} + icon={<AiOutlineSend />} + size={Size.LARGE} + /> <DictationButton ref={r => { this._dictation = r; @@ -1500,7 +1532,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { The tool <strong>{this._toolReloadModal.toolName}</strong> has been created and saved successfully. </p> <p>To make the tool available for future use, the page needs to be reloaded to rebuild the application bundle.</p> - <p>Click "Reload Page" to complete the tool installation.</p> + <p>Click "Reload Page" to complete the tool installation.</p> </div> <div className="tool-reload-modal-actions"> <button className="reload-button primary" onClick={this.handleReloadConfirmation}> @@ -1523,5 +1555,5 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { */ Docs.Prototypes.TemplateMap.set(DocumentType.CHAT, { layout: { view: ChatBox, dataField: 'data' }, - options: { acl: '', _layout_fitWidth: true, chat: '', chat_history: '', chat_thread_id: '', chat_assistant_id: '', chat_vector_store_id: '', _forceActive: true }, + options: { acl: '', _layout_nativeDimEditable: true, _layout_fitWidth: true, chat: '', chat_history: '', chat_thread_id: '', chat_assistant_id: '', chat_vector_store_id: '', _forceActive: true }, }); diff --git a/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts b/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts index da4a4ae29..fd44cc60f 100644 --- a/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts +++ b/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts @@ -1,3 +1,5 @@ +import { OmitKeys } from '../../../../../ClientUtils'; +import { DocumentOptions } from '../../../../documents/Documents'; import { Parameter, ParametersType, supportedDocTypes, ToolInfo } from '../types/tool_types'; import { Observation } from '../types/types'; import { AgentDocumentManager } from '../utils/AgentDocumentManager'; @@ -21,8 +23,7 @@ const parameterDefinitions: ReadonlyArray<Parameter> = [ name: 'fieldEdits', type: 'string', required: false, - description: - 'JSON array of field edits for editing fields. Each item should have fieldName and fieldValue. For single field edits, use an array with one item. Example: [{"fieldName":"layout_autoHeight","fieldValue":false},{"fieldName":"height","fieldValue":300}]', + description: `JSON array of field edits for editing fields. Each item should have fieldName and fieldValue. For single field edits, use an array with one item. fieldName values MUST be in this list: [${Object.keys(DocumentOptions)}]. Example: [{"fieldName":"layout_autoHeight","fieldValue":false},{"fieldName":"height","fieldValue":300}]`, }, { name: 'title', @@ -402,7 +403,8 @@ To CREATE a new document: - title: The title of the document to create - data: The content data for the document (text content, URL, etc.) - doc_type: The type of document to create (text, web, image, etc.) -- Example: {...inputs: { action: "create", title: "My Notes", data: "This is the content", doc_type: "text" }} + - fieldEdits: Optional JSON array of fields to set during creation +- Example: {...inputs: { action: "create", title: "My Notes", data: "This is the content", doc_type: "text", fieldEdits: [{ fieldName: "text", fieldValue: "Hello world" }] }} - After creation, you can edit the document with more specific properties To EDIT document metadata: @@ -694,8 +696,15 @@ export class DocumentMetadataTool extends BaseTool<DocumentMetadataToolParamsTyp const docType = String(args.doc_type); const title = String(args.title); const data = String(args.data); + const json = typeof args.fieldEdits === 'string' ? JSON.parse(args.fieldEdits) : {}; + const docopts = json.length + ? (json as Array<{ fieldName: string; fieldValue: string | number | boolean }>).reduce((opts, opt) => { + opts[opt.fieldName] = opt.fieldValue; + return opts; + }, {} as DocumentOptions) + : {}; - const id = await this._docManager.createDocInDash(docType, data, { title: title }); + const id = await this._docManager.createDocInDash(docType, data, docopts); if (!id) { return [ @@ -715,6 +724,7 @@ export class DocumentMetadataTool extends BaseTool<DocumentMetadataToolParamsTyp Document ID: ${id} Type: ${docType} Title: "${title}" +Options: ${JSON.stringify(OmitKeys(args, ['doc_type', 'data', 'title']).omit, null, 2)} The document has been created with default dimensions and positioning. You can now use the "edit" action to modify additional properties of this document. diff --git a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts index 37907fd4f..c5b1e028b 100644 --- a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts +++ b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts @@ -26,6 +26,8 @@ const imageCreationToolInfo: ToolInfo<ImageCreationToolParamsType> = { }; export class ImageCreationTool extends BaseTool<ImageCreationToolParamsType> { + + private _createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void; constructor(createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void) { super(imageCreationToolInfo); diff --git a/src/client/views/nodes/chatbot/tools/SortDocsTool.ts b/src/client/views/nodes/chatbot/tools/SortDocsTool.ts new file mode 100644 index 000000000..45d7b4f15 --- /dev/null +++ b/src/client/views/nodes/chatbot/tools/SortDocsTool.ts @@ -0,0 +1,98 @@ +import { BaseTool } from './BaseTool'; +import { Observation } from '../types/types'; +import { ParametersType, ToolInfo } from '../types/tool_types'; +import { AgentDocumentManager } from '../utils/AgentDocumentManager'; +import { gptAPICall, GPTCallType, DescriptionSeperator } from '../../../../apis/gpt/GPT'; +import { ChatSortField } from '../../../collections/CollectionSubView'; +import { v4 as uuidv4 } from 'uuid'; +import { DocumentView } from '../../DocumentView'; +import { docSortings } from '../../../collections/CollectionSubView'; +import { collect } from '@turf/turf'; + +const parameterRules = [ + { + name: 'sortCriteria', + type: 'string', + description: 'Criteria provided by the user to sort the documents.', + required: true, + }, +] as const; + +const toolInfo: ToolInfo<typeof parameterRules> = { + name: 'sortDocs', + description: + 'Sorts documents within the current Dash environment based on user-specified criteria.', + parameterRules, + citationRules: 'No citation needed for sorting operations.', +}; + +export class SortDocsTool extends BaseTool<typeof parameterRules> { + private _docManager: AgentDocumentManager; + private _collectionView: DocumentView; + + constructor(docManager: AgentDocumentManager, collectionView: DocumentView) + { + super(toolInfo); + // Grab the parent collection’s DocumentView (the ChatBox container) + // We assume the ChatBox itself is currently selected in its parent view. + this._collectionView = collectionView; + this._docManager = docManager; + this._docManager.initializeFindDocsFreeform(); + } + + async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[]> { + const chunkId = uuidv4(); + + // 1) gather metadata & build map from text→id + const textToId = new Map<string, string>(); + + const chunks = (await Promise.all( + this._docManager.docIds.map(async id => { + const text = await this._docManager.getDocDescription(id); + textToId.set(text,id); + return DescriptionSeperator + text + DescriptionSeperator; + }) + )) + .join(''); + try { + // 2) call GPT to sort those chunks + const gptResponse = await gptAPICall(args.sortCriteria, GPTCallType.SORTDOCS, chunks); + console.log('GPT RESP:', gptResponse); + + // 3) parse & map back to IDs + const sortedIds = gptResponse + .split(DescriptionSeperator) + .filter(s => s.trim() !== '') + .map(s => s.replace(/\n/g, ' ').trim()) + .map(s => textToId.get(s)) // lookup in our map + .filter((id): id is string => !!id); + + // 4) write back the ordering + sortedIds.forEach((docId, idx) => { + this._docManager.editDocumentField(docId, ChatSortField, idx); + }); + + const fieldKey = this._collectionView.ComponentView!.fieldKey; + this._collectionView.Document[ `${fieldKey}_sort` ] = docSortings.Chat; + + + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="sort_status"> +Successfully sorted ${sortedIds.length} documents by "${args.sortCriteria}". +</chunk>`, + }, + ]; + } catch (err) { + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error"> +Sorting failed: ${err instanceof Error ? err.message : err} +</chunk>`, + }, + ]; + } + } +} diff --git a/src/client/views/nodes/chatbot/tools/TagDocsTool.ts b/src/client/views/nodes/chatbot/tools/TagDocsTool.ts new file mode 100644 index 000000000..c88c32e50 --- /dev/null +++ b/src/client/views/nodes/chatbot/tools/TagDocsTool.ts @@ -0,0 +1,126 @@ +import { BaseTool } from './BaseTool'; +import { Observation } from '../types/types'; +import { ParametersType, ToolInfo } from '../types/tool_types'; +import { AgentDocumentManager } from '../utils/AgentDocumentManager'; +import { gptAPICall, GPTCallType, DescriptionSeperator, DataSeperator } from '../../../../apis/gpt/GPT'; +import { v4 as uuidv4 } from 'uuid'; +import { TagItem } from '../../../TagsView'; + +const parameterRules = [ + { + name: 'taggingCriteria', + type: 'string', + description: 'Natural‐language criteria for tagging documents.', + required: true, + }, +] as const; + +const toolInfo: ToolInfo<typeof parameterRules> = { + name: 'tagDocs', + description: 'Automatically generate and apply tags to docs based on criteria.', + parameterRules, + citationRules: 'No citation needed for tagging operations.', +}; + +export class TagDocsTool extends BaseTool<typeof parameterRules> { + private _docManager: AgentDocumentManager; + + constructor(docManager: AgentDocumentManager) { + super(toolInfo); + this._docManager = docManager; + // ensure our manager has scanned all freeform docs + this._docManager.initializeFindDocsFreeform(); + } + + async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[]> { + const chunkId = uuidv4(); + + try { + // 1) Build description→ID map and the prompt string + const textToId = new Map<string, string>(); + const descriptionsArray: string[] = []; + + // We assume getDocDescription returns a Promise<string> with the text + for (const id of this._docManager.docIds) { + const desc = (await this._docManager.getDocDescription(id)).replace(/\n/g, ' ').trim(); + if (!desc) continue; + textToId.set(desc, id); + // wrap in separators exactly as GPTPopup does + descriptionsArray.push(`${DescriptionSeperator}${desc}${DescriptionSeperator}`); + } + + const promptDescriptions = descriptionsArray.join(''); + + // 2) Call GPT + const raw = await gptAPICall( + args.taggingCriteria, + GPTCallType.TAGDOCS, + promptDescriptions + ); + console.log('[TagDocsTool] GPT raw:', raw); + + // 3) Parse GPT’s response, look up each description, and apply tags + const appliedTags: Record<string, string[]> = {}; + + raw + .split(DescriptionSeperator) // split into blocks + .filter(block => block.trim() !== '') // remove empties + .map(block => block.replace(/\n/g, ' ').trim()) // flatten whitespace + .forEach(block => { + // each block should look like: "<desc_text>>>>><tag1,tag2>" + const [descText, tagsText = ''] = block.split(DataSeperator).map(s => s.trim()); + if (!descText || !tagsText) { + console.warn('[TagDocsTool] skipping invalid block:', block); + return; + } + const docId = textToId.get(descText); + if (!docId) { + console.warn('[TagDocsTool] no docId for description:', descText); + return; + } + const doc = this._docManager.getDocument(docId); + if (!doc) { + console.warn('[TagDocsTool] no Doc instance for ID:', docId); + return; + } + + // split/normalize tags + const normalized = tagsText + .split(',') + .map(tag => tag.trim()) + .filter(tag => tag.length > 0) + .map(tag => (tag.startsWith('#') ? tag : `#${tag}`)); + + // apply tags + normalized.forEach(tag => TagItem.addTagToDoc(doc, tag)); + + // record for our summary + appliedTags[docId] = normalized; + }); + + // 4) Build a summary observation + const summary = Object.entries(appliedTags) + .map(([id, tags]) => `${id}: ${tags.join(', ')}`) + .join('; '); + + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="tagging_status"> +Successfully tagged documents based on "${args.taggingCriteria}". Tags applied: ${summary} +</chunk>`, + }, + ]; + } catch (err) { + console.error('[TagDocsTool] error:', err); + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error"> +Tagging failed: ${err instanceof Error ? err.message : String(err)} +</chunk>`, + }, + ]; + } + } +} diff --git a/src/client/views/nodes/chatbot/tools/TakeQuizTool.ts b/src/client/views/nodes/chatbot/tools/TakeQuizTool.ts new file mode 100644 index 000000000..f025e95cd --- /dev/null +++ b/src/client/views/nodes/chatbot/tools/TakeQuizTool.ts @@ -0,0 +1,88 @@ +import { BaseTool } from './BaseTool'; +import { Observation } from '../types/types'; +import { ParametersType, ToolInfo } from '../types/tool_types'; +import { AgentDocumentManager } from '../utils/AgentDocumentManager'; +import { GPTCallType, gptAPICall } from '../../../../apis/gpt/GPT'; +import { v4 as uuidv4 } from 'uuid'; + +const parameterRules = [ + { + name: 'userAnswer', + type: 'string', + description: 'User-provided answer to the quiz question.', + required: true, + }, +] as const; + +const toolInfo: ToolInfo<typeof parameterRules> = { + name: 'takeQuiz', + description: + 'Evaluates a user\'s answer for a randomly selected document using GPT, mirroring GPTPopup\'s quiz functionality.', + parameterRules, + citationRules: 'No citation needed for quiz operations.', +}; + +export class TakeQuizTool extends BaseTool<typeof parameterRules> { + private _docManager: AgentDocumentManager; + + constructor(docManager: AgentDocumentManager) { + super(toolInfo); + this._docManager = docManager; + this._docManager.initializeFindDocsFreeform(); + } + + private async generateRubric(docId: string, description: string): Promise<string> { + const docMeta = this._docManager.extractDocumentMetadata(docId); + if (docMeta && docMeta.fields.layout.gptRubric) { + return docMeta.fields.layout.gptRubric; + } else { + const rubric = await gptAPICall(description, GPTCallType.MAKERUBRIC); + if (rubric) { + await this._docManager.editDocumentField(docId, 'layout.gptRubric', rubric); + } + return rubric || ''; + } + } + + async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[]> { + const chunkId = uuidv4(); + + try { + const allDocIds = this._docManager.docIds; + const randomDocId = allDocIds[Math.floor(Math.random() * allDocIds.length)]; + const docMeta = this._docManager.extractDocumentMetadata(randomDocId); + + if (!docMeta) throw new Error('Randomly selected document metadata is undefined'); + + const description = docMeta.fields.layout.description.replace(/\n/g, ' ').trim(); + const rubric = await this.generateRubric(randomDocId, description); + + const prompt = ` + Question: ${description}; + UserAnswer: ${args.userAnswer}; + Rubric: ${rubric} + `; + + const evaluation = await gptAPICall(prompt, GPTCallType.QUIZDOC); + + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="quiz_result"> +Evaluation result: ${evaluation || 'GPT provided no answer'}. +Document evaluated: "${docMeta.title}" +</chunk>`, + }, + ]; + } catch (err) { + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error"> +Quiz evaluation failed: ${err instanceof Error ? err.message : err} +</chunk>`, + }, + ]; + } + } +} diff --git a/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts b/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts index dcb708450..088891022 100644 --- a/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts +++ b/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts @@ -27,6 +27,7 @@ interface AgentDocument { export class AgentDocumentManager { @observable private documentsById: ObservableMap<string, AgentDocument>; private chatBox: ChatBox; + private parentView: DocumentView; private chatBoxDocument: Doc | null = null; private fieldMetadata: Record<string, any> = {}; // bcz: CHANGE any to a proper type! @observable private simplifiedChunks: ObservableMap<string, SimplifiedChunk>; @@ -35,8 +36,9 @@ export class AgentDocumentManager { * Creates a new DocumentManager * @param templateDocument The document that serves as a template for new documents */ - constructor(chatBox: ChatBox) { + constructor(chatBox: ChatBox, parentView: DocumentView) { makeObservable(this); + this.parentView = parentView; const agentDoc = DocCast(chatBox.Document.agentDocument) ?? new Doc(); const chunk_simpl = DocCast(agentDoc.chunk_simpl) ?? new Doc(); @@ -164,6 +166,10 @@ export class AgentDocumentManager { } } + public get parentViewDocument(): DocumentView { + return this.parentView; + } + /** * Process a document by ensuring it has an ID and adding it to the appropriate collections * @param doc The document to process @@ -851,6 +857,7 @@ export class AgentDocumentManager { try { // Create simple document with just title and data const simpleDoc: parsedDoc = { + ...(options as parsedDoc), // bcz: hack .. why do we need parsedDoc and not DocumentOptions here? doc_type: docType, title: options?.title ?? `Untitled Document ${this.documentsById.size + 1}`, data: data, @@ -1011,6 +1018,19 @@ export class AgentDocumentManager { const docInfo = this.documentsById.get(docId); return docInfo?.dataDoc; } + + // In AgentDocumentManager + private descriptionCache = new Map<string, string>(); + + public async getDocDescription(id: string): Promise<string> { + if (!this.descriptionCache.has(id)) { + const doc = this.getDocument(id)!; + const desc = await Doc.getDescription(doc); + this.descriptionCache.set(id, desc.replace(/\n/g, ' ').trim()); + } + return this.descriptionCache.get(id)!; + } + /** * Adds simplified chunks to a document for citation handling * @param doc The document to add simplified chunks to diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index e8a5235c9..69dda89cb 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -119,9 +119,9 @@ export class Annotation extends ObservableReactComponent<IAnnotationProps> { {StrListCast(this._props.annoDoc.text_inlineAnnotations) .map(a => a.split?.(':')) .filter(fields => fields) - .map(([x, y, width, height]) => ( + .map(([x, y, width, height], i) => ( <div - key={'' + x + y + width + height} + key={'' + x + y + width + height + i} style={{ pointerEvents: this._props.pointerEvents?.() as Property.PointerEvents }} onPointerDown={this.onPointerDown} onContextMenu={this.onContextMenu} |