aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/chatbot/tools/UIControlTool.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/chatbot/tools/UIControlTool.ts')
-rw-r--r--src/client/views/nodes/chatbot/tools/UIControlTool.ts566
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