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.tsx523
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx42
-rw-r--r--src/client/views/nodes/chatbot/agentsystem/Agent.ts17
-rw-r--r--src/client/views/nodes/chatbot/agentsystem/prompts.ts16
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx90
-rw-r--r--src/client/views/nodes/chatbot/tools/TutorialTool.ts212
-rw-r--r--src/client/views/topbar/TopBar.scss7
-rw-r--r--src/client/views/topbar/TopBar.tsx68
10 files changed, 724 insertions, 310 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..efc22f523 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
@@ -1,284 +1,343 @@
import { makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, DocListCast, FieldResult, FieldType } from '../../../../fields/Doc';
-import { InkTool } from '../../../../fields/InkField';
-import { StrCast } from '../../../../fields/Types';
+import { Doc } from '../../../../fields/Doc';
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 { CollectionFreeFormInfoState, infoState } from './CollectionFreeFormInfoState';
+import { Button } from '../../../util/CurrentUserUtils';
+import { InfoState } from './CollectionFreeFormInfoState';
+import { DocButtonState } from '../../nodes/DocumentLinksButton';
+import { InkTool } from '../../../../fields/InkField';
+import { DocumentLinksButton } from '../../nodes/DocumentLinksButton';
+import { CollectionFreeFormView } from '.';
+import { DocListCast } from '../../../../fields/Doc';
+import { ButtonType } from '../../nodes/FontIconBox/FontIconBox';
export interface CollectionFreeFormInfoUIProps {
- Doc: Doc;
- layoutDoc: Doc;
+ Document: Doc;
+ LayoutDoc: Doc;
childDocs: () => Doc[];
close: () => void;
}
@observer
export class CollectionFreeFormInfoUI extends ObservableReactComponent<CollectionFreeFormInfoUIProps> {
+ _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;
+ this._props.Document.backgroundColor = this._originalBackground;
}
- setCurrState = (state: infoState) => {
- if (state) {
- this.currState = state;
- this.currState[StateEntryFunc]?.();
+ skipToState = (newState: infoState) => {
+ runInAction(() => {
+ if (!this._currState) {
+ this._currState = newState; // Assign directly if undefined
+ } else {
+ this._currState = 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 docX: FieldResult<FieldType>;
- let docY: FieldResult<FieldType>;
-
- const docNewX = () => firstDoc()?.x;
- const docNewY = () => firstDoc()?.y;
-
- const linkStart = () => DocumentLinksButton.StartLink;
- const linkUnstart = () => !DocumentLinksButton.StartLink;
-
- const numDocLinks = () => Doc.Links(firstDoc())?.length;
- const linkMenuOpen = () => DocButtonState.Instance.LinkEditorDocView;
-
- const activeTool = () => Doc.ActiveTool;
-
- const pin = () => DocListCast(Doc.ActivePresentation?.data);
-
- let trail: number;
-
- const presentationMode = () => Doc.ActivePresentation?.presentation_status;
-
- // 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.',
- {
- // 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 (":")',
- {
- // eslint-disable-next-line no-use-before-define
- docCreated: [() => numDocs() === 2, () => multipleDocs],
- docDeleted: [() => numDocs() < 1, () => start],
+ createNextButton = (newState: ReturnType<typeof InfoState>) => {
+ return {
+ title: "Next",
+ toolTip: "Next",
+ btnType: ButtonType.ClickButton,
+ scripts: {
+ onClick: `this.skipToState(${newState})`
},
- 'dash-colon-menu.gif',
- () => TopBar.Instance.FlipDocumentationIcon()
- ); // prettier-ignore
+ targetState: newState
+ };
+ };
- const multipleDocs = InfoState(
- 'Let\'s create a new link. Click the link icon on one of your documents.',
- {
+ setupStates = () => {
+ 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.", {
+ // eslint-disable-next-line no-use-before-define
+ linkStarted: [() => DocumentLinksButton.StartLink, () => {
+ linkCounter = Doc.Links(lastDocCreated).length
// eslint-disable-next-line no-use-before-define
- linkStarted: [() => linkStart(), () => startedLink],
- docRemoved: [() => numDocs() < 2, () => oneDoc],
+ 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'
- ); // prettier-ignore
+ );
- const startedLink = InfoState(
- 'Now click the highlighted link icon on your other document.',
- {
- linkUnstart: [() => linkUnstart(), () => multipleDocs],
+ this.tutorialStates.presentDocs = InfoState("Select a document then click the 'pin' button in the top left to create your presentation.", {
// eslint-disable-next-line no-use-before-define
- linkCreated: [() => numDocLinks(), () => madeLink],
- docRemoved: [() => numDocs() < 2, () => oneDoc],
- },
- 'dash-create-link-board.gif'
- ); // prettier-ignore
-
- 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;
- }],
- },
- '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.',
- {
- linkDeleted: [() => !numDocLinks(), () => multipleDocs],
- docRemoved: [() => numDocs() < 2, () => oneDoc],
- docCreated: [() => numDocs() === 3, () => {
- trail = pin().length;
- // eslint-disable-next-line no-use-before-define
- return presentDocs;
- }],
+ docPinned: [() => DocListCast(Doc.ActivePresentation?.data).length > presentationCounter, () => {
+ presentationCounter++
// eslint-disable-next-line no-use-before-define
- activePen: [() => activeTool() === InkTool.Ink, () => penMode],
+ return pinnedDoc}],
},
- 'documentation.png',
- () => TopBar.Instance.FlipDocumentationIcon()
- ); // prettier-ignore
-
- const presentDocs = InfoState(
- 'Another document! You could make a presentation. Click the pin icon in the top left corner.',
- {
- docPinned: [
- () => pin().length > trail,
- () => {
- trail = pin().length;
- // eslint-disable-next-line no-use-before-define
- return pinnedDoc1;
- },
- ],
- docRemoved: [() => numDocs() < 3, () => viewedLink],
- },
- '/assets/dash-pin-with-view.gif'
- );
+ 'pin-explanation.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 eraserMode = InfoState('You\'re in eraser mode. Say goodbye to your first masterpiece.', {
- // docsRemoved: [() => numDocs() == 3, () => demos],
- // }); // prettier-ignore
-
- const pinnedDoc1 = InfoState('You just pinned your doc.', {
- docPinned: [
- () => pin().length > trail,
- () => {
- trail = pin().length;
- // eslint-disable-next-line no-use-before-define
- return pinnedDoc2;
- },
- ],
- // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode],
- // manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode],
+ this.tutorialStates.nestedCollections = InfoState("Want to learn how to create a nested collection? Click the : button and add a 'collection' doc", {
+ // eslint-disable-next-line no-use-before-define
+ 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
- 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],
+ return marqueeSelection}]
+ },
+ 'dash-nested-collection.gif')
+
+
+ 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
- autoPresentation: [() => presentationMode() === 'auto', () => autoPresentationMode],
- docRemoved: [() => numDocs() < 3, () => viewedLink],
- });
+ return this.tutorialStates.presentDocs}],
+ });
+
+ const skipToLinksButton: Button = {
+ title: "Links Tutorial",
+ toolTip: "Skip",
+ btnType: ButtonType.ClickButton,
+ scripts: {
+ onClick: "this.skipToState(this.tutorialStates.multipleDocs)"
+ },
+ 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 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 ending = InfoState("If you have any more questions, feel free to ask Dash's AI Bot!")
- // const openedTrail = InfoState('This is your trails tab.', {
- // trailView: [() => presentationMode() === 'edit', () => editPresentationMode],
- // });
+ // Traditional tutorial
- // 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 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 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],
+ 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.", {
+ 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).", {
+ 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}]
+ },
+ 'link-presentation.gif');
+
+ const pinnedPresentation = InfoState("Want to see something cool? Zoom out, 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 pinnedDoc2 = InfoState("You pinned another doc. Press autoplay to the right to show your presentation!", {
+ autoPresentation: [() => Doc.ActivePresentation?.presentation_status === "auto", () => pinnedPresentation],
});
- const autoPresentationMode = InfoState("You're in auto presentation mode.", {
- // editPresentation: [() => presentationMode() === 'edit', () => editPresentationMode],
- manualPresentation: [() => presentationMode() === 'manual', () => manualPresentationMode],
- docRemoved: [() => numDocs() < 3, () => viewedLink],
+ const pinnedDoc = InfoState("You just pinned your doc. Pin another doc to add to the presentation!", {
// eslint-disable-next-line no-use-before-define
- docCreated: [() => numDocs() === 4, () => completed],
+ 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++
+ // eslint-disable-next-line no-use-before-define
+ return pinnedDoc2}],
});
-
- 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
-
- return start;
+
+ 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, () => {
+ docCounter = this._props.childDocs().length
+ return 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 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])
+
+ // 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, () => {
+ docCounter += 1
+ lastDocCreated = this._props.childDocs()[this.props.childDocs().length - 1]
+ return createdMarquee}]
+ })
+
+ // Explanation of importing
+
+ 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)])
+
+ 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)])
+
+ // 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 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}
+ />
+ );
}
-}
+} \ No newline at end of file
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/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
index 8516f054b..a16794e10 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';
@@ -30,7 +27,7 @@ import { CodebaseSummarySearchTool } from '../tools/CodebaseSummarySearchTool';
import { FileContentTool } from '../tools/FileContentTool';
import { FileNamesTool } from '../tools/FileNamesTool';
import { CreateNewTool } from '../tools/CreateNewTool';
-//import { CreateTextDocTool } from '../tools/CreateTextDocumentTool';
+import { GPTTutorialTool } from '../tools/TutorialTool';
dotenv.config();
@@ -53,6 +50,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
@@ -77,7 +75,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 });
@@ -85,6 +84,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();
@@ -103,6 +103,7 @@ export class Agent {
codebaseSummarySearch: new CodebaseSummarySearchTool(this.vectorstore),
fileContent: new FileContentTool(this.vectorstore),
fileNames: new FileNamesTool(this.vectorstore),
+ generateTutorialNode: new GPTTutorialTool(this._docManager),
};
// Add the createNewTool after other tools are defined
@@ -142,7 +143,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
@@ -297,7 +298,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
});
@@ -759,7 +760,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.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index 6e6ef6212..19459b025 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -7,7 +7,6 @@
* with support for follow-up questions and citation management.
*/
-import dotenv from 'dotenv';
import { ObservableSet, action, computed, makeObservable, observable, observe, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import OpenAI, { ClientOptions } from 'openai';
@@ -19,11 +18,10 @@ 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 { CsvCast, DocCast, PDFCast, RTFCast, StrCast, VideoCast, AudioCast } from '../../../../../fields/Types';
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';
@@ -46,8 +44,6 @@ import { OpenWhere } from '../../OpenWhere';
import { Upload } from '../../../../../server/SharedMediaTypes';
import { AgentDocumentManager } from '../utils/AgentDocumentManager';
-dotenv.config();
-
export type parsedDocData = {
doc_type: string;
data: unknown;
@@ -57,6 +53,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 +120,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 +378,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
}
});
- this.scrollToBottom();
};
const onAnswerUpdate = (answerUpdate: string) => {
@@ -381,41 +385,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 +484,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 +495,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 +536,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 +545,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);
@@ -659,8 +655,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
} else {
console.warn(`Chunk not found for chunk ID: ${chunkId}`);
}
- return;
- }
+ return;
+ }
console.log(`Found chunk in document:`, foundChunk);
@@ -669,7 +665,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const directMatchSegmentStart = this.getDirectMatchingSegmentStart(doc, citation.direct_text || '', foundChunk.indexes || []);
if (directMatchSegmentStart) {
await this.goToMediaTimestamp(doc, directMatchSegmentStart, foundChunk.chunkType);
- } else {
+ } else {
console.error('No direct matching segment found for the citation.');
}
} else if (foundChunk.chunkType === CHUNK_TYPE.TABLE || foundChunk.chunkType === CHUNK_TYPE.IMAGE) {
@@ -926,7 +922,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
console.error(`Maximum verification attempts (${attempt}) reached for document ${doc.id}`);
// Last resort: force re-creation of the document view
- if (isPDF) {
+ if (isPDF) {
console.log('Forcing document recreation as last resort');
DocumentManager.Instance.showDocument(doc, {
willZoomCentered: true,
@@ -954,7 +950,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return;
}
- this.processPDFDocumentView(doc, isPDF, citation, foundChunk);
+ this.processPDFDocumentView(doc, isPDF, citation, foundChunk);
} catch (error) {
console.error(`Error on verification attempt ${attempt}:`, error);
if (attempt < 5) {
@@ -1068,7 +1064,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 +1407,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
</div>
)}
<div className="chat-header">
- <h2>{this.userName()}&apos;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>
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/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}