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/CollectionFreeFormInfoUI.tsx3
-rw-r--r--src/client/views/nodes/chatbot/agentsystem/Agent.ts12
-rw-r--r--src/client/views/nodes/chatbot/agentsystem/prompts.ts16
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx60
-rw-r--r--src/client/views/nodes/chatbot/tools/TutorialTool.ts39
-rw-r--r--src/client/views/topbar/TopBar.tsx15
6 files changed, 87 insertions, 58 deletions
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
index 595bbf2e9..5a54553b1 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
@@ -9,6 +9,9 @@ import { DocButtonState, DocumentLinksButton } from '../../nodes/DocumentLinksBu
import { TopBar } from '../../topbar/TopBar';
import { CollectionFreeFormInfoState, InfoState, StateEntryFunc, infoState } from './CollectionFreeFormInfoState';
import { CollectionFreeFormView } from './CollectionFreeFormView';
+import { Button } from '@dash/components';
+import { ButtonType } from '../../nodes/FontIconBox/FontIconBox';
+
import './CollectionFreeFormView.scss';
export interface CollectionFreeFormInfoUIProps {
diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
index 361c5eb2b..a16794e10 100644
--- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts
+++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
@@ -27,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();
@@ -50,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
@@ -74,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 });
@@ -82,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();
@@ -100,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
@@ -139,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
@@ -756,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 db01b7c88..18d0266af 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -7,23 +7,21 @@
* 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';
import * as React from 'react';
import { v4 as uuidv4 } from 'uuid';
import { ClientUtils, OmitKeys } from '../../../../../ClientUtils';
-import { Doc, DocListCast, Opt } from '../../../../../fields/Doc';
-import { DocData, DocLayout, DocViews } from '../../../../../fields/DocSymbols';
+import { Doc, Opt } from '../../../../../fields/Doc';
+import { 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';
@@ -44,11 +42,8 @@ import './ChatBox.scss';
import MessageComponentBox from './MessageComponent';
import { OpenWhere } from '../../OpenWhere';
import { Upload } from '../../../../../server/SharedMediaTypes';
-import { DocumentMetadataTool } from '../tools/DocumentMetadataTool';
import { AgentDocumentManager } from '../utils/AgentDocumentManager';
-dotenv.config();
-
export type parsedDocData = {
doc_type: string;
data: unknown;
@@ -58,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,
@@ -124,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);
@@ -388,7 +392,11 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
// Get the response from the agent
- const response = await this.agent.askAgent(trimmedText, onProcessingUpdate, onAnswerUpdate);
+ 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);
// Push the final message to history
runInAction(() => {
@@ -476,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);
@@ -487,22 +495,22 @@ 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 as any)._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',
@@ -527,7 +535,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);
@@ -536,9 +544,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);
@@ -646,8 +654,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);
@@ -656,7 +664,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) {
@@ -911,7 +919,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,
@@ -939,7 +947,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) {
@@ -1395,7 +1403,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
index 08e4e1409..1624f0439 100644
--- a/src/client/views/nodes/chatbot/tools/TutorialTool.ts
+++ b/src/client/views/nodes/chatbot/tools/TutorialTool.ts
@@ -11,7 +11,9 @@ import { RichTextField } from '../../../../../fields/RichTextField';
import { DocumentViewInternal } from '../../DocumentView';
import { Docs } from '../../../../documents/Documents';
import { OpenWhere } from '../../OpenWhere';
-import { CollectionFreeFormView } from '../../../collections/collectionFreeForm';
+import { CollectionFreeFormView } from '../../../collections/collectionFreeForm/CollectionFreeFormView';
+import { AgentDocumentManager } from '../utils/AgentDocumentManager';
+import { Node as ProseMirrorNode } from 'prosemirror-model';
const generateTutorialNodeToolParams = [
{
@@ -28,20 +30,26 @@ const generateTutorialNodeToolInfo: ToolInfo<typeof generateTutorialNodeToolPara
parameterRules: generateTutorialNodeToolParams,
citationRules: "No citation needed for this tool's output.",
};
-const applyFormatting = (markdownText: string): { doc: any; plainText: string } => {
+
+interface FormattedDocument {
+ doc: ProseMirrorNode;
+ plainText: string;
+}
+
+const applyFormatting = (markdownText: string): FormattedDocument => {
const lines = markdownText.split('\n');
- const nodes: any[] = [];
+ const nodes: ProseMirrorNode[] = [];
let plainText = '';
let i = 0;
- let currentListItems: any[] = [];
- let currentParagraph: any[] = [];
- let currentOrderedListItems: any[] = [];
+ let currentListItems: ProseMirrorNode[] = [];
+ let currentParagraph: ProseMirrorNode[] = [];
+ let currentOrderedListItems: ProseMirrorNode[] = [];
let inOrderedList = false;
let inBulletList = false;
- const processBoldText = (text: string) => {
+ const processBoldText = (text: string): ProseMirrorNode[] => {
const boldRegex = /\*\*(.*?)\*\*/g;
- const parts = [];
+ const parts: ProseMirrorNode[] = [];
let lastIndex = 0;
let match;
@@ -58,7 +66,7 @@ const applyFormatting = (markdownText: string): { doc: any; plainText: string }
return parts.length > 0 ? parts : [schema.text(text)];
};
- const flushListItems = () => {
+ const flushListItems = (): void => {
if (currentListItems.length > 0) {
nodes.push(schema.nodes.ordered_list.create({ mapStyle: 'bullet' }, currentListItems));
nodes.push(schema.nodes.paragraph.create());
@@ -73,14 +81,14 @@ const applyFormatting = (markdownText: string): { doc: any; plainText: string }
}
};
- const flushParagraph = () => {
+ const flushParagraph = (): void => {
if (currentParagraph.length > 0) {
nodes.push(schema.nodes.paragraph.create({}, currentParagraph));
currentParagraph = [];
}
};
- const processHeader = (line: string) => {
+ 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
@@ -138,12 +146,11 @@ const applyFormatting = (markdownText: string): { doc: any; plainText: string }
};
export class GPTTutorialTool extends BaseTool<typeof generateTutorialNodeToolParams> {
- private _createDocInDash: (doc: parsedDoc) => Doc | undefined;
+ private _docManager: AgentDocumentManager;
- constructor(createDocInDash: (doc: parsedDoc) => Doc | undefined) {
+ constructor(docManager: AgentDocumentManager) {
super(generateTutorialNodeToolInfo);
-
- this._createDocInDash = createDocInDash;
+ this._docManager = docManager;
}
async execute(args: ParametersType<typeof generateTutorialNodeToolParams>): Promise<Observation[]> {
@@ -158,7 +165,7 @@ export class GPTTutorialTool extends BaseTool<typeof generateTutorialNodeToolPar
// Build the ProseMirror‐in‐JSON + plain-text for RichTextField
const rtfData = {
- doc: (doc as any).toJSON ? (doc as any).toJSON() : doc,
+ doc: doc.toJSON ? doc.toJSON() : doc,
selection: { type: 'text', anchor: 0, head: 0 },
storedMarks: [],
};
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index 5d8583873..9b24219cf 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -3,10 +3,9 @@ import { Button, Dropdown, DropdownType, IconButton, isDark, Size, Type } from '
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, Opt, returnEmptyDoclist } from '../../../fields/Doc';
+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';
import { GetEffectiveAcl } from '../../../fields/util';
@@ -28,10 +27,6 @@ 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';
/**
@@ -90,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}
/>
@@ -231,9 +226,11 @@ export class TopBar extends ObservableReactComponent<object> {
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: 'Dash Documentation Assistant',
+ title: `${userName}'s Dash Help Assistant`,
is_dash_doc_assistant: 'true',
});
DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight);