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 = { 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 { private _docManager: AgentDocumentManager; constructor(docManager: AgentDocumentManager) { super(generateTutorialNodeToolInfo); this._docManager = docManager; } async execute(args: ParametersType): Promise { const chunkId = uuidv4(); try { const query = (args.query || '').trim(); if (!query) { return [{ type: 'text', text: `Please provide a query.` }]; } 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: `Created tutorial node with ID ${formattedDoc[Id]}.`, }, ]; } catch (error) { return [ { type: 'text', text: `Error generating tutorial node: ${error}`, }, ]; } } }