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.ts32
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx45
-rw-r--r--src/client/views/nodes/chatbot/tools/TutorialTool.ts339
-rw-r--r--src/client/views/topbar/TopBar.scss7
-rw-r--r--src/client/views/topbar/TopBar.tsx67
9 files changed, 669 insertions, 342 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 2364fd74a..93314e383 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
@@ -1760,11 +1762,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 e93fb87db..8075cab5f 100644
--- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts
+++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
@@ -7,24 +7,21 @@ 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 { CreateDocTool } from '../tools/CreateDocumentTool';
import { DataAnalysisTool } from '../tools/DataAnalysisTool';
-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 { parsedDoc } from '../chatboxcomponents/ChatBox';
import { WebsiteInfoScraperTool } from '../tools/WebsiteInfoScraperTool';
import { Upload } from '../../../../../server/SharedMediaTypes';
import { RAGTool } from '../tools/RAGTool';
-//import { CreateTextDocTool } from '../tools/CreateTextDocumentTool';
+import { GPTTutorialTool } from '../tools/TutorialTool';
dotenv.config();
@@ -47,6 +44,7 @@ export class Agent {
private processingInfo: ProcessingInfo[] = [];
private streamedAnswerParser: StreamedAnswerParser = new StreamedAnswerParser();
private tools: Record<string, BaseTool<ReadonlyArray<Parameter>>>;
+ private Document: Doc;
/**
* The constructor initializes the agent with the vector store and toolset, and sets up the OpenAI client.
@@ -65,8 +63,8 @@ export class Agent {
addLinkedUrlDoc: (url: string, id: string) => void,
createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void,
addLinkedDoc: (doc: parsedDoc) => Doc | undefined,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- createCSVInDash: (url: string, title: string, id: string, data: string) => void
+ createCSVInDash: (url: string, title: string, id: string, data: string) => void,
+ document: Doc
) {
// Initialize OpenAI client with API key from environment
this.client = new OpenAI({ apiKey: process.env.OPENAI_KEY, dangerouslyAllowBrowser: true });
@@ -74,6 +72,7 @@ export class Agent {
this._history = history;
this._summaries = summaries;
this._csvData = csvData;
+ this.Document = document;
// Define available tools for the assistant
this.tools = {
@@ -82,13 +81,9 @@ export class Agent {
dataAnalysis: new DataAnalysisTool(csvData),
websiteInfoScraper: new WebsiteInfoScraperTool(addLinkedUrlDoc),
searchTool: new SearchTool(addLinkedUrlDoc),
- // createCSV: new CreateCSVTool(createCSVInDash),
noTool: new NoTool(),
- imageCreationTool: new ImageCreationTool(createImage),
- // createTextDoc: new CreateTextDocTool(addLinkedDoc),
createDoc: new CreateDocTool(addLinkedDoc),
- // createAnyDocument: new CreateAnyDocumentTool(addLinkedDoc),
- // dictionary: new DictionaryTool(),
+ generateTutorialNode: new GPTTutorialTool(addLinkedDoc),
};
}
@@ -117,7 +112,18 @@ export class Agent {
// Retrieve chat history and generate system prompt
const chatHistory = this._history();
- const systemPrompt = getReactPrompt(Object.values(this.tools), this._summaries, chatHistory);
+ let systemPrompt = getReactPrompt(Object.values(this.tools), this._summaries, chatHistory);
+
+ // If this is a Dash documentation assistant chat, modify the system prompt
+ if (this.Document?.is_dash_doc_assistant) {
+ systemPrompt = systemPrompt.replace(
+ '<task>',
+ `<task>
+ IMPORTANT: You are specifically focused on helping users with questions about Dash documentation and usage. When users ask questions, interpret them in the context of Dash documentation and features, even if they don't explicitly mention Dash. For example, if a user asks "How do I create a document?", interpret this as "How do I create a document in Dash?" and provide relevant Dash-specific guidance.
+
+ For any questions about Dash features, functionality, or usage, you should use the generateTutorialNode tool to create a tutorial document that explains the concept in detail. This tool will help create well-formatted, interactive tutorials that guide users through Dash features.`
+ );
+ }
// Initialize intermediate messages
this.interMessages = [{ role: 'system', content: systemPrompt }];
@@ -132,7 +138,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 6c3da8977..fc51ca5f9 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -106,7 +106,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.vectorstore_id = StrCast(this.dataDoc.vectorstore_id);
}
this.vectorstore = new Vectorstore(this.vectorstore_id, this.retrieveDocIds);
- this.agent = new Agent(this.vectorstore, this.retrieveSummaries, this.retrieveFormattedHistory, this.retrieveCSVData, this.addLinkedUrlDoc, this.createImageInDash, this.createDocInDash, this.createCSVInDash);
+ this.agent = new Agent(this.vectorstore, this.retrieveSummaries, this.retrieveFormattedHistory, this.retrieveCSVData, this.addLinkedUrlDoc, this.createImageInDash, this.createDocInDash, this.createCSVInDash, this.Document);
this.messagesRef = React.createRef<HTMLDivElement>();
// Reaction to update dataDoc when chat history changes
@@ -309,7 +309,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
}
});
- this.scrollToBottom();
};
const onAnswerUpdate = (answerUpdate: string) => {
@@ -317,41 +316,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();
};
/**
@@ -408,7 +395,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
if (doc) {
LinkManager.Instance.addLink(Docs.Create.LinkDocument(this.Document, doc));
this._props.addDocument?.(doc);
- DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {}).then(() => this.addCSVForAnalysis(doc, id));
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => this.addCSVForAnalysis(doc, id));
}
});
@@ -446,7 +433,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const ndoc = (() => {
switch (doc.doc_type) {
default:
- case supportedDocTypes.text: return Docs.Create.TextDocument(data as string, options);
+ case supportedDocTypes.text: return Docs.Create.TextDocument(doc.text 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);
case supportedDocTypes.deck: return this.createDeck(JSON.parse(data as string) as parsedDoc[], options);
@@ -826,7 +813,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,
},
],
@@ -989,7 +978,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
</div>
)}
<div className="chat-header">
- <h2>{this.userName()}&apos;s AI Assistant</h2>
+ <h2>{this.dataDoc.is_dash_doc_assistant ? 'Dash Help Assistant' : `${this.userName()}'s AI Assistant`}</h2>
</div>
<div className="chat-messages" ref={this.messagesRef}>
{this._history.map((message, index) => (
diff --git a/src/client/views/nodes/chatbot/tools/TutorialTool.ts b/src/client/views/nodes/chatbot/tools/TutorialTool.ts
index 69ae9c618..08e4e1409 100644
--- a/src/client/views/nodes/chatbot/tools/TutorialTool.ts
+++ b/src/client/views/nodes/chatbot/tools/TutorialTool.ts
@@ -1,166 +1,205 @@
import { BaseTool } from './BaseTool';
import { Observation } from '../types/types';
-import { Parameter, ParametersType, ToolInfo } from '../types/tool_types';
-// import { gptAPICall } from '../../../../apis/gpt/GPT';
+import { ParametersType, ToolInfo } from '../types/tool_types';
import { schema } from '../../../../views/nodes/formattedText/schema_rts';
-import { RichTextField } from '../../../../../fields/RichTextField';
-import { Docs } from '../../../../documents/Documents';
-import { DocumentViewInternal } from '../../../nodes/DocumentView';
import { v4 as uuidv4 } from 'uuid';
-import { OpenWhere } from '../../../../views/nodes/OpenWhere';
-import { gptAPICall } from '../../../../apis/gpt/GPT';
+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\'s query about Dash functionality.',
- required: true,
- },
+ {
+ 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.',
+ 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;
- }
-
-// private applyFormatting(markdownText: string): { doc: any; plainText: string } {
-// const lines = markdownText.split('\n');
-// const nodes: any[] = [];
-// let plainText = '';
-// let i = 0;
-// let currentListItems: any[] = [];
-
-// 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));
-// currentListItems = [];
-// }
-// };
-
-// while (i < lines.length) {
-// const line = lines[i].trim();
-// if (line) {
-// if (line.startsWith('## ')) {
-// flushListItems();
-// const textContent = line.replace('## ', '');
-// nodes.push(schema.nodes.heading.create({ level: 1 }, processBoldText(textContent)));
-// plainText += textContent + '\n';
-// } else if (line.startsWith('- ')) {
-// const textContent = line.replace('- ', '');
-// currentListItems.push(
-// schema.nodes.list_item.create(
-// {},
-// schema.nodes.paragraph.create({}, processBoldText(textContent))
-// )
-// );
-// plainText += textContent + '\n';
-// } else {
-// flushListItems();
-// nodes.push(schema.nodes.paragraph.create({}, processBoldText(line)));
-// plainText += line + '\n';
-// }
-// } else {
-// flushListItems();
-// nodes.push(schema.nodes.paragraph.create());
-// plainText += '\n';
-// }
-// i++;
-// }
-// flushListItems();
-
-// const doc = schema.nodes.doc.create({}, nodes);
-// return { doc, plainText: plainText.trim() };
-// }
-
- async execute(args: ParametersType<typeof generateTutorialNodeToolParams>): Promise<Observation[]> {
- const chunkId = uuidv4();
- try {
- console.log('Executing with args:', args);
- const query = args.query;
- if (typeof query !== 'string' || !query.trim()) {
- return [{
- type: 'text',
- text: `<chunk chunk_id="${chunkId}" chunk_type="error">Invalid input: Query must be a non-empty string.</chunk>`
- }];
- }
-
- const markdownResponse = await gptAPICall(query);
- if (!markdownResponse || typeof markdownResponse !== 'string') {
- throw new Error('Invalid GPT API response');
- }
- console.log('Markdown response:', markdownResponse);
-
- // const { doc } = this.applyFormatting(markdownResponse);
- // const rtfData = {
- // doc: doc.toJSON(),
- // selection: { type: 'text', anchor: 1, head: 1 },
- // storedMarks: [],
- // };
- // const serializedData = JSON.stringify(rtfData);
- // console.log('Serialized data:', serializedData);
-
- const tutorialDoc: parsedDoc = {
- doc_type: 'text',
- data: markdownResponse,
- title: 'Tutorial Node',
- _width: 600,
- _layout_fitWidth: true,
- _layout_autoHeight: true,
- text_fontSize: '16px',
- };
-
- const createdDoc = this._createDocInDash(tutorialDoc);
- console.log('Created doc:', createdDoc);
- if (!createdDoc || !createdDoc[Id]) {
- throw new Error('Failed to create tutorial node');
- }
-
- return [{
- type: 'text',
- text: `<chunk chunk_id="${chunkId}" chunk_type="tutorial_node_creation">Created tutorial node with ID ${createdDoc[Id]}.</chunk>`
- }];
- } catch (error) {
- console.error('Error in GPTTutorialTool:', error);
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
- return [{
- type: 'text',
- text: `<chunk chunk_id="${chunkId}" chunk_type="error">Error generating tutorial node: ${errorMessage}</chunk>`
- }];
+ 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>`,
+ },
+ ];
+ }
}
- }
-} \ No newline at end of file
+}
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}