diff options
Diffstat (limited to 'src/client/views')
16 files changed, 842 insertions, 383 deletions
diff --git a/src/client/views/DictationButton.tsx b/src/client/views/DictationButton.tsx index fc3165f67..882e857c5 100644 --- a/src/client/views/DictationButton.tsx +++ b/src/client/views/DictationButton.tsx @@ -1,9 +1,10 @@ -import { makeObservable, observable, action } from 'mobx'; +import { Toggle, ToggleType } from '@dash/components'; +import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import './DictationButton.scss'; import { DictationManager } from '../util/DictationManager'; import { SnappingManager } from '../util/SnappingManager'; +import './DictationButton.scss'; export interface DictationButtonProps { setInput: (val: string) => void; @@ -26,9 +27,21 @@ export class DictationButton extends React.Component<DictationButtonProps> { render() { return ( - <button - className={`dictation-button ${this._isRecording ? 'recording' : ''}`} - title="Record" + <Toggle + // className={`dictation-button ${this._isRecording ? 'recording' : ''}`} + // title="Record" + tooltip={`Dictation: ${this._isRecording ? 'on' : 'off'}`} + icon={ + <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> + <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path> + <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path> + <line x1="12" y1="19" x2="12" y2="23"></line> + <line x1="8" y1="23" x2="16" y2="23"></line> + </svg> + } + color={SnappingManager.userVariantColor} + toggleType={ToggleType.BUTTON} + toggleStatus={this._isRecording} onClick={action(() => { if (!this._isRecording) { this._isRecording = true; @@ -49,14 +62,8 @@ export class DictationButton extends React.Component<DictationButtonProps> { } else { this.stopDictation(); } - })}> - <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> - <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path> - <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path> - <line x1="12" y1="19" x2="12" y2="23"></line> - <line x1="8" y1="23" x2="16" y2="23"></line> - </svg> - </button> + })} + /> ); } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormController.ts b/src/client/views/collections/collectionFreeForm/CollectionFreeFormController.ts new file mode 100644 index 000000000..6752b46b8 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormController.ts @@ -0,0 +1,7 @@ +import { CollectionFreeFormView } from './CollectionFreeFormView'; + +export class TutorialController { + public static startTutorial(kind: 'links' | 'pins' | 'presentation') { + CollectionFreeFormView.showTutorial(kind); + } +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx index 437888ef2..48cab9c7b 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx @@ -3,30 +3,43 @@ import { IReactionDisposer, action, makeObservable, observable, reaction } from import { observer } from 'mobx-react'; import * as React from 'react'; import { SettingsManager } from '../../../util/SettingsManager'; +import { ButtonType } from '../../nodes/FontIconBox/FontIconBox'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import './CollectionFreeFormView.scss'; +export interface InfoButton { + targetState?: infoState; + // DocumentOptions fields a button can set + title?: string; + toolTip?: string; + btnType?: ButtonType; + // fields that do not correspond to DocumentOption fields + scripts?: { script?: string; onClick?: string; onDoubleClick?: string }; +} /** * An Fsa Arc. The first array element is a test condition function that will be observed. * The second array element is a function that will be invoked when the first test function * returns a truthy value */ -// eslint-disable-next-line no-use-before-define export type infoArc = [() => unknown, (res?: unknown) => infoState]; export const StateMessage = Symbol('StateMessage'); export const StateMessageGIF = Symbol('StateMessageGIF'); export const StateEntryFunc = Symbol('StateEntryFunc'); +export const StateMessageButton = Symbol('StateMessageButton'); export class infoState { [StateMessage]: string = ''; [StateMessageGIF]?: string = ''; + [StateMessageButton]?: InfoButton[]; [StateEntryFunc]?: () => unknown; [key: string]: infoArc; - constructor(message: string, arcs: { [key: string]: infoArc }, messageGif?: string, entryFunc?: () => unknown) { + + constructor(message: string, arcs?: { [key: string]: infoArc }, messageGif?: string, buttons?: InfoButton[], entryFunc?: () => unknown) { this[StateMessage] = message; - Object.assign(this, arcs); + Object.assign(this, arcs ?? {}); this[StateMessageGIF] = messageGif; this[StateEntryFunc] = entryFunc; + this[StateMessageButton] = buttons; } } @@ -42,16 +55,17 @@ export class infoState { */ export function InfoState( msg: string, // - arcs: { [key: string]: infoArc }, + arcs?: { [key: string]: infoArc }, gif?: string, + button?: InfoButton[], entryFunc?: () => unknown ) { - return new infoState(msg, arcs, gif, entryFunc); + return new infoState(msg, arcs, gif, button, entryFunc); } export interface CollectionFreeFormInfoStateProps { infoState: infoState; - next: (state: infoState) => unknown; + next: (state: infoState) => unknown; // Ensure it's properly defined close: () => void; } @@ -68,6 +82,10 @@ export class CollectionFreeFormInfoState extends ObservableReactComponent<Collec get State() { return this._props.infoState; } + + set State(value: infoState) { + this._props.infoState = value; + } get Arcs() { return Object.keys(this.State ?? []).map(key => this.State?.[key]); } @@ -97,6 +115,9 @@ export class CollectionFreeFormInfoState extends ObservableReactComponent<Collec render() { const gif = this.State?.[StateMessageGIF]; + const buttons = this.State?.[StateMessageButton]; + console.log('Rendering CollectionFreeFormInfoState with state:', this.props.infoState); + console.log(buttons); return ( <div className="collectionFreeform-infoUI"> <p className="collectionFreeform-infoUI-msg"> @@ -110,9 +131,27 @@ export class CollectionFreeFormInfoState extends ObservableReactComponent<Collec {this._expanded ? 'Less...' : 'More...'} </button> </p> + <div className={'collectionFreeform-' + (!this._expanded || !gif ? 'hidden' : 'infoUI-gif-container')}> <img src={`/assets/${gif}`} alt="state message gif" /> </div> + + {/* Render the buttons for skipping */} + <div className={'collectionFreeform-' + (!buttons || buttons.length === 0 ? 'hidden' : 'infoUI-button-container')}> + {buttons?.map((button, index) => ( + <button + key={index} + type="button" + className="collectionFreeform-infoUI-skip-button" + onClick={action(() => { + console.log('Attempting transition to:', button.targetState); + this.props.next(button.targetState as infoState); // ✅ Use the prop instead + })}> + {button.title} + </button> + ))} + </div> + <div className="collectionFreeform-infoUI-close"> <IconButton icon="x" color={SettingsManager.userColor} size={Size.XSMALL} type={Type.TERT} background={SettingsManager.userBackgroundColor} onClick={action(() => this.props.close())} /> </div> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx index 89d2bf2c3..147c900be 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx @@ -1,284 +1,393 @@ -import { makeObservable, observable, runInAction } from 'mobx'; +import { action, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, FieldResult, FieldType } from '../../../../fields/Doc'; +import { CollectionFreeFormView } from '.'; +import { Doc, DocListCast } from '../../../../fields/Doc'; import { InkTool } from '../../../../fields/InkField'; -import { StrCast } from '../../../../fields/Types'; -import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DocButtonState, DocumentLinksButton } from '../../nodes/DocumentLinksButton'; -import { TopBar } from '../../topbar/TopBar'; -import { CollectionFreeFormInfoState, InfoState, StateEntryFunc, infoState } from './CollectionFreeFormInfoState'; -import { CollectionFreeFormView } from './CollectionFreeFormView'; -import './CollectionFreeFormView.scss'; +import { ButtonType } from '../../nodes/FontIconBox/FontIconBox'; +import { ObservableReactComponent } from '../../ObservableReactComponent'; +import { CollectionFreeFormInfoState, InfoButton, infoState, InfoState } from './CollectionFreeFormInfoState'; export interface CollectionFreeFormInfoUIProps { - Doc: Doc; - layoutDoc: Doc; + Document: Doc; + LayoutDoc: Doc; childDocs: () => Doc[]; close: () => void; } @observer export class CollectionFreeFormInfoUI extends ObservableReactComponent<CollectionFreeFormInfoUIProps> { + private _originalBackground: string | undefined; + private _tutorialStates: { [key: string]: infoState } = {}; + public static Init() { - CollectionFreeFormView.SetInfoUICreator((doc: Doc, layout: Doc, childDocs: () => Doc[], close: () => void) => ( - // - <CollectionFreeFormInfoUI Doc={doc} layoutDoc={layout} childDocs={childDocs} close={close} /> - )); + CollectionFreeFormView.SetInfoUICreator((doc: Doc, layout: Doc, childDocs: () => Doc[], close: () => void) => <CollectionFreeFormInfoUI Document={doc} LayoutDoc={layout} childDocs={childDocs} close={close} />); } - _firstDocPos = { x: 0, y: 0 }; constructor(props: CollectionFreeFormInfoUIProps) { super(props); makeObservable(this); - this._currState = this.setupStates(); + this._tutorialStates = {}; // Initialize an empty object + this.currState = this.setupStates(); // Call setupStates() here } - _originalbackground: string | undefined; @observable _currState: infoState | undefined = undefined; - get currState() { return this._currState; } // prettier-ignore - set currState(val) { runInAction(() => {this._currState = val;}); } // prettier-ignore + @observable _nextState: infoState | undefined = undefined; // Track next state + + get currState() { + return this._currState; + } + + set currState(val) { + runInAction(() => (this._currState = val)); + } - componentWillUnmount(): void { - this._props.Doc.$backgroundColor = this._originalbackground; + componentWillUnmount() { + this._props.Document.backgroundColor = this._originalBackground; } - setCurrState = (state: infoState) => { - if (state) { - this.currState = state; - this.currState[StateEntryFunc]?.(); - } + skipToState = action((newState: infoState) => (this._currState = newState)); + + createNextButton = (newState: ReturnType<typeof InfoState>) => { + return { + title: 'Next', + toolTip: 'Next', + btnType: ButtonType.ClickButton, + scripts: { + onClick: `this.skipToState(${newState})`, + }, + targetState: newState, + }; }; setupStates = () => { - this._originalbackground = StrCast(this._props.Doc.$backgroundColor); - // state entry functions - // const setBackground = (colour: string) => () => {this._props.Doc.$backgroundColor = colour;} // prettier-ignore - // const setOpacity = (opacity: number) => () => {this._props.layoutDoc.opacity = opacity;} // prettier-ignore - // arc transition trigger conditions - const firstDoc = () => (this._props.childDocs().length ? this._props.childDocs()[0] : undefined); - const numDocs = () => this._props.childDocs().length; + let docCounter = this._props.childDocs().length; + let lastDocCreated = this._props.childDocs()[this.props.childDocs().length - 1]; + let linkCounter = Doc.Links(lastDocCreated)?.length; + let presentationCounter = DocListCast(Doc.ActivePresentation?.data).length; + this._originalBackground = this._props.Document.backgroundColor as string; + + this._tutorialStates.multipleDocs = InfoState( + "Let's create a new link! Click the link icon on one document and connect it to another.", + { + linkStarted: [ + () => DocumentLinksButton.StartLink, + () => { + linkCounter = Doc.Links(lastDocCreated).length; + // eslint-disable-next-line no-use-before-define + return startedLink; + }, + ], + // docCreated: [() => this._props.childDocs().length > docCounter, () => { + // docCounter += 1 + // lastDocCreated = this._props.childDocs()[this.props.childDocs().length - 1] + // // eslint-disable-next-line no-use-before-define + // return this.tutorialStates.makePresentation}] + }, + 'dash-create-link-board.gif' + ); - let docX: FieldResult<FieldType>; - let docY: FieldResult<FieldType>; + this._tutorialStates.presentDocs = InfoState( + "Select a document then click the 'pin' button in the top left to create your presentation.", + { + docPinned: [ + () => DocListCast(Doc.ActivePresentation?.data).length > presentationCounter, + () => { + presentationCounter++; + // eslint-disable-next-line no-use-before-define + return pinnedDoc; + }, + ], + }, + 'pin-explanation.gif' + ); - const docNewX = () => firstDoc()?.x; - const docNewY = () => firstDoc()?.y; + this._tutorialStates.nestedCollections = InfoState( + "Want to learn how to create a nested collection? Click the : button and add a 'collection' doc", + { + docCreated: [ + () => this._props.childDocs().length > docCounter, + () => { + docCounter += 1; + lastDocCreated = this._props.childDocs()[this.props.childDocs().length - 1]; + // eslint-disable-next-line no-use-before-define + return marqueeSelection; + }, + ], + }, + 'dash-nested-collection.gif' + ); - const linkStart = () => DocumentLinksButton.StartLink; - const linkUnstart = () => !DocumentLinksButton.StartLink; + this._tutorialStates.makePresentation = InfoState('Add a new document to create a presentation!', { + docCreated: [ + () => this._props.childDocs().length > docCounter, + () => { + docCounter += 1; + lastDocCreated = this._props.childDocs()[this.props.childDocs().length - 1]; + return this._tutorialStates.presentDocs; + }, + ], + }); - const numDocLinks = () => Doc.Links(firstDoc())?.length; - const linkMenuOpen = () => DocButtonState.Instance.LinkEditorDocView; + const skipToLinksButton: InfoButton = { + title: 'Links Tutorial', + toolTip: 'Skip', + btnType: ButtonType.ClickButton, + scripts: { + onClick: 'this.skipToState(this.tutorialStates.multipleDocs)', + }, + targetState: this._tutorialStates.multipleDocs, + }; + + const skipToPinsButton: InfoButton = { + title: 'Pins Tutorial', + toolTip: 'Skip', + btnType: ButtonType.ClickButton, + scripts: { + onClick: 'this.skipToState(this.tutorialStates.makePresentation)', + }, + targetState: this._tutorialStates.makePresentation, + }; - const activeTool = () => Doc.ActiveTool; + // const skipToPresentationButton: Button = { + // title: "Collections Tutorial", + // toolTip: "Skip", + // btnType: ButtonType.ClickButton, + // scripts: { + // onClick: "this.skipToState(this.tutorialStates.nestedCollections)" + // }, + // targetState: this.tutorialStates.nestedCollections + // }; - const pin = () => DocListCast(Doc.ActivePresentation?.data); + const ending = InfoState("If you have any more questions, feel free to ask Dash's AI Bot!"); - let trail: number; + // Traditional tutorial - const presentationMode = () => Doc.ActivePresentation?.presentation_status; + const completed = InfoState('Eager to learn more? Click the ? icon in the top right corner to read our full documentation.', { docRemoved: [() => this._props.childDocs().length === 1, () => this._tutorialStates.start] }, 'documentation.png'); - // set of states - const start = InfoState( - 'Click anywhere and begin typing to create your first text document.', - { - docCreated: [() => numDocs(), () => { - docX = firstDoc()?.x; - docY = firstDoc()?.y; - // eslint-disable-next-line no-use-before-define - return oneDoc; - }], - } - ); // prettier-ignore - - const oneDoc = InfoState( - 'Hello world! You can drag and drop to move your document around.', + const penMode = InfoState("You're in pen mode! Click and drag to draw your first masterpiece, then click the Ink button once you're done.", { + activePen: [() => Doc.ActiveTool !== InkTool.Ink, () => completed], + }); + + const briefArtisticFeature = InfoState("Finally, want to explore the art feature of Dash? Click the 'Ink' button on the hotbar then click the pen button.", { + penModeActivated: [() => Doc.ActiveTool === InkTool.Ink, () => penMode], + }); + + const activatePresentation = InfoState('Lastly, click the linked node and start the presentation!', { + presentation: [() => Doc.ActivePresentation?.presentation_status === 'auto', () => briefArtisticFeature], + }); + + const deletePresentation = InfoState( + "Cool! Click 'setOnClick to follow primary link' for your non-presentation doc and try deleting the presentation.", { - // docCreated: [() => numDocs() > 1, () => multipleDocs], - docDeleted: [() => numDocs() < 1, () => start], - docMoved: [() => (docX && docX !== docNewX()) || (docY && docY !== docNewY()), () => { - docX = firstDoc()?.x; - docY = firstDoc()?.y; - // eslint-disable-next-line no-use-before-define - return movedDoc; - }], - } - ); // prettier-ignore - - const movedDoc = InfoState( - 'Great moves. Try creating a second document. You can see the list of supported document types by typing a colon (":")', + docRemoved: [ + () => this._props.childDocs().length < docCounter, + () => { + docCounter -= 1; + return activatePresentation; + }, + ], + }, + 'onclick-node.gif' + ); + + const trailedPresentation = InfoState( + 'Try linking your presentation to the last doc you created (now highlighted).', { - // eslint-disable-next-line no-use-before-define - docCreated: [() => numDocs() === 2, () => multipleDocs], - docDeleted: [() => numDocs() < 1, () => start], + linkAdd: [ + () => Doc.Links(lastDocCreated)?.length > linkCounter, + () => { + linkCounter += 1; + return deletePresentation; + }, + ], + docAdded: [ + () => this._props.childDocs().length > docCounter, + () => { + docCounter += 1; + // Last doc that is not the presentation + lastDocCreated = this._props.childDocs()[this.props.childDocs().length - 2]; + linkCounter = Doc.Links(lastDocCreated)?.length; + return deletePresentation; + }, + ], }, - 'dash-colon-menu.gif', - () => TopBar.Instance.FlipDocumentationIcon() - ); // prettier-ignore + 'link-presentation.gif' + ); - const multipleDocs = InfoState( - 'Let\'s create a new link. Click the link icon on one of your documents.', + const pinnedPresentation = InfoState( + 'Want to see something cool? Zoom out, click the trail button on the presentation, and drag it inside the canvas.', { - // eslint-disable-next-line no-use-before-define - linkStarted: [() => linkStart(), () => startedLink], - docRemoved: [() => numDocs() < 2, () => oneDoc], + docAdded: [ + () => this._props.childDocs().length > docCounter, + () => { + docCounter += 1; + // Last doc that is not the presentation + lastDocCreated = this._props.childDocs()[this.props.childDocs().length - 2]; + Doc.HighlightDoc(lastDocCreated); + linkCounter = Doc.Links(lastDocCreated)?.length; + return trailedPresentation; + }, + ], }, - 'dash-create-link-board.gif' - ); // prettier-ignore + 'dash-trail-explanation.gif' + ); - const startedLink = InfoState( - 'Now click the highlighted link icon on your other document.', + const pinnedDoc2 = InfoState('You pinned another doc. Press autoplay to the right to show your presentation!', { + autoPresentation: [() => Doc.ActivePresentation?.presentation_status === 'auto', () => pinnedPresentation], + }); + + const pinnedDoc = InfoState('You just pinned your doc. Pin another doc to add to the presentation!', { + addedDoc: [ + () => this._props.childDocs().length > docCounter, + () => { + docCounter += 1; + lastDocCreated = this._props.childDocs()[this.props.childDocs().length - 1]; + return pinnedDoc; + }, + ], + docPinned: [ + () => DocListCast(Doc.ActivePresentation?.data).length > presentationCounter, + () => { + presentationCounter++; + return pinnedDoc2; + }, + ], + }); + + const editLink = InfoState( + "Want to make your link visible? Click 'show link'.", { - linkUnstart: [() => linkUnstart(), () => multipleDocs], - // eslint-disable-next-line no-use-before-define - linkCreated: [() => numDocLinks(), () => madeLink], - docRemoved: [() => numDocs() < 2, () => oneDoc], + docCreated: [ + () => this._props.childDocs().length > docCounter, + () => { + docCounter += 1; + lastDocCreated = this._props.childDocs()[this.props.childDocs().length - 1]; + return this._tutorialStates.makePresentation; + }, + ], }, - 'dash-create-link-board.gif' - ); // prettier-ignore + 'show-link.gif' + ); const madeLink = InfoState( 'You made your first link! You can view your links by selecting the blue dot.', { - linkCreated: [() => !numDocLinks(), () => multipleDocs], - linkViewed: [() => linkMenuOpen(), () => { - alert(numDocLinks() + " cheer for " + numDocLinks() + " link!"); - // eslint-disable-next-line no-use-before-define - return viewedLink; - }], + linkViewed: [ + () => DocButtonState.Instance.LinkEditorDocView, + () => { + docCounter = this._props.childDocs().length; + return editLink; + }, + ], }, 'dash-following-link.gif' - ); // prettier-ignore + ); - const viewedLink = InfoState( - 'Great work. You are now ready to create your own hypermedia world. Click the ? icon in the top right corner to learn more.', + const startedLink = InfoState( + 'Now click the highlighted link icon on your other document.', { - linkDeleted: [() => !numDocLinks(), () => multipleDocs], - docRemoved: [() => numDocs() < 2, () => oneDoc], - docCreated: [() => numDocs() === 3, () => { - trail = pin().length; - // eslint-disable-next-line no-use-before-define - return presentDocs; - }], - // eslint-disable-next-line no-use-before-define - activePen: [() => activeTool() === InkTool.Ink, () => penMode], + linkAdd: [ + () => Doc.Links(lastDocCreated)?.length > linkCounter, + () => { + linkCounter += 1; + return madeLink; + }, + ], }, - 'documentation.png', - () => TopBar.Instance.FlipDocumentationIcon() - ); // prettier-ignore + 'dash-create-link-board.gif' + ); - const presentDocs = InfoState( - 'Another document! You could make a presentation. Click the pin icon in the top left corner.', + this._tutorialStates.movedDoc = InfoState( + "Great moves! Try creating a second document.", { - docPinned: [ - () => pin().length > trail, + docCreated: [ + () => this._props.childDocs().length > docCounter, () => { - trail = pin().length; - // eslint-disable-next-line no-use-before-define - return pinnedDoc1; + docCounter += 1 + lastDocCreated = this._props.childDocs()[this.props.childDocs().length - 1] + return this._tutorialStates.multipleDocs + } + ], + }, + 'dash-colon-menu.gif'); // prettier-ignore + + this._tutorialStates.start = InfoState( + "Welcome to Dash! Click anywhere and begin typing ':' to create your first document.", + { + docCreated: [ + () => this._props.childDocs().length > docCounter, + () => { + docCounter += 1; + lastDocCreated = this._props.childDocs()[this.props.childDocs().length - 1]; + return this._tutorialStates.movedDoc; }, ], - docRemoved: [() => numDocs() < 3, () => viewedLink], }, - '/assets/dash-pin-with-view.gif' + undefined, + [skipToLinksButton, skipToPinsButton] ); - const penMode = InfoState('You\'re in pen mode. Click and drag to draw your first masterpiece.', { - // activePen: [() => activeTool() === InkTool.Eraser, () => eraserMode], - activePen: [() => activeTool() !== InkTool.Ink, () => viewedLink], - }); // prettier-ignore - - // const eraserMode = InfoState('You\'re in eraser mode. Say goodbye to your first masterpiece.', { - // docsRemoved: [() => numDocs() == 3, () => demos], - // }); // prettier-ignore + // Information on created nested collections + const createdMarquee = InfoState( + 'Next, right click and drag a square to create the collection', + { + marqueeMade: [ + () => this._props.childDocs().length < docCounter, + () => { + docCounter -= 1; + return ending; + }, + ], + }, + 'dash-create-collection-marquee.gif' + ); - const pinnedDoc1 = InfoState('You just pinned your doc.', { - docPinned: [ - () => pin().length > trail, + const marqueeSelection = InfoState('Want an easier way to make a collection of docs? First add two docs you want to make a collection of', { + marqueeMade: [ + () => this._props.childDocs().length > docCounter, () => { - trail = pin().length; - // eslint-disable-next-line no-use-before-define - return pinnedDoc2; + docCounter += 1; + lastDocCreated = this._props.childDocs()[this.props.childDocs().length - 1]; + return createdMarquee; }, ], - // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode], - // manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode], - // eslint-disable-next-line no-use-before-define - autoPresentation: [() => presentationMode() === 'auto', () => autoPresentationMode], - docRemoved: [() => numDocs() < 3, () => viewedLink], }); - const pinnedDoc2 = InfoState(`You pinned another doc.`, { - docPinned: [ - () => pin().length > trail, - () => { - trail = pin().length; - // eslint-disable-next-line no-use-before-define - return pinnedDoc3; - }, - ], - // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode], - // manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode], - // eslint-disable-next-line no-use-before-define - autoPresentation: [() => presentationMode() === 'auto', () => autoPresentationMode], - docRemoved: [() => numDocs() < 3, () => viewedLink], - }); + // Explanation of importing - const pinnedDoc3 = InfoState(`You pinned yet another doc.`, { - docPinned: [ - () => pin().length > trail, - () => { - trail = pin().length; - return pinnedDoc2; - }, - ], - // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode], - // manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode], - // eslint-disable-next-line no-use-before-define - autoPresentation: [() => presentationMode() === 'auto', () => autoPresentationMode], - docRemoved: [() => numDocs() < 3, () => viewedLink], - }); + const easierImport = InfoState('Or, for easier access, you can drag any of the accepted file types from your computer or a webpage and drop it into your dashboard. This includes images, videos, audio, pdfs, and more!', {}, 'dash-', [ + this.createNextButton(ending), + ]); - // const openedTrail = InfoState('This is your trails tab.', { - // trailView: [() => presentationMode() === 'edit', () => editPresentationMode], - // }); - - // const editPresentationMode = InfoState('You are editing your presentation.', { - // manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode], - // autoPresentation: [() => presentationMode() === 'auto', () => autoPresentationMode], - // docRemoved: [() => numDocs() < 3, () => demos], - // docCreated: [() => numDocs() == 4, () => completed], - // }); - - const manualPresentationMode = InfoState("You're in manual presentation mode.", { - // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode], - // eslint-disable-next-line no-use-before-define - autoPresentation: [() => presentationMode() === 'auto', () => autoPresentationMode], - docRemoved: [() => numDocs() < 3, () => viewedLink], - // eslint-disable-next-line no-use-before-define - docCreated: [() => numDocs() === 4, () => completed], - }); + this._tutorialStates.importFile = InfoState('Want to learn how to import a file? Import using the import menu on the left hand side', {}, 'dash-import.gif', [this.createNextButton(easierImport)]); - const autoPresentationMode = InfoState("You're in auto presentation mode.", { - // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode], - manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode], - docRemoved: [() => numDocs() < 3, () => viewedLink], - // eslint-disable-next-line no-use-before-define - docCreated: [() => numDocs() === 4, () => completed], - }); + // Editing documents + + // Accessed by right-clicking anywhere on the target document or selecting the three bars menu at the bottom of the document chrome - const completed = InfoState( - 'Eager to learn more? Click the ? icon in the top right corner to read our full documentation.', - { docRemoved: [() => numDocs() === 1, () => oneDoc] }, - 'documentation.png', - () => TopBar.Instance.FlipDocumentationIcon() - ); // prettier-ignore + const extraContentsOfDoc = InfoState('Lastly, all documents also have a context-sensitive toolbar. The toolbar contents vary depending on the document type.', {}, 'context-toolbar.png', [this.createNextButton(ending)]); - return start; + const contentsofDoc = InfoState('You can access the context of a doc through right-clicking anywhere on the target document or selecting the three bars menu at the bottom of the document chrome', {}, 'dash-context-menu.gif', [ + this.createNextButton(extraContentsOfDoc), + ]); + + const propertiesofDoc = InfoState('You can also access the properties of a doc through the double arrows in the top right or the single arrow on the right edge of the screen', {}, 'dash-properties-pane.gif', [ + this.createNextButton(contentsofDoc), + ]); + + this._tutorialStates.editingDocuments = InfoState('Want to learn how to edit a document? Either left or right click the document', {}, 'document-chrome.png', [this.createNextButton(propertiesofDoc)]); + return this._tutorialStates.start; }; render() { - return !this.currState ? null : <CollectionFreeFormInfoState next={this.setCurrState} close={this._props.close} infoState={this.currState} />; + return !this.currState ? null : ( + <CollectionFreeFormInfoState + next={this.skipToState} // This ensures skipToState is passed correctly + close={this._props.close} + infoState={this.currState} + /> + ); } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 6e9e503f4..a447a6ae4 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -93,6 +93,8 @@ 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 @@ -1754,11 +1756,49 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection static SetInfoUICreator(func: (doc: Doc, layout: Doc, childDocs: () => Doc[], close: () => void) => JSX.Element) { CollectionFreeFormView._infoUI = func; } - infoUI = () => + /** + * 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; + } + } + + 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; + + }; componentDidMount() { this._props.setContentViewBox?.(this); super.componentDidMount?.(); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 78bacdcac..fb2346bd1 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -49,7 +49,6 @@ import { gptImageLabel } from '../../apis/gpt/GPT'; const DefaultPath = '/assets/unknown-file-icon-hi.png'; export class ImageEditorData { - // eslint-disable-next-line no-use-before-define private static _instance: ImageEditorData; private static get imageData() { return (ImageEditorData._instance ?? new ImageEditorData()).imageData; } // prettier-ignore @observable imageData: { rootDoc: Doc | undefined; open: boolean; source: string; addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean> } = observable({ rootDoc: undefined, open: false, source: '', addDoc: undefined }); diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts index d1248d098..3acdc6aa8 100644 --- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts +++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts @@ -7,17 +7,14 @@ import { AnswerParser } from '../response_parsers/AnswerParser'; import { StreamedAnswerParser } from '../response_parsers/StreamedAnswerParser'; import { BaseTool } from '../tools/BaseTool'; import { CalculateTool } from '../tools/CalculateTool'; -//import { CreateAnyDocumentTool } from '../tools/CreateAnyDocTool'; import { DataAnalysisTool } from '../tools/DataAnalysisTool'; import { DocumentMetadataTool } from '../tools/DocumentMetadataTool'; -import { ImageCreationTool } from '../tools/ImageCreationTool'; import { NoTool } from '../tools/NoTool'; import { SearchTool } from '../tools/SearchTool'; import { Parameter, ParametersType, TypeMap } from '../types/tool_types'; import { AgentMessage, ASSISTANT_ROLE, AssistantMessage, Observation, PROCESSING_TYPE, ProcessingInfo, TEXT_TYPE } from '../types/types'; 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'; @@ -32,7 +29,7 @@ import { FileNamesTool } from '../tools/FileNamesTool'; import { CreateNewTool } from '../tools/CreateNewTool'; import { SortDocsTool} from '../tools/SortDocsTool'; import { TagDocsTool } from '../tools/TagDocsTool'; -//import { CreateTextDocTool } from '../tools/CreateTextDocumentTool'; +import { GPTTutorialTool } from '../tools/TutorialTool'; dotenv.config(); @@ -55,6 +52,7 @@ export class Agent { private streamedAnswerParser: StreamedAnswerParser = new StreamedAnswerParser(); private tools: Record<string, BaseTool<ReadonlyArray<Parameter>>>; private _docManager: AgentDocumentManager; + private is_dash_doc_assistant: boolean; // 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 @@ -79,7 +77,8 @@ export class Agent { csvData: () => { filename: string; id: string; text: string }[], createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void, createCSVInDash: (url: string, title: string, id: string, data: string) => void, - docManager: AgentDocumentManager + docManager: AgentDocumentManager, + isDashDocAssistant: boolean ) { // Initialize OpenAI client with API key from environment this.client = new OpenAI({ apiKey: process.env.OPENAI_KEY, dangerouslyAllowBrowser: true }); @@ -87,6 +86,7 @@ export class Agent { this._history = history; this._csvData = csvData; this._docManager = docManager; + this.is_dash_doc_assistant = isDashDocAssistant; // Initialize dynamic tool registry this.dynamicToolRegistry = new Map(); @@ -105,6 +105,7 @@ export class Agent { 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), tagDocs: new TagDocsTool(this._docManager), }; @@ -146,7 +147,7 @@ export class Agent { const instance: BaseTool<ReadonlyArray<Parameter>> = new ToolClass(); - // Prefer the tool’s self-declared name (matches <action> tag) + // Prefer the tool's self-declared name (matches <action> tag) const key = (instance.name || '').trim() || legacyKey; // Check for duplicates @@ -301,7 +302,7 @@ export class Agent { ignoreAttributes: false, attributeNamePrefix: '@_', textNodeName: '_text', - isArray: name => ['query', 'url'].indexOf(name) !== -1, + isArray: name => name === 'url', processEntities: false, // Disable processing of entities stopNodes: ['*.entity'], // Do not process any entities }); @@ -763,7 +764,7 @@ export class Agent { const docSummaries = () => JSON.stringify(this._docManager.listDocs); const chatHistory = this._history(); - return getReactPrompt(allTools, docSummaries, chatHistory); + return getReactPrompt(allTools, docSummaries, chatHistory, this.is_dash_doc_assistant); } /** diff --git a/src/client/views/nodes/chatbot/agentsystem/prompts.ts b/src/client/views/nodes/chatbot/agentsystem/prompts.ts index fcb4ab450..b7678bd08 100644 --- a/src/client/views/nodes/chatbot/agentsystem/prompts.ts +++ b/src/client/views/nodes/chatbot/agentsystem/prompts.ts @@ -10,7 +10,7 @@ import { BaseTool } from '../tools/BaseTool'; import { Parameter } from '../types/tool_types'; -export function getReactPrompt(tools: BaseTool<ReadonlyArray<Parameter>>[], summaries: () => string, chatHistory: string): string { +export function getReactPrompt(tools: BaseTool<ReadonlyArray<Parameter>>[], summaries: () => string, chatHistory: string, isDashDocAssistant?: boolean): string { const toolDescriptions = tools .map( tool => ` @@ -21,11 +21,21 @@ export function getReactPrompt(tools: BaseTool<ReadonlyArray<Parameter>>[], summ ) .join('\n'); + const dashDocContext = isDashDocAssistant + ? ` + <dash_doc_assistant_context> + <point>You are acting as a help assistant for a software application called Dash.</point> + <point>All user queries, unless otherwise specified, should be interpreted as questions about how to use Dash or about Dash's functionality.</point> + <point>You should prioritize using the 'generateTutorialNode' tool to answer user questions about Dash.</point> + </dash_doc_assistant_context> + ` + : ''; + return `<system_message> <task> You are an advanced AI assistant equipped with tools to answer user queries efficiently. You operate in a loop that is RIGIDLY structured and requires the use of specific tags and formats for your responses. Your goal is to provide accurate and well-structured answers to user queries. Below are the guidelines and information you can use to structure your approach to accomplishing this task. </task> - + ${dashDocContext} <critical_points> <point>**STRUCTURE**: Always use the correct stage tags (e.g., <stage number="2" role="assistant">) for every response. Use only even-numbered assisntant stages for your responses.</point> <point>**STOP after every stage and wait for input. Do not combine multiple stages in one response.**</point> @@ -189,7 +199,7 @@ export function getReactPrompt(tools: BaseTool<ReadonlyArray<Parameter>>[], summ <action_input> <action_input_description>Getting information from the relevant websites about Qatar's tourism impact during the World Cup.</action_input_description> <inputs> - <urls>[***URLS to search elided, but they will be comma seperated double quoted strings"]</urls> + <chunk_ids>[***CHUNK IDS to search elided, but they will be comma separated double quoted strings"]</chunk_ids> </inputs> </action_input> </stage> diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss index 4a916e86c..0bacc70c2 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss @@ -183,7 +183,6 @@ $font-size-xlarge: 18px; box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.05); position: relative; align-items: center; - gap: 12px; z-index: 5; transition: padding 0.2s ease; diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx index 6e6ef6212..636b77b38 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx @@ -7,11 +7,12 @@ * with support for follow-up questions and citation management. */ -import dotenv from 'dotenv'; +import { Button, Size, Type } from '@dash/components'; import { ObservableSet, action, computed, makeObservable, observable, observe, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import OpenAI, { ClientOptions } from 'openai'; import * as React from 'react'; +import { AiOutlineSend } from 'react-icons/ai'; import { v4 as uuidv4 } from 'uuid'; import { ClientUtils, OmitKeys } from '../../../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../../../fields/Doc'; @@ -19,34 +20,33 @@ import { DocData, DocViews } from '../../../../../fields/DocSymbols'; import { Id } from '../../../../../fields/FieldSymbols'; import { RichTextField } from '../../../../../fields/RichTextField'; import { ScriptField } from '../../../../../fields/ScriptField'; -import { CsvCast, DocCast, NumCast, PDFCast, RTFCast, StrCast, VideoCast, AudioCast } from '../../../../../fields/Types'; +import { AudioCast, CsvCast, DocCast, NumCast, PDFCast, RTFCast, StrCast, VideoCast } from '../../../../../fields/Types'; +import { Upload } from '../../../../../server/SharedMediaTypes'; +import { DocServer } from '../../../../DocServer'; import { DocUtils } from '../../../../documents/DocUtils'; import { CollectionViewType, DocumentType } from '../../../../documents/DocumentTypes'; import { Docs, DocumentOptions } from '../../../../documents/Documents'; -import { DocServer } from '../../../../DocServer'; import { DocumentManager } from '../../../../util/DocumentManager'; import { ImageUtils } from '../../../../util/Import & Export/ImageUtils'; import { LinkManager } from '../../../../util/LinkManager'; import { CompileError, CompileScript } from '../../../../util/Scripting'; +import { SnappingManager } from '../../../../util/SnappingManager'; import { DictationButton } from '../../../DictationButton'; import { ViewBoxAnnotatableComponent } from '../../../DocComponent'; import { AudioBox } from '../../AudioBox'; import { DocumentView, DocumentViewInternal } from '../../DocumentView'; import { FieldView, FieldViewProps } from '../../FieldView'; +import { OpenWhere } from '../../OpenWhere'; import { PDFBox } from '../../PDFBox'; import { ScriptingBox } from '../../ScriptingBox'; import { VideoBox } from '../../VideoBox'; import { Agent } from '../agentsystem/Agent'; import { supportedDocTypes } from '../types/tool_types'; import { ASSISTANT_ROLE, AssistantMessage, CHUNK_TYPE, Citation, ProcessingInfo, SimplifiedChunk, TEXT_TYPE } from '../types/types'; +import { AgentDocumentManager } from '../utils/AgentDocumentManager'; import { Vectorstore } from '../vectorstore/Vectorstore'; import './ChatBox.scss'; import MessageComponentBox from './MessageComponent'; -import { OpenWhere } from '../../OpenWhere'; -import { Upload } from '../../../../../server/SharedMediaTypes'; -import { AgentDocumentManager } from '../utils/AgentDocumentManager'; - -dotenv.config(); export type parsedDocData = { doc_type: string; @@ -57,6 +57,7 @@ export type parsedDocData = { data_useCors?: boolean; }; export type parsedDoc = DocumentOptions & parsedDocData; + /** * ChatBox is the main class responsible for managing the interaction between the user and the assistant, * handling documents, and integrating with OpenAI for tasks such as document analysis, chat functionality, @@ -123,7 +124,15 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { 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); + this.agent = new Agent( + this.vectorstore, + this.retrieveFormattedHistory.bind(this), + this.retrieveCSVData.bind(this), + this.createImageInDash.bind(this), + this.createCSVInDash.bind(this), + this.docManager, + this.dataDoc.is_dash_doc_assistant === 'true' + ); // Set up the tool created callback this.agent.setToolCreatedCallback(this.handleToolCreated); @@ -373,7 +382,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; } }); - this.scrollToBottom(); }; const onAnswerUpdate = (answerUpdate: string) => { @@ -381,41 +389,33 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { if (this._current_message) { this._current_message = { ...this._current_message, - content: [{ text: answerUpdate, type: TEXT_TYPE.NORMAL, index: 0, citation_ids: [] }], + content: [{ index: 0, type: TEXT_TYPE.NORMAL, text: answerUpdate, citation_ids: null }], }; } }); }; - // Send the user's question to the assistant and get the final message - const finalMessage = await this.agent.askAgent(trimmedText, onProcessingUpdate, onAnswerUpdate); + // Get the response from the agent + let userQuery = trimmedText; + if (this.dataDoc.is_dash_doc_assistant) { + userQuery = `The user is asking a question about Dash functionality. Their question is: "${trimmedText}". You should use the generateTutorialNode tool to answer this question.`; + } + const response = await this.agent.askAgent(userQuery, onProcessingUpdate, onAnswerUpdate); - // Update the history with the final assistant message + // Push the final message to history runInAction(() => { - if (this._current_message) { - this._history.push({ ...finalMessage }); - this._current_message = undefined; - this.dataDoc.data = JSON.stringify(this._history); - } + this._history.push(response); + this._isLoading = false; + this._current_message = undefined; }); - } catch (err) { - console.error('Error:', err); - // Handle error in processing - runInAction(() => - this._history.push({ - role: ASSISTANT_ROLE.ASSISTANT, - content: [{ index: 0, type: TEXT_TYPE.ERROR, text: `Sorry, I encountered an error while processing your request: ${err} `, citation_ids: null }], - processing_info: [], - }) - ); - } finally { + } catch (error) { + console.error('Error in askGPT:', error); runInAction(() => { this._isLoading = false; + this._current_message = undefined; }); - this.scrollToBottom(); } } - this.scrollToBottom(); }; /** @@ -488,7 +488,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const data = (doc as parsedDocData).data; const ndoc = (() => { switch (doc.doc_type) { - default: + default: case supportedDocTypes.note: return Docs.Create.TextDocument(data as string, options); case supportedDocTypes.comparison: return this.createComparison(JSON.parse(data as string) as parsedDoc[], options); case supportedDocTypes.flashcard: return this.createFlashcard(JSON.parse(data as string) as parsedDoc[], options); @@ -499,23 +499,23 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { case supportedDocTypes.web: { // Create web document with enhanced safety options const webOptions = { - ...options, + ...options, data_useCors: true }; - + // If iframe_sandbox was passed from AgentDocumentManager, add it to the options if ('_iframe_sandbox' in options) { webOptions._iframe_sandbox = options._iframe_sandbox; } - + return Docs.Create.WebDocument(data as string, webOptions); } case supportedDocTypes.dataviz: return Docs.Create.DataVizDocument('/users/rz/Downloads/addresses.csv', options); case supportedDocTypes.pdf: return Docs.Create.PdfDocument(data as string, options); case supportedDocTypes.video: return Docs.Create.VideoDocument(data as string, options); case supportedDocTypes.diagram: return Docs.Create.DiagramDocument(undefined, { text: data as unknown as RichTextField, ...options}); // text: can take a string or RichTextField but it's typed for RichTextField. - - // case supportedDocumentTypes.dataviz: + + // case supportedDocumentTypes.dataviz: // { // const { fileUrl, id } = await Networking.PostToServer('/createCSV', { // filename: (options.title as string).replace(/\s+/g, '') + '.csv', @@ -540,7 +540,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const arr = this.createCollectionWithChildren(JSON.parse(data as string) as parsedDoc[], true).filter(d=>d).map(d => d!); const collOpts = { _width:300, _height: 300, _layout_fitWidth: true, _freeform_backgroundGrid: true, ...options, }; return (() => { - switch (options.type_collection) { + switch (options.type_collection) { case CollectionViewType.Tree: return Docs.Create.TreeDocument(arr, collOpts); case CollectionViewType.Stacking: return Docs.Create.StackingDocument(arr, collOpts); case CollectionViewType.Masonry: return Docs.Create.MasonryDocument(arr, collOpts); @@ -549,9 +549,9 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { case CollectionViewType.Carousel3D: return Docs.Create.Carousel3DDocument(arr, collOpts); case CollectionViewType.Multicolumn: return Docs.Create.CarouselDocument(arr, collOpts); default: return Docs.Create.FreeformDocument(arr, collOpts); - } - })(); - } + } + })(); + } // case supportedDocumentTypes.map: return Docs.Create.MapDocument([], options); // case supportedDocumentTypes.button: return Docs.Create.ButtonDocument(options); // case supportedDocumentTypes.trail: return Docs.Create.PresDocument(options); @@ -559,8 +559,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { })(); if (ndoc) { - ndoc.x = NumCast((options.x as number) ?? 0) + (insideCol ? 0 : NumCast(this.layoutDoc.x) + NumCast(this.layoutDoc.width)) + 100; - ndoc.y = NumCast(options.y as number) + (insideCol ? 0 : NumCast(this.layoutDoc.y)); + ndoc.x = ((options.x as number) ?? 0) + (insideCol ? 0 : NumCast(this.layoutDoc.x) + NumCast(this.layoutDoc.width)) + 100; + ndoc.y = ((options.y as number) ?? 0) + (insideCol ? 0 : NumCast(this.layoutDoc.y)); } return ndoc; }; @@ -714,7 +714,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 @@ -723,7 +723,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; @@ -832,7 +832,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { existingDoc._width = x2 - x1; existingDoc._height = y2 - y1; } - const highlightDoc = existingDoc ?? this.createImageCitationHighlight(x1, y1, x2, y2, citation, annotationKey, doc); + // const highlightDoc = + existingDoc ?? this.createImageCitationHighlight(x1, y1, x2, y2, citation, annotationKey, doc); //doc.layout_scroll = y1; doc._layout_curPage = foundChunk.startPage + 1; @@ -1068,7 +1069,9 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { { index: 0, type: TEXT_TYPE.NORMAL, - text: `Hey, ${this.userName()}! Welcome to Your Friendly Assistant. Link a document or ask questions to get started.`, + text: this.dataDoc.is_dash_doc_assistant + ? 'Welcome to your help assistant for Dash. Ask any Dash-related questions to get started.' + : `Hey, ${this.userName()}! Welcome to Your Friendly Assistant. Link a document or ask questions to get started.`, citation_ids: null, }, ], @@ -1409,7 +1412,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { </div> )} <div className="chat-header"> - <h2>{this.userName()}'s AI Assistant</h2> + <h2>{StrCast(this.dataDoc.title) || `${this.userName()}'s AI Assistant`}</h2> <div className="font-size-control" onClick={this.toggleFontSizeModal}> {this.renderFontSizeIcon()} </div> @@ -1456,16 +1459,16 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { 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._dictation?.stopDictation()} + type={Type.PRIM} + tooltip="Send to AI" + color={SnappingManager.userVariantColor} + inactive={this._isLoading || !this._inputValue.trim()} + icon={<AiOutlineSend />} + size={Size.LARGE} + /> <DictationButton ref={this.setDictationRef} setInput={this.setChatInput} inputRef={this._textInputRef} /> </form> {/* Popup for citation */} diff --git a/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts b/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts index a55f901e1..da4a4ae29 100644 --- a/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts +++ b/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts @@ -1,16 +1,7 @@ -import { Doc, FieldType } from '../../../../../fields/Doc'; -import { DocData } from '../../../../../fields/DocSymbols'; +import { Parameter, ParametersType, supportedDocTypes, ToolInfo } from '../types/tool_types'; import { Observation } from '../types/types'; -import { ParametersType, ToolInfo, Parameter } from '../types/tool_types'; -import { BaseTool } from './BaseTool'; -import { DocumentOptions } from '../../../../documents/Documents'; -import { CollectionFreeFormDocumentView } from '../../../nodes/CollectionFreeFormDocumentView'; -import { v4 as uuidv4 } from 'uuid'; -import { LinkManager } from '../../../../util/LinkManager'; -import { DocCast, StrCast } from '../../../../../fields/Types'; -import { supportedDocTypes } from '../types/tool_types'; -import { parsedDoc } from '../chatboxcomponents/ChatBox'; import { AgentDocumentManager } from '../utils/AgentDocumentManager'; +import { BaseTool } from './BaseTool'; // Define the parameters for the DocumentMetadataTool const parameterDefinitions: ReadonlyArray<Parameter> = [ @@ -598,7 +589,7 @@ export class DocumentMetadataTool extends BaseTool<DocumentMetadataToolParamsTyp message: string; fieldName?: string; originalFieldName?: string; - newValue?: any; + newValue?: string | number | boolean | object; warning?: string; }[] = []; diff --git a/src/client/views/nodes/chatbot/tools/TutorialTool.ts b/src/client/views/nodes/chatbot/tools/TutorialTool.ts new file mode 100644 index 000000000..1624f0439 --- /dev/null +++ b/src/client/views/nodes/chatbot/tools/TutorialTool.ts @@ -0,0 +1,212 @@ +import { BaseTool } from './BaseTool'; +import { Observation } from '../types/types'; +import { ParametersType, ToolInfo } from '../types/tool_types'; +import { schema } from '../../../../views/nodes/formattedText/schema_rts'; +import { v4 as uuidv4 } from 'uuid'; +import { gptTutorialAPICall } from '../../../../apis/gpt/TutorialGPT'; +import { parsedDoc } from '../chatboxcomponents/ChatBox'; +import { Id } from '../../../../../fields/FieldSymbols'; +import { Doc } from '../../../../../fields/Doc'; +import { RichTextField } from '../../../../../fields/RichTextField'; +import { DocumentViewInternal } from '../../DocumentView'; +import { Docs } from '../../../../documents/Documents'; +import { OpenWhere } from '../../OpenWhere'; +import { CollectionFreeFormView } from '../../../collections/collectionFreeForm/CollectionFreeFormView'; +import { AgentDocumentManager } from '../utils/AgentDocumentManager'; +import { Node as ProseMirrorNode } from 'prosemirror-model'; + +const generateTutorialNodeToolParams = [ + { + name: 'query', + type: 'string', + description: 'The user query that asks how to use the environment', + required: true, + }, +] as const; + +const generateTutorialNodeToolInfo: ToolInfo<typeof generateTutorialNodeToolParams> = { + name: 'generateTutorialNode', + description: "Generates a tutorial text node based on the user's query about Dash functionality. Use this when the user asks for help or tutorials on how to use Dash features.", + parameterRules: generateTutorialNodeToolParams, + citationRules: "No citation needed for this tool's output.", +}; + +interface FormattedDocument { + doc: ProseMirrorNode; + plainText: string; +} + +const applyFormatting = (markdownText: string): FormattedDocument => { + const lines = markdownText.split('\n'); + const nodes: ProseMirrorNode[] = []; + let plainText = ''; + let i = 0; + let currentListItems: ProseMirrorNode[] = []; + let currentParagraph: ProseMirrorNode[] = []; + let currentOrderedListItems: ProseMirrorNode[] = []; + let inOrderedList = false; + let inBulletList = false; + + const processBoldText = (text: string): ProseMirrorNode[] => { + const boldRegex = /\*\*(.*?)\*\*/g; + const parts: ProseMirrorNode[] = []; + let lastIndex = 0; + let match; + + while ((match = boldRegex.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push(schema.text(text.substring(lastIndex, match.index))); + } + parts.push(schema.text(match[1], [schema.marks.strong.create()])); + lastIndex = match.index + match[0].length; + } + if (lastIndex < text.length) { + parts.push(schema.text(text.substring(lastIndex))); + } + return parts.length > 0 ? parts : [schema.text(text)]; + }; + + const flushListItems = (): void => { + if (currentListItems.length > 0) { + nodes.push(schema.nodes.ordered_list.create({ mapStyle: 'bullet' }, currentListItems)); + nodes.push(schema.nodes.paragraph.create()); + currentListItems = []; + inBulletList = false; + } + if (currentOrderedListItems.length > 0) { + nodes.push(schema.nodes.ordered_list.create({ mapStyle: 'number' }, currentOrderedListItems)); + nodes.push(schema.nodes.paragraph.create()); + currentOrderedListItems = []; + inOrderedList = false; + } + }; + + const flushParagraph = (): void => { + if (currentParagraph.length > 0) { + nodes.push(schema.nodes.paragraph.create({}, currentParagraph)); + currentParagraph = []; + } + }; + + const processHeader = (line: string): boolean => { + const headerMatch = line.match(/^(#{1,6})\s+(.+)$/); + if (headerMatch) { + const level = Math.min(headerMatch[1].length, 6); // Cap at h6 + const textContent = headerMatch[2]; + flushParagraph(); + nodes.push(schema.nodes.heading.create({ level }, processBoldText(textContent))); + plainText += textContent + '\n'; + return true; + } + return false; + }; + + while (i < lines.length) { + const line = lines[i].trim(); + if (line) { + if (processHeader(line)) { + flushListItems(); + flushParagraph(); + } else if (line.startsWith('- ')) { + flushParagraph(); + if (!inBulletList) { + flushListItems(); + inBulletList = true; + } + const textContent = line.replace('- ', ''); + currentListItems.push(schema.nodes.list_item.create({}, schema.nodes.paragraph.create({}, processBoldText(textContent)))); + plainText += textContent + '\n'; + } else if (/^\d+\.\s+/.test(line)) { + flushParagraph(); + if (!inOrderedList) { + flushListItems(); + inOrderedList = true; + } + const textContent = line.replace(/^\d+\.\s+/, ''); + currentOrderedListItems.push(schema.nodes.list_item.create({}, schema.nodes.paragraph.create({}, processBoldText(textContent)))); + plainText += textContent + '\n'; + } else { + flushListItems(); + currentParagraph = currentParagraph.concat(processBoldText(line)); + plainText += line + '\n'; + } + } else { + flushListItems(); + flushParagraph(); + nodes.push(schema.nodes.paragraph.create()); + plainText += '\n'; + } + i++; + } + flushListItems(); + flushParagraph(); + + const doc = schema.nodes.doc.create({}, nodes); + return { doc, plainText: plainText.trim() }; +}; + +export class GPTTutorialTool extends BaseTool<typeof generateTutorialNodeToolParams> { + private _docManager: AgentDocumentManager; + + constructor(docManager: AgentDocumentManager) { + super(generateTutorialNodeToolInfo); + this._docManager = docManager; + } + + async execute(args: ParametersType<typeof generateTutorialNodeToolParams>): Promise<Observation[]> { + const chunkId = uuidv4(); + try { + const query = (args.query || '').trim(); + if (!query) { + return [{ type: 'text', text: `<chunk chunk_id="${chunkId}" chunk_type="error">Please provide a query.</chunk>` }]; + } + const markdown = await gptTutorialAPICall(query); + const { doc, plainText } = applyFormatting(markdown); + + // Build the ProseMirror‐in‐JSON + plain-text for RichTextField + const rtfData = { + doc: doc.toJSON ? doc.toJSON() : doc, + selection: { type: 'text', anchor: 0, head: 0 }, + storedMarks: [], + }; + const rtf = new RichTextField(JSON.stringify(rtfData), plainText); + + // Create and show the TextDocument directly: + const formattedDoc = Docs.Create.TextDocument(rtf, { + title: 'Tutorial Node', + _width: 600, + _layout_fitWidth: true, + _layout_autoHeight: true, + text_fontSize: '16px', + }); + DocumentViewInternal.addDocTabFunc(formattedDoc, OpenWhere.addRight); + + // If user asked about linking/pinning/presentation, also fire the in-app tutorial: + const q = query.toLowerCase(); + if (q.includes('link')) { + Doc.IsInfoUIDisabled = false; + CollectionFreeFormView.showTutorial('links'); + } else if (q.includes('presentation')) { + Doc.IsInfoUIDisabled = false; + CollectionFreeFormView.showTutorial('presentation'); + } else if (q.includes('pin')) { + Doc.IsInfoUIDisabled = false; + CollectionFreeFormView.showTutorial('pins'); + } + + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="tutorial_node_creation">Created tutorial node with ID ${formattedDoc[Id]}.</chunk>`, + }, + ]; + } catch (error) { + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error">Error generating tutorial node: ${error}</chunk>`, + }, + ]; + } + } +} diff --git a/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts b/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts index 3c8b49f33..dcb708450 100644 --- a/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts +++ b/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts @@ -1,7 +1,5 @@ import { action, computed, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import { v4 as uuidv4 } from 'uuid'; -import { Doc, StrListCast } from '../../../../../fields/Doc'; +import { Doc, FieldResult, StrListCast } from '../../../../../fields/Doc'; import { DocData } from '../../../../../fields/DocSymbols'; import { Id } from '../../../../../fields/FieldSymbols'; import { List } from '../../../../../fields/List'; @@ -30,7 +28,7 @@ export class AgentDocumentManager { @observable private documentsById: ObservableMap<string, AgentDocument>; private chatBox: ChatBox; private chatBoxDocument: Doc | null = null; - private fieldMetadata: Record<string, any> = {}; + private fieldMetadata: Record<string, any> = {}; // bcz: CHANGE any to a proper type! @observable private simplifiedChunks: ObservableMap<string, SimplifiedChunk>; /** @@ -103,6 +101,7 @@ export class AgentDocumentManager { for (const [fieldName, fieldInfo] of documentOptionsEntries) { // Extract field information const fieldData: Record<string, any> = { + // bcz: CHANGE any to a proper type! name: fieldName, withoutUnderscore: fieldName.startsWith('_') ? fieldName.substring(1) : fieldName, description: '', @@ -223,6 +222,7 @@ export class AgentDocumentManager { const dataDoc = agentDoc.dataDoc; const metadata: Record<string, any> = { + // bcz: CHANGE any to a proper type! id: layoutDoc[Id] || dataDoc[Id] || '', title: layoutDoc.title || '', type: layoutDoc.type || '', @@ -235,7 +235,7 @@ export class AgentDocumentManager { // Process all known field definitions Object.keys(this.fieldMetadata).forEach(fieldName => { - const fieldDef = this.fieldMetadata[fieldName]; + // const fieldDef = this.fieldMetadata[fieldName]; const strippedName = fieldName.startsWith('_') ? fieldName.substring(1) : fieldName; // Check if field exists on layout document @@ -307,7 +307,7 @@ export class AgentDocumentManager { * @param value The field value to format * @returns A JSON-friendly representation of the field value */ - private formatFieldValue(value: any): any { + private formatFieldValue(value: FieldResult | undefined) { if (value === undefined || value === null) { return null; } @@ -330,12 +330,12 @@ export class AgentDocumentManager { if (rtfObj.doc && rtfObj.doc.content) { // Recursively extract text from the content let plainText = ''; - const extractText = (node: any) => { + const extractText = (node: { text: string; content?: unknown[] }) => { if (node.text) { plainText += node.text; } if (node.content && Array.isArray(node.content)) { - node.content.forEach((child: any) => extractText(child)); + node.content.forEach(child => extractText(child as { text: string; content?: unknown[] })); } }; @@ -351,7 +351,7 @@ export class AgentDocumentManager { }; } } - } catch (e) { + } catch { // If parsing fails, just treat as a regular string } } @@ -366,7 +366,7 @@ export class AgentDocumentManager { try { // Try to convert to JSON string return JSON.stringify(value); - } catch (e) { + } catch { return '[Complex Object]'; } } @@ -381,26 +381,24 @@ export class AgentDocumentManager { * @param fieldValue The string value to convert * @returns The converted value with the appropriate type */ - private convertFieldValue(fieldName: string, fieldValue: any): any { + private convertFieldValue(fieldName: string, fieldValueIn: string | number | boolean): FieldResult | undefined { // If fieldValue is already a number or boolean, we don't need to convert it from string - if (typeof fieldValue === 'number' || typeof fieldValue === 'boolean') { - return fieldValue; + if (typeof fieldValueIn === 'number' || typeof fieldValueIn === 'boolean') { + return fieldValueIn; } // If fieldValue is a string "true" or "false", convert to boolean - if (typeof fieldValue === 'string') { - if (fieldValue.toLowerCase() === 'true') { + if (typeof fieldValueIn === 'string') { + if (fieldValueIn.toLowerCase() === 'true') { return true; } - if (fieldValue.toLowerCase() === 'false') { + if (fieldValueIn.toLowerCase() === 'false') { return false; } } - // If fieldValue is not a string (and not a number or boolean), convert it to string - if (typeof fieldValue !== 'string') { - fieldValue = String(fieldValue); - } + // coerce fieldvValue to a string + const fieldValue = typeof fieldValueIn !== 'string' ? String(fieldValueIn) : fieldValueIn; // Special handling for text field - convert to proper RichTextField format if (fieldName === 'text') { @@ -408,7 +406,7 @@ export class AgentDocumentManager { // Check if it's already a valid JSON RichTextField JSON.parse(fieldValue); return fieldValue; - } catch (e) { + } catch { // It's a plain text string, so convert it to RichTextField format const rtf = { doc: { @@ -462,21 +460,21 @@ export class AgentDocumentManager { // Try to convert to date (stored as number timestamp) try { return new Date(fieldValue).getTime(); - } catch (e) { + } catch { return fieldValue; } } else if (fieldType.includes('list') || fieldType.includes('array')) { // Try to parse as JSON array try { - return JSON.parse(fieldValue); - } catch (e) { + return JSON.parse(fieldValue) as FieldResult; // bcz: this needs to be typed properly. Dash fields can't accept a generic 'objext' + } catch { return fieldValue; } } else if (fieldType === 'json' || fieldType === 'object') { // Try to parse as JSON object try { - return JSON.parse(fieldValue); - } catch (e) { + return JSON.parse(fieldValue) as FieldResult; // bcz: this needs to be typed properly. Dash fields can't accept a generic 'objext' + } catch { return fieldValue; } } @@ -492,6 +490,7 @@ export class AgentDocumentManager { public getAllFieldMetadata() { // Start with our already populated fieldMetadata from the DocumentOptions class const result: Record<string, any> = { + // bcz: CHANGE any to a proper type! fieldCount: Object.keys(this.fieldMetadata).length, fields: {}, fieldsByType: { @@ -526,6 +525,7 @@ export class AgentDocumentManager { // Create structured field metadata const fieldData: Record<string, any> = { + // bcz: CHANGE any to a proper type! name: fieldName, displayName: strippedName, description: fieldInfo.description || '', @@ -618,12 +618,12 @@ export class AgentDocumentManager { message: string; fieldName?: string; originalFieldName?: string; - newValue?: any; + newValue?: string | number | boolean | object; warning?: string; } { // Normalize field name (handle with/without underscore) let normalizedFieldName = fieldName.startsWith('_') ? fieldName : fieldName; - const strippedFieldName = fieldName.startsWith('_') ? fieldName.substring(1) : fieldName; + // const strippedFieldName = fieldName.startsWith('_') ? fieldName.substring(1) : fieldName; // Handle common field name aliases (width → _width, height → _height) // Many document fields use '_' prefix for layout properties @@ -690,7 +690,7 @@ export class AgentDocumentManager { } // Set the field value on the target document - targetDoc[normalizedFieldName] = convertedValue; + targetDoc[normalizedFieldName] = convertedValue; // bcz: converteValue needs to be typed properly. Dash fields can't accept a generic 'objext' return { success: true, @@ -712,19 +712,19 @@ export class AgentDocumentManager { * @param documentId Optional ID of a specific document to get metadata for * @returns Document metadata or metadata for all documents */ - public getDocumentMetadata(documentId?: string): any { + public getDocumentMetadata(documentId?: string) { if (documentId) { console.log(`Returning document metadata for docID, ${documentId}:`, this.extractDocumentMetadata(documentId)); return this.extractDocumentMetadata(documentId); } else { // Get metadata for all documents - const documentsMetadata: Record<string, Record<string, any>> = {}; - for (const documentId of this.documentsById.keys()) { - const metadata = this.extractDocumentMetadata(documentId); + const documentsMetadata: Record<string, Record<string, any>> = {}; // bcz: CHANGE any to a proper type! + for (const docid of this.documentsById.keys()) { + const metadata = this.extractDocumentMetadata(docid); if (metadata) { - documentsMetadata[documentId] = metadata; + documentsMetadata[docid] = metadata; } else { - console.warn(`No metadata found for document with ID: ${documentId}`); + console.warn(`No metadata found for document with ID: ${docid}`); } } return { @@ -842,7 +842,7 @@ export class AgentDocumentManager { * @returns The ID of the created document */ - public async createDocInDash(docType: string, data: string, options?: any): Promise<string> { + public async createDocInDash(docType: string, data: string, options?: DocumentOptions): Promise<string> { // Validate doc_type if (!this.isValidDocType(docType)) { throw new Error(`Invalid document type: ${docType}`); @@ -1054,13 +1054,13 @@ export class AgentDocumentManager { endPage: chunk.metadata.end_page, location: chunk.metadata.location, } as SimplifiedChunk); - } else if (docType === 'csv') { + } else if (docType === 'csv' && 'row_start' in chunk.metadata && 'row_end' in chunk.metadata && 'col_start' in chunk.metadata && 'col_end' in chunk.metadata) { simplifiedChunks.push({ ...baseChunk, - rowStart: (chunk.metadata as any).row_start, - rowEnd: (chunk.metadata as any).row_end, - colStart: (chunk.metadata as any).col_start, - colEnd: (chunk.metadata as any).col_end, + rowStart: chunk.metadata.row_start, + rowEnd: chunk.metadata.row_end, + colStart: chunk.metadata.col_start, + colEnd: chunk.metadata.col_end, } as SimplifiedChunk); } else { // Default for other document types @@ -1077,7 +1077,7 @@ export class AgentDocumentManager { * @returns The simplified chunk if found, undefined otherwise */ @action - public getSimplifiedChunkById(chunkId: string): any | undefined { + public getSimplifiedChunkById(chunkId: string) { return { foundChunk: this.simplifiedChunks.get(chunkId), doc: this.getDocument(this.simplifiedChunks.get(chunkId)?.doc_id || chunkId), dataDoc: this.getDataDocument(this.simplifiedChunks.get(chunkId)?.doc_id || chunkId) }; } @@ -1098,7 +1098,7 @@ export class AgentDocumentManager { * @param doc The document containing original media segments * @returns Array of media segments or empty array if none exist */ - public getOriginalSegments(doc: Doc): any[] { + public getOriginalSegments(doc: Doc): { text: string; index: string; start: number }[] { if (!doc || !doc.original_segments) { return []; } diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index 9c37428ee..6e0d58932 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -1,4 +1,4 @@ -import { Button, IconButton, Toggle, ToggleType, Type } from '@dash/components'; +import { Button, IconButton, Size, Toggle, ToggleType, Type } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; @@ -9,7 +9,10 @@ import ReactLoading from 'react-loading'; import { TypeAnimation } from 'react-type-animation'; import { ClientUtils } from '../../../../ClientUtils'; import { Doc } from '../../../../fields/Doc'; +import { List } from '../../../../fields/List'; import { NumCast, StrCast } from '../../../../fields/Types'; +import { ImageField } from '../../../../fields/URLField'; +import { Upload } from '../../../../server/SharedMediaTypes'; import { Networking } from '../../../Network'; import { DataSeperator, DescriptionSeperator, DocSeperator, GPTCallType, GPTDocCommand, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT'; import { DocUtils } from '../../../documents/DocUtils'; @@ -21,17 +24,14 @@ import { DictationButton } from '../../DictationButton'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { TagItem } from '../../TagsView'; import { ChatSortField, docSortings } from '../../collections/CollectionSubView'; +import { ComparisonBox } from '../../nodes/ComparisonBox'; import { DocumentView, DocumentViewInternal } from '../../nodes/DocumentView'; +import { OpenWhere } from '../../nodes/OpenWhere'; +import { DrawingFillHandler } from '../../smartdraw/DrawingFillHandler'; +import { FireflyImageDimensions } from '../../smartdraw/FireflyConstants'; import { SmartDrawHandler } from '../../smartdraw/SmartDrawHandler'; import { AnchorMenu } from '../AnchorMenu'; import './GPTPopup.scss'; -import { FireflyImageDimensions } from '../../smartdraw/FireflyConstants'; -import { Upload } from '../../../../server/SharedMediaTypes'; -import { OpenWhere } from '../../nodes/OpenWhere'; -import { DrawingFillHandler } from '../../smartdraw/DrawingFillHandler'; -import { ImageField } from '../../../../fields/URLField'; -import { List } from '../../../../fields/List'; -import { ComparisonBox } from '../../nodes/ComparisonBox'; export enum GPTPopupMode { SUMMARY, // summary of seleted document text @@ -45,7 +45,6 @@ export enum GPTPopupMode { @observer export class GPTPopup extends ObservableReactComponent<object> { - // eslint-disable-next-line no-use-before-define static Instance: GPTPopup; static ChatTag = '#chat'; // tag used by GPT popup to filter docs private _askDictation: DictationButton | null = null; @@ -530,14 +529,14 @@ export class GPTPopup extends ObservableReactComponent<object> { style={{ color: 'black' }} placeholder={placeholder} /> - <Button // - text="Send" - type={Type.TERT} + <Button //\ + type={Type.PRIM} + tooltip="Send to AI" icon={<AiOutlineSend />} iconPlacement="right" - color={SettingsManager.userColor} - background={SettingsManager.userVariantColor} + background={SnappingManager.userVariantColor} onClick={() => this.callGpt(this._mode)} + size={Size.LARGE} /> <DictationButton ref={this.setDictationRef} setInput={onChange} /> </div> diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss index ca177c746..9bae92586 100644 --- a/src/client/views/topbar/TopBar.scss +++ b/src/client/views/topbar/TopBar.scss @@ -238,3 +238,10 @@ font-weight: bold; } } + +.topbar-right .dropdown-container { + width: 30px !important; + display: inline-flex !important; + margin: 0 !important; + padding: 0 !important; +} diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 18e30b3c2..9b24219cf 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -1,11 +1,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, IconButton, isDark, Size, Type } from '@dash/components'; +import { Button, Dropdown, DropdownType, IconButton, isDark, Size, Type } from '@dash/components'; import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Flip } from 'react-awesome-reveal'; import { FaBug } from 'react-icons/fa'; -import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils'; +import { ClientUtils, returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils'; import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc'; import { AclAdmin, DashVersion } from '../../../fields/DocSymbols'; import { StrCast } from '../../../fields/Types'; @@ -27,6 +26,8 @@ import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; import './TopBar.scss'; +import { OpenWhere } from '../nodes/OpenWhere'; +import { Docs } from '../../documents/Documents'; /** * ABOUT: This is the topbar in Dash, which included the current Dashboard as well as access to information on the user @@ -84,7 +85,7 @@ export class TopBar extends ObservableReactComponent<object> { {Doc.ActiveDashboard ? ( <IconButton onClick={this.navigateToHome} - icon={<FontAwesomeIcon icon={DocListCast(Doc.MySharedDocs.data_dashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'portrait' : 'home'} />} + icon={<FontAwesomeIcon icon={DocListCast(Doc.MySharedDocs?.data_dashboards)?.some(dash => !DocListCast(Doc.MySharedDocs?.viewed)?.includes(dash)) ? 'portrait' : 'home'} />} color={this.color} background={this.backgroundColor} /> @@ -196,18 +197,53 @@ export class TopBar extends ObservableReactComponent<object> { onClick={() => SharingManager.Instance.open(undefined, Doc.ActiveDashboard)} /> ) : null} - <IconButton tooltip="Issue Reporter ⌘I" size={Size.SMALL} color={this.color} background={this.backgroundColor} onClick={ReportManager.Instance.open} icon={<FaBug />} /> - <Flip key={this._flipDocumentation}> - <IconButton - tooltip="Documentation ⌘D" - size={Size.SMALL} - color={this.color} - background={this.backgroundColor} - onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} - icon={<FontAwesomeIcon icon="question-circle" />} - /> - </Flip> - <IconButton tooltip="Settings ⌘⇧S" size={Size.SMALL} color={this.color} background={this.backgroundColor} onClick={SettingsManager.Instance.openMgr} icon={<FontAwesomeIcon icon="cog" />} /> + <IconButton tooltip="Issue Reporter ⌘I" size={Size.SMALL} color={this.color} onClick={ReportManager.Instance.open} icon={<FaBug />} /> + {/* <IconButton tooltip="Documentation ⌘D" size={Size.SMALL} color={this.color} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} /> */} + <Dropdown + iconProvider={() => <FontAwesomeIcon icon="question-circle" />} + dropdownType={DropdownType.CLICK} + background={this.backgroundColor} + style={{ padding: 0, minWidth: 'unset', margin: 0, width: 30, display: 'inline-flex' }} + toolTip="Help" + placement="bottom" + items={[ + { + val: 'documentation', + text: 'Documentation', + tooltip: 'Documentation ⌘D', + onClick: () => { + window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank'); + }, + }, + { + val: 'tutorial', + text: 'Tutorial', + onClick: () => { + Doc.IsInfoUIDisabled = false; + }, + }, + { + val: 'tutorialagent', + text: 'Ask AI!', + onClick: () => { + const userEmail = ClientUtils.CurrentUserEmail(); + const userName = userEmail.split('@')[0]; + const doc = Docs.Create.ChatDocument({ + chat: 'Welcome to your help assistant for Dash. Ask any Dash-related questions to get started.', + title: `${userName}'s Dash Help Assistant`, + is_dash_doc_assistant: 'true', + }); + DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight); + }, + }, + ]} + width={30} + size={Size.SMALL} + color={this.color} + closeOnSelect={true} + onPointerLeave={() => {}} + /> + <IconButton tooltip="Settings ⌘⇧S" size={Size.SMALL} color={this.color} onClick={SettingsManager.Instance.openMgr} icon={<FontAwesomeIcon icon="cog" />} style={{ margin: 0, padding: 0 }} /> <IconButton size={Size.SMALL} onClick={ServerStats.Instance.open} |
