diff options
Diffstat (limited to 'src/client/views/nodes/chatbot/tools/UIControlTool.ts')
-rw-r--r-- | src/client/views/nodes/chatbot/tools/UIControlTool.ts | 566 |
1 files changed, 566 insertions, 0 deletions
diff --git a/src/client/views/nodes/chatbot/tools/UIControlTool.ts b/src/client/views/nodes/chatbot/tools/UIControlTool.ts new file mode 100644 index 000000000..252e77956 --- /dev/null +++ b/src/client/views/nodes/chatbot/tools/UIControlTool.ts @@ -0,0 +1,566 @@ +import { Doc, DocListCast } from '../../../../../fields/Doc'; +import { ScriptField } from '../../../../../fields/ScriptField'; +import { Cast, PromiseValue } from '../../../../../fields/Types'; +import { InkInkTool, InkTool } from '../../../../../fields/InkField'; +import { MainView } from '../../../MainView'; +import { DocumentView } from '../../DocumentView'; +import { RichTextMenu } from '../../formattedText/RichTextMenu'; +import { PropertiesView } from '../../../PropertiesView'; +import { CollectionViewType } from '../../../../documents/DocumentTypes'; +import { ParametersType, ToolInfo } from '../types/tool_types'; +import { Observation } from '../types/types'; +import { BaseTool } from './BaseTool'; + +const uiControlParams = [ + { + name: 'action', + type: 'string', + description: 'The UI action to perform. Options: "open_tab" (Files, Tools, Imports), "close_tab" (closes current sidebar tab), "select_tool" (pen, highlighter, eraser, text, circle, etc.), "change_font_size", "select_font", "switch_view", "toggle_tags", "toggle_properties_submenu", "toggle_properties", "toggle_header"', + required: true, + }, + { + name: 'target', + type: 'string', + description: 'The target of the action. For open_tab: "Files", "Tools", "Imports", "Trails", "Search", "Properties". For select_tool: "pen", "highlighter", "write", "math", "eraser", "text", "ink", "none". For select_font: "Arial", "Comic Sans MS", etc. For change_font_size: number as string. For switch_view: "freeform", "card", "carousel", "stacking", etc. For toggle_properties_submenu: "options", "fields", "appearance", "layout", "sharing", etc.', + required: false, + }, +] as const; + +type UIControlToolParamsType = typeof uiControlParams; + +const uiControlToolInfo: ToolInfo<UIControlToolParamsType> = { + name: 'uiControl', + citationRules: 'No citation needed for UI control actions.', + parameterRules: uiControlParams, + description: 'Control the Dash UI by opening/closing tabs, selecting tools, changing fonts, switching views, toggling tags, and managing properties sub-menus. Supports: tab management (Files, Tools, Properties, etc.), tool dropdowns (ink button opens pen/highlighter/write/math options, text button opens font/size/color options), font changes, view switching (freeform, card, etc.), tag visibility toggle, and properties panel sub-menu control (options, fields, appearance, etc.).', +}; + +export class UIControlTool extends BaseTool<UIControlToolParamsType> { + constructor() { + super(uiControlToolInfo); + } + + async execute(args: ParametersType<UIControlToolParamsType>): Promise<Observation[]> { + const { action, target } = args; + + try { + let result = ''; + + switch (action) { + case 'open_tab': + result = await this.openTab(target || ''); + break; + case 'close_tab': + result = this.closeTab(); + break; + case 'select_tool': + result = await this.selectTool(target || ''); + break; + case 'change_font_size': + result = await this.changeFontSize(String(target || '')); + break; + case 'select_font': + result = await this.selectFont(target || ''); + break; + case 'toggle_properties': + result = this.toggleProperties(); + break; + case 'toggle_header': + result = this.toggleHeader(); + break; + case 'switch_view': + result = this.switchView(target || ''); + break; + case 'toggle_tags': + result = this.toggleTags(); + break; + case 'toggle_properties_submenu': + result = this.togglePropertiesSubmenu(target || ''); + break; + default: + result = `Unknown action: ${action}`; + } + + return [ + { + type: 'text', + text: result, + }, + ]; + } catch (error) { + console.error('UIControlTool error:', error); + return [ + { + type: 'text', + text: `Error performing UI action: ${error}`, + }, + ]; + } + } + + private async openTab(tab: string): Promise<string> { + console.log(`[UIControlTool] Attempting to open tab: ${tab}`); + + const mainView = MainView.Instance; + if (!mainView) { + console.error('[UIControlTool] MainView.Instance is not available'); + return 'MainView not available'; + } + + const normalizedTab = tab.toLowerCase(); + console.log(`[UIControlTool] Normalized tab name: ${normalizedTab}`); + + try { + switch (normalizedTab) { + case 'files': + case 'filesystem': + console.log('[UIControlTool] Trying to open Files tab'); + const sidebarMenu = Doc.MyLeftSidebarMenu; + console.log('[UIControlTool] MyLeftSidebarMenu:', sidebarMenu); + + if (sidebarMenu?.data) { + const menuItems = DocListCast(sidebarMenu.data); + console.log('[UIControlTool] Menu items count:', menuItems.length); + + const filesBtn = menuItems.find(d => d.target === Doc.MyFilesystem); + console.log('[UIControlTool] Files button found:', !!filesBtn); + + if (filesBtn) { + mainView.selectLeftSidebarButton(filesBtn); + return 'Opened Files tab'; + } else { + return 'Files button not found in sidebar menu'; + } + } else { + return 'Sidebar menu data not available'; + } + + case 'tools': + console.log('[UIControlTool] Trying to open Tools tab'); + const toolsBtn = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MyTools); + console.log('[UIControlTool] Tools button found:', !!toolsBtn); + + if (toolsBtn) { + mainView.selectLeftSidebarButton(toolsBtn); + return 'Opened Tools tab'; + } else { + return 'Tools button not found in sidebar menu'; + } + + case 'imports': + console.log('[UIControlTool] Trying to open Imports tab'); + const importBtn = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MyImports); + console.log('[UIControlTool] Import button found:', !!importBtn); + + if (importBtn) { + mainView.selectLeftSidebarButton(importBtn); + return 'Opened Imports tab'; + } else { + return 'Imports button not found in sidebar menu'; + } + + case 'trails': + case 'presentations': + console.log('[UIControlTool] Trying to open Trails tab'); + const trailsBtn = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MyTrails); + console.log('[UIControlTool] Trails button found:', !!trailsBtn); + + if (trailsBtn) { + mainView.selectLeftSidebarButton(trailsBtn); + return 'Opened Trails/Presentations tab'; + } else { + return 'Trails button not found in sidebar menu'; + } + + case 'search': + console.log('[UIControlTool] Trying to open Search tab'); + const searchBtn = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MySearcher); + console.log('[UIControlTool] Search button found:', !!searchBtn); + + if (searchBtn) { + mainView.selectLeftSidebarButton(searchBtn); + return 'Opened Search tab'; + } else { + return 'Search button not found in sidebar menu'; + } + + case 'properties': + case 'property': + console.log('[UIControlTool] Trying to open Properties panel'); + // Properties is not a sidebar tab, it's a right-side panel + mainView.togglePropertiesFlyout(); + return 'Opened Properties panel'; + + default: + return `Unknown tab: ${tab}. Available: Files, Tools, Imports, Trails, Search, Properties`; + } + } catch (error) { + console.error(`[UIControlTool] Error opening tab ${tab}:`, error); + return `Error opening tab ${tab}: ${error}`; + } + } + + private closeTab(): string { + const mainView = MainView.Instance; + if (!mainView) { + return 'MainView not available'; + } + + try { + mainView.closeFlyout(); + return 'Closed sidebar tab'; + } catch (error) { + console.error('[UIControlTool] Error closing tab:', error); + return `Error closing tab: ${error}`; + } + } + + private async selectTool(tool: string): Promise<string> { + const normalizedTool = tool.toLowerCase(); + console.log(`[UIControlTool] Selecting tool: ${normalizedTool}`); + + try { + switch (normalizedTool) { + case 'pen': + case 'pen-nib': + Doc.ActiveInk = InkInkTool.Pen; + Doc.ActiveTool = InkTool.Ink; + console.log(`[UIControlTool] Set ActiveTool to ${Doc.ActiveTool}, ActiveInk to ${Doc.ActiveInk}`); + return 'Selected pen tool'; + + case 'highlighter': + case 'highlight': + Doc.ActiveInk = InkInkTool.Highlight; + Doc.ActiveTool = InkTool.Ink; + console.log(`[UIControlTool] Set ActiveTool to ${Doc.ActiveTool}, ActiveInk to ${Doc.ActiveInk}`); + return 'Selected highlighter tool'; + + case 'write': + case 'writing': + case 'handwriting': + Doc.ActiveInk = InkInkTool.Write; + Doc.ActiveTool = InkTool.Ink; + console.log(`[UIControlTool] Set ActiveTool to ${Doc.ActiveTool}, ActiveInk to ${Doc.ActiveInk}`); + return 'Selected handwriting tool'; + + case 'math': + case 'calculator': + Doc.ActiveInk = InkInkTool.Math; + Doc.ActiveTool = InkTool.Ink; + console.log(`[UIControlTool] Set ActiveTool to ${Doc.ActiveTool}, ActiveInk to ${Doc.ActiveInk}`); + return 'Selected math tool'; + + case 'eraser': + Doc.ActiveTool = InkTool.Eraser; + console.log(`[UIControlTool] Set ActiveTool to ${Doc.ActiveTool}`); + return 'Selected eraser tool'; + + case 'text': + case 'text tool': + case 'text button': + // Text button should open the text formatting dropdown, not just select text tool + // First ensure Tools panel is open + const mainView = MainView.Instance; + if (mainView) { + const toolsBtn = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MyTools); + if (toolsBtn) { + mainView.selectLeftSidebarButton(toolsBtn); + console.log(`[UIControlTool] Opened Tools panel for text button`); + + // The text dropdown should open automatically when Tools panel is opened + // and text documents are selected. This matches existing UI behavior. + return 'Opened text button dropdown in Tools panel. You can now access font, size, color, and other text formatting options.'; + } + } + return 'Could not open text button dropdown - Tools panel not available'; + + case 'ink': + case 'ink tool': + case 'ink button': + // Ink button should open the ink tools dropdown with pen, highlighter, write, math options + const mainViewInk = MainView.Instance; + if (mainViewInk) { + const toolsBtnInk = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MyTools); + if (toolsBtnInk) { + mainViewInk.selectLeftSidebarButton(toolsBtnInk); + console.log(`[UIControlTool] Opened Tools panel for ink button`); + + // The ink dropdown should open automatically when Tools panel is opened + // This matches existing UI behavior for MultiToggleButton types + return 'Opened ink button dropdown in Tools panel. You can now access pen, highlighter, write, and math tools.'; + } + } + return 'Could not open ink button dropdown - Tools panel not available'; + + case 'none': + case 'select': + case 'selection': + Doc.ActiveTool = InkTool.None; + console.log(`[UIControlTool] Set ActiveTool to ${Doc.ActiveTool} (selection tool)`); + return 'Selected selection tool'; + + case 'circle': + case 'shape': + // TODO: Implement shape tool selection when we find the API + return 'Shape tool selection not yet implemented'; + + default: + return `Unknown tool: ${tool}. Available tools: pen, highlighter, write, math, eraser, text, circle`; + } + } catch (error) { + console.error('[UIControlTool] Error selecting tool:', error); + return `Error selecting tool: ${error}`; + } + } + + private async selectFont(fontName: string): Promise<string> { + if (!fontName) { + return 'Font name is required'; + } + + try { + // First ensure text tool is selected + Doc.ActiveTool = InkTool.None; + + // Validate font name + const validFonts = ['Roboto', 'Roboto Mono', 'Nunito', 'Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text', 'Math']; + const normalizedFont = validFonts.find(font => + font.toLowerCase() === fontName.toLowerCase() || + font.toLowerCase().includes(fontName.toLowerCase()) + ); + + if (!normalizedFont) { + return `Font "${fontName}" not found. Available fonts: ${validFonts.join(', ')}`; + } + + // Try to set font using RichTextMenu + if (RichTextMenu.Instance) { + RichTextMenu.Instance.setFontField(normalizedFont, 'fontFamily'); + return `Selected font: ${normalizedFont}`; + } else { + // Fallback: Set on user document + Doc.UserDoc().fontFamily = normalizedFont; + return `Set default font to: ${normalizedFont}`; + } + } catch (error) { + console.error('[UIControlTool] Error selecting font:', error); + return `Error selecting font: ${error}`; + } + } + + private changeFontSize(size: string): string { + const fontSize = parseInt(size); + if (isNaN(fontSize) || fontSize < 1 || fontSize > 200) { + return 'Invalid font size. Please provide a number between 1 and 200.'; + } + + // Get selected text documents + const selectedViews = DocumentView.Selected(); + if (selectedViews.length === 0) { + return 'No text document selected. Please select a text document first.'; + } + + let changedCount = 0; + selectedViews.forEach(view => { + const doc = view.Document; + if (doc.type === 'rich text' || doc.type === 'text') { + // TODO: Find the correct property for font size + // This is a placeholder - need to find actual implementation + doc.$fontSize = fontSize; + changedCount++; + } + }); + + if (changedCount > 0) { + return `Changed font size to ${fontSize} for ${changedCount} document(s)`; + } else { + return 'No text documents selected to change font size'; + } + } + + private toggleProperties(): string { + MainView.Instance?.togglePropertiesFlyout(); + return 'Toggled properties panel'; + } + + private toggleHeader(): string { + MainView.Instance?.toggleTopBar(); + return 'Toggled header bar'; + } + + private switchView(viewType: string): string { + try { + // Use the existing setView function from globalScripts, which handles view switching properly + const normalizedView = viewType.toLowerCase(); + let mappedViewType: string; + + // Map common view names to CollectionViewType values + switch (normalizedView) { + case 'freeform': + case 'free form': + mappedViewType = CollectionViewType.Freeform; + break; + case 'card': + case 'card view': + mappedViewType = CollectionViewType.Card; + break; + case 'carousel': + mappedViewType = CollectionViewType.Carousel; + break; + case '3d carousel': + case 'carousel3d': + mappedViewType = CollectionViewType.Carousel3D; + break; + case 'stacking': + case 'stack': + mappedViewType = CollectionViewType.Stacking; + break; + case 'grid': + mappedViewType = CollectionViewType.Grid; + break; + case 'tree': + mappedViewType = CollectionViewType.Tree; + break; + case 'masonry': + mappedViewType = CollectionViewType.Masonry; + break; + case 'notetaking': + case 'note taking': + mappedViewType = CollectionViewType.NoteTaking; + break; + case 'schema': + mappedViewType = CollectionViewType.Schema; + break; + default: + return `Unknown view type: ${viewType}. Available: freeform, card, carousel, 3d carousel, stacking, grid, tree, masonry, notetaking, schema`; + } + + // Apply view change directly to selected document, mirroring setView function logic + const selected = DocumentView.Selected().lastElement(); + if (!selected) { + return 'No documents selected to switch view'; + } + + // Apply the view change (like setView function does) + if (selected.Document.type === 'collection') { + selected.Document._type_collection = mappedViewType; + return `Successfully switched to ${viewType} view`; + } else { + return 'Selected document is not a collection, cannot switch view'; + } + } catch (error) { + console.error('[UIControlTool] Error switching view:', error); + return `Error switching view: ${error}`; + } + } + + private toggleTags(): string { + try { + // Use the exact same logic as DocumentButtonBar keywordButton + const selectedDocs = DocumentView.Selected(); + if (selectedDocs.length === 0) { + return 'No documents selected to toggle tags'; + } + + // Check if ANY document is currently showing tags (like DocumentButtonBar does) + const showing = selectedDocs.some(dv => dv.showTags); + + // Set ALL documents to the OPPOSITE state (like DocumentButtonBar does) + selectedDocs.forEach(dv => { + dv.layoutDoc._layout_showTags = !showing; + }); + + const newState = !showing; + return `${newState ? 'Enabled' : 'Disabled'} tag display for ${selectedDocs.length} document(s)`; + } catch (error) { + console.error('[UIControlTool] Error toggling tags:', error); + return `Error toggling tags: ${error}`; + } + } + + private togglePropertiesSubmenu(submenuName: string): string { + try { + const propertiesView = PropertiesView.Instance; + if (!propertiesView) { + return 'Properties panel is not available'; + } + + const normalizedName = submenuName.toLowerCase(); + let toggledSubmenu = ''; + + switch (normalizedName) { + case 'options': + propertiesView.openOptions = !propertiesView.openOptions; + toggledSubmenu = 'Options'; + break; + case 'fields': + case 'fields & tags': + propertiesView.openFields = !propertiesView.openFields; + toggledSubmenu = 'Fields & Tags'; + break; + case 'appearance': + propertiesView.openAppearance = !propertiesView.openAppearance; + toggledSubmenu = 'Appearance'; + break; + case 'layout': + propertiesView.openLayout = !propertiesView.openLayout; + toggledSubmenu = 'Layout'; + break; + case 'sharing': + propertiesView.openSharing = !propertiesView.openSharing; + toggledSubmenu = 'Sharing'; + break; + case 'links': + propertiesView.openLinks = !propertiesView.openLinks; + toggledSubmenu = 'Links'; + break; + case 'contexts': + case 'other contexts': + propertiesView.openContexts = !propertiesView.openContexts; + toggledSubmenu = 'Other Contexts'; + break; + case 'filters': + propertiesView.openFilters = !propertiesView.openFilters; + toggledSubmenu = 'Filters'; + break; + case 'transform': + propertiesView.openTransform = !propertiesView.openTransform; + toggledSubmenu = 'Transform'; + break; + case 'firefly': + propertiesView.openFirefly = !propertiesView.openFirefly; + toggledSubmenu = 'Firefly'; + break; + case 'styling': + propertiesView.openStyling = !propertiesView.openStyling; + toggledSubmenu = 'Styling'; + break; + default: + return `Unknown submenu: ${submenuName}. Available: options, fields, appearance, layout, sharing, links, contexts, filters, transform, firefly, styling`; + } + + const currentState = this.getSubmenuState(propertiesView, normalizedName); + return `${currentState ? 'Opened' : 'Closed'} ${toggledSubmenu} submenu in Properties panel`; + } catch (error) { + console.error('[UIControlTool] Error toggling properties submenu:', error); + return `Error toggling properties submenu: ${error}`; + } + } + + private getSubmenuState(propertiesView: PropertiesView, submenuName: string): boolean { + switch (submenuName) { + case 'options': return propertiesView.openOptions; + case 'fields': return propertiesView.openFields; + case 'appearance': return propertiesView.openAppearance; + case 'layout': return propertiesView.openLayout; + case 'sharing': return propertiesView.openSharing; + case 'links': return propertiesView.openLinks; + case 'contexts': return propertiesView.openContexts; + case 'filters': return propertiesView.openFilters; + case 'transform': return propertiesView.openTransform; + case 'firefly': return propertiesView.openFirefly; + case 'styling': return propertiesView.openStyling; + default: return false; + } + } +}
\ No newline at end of file |