aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormController.ts7
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx52
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx420
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx42
-rw-r--r--src/client/views/nodes/chatbot/agentsystem/Agent.ts5
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx37
-rw-r--r--src/client/views/nodes/chatbot/tools/TutorialTool.ts205
-rw-r--r--src/client/views/topbar/TopBar.scss7
-rw-r--r--src/client/views/topbar/TopBar.tsx67
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}