diff options
Diffstat (limited to 'src/client/views')
9 files changed, 663 insertions, 179 deletions
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..8ed3e8e30 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import { SettingsManager } from '../../../util/SettingsManager'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import './CollectionFreeFormView.scss'; +import { Button } from '../../../util/CurrentUserUtils'; /** * An Fsa Arc. The first array element is a test condition function that will be observed. @@ -17,16 +18,26 @@ 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]?: Button[]; [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?: Button[], + entryFunc?: () => unknown + ) { this[StateMessage] = message; Object.assign(this, arcs); this[StateMessageGIF] = messageGif; this[StateEntryFunc] = entryFunc; + this[StateMessageButton] = buttons; } } @@ -44,14 +55,15 @@ export function InfoState( msg: string, // arcs: { [key: string]: infoArc }, gif?: string, + button?: Button[], 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 +80,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 +113,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,11 +129,36 @@ 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())} /> + <IconButton + icon="x" + color={SettingsManager.userColor} + size={Size.XSMALL} + type={Type.TERT} + background={SettingsManager.userBackgroundColor} + onClick={action(() => this.props.close())} + /> </div> </div> ); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx index 89d2bf2c3..595bbf2e9 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx @@ -20,34 +20,61 @@ export interface CollectionFreeFormInfoUIProps { @observer export class CollectionFreeFormInfoUI extends ObservableReactComponent<CollectionFreeFormInfoUIProps> { + _originalBackground: string | undefined; + public 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} /> )); } - _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; } - setCurrState = (state: infoState) => { - if (state) { - this.currState = state; - this.currState[StateEntryFunc]?.(); - } + skipToState = (newState: infoState) => { + runInAction(() => { + console.log('Transitioning to next state:', newState); + if (!this._currState) { + this._currState = newState; // Assign directly if undefined + } else { + this._currState = newState; + } + }); + }; + + createNextButton = (newState: ReturnType<typeof InfoState>) => { + return { + title: 'Next', + toolTip: 'Next', + btnType: ButtonType.ClickButton, + scripts: { + onClick: `this.skipToState(${newState})`, + }, + targetState: newState, + }; }; setupStates = () => { @@ -110,175 +137,304 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio 'Great moves. Try creating a second document. You can see the list of supported document types by typing a colon (":")', { // eslint-disable-next-line no-use-before-define - docCreated: [() => numDocs() === 2, () => multipleDocs], - docDeleted: [() => numDocs() < 1, () => start], + linkStarted: [ + () => DocumentLinksButton.StartLink, + () => { + linkCounter = Doc.Links(lastDocCreated).length; + // eslint-disable-next-line no-use-before-define + return startedLink; + }, + ], }, - 'dash-colon-menu.gif', - () => TopBar.Instance.FlipDocumentationIcon() - ); // prettier-ignore + 'dash-create-link-board.gif' + ); - const multipleDocs = InfoState( - 'Let\'s create a new link. Click the link icon on one of your documents.', + this.tutorialStates.presentDocs = InfoState( + 'Click the pin icon in the top left corner while clicking a doc to create your presentation.', { // eslint-disable-next-line no-use-before-define - linkStarted: [() => linkStart(), () => startedLink], - docRemoved: [() => numDocs() < 2, () => oneDoc], + docPinned: [ + () => DocListCast(Doc.ActivePresentation?.data).length > presentationCounter, + () => { + presentationCounter++; + // eslint-disable-next-line no-use-before-define + return pinnedDoc; + }, + ], }, - 'dash-create-link-board.gif' - ); // prettier-ignore + 'pin-explanation.gif' + ); - const startedLink = InfoState( - 'Now click the highlighted link icon on your other document.', + this.tutorialStates.nestedCollections = InfoState( + "Want to learn how to create a nested collection? Click the : button and add a 'collection' doc", { - 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]; + // eslint-disable-next-line no-use-before-define + return marqueeSelection; + }, + ], }, - 'dash-create-link-board.gif' - ); // prettier-ignore + 'dash-nested-collection.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!"); + 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]; // eslint-disable-next-line no-use-before-define - return viewedLink; - }], + return this.tutorialStates.presentDocs; + }, + ], + }); + + const skipToLinksButton: Button = { + title: 'Links Tutorial', + toolTip: 'Skip', + btnType: ButtonType.ClickButton, + scripts: { + onClick: 'this.skipToState(this.tutorialStates.multipleDocs)', }, - 'dash-following-link.gif' - ); // prettier-ignore + targetState: this.tutorialStates.multipleDocs, + }; + + const skipToPinsButton: Button = { + title: 'Pins Tutorial', + toolTip: 'Skip', + btnType: ButtonType.ClickButton, + scripts: { + onClick: 'this.skipToState(this.tutorialStates.makePresentation)', + }, + targetState: this.tutorialStates.makePresentation, + }; + + const skipToPresentationButton: Button = { + title: 'Collections Tutorial', + toolTip: 'Skip', + btnType: ButtonType.ClickButton, + scripts: { + onClick: 'this.skipToState(this.tutorialStates.nestedCollections)', + }, + targetState: this.tutorialStates.nestedCollections, + }; + + const ending = InfoState("If you have any more questions, feel free to ask Dash's AI Bot!", {}); + + // Traditional tutorial + + 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'); - 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 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 linked doc and try deleting the presentation.", { - 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], + docRemoved: [ + () => this._props.childDocs().length < docCounter, + () => { + docCounter -= 1; + return activatePresentation; + }, + ], }, - 'documentation.png', - () => TopBar.Instance.FlipDocumentationIcon() - ); // prettier-ignore + 'onclick-node.gif' + ); - const presentDocs = InfoState( - 'Another document! You could make a presentation. Click the pin icon in the top left corner.', + const trailedPresentation = InfoState( + 'See the new dragged-in presentation? Try linking it to the highlighted doc.', { - docPinned: [ - () => pin().length > trail, + linkAdd: [ + () => Doc.Links(lastDocCreated)?.length > linkCounter, () => { - trail = pin().length; - // eslint-disable-next-line no-use-before-define - return pinnedDoc1; + linkCounter += 1; + return deletePresentation; }, ], - docRemoved: [() => numDocs() < 3, () => viewedLink], }, - '/assets/dash-pin-with-view.gif' + 'link-presentation.gif' ); - 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 pinnedPresentation = InfoState( + 'Want to see something cool? Click the trail button on the presentation and drag it inside the canvas.', + { + 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-trail-explanation.gif' + ); - // const eraserMode = InfoState('You\'re in eraser mode. Say goodbye to your first masterpiece.', { - // docsRemoved: [() => numDocs() == 3, () => demos], - // }); // prettier-ignore + const pinnedDoc2 = InfoState('You pinned another doc. Press play to the right to show your presentation!', { + autoPresentation: [() => Doc.ActivePresentation?.presentation_status === 'auto', () => pinnedPresentation], + }); - const pinnedDoc1 = InfoState('You just pinned your doc.', { - docPinned: [ - () => pin().length > trail, + const pinnedDoc = InfoState('You just pinned your doc. Pin another doc to add to the presentation!', { + // eslint-disable-next-line no-use-before-define + addedDoc: [ + () => 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 pinnedDoc; }, ], - // 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, + () => DocListCast(Doc.ActivePresentation?.data).length > presentationCounter, () => { - trail = pin().length; + presentationCounter++; // eslint-disable-next-line no-use-before-define - return pinnedDoc3; + 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 pinnedDoc3 = InfoState(`You pinned yet another doc.`, { - docPinned: [ - () => pin().length > trail, + const editLink = InfoState( + "Want to make your link visible? Click 'show link'.", + { + docCreated: [ + () => this._props.childDocs().length > docCounter, + () => { + docCounter += 1; + lastDocCreated = this._props.childDocs()[this.props.childDocs().length - 1]; + return this.tutorialStates.makePresentation; + }, + ], + }, + 'show-link.gif' + ); + + const madeLink = InfoState( + 'You made your first link! You can view your links by selecting the blue dot.', + { + linkViewed: [() => DocButtonState.Instance.LinkEditorDocView, () => editLink], + }, + 'dash-following-link.gif' + ); + + const startedLink = InfoState( + 'Now click the highlighted link icon on your other document.', + { + linkAdd: [ + () => Doc.Links(lastDocCreated)?.length > linkCounter, + () => { + linkCounter += 1; + return madeLink; + }, + ], + }, + 'dash-create-link-board.gif' + ); + + this.tutorialStates.movedDoc = InfoState("Great moves! Try creating a second document.", { + docCreated: [() => this._props.childDocs().length > docCounter, () => { + 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 text document.', + { + docCreated: [ + () => this._props.childDocs().length > docCounter, + () => { + docCounter += 1; + lastDocCreated = this._props.childDocs()[this.props.childDocs().length - 1]; + return this.tutorialStates.movedDoc; + }, + ], + }, + undefined, + [skipToLinksButton, skipToPinsButton, skipToPresentationButton] + ); + + // Information on created nested collections + const createdMarquee = InfoState( + 'Next, right click and drag a square to create the collection', + { + // eslint-disable-next-line no-use-before-define + marqueeMade: [ + () => this._props.childDocs().length < docCounter, + () => { + docCounter -= 1; + return ending; + }, + ], + }, + 'dash-create-collection-marquee.gif' + ); + + 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; - 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 openedTrail = InfoState('This is your trails tab.', { - // trailView: [() => presentationMode() === 'edit', () => editPresentationMode], - // }); + // Explanation of importing - // 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 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 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 - 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 + // Accessed by right-clicking anywhere on the target document or selecting the three bars menu at the bottom of the document chrome - return start; + 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)]); + + 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} />; + if (!this.currState) return null; + + return ( + <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 bb3c59eae..5bbe93a90 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 @@ -1753,11 +1755,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/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts index 8516f054b..361c5eb2b 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'; @@ -297,7 +294,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 }); diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx index df6c5627c..db01b7c88 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx @@ -374,7 +374,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; } }); - this.scrollToBottom(); }; const onAnswerUpdate = (answerUpdate: string) => { @@ -382,41 +381,29 @@ 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 + const response = await this.agent.askAgent(trimmedText, 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(); }; /** @@ -1066,7 +1053,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, }, ], 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..08e4e1409 --- /dev/null +++ b/src/client/views/nodes/chatbot/tools/TutorialTool.ts @@ -0,0 +1,205 @@ +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'; + +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.", +}; +const applyFormatting = (markdownText: string): { doc: any; plainText: string } => { + const lines = markdownText.split('\n'); + const nodes: any[] = []; + let plainText = ''; + let i = 0; + let currentListItems: any[] = []; + let currentParagraph: any[] = []; + let currentOrderedListItems: any[] = []; + let inOrderedList = false; + let inBulletList = false; + + const processBoldText = (text: string) => { + const boldRegex = /\*\*(.*?)\*\*/g; + const parts = []; + 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 = () => { + 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 = () => { + if (currentParagraph.length > 0) { + nodes.push(schema.nodes.paragraph.create({}, currentParagraph)); + currentParagraph = []; + } + }; + + const processHeader = (line: string) => { + 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 _createDocInDash: (doc: parsedDoc) => Doc | undefined; + + constructor(createDocInDash: (doc: parsedDoc) => Doc | undefined) { + super(generateTutorialNodeToolInfo); + + this._createDocInDash = createDocInDash; + } + + 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 as any).toJSON ? (doc as any).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/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss index 35a3da312..2200d11d5 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..5d8583873 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -1,12 +1,12 @@ 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 { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt, returnEmptyDoclist } from '../../../fields/Doc'; import { AclAdmin, DashVersion } from '../../../fields/DocSymbols'; import { StrCast } from '../../../fields/Types'; import { GetEffectiveAcl } from '../../../fields/util'; @@ -27,6 +27,12 @@ import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; import './TopBar.scss'; +import { OpenWhere } from '../nodes/OpenWhere'; +import { ChatBox } from '../nodes/chatbot/chatboxcomponents/ChatBox'; +import { FieldViewProps } from '../nodes/FieldView'; +import { FocusViewOptions } from '../nodes/FocusViewOptions'; +import { PinProps } from '../PinFuncs'; +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 @@ -196,18 +202,51 @@ 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 doc = Docs.Create.ChatDocument({ + chat: 'Welcome to your help assistant for Dash. Ask any Dash-related questions to get started.', + title: 'Dash Documentation 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} |
