From dc712e52ca9df83c29ffc46706952efd130c0ee2 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 20 May 2026 13:14:08 -0700 Subject: [PATCH 1/3] feat(google-slides): complete API surface for branded slide generation --- apps/sim/blocks/blocks/google_slides.ts | 2503 ++++++++++++++++- apps/sim/tools/google_slides/batch_update.ts | 139 + .../tools/google_slides/copy_presentation.ts | 128 + apps/sim/tools/google_slides/create_line.ts | 157 ++ .../google_slides/create_paragraph_bullets.ts | 160 ++ .../google_slides/create_sheets_chart.ts | 175 ++ apps/sim/tools/google_slides/create_video.ts | 165 ++ .../google_slides/delete_paragraph_bullets.ts | 150 + .../google_slides/delete_table_column.ts | 116 + .../tools/google_slides/delete_table_row.ts | 115 + apps/sim/tools/google_slides/delete_text.ts | 148 + .../google_slides/export_presentation.ts | 137 + apps/sim/tools/google_slides/group_objects.ts | 131 + apps/sim/tools/google_slides/index.ts | 85 + .../google_slides/insert_table_columns.ts | 139 + .../tools/google_slides/insert_table_rows.ts | 136 + .../tools/google_slides/merge_table_cells.ts | 137 + .../google_slides/refresh_sheets_chart.ts | 95 + .../replace_all_shapes_with_image.ts | 151 + .../replace_all_shapes_with_sheets_chart.ts | 164 ++ apps/sim/tools/google_slides/replace_image.ts | 125 + apps/sim/tools/google_slides/reroute_line.ts | 92 + .../tools/google_slides/ungroup_objects.ts | 103 + .../google_slides/unmerge_table_cells.ts | 137 + .../google_slides/update_image_properties.ts | 267 ++ .../google_slides/update_line_category.ts | 115 + .../google_slides/update_line_properties.ts | 201 ++ .../update_page_element_alt_text.ts | 114 + .../update_page_element_transform.ts | 171 ++ .../update_page_elements_z_order.ts | 122 + .../google_slides/update_page_properties.ts | 185 ++ .../google_slides/update_paragraph_style.ts | 288 ++ .../google_slides/update_shape_properties.ts | 246 ++ .../google_slides/update_slide_properties.ts | 140 + .../update_table_border_properties.ts | 227 ++ .../update_table_cell_properties.ts | 210 ++ .../update_table_column_properties.ts | 158 ++ .../update_table_row_properties.ts | 158 ++ .../tools/google_slides/update_text_style.ts | 311 ++ .../google_slides/update_video_properties.ts | 208 ++ apps/sim/tools/google_slides/utils.ts | 148 + apps/sim/tools/registry.ts | 77 + 42 files changed, 8933 insertions(+), 1 deletion(-) create mode 100644 apps/sim/tools/google_slides/batch_update.ts create mode 100644 apps/sim/tools/google_slides/copy_presentation.ts create mode 100644 apps/sim/tools/google_slides/create_line.ts create mode 100644 apps/sim/tools/google_slides/create_paragraph_bullets.ts create mode 100644 apps/sim/tools/google_slides/create_sheets_chart.ts create mode 100644 apps/sim/tools/google_slides/create_video.ts create mode 100644 apps/sim/tools/google_slides/delete_paragraph_bullets.ts create mode 100644 apps/sim/tools/google_slides/delete_table_column.ts create mode 100644 apps/sim/tools/google_slides/delete_table_row.ts create mode 100644 apps/sim/tools/google_slides/delete_text.ts create mode 100644 apps/sim/tools/google_slides/export_presentation.ts create mode 100644 apps/sim/tools/google_slides/group_objects.ts create mode 100644 apps/sim/tools/google_slides/insert_table_columns.ts create mode 100644 apps/sim/tools/google_slides/insert_table_rows.ts create mode 100644 apps/sim/tools/google_slides/merge_table_cells.ts create mode 100644 apps/sim/tools/google_slides/refresh_sheets_chart.ts create mode 100644 apps/sim/tools/google_slides/replace_all_shapes_with_image.ts create mode 100644 apps/sim/tools/google_slides/replace_all_shapes_with_sheets_chart.ts create mode 100644 apps/sim/tools/google_slides/replace_image.ts create mode 100644 apps/sim/tools/google_slides/reroute_line.ts create mode 100644 apps/sim/tools/google_slides/ungroup_objects.ts create mode 100644 apps/sim/tools/google_slides/unmerge_table_cells.ts create mode 100644 apps/sim/tools/google_slides/update_image_properties.ts create mode 100644 apps/sim/tools/google_slides/update_line_category.ts create mode 100644 apps/sim/tools/google_slides/update_line_properties.ts create mode 100644 apps/sim/tools/google_slides/update_page_element_alt_text.ts create mode 100644 apps/sim/tools/google_slides/update_page_element_transform.ts create mode 100644 apps/sim/tools/google_slides/update_page_elements_z_order.ts create mode 100644 apps/sim/tools/google_slides/update_page_properties.ts create mode 100644 apps/sim/tools/google_slides/update_paragraph_style.ts create mode 100644 apps/sim/tools/google_slides/update_shape_properties.ts create mode 100644 apps/sim/tools/google_slides/update_slide_properties.ts create mode 100644 apps/sim/tools/google_slides/update_table_border_properties.ts create mode 100644 apps/sim/tools/google_slides/update_table_cell_properties.ts create mode 100644 apps/sim/tools/google_slides/update_table_column_properties.ts create mode 100644 apps/sim/tools/google_slides/update_table_row_properties.ts create mode 100644 apps/sim/tools/google_slides/update_text_style.ts create mode 100644 apps/sim/tools/google_slides/update_video_properties.ts create mode 100644 apps/sim/tools/google_slides/utils.ts diff --git a/apps/sim/blocks/blocks/google_slides.ts b/apps/sim/blocks/blocks/google_slides.ts index 09260f1b394..4868e327e8e 100644 --- a/apps/sim/blocks/blocks/google_slides.ts +++ b/apps/sim/blocks/blocks/google_slides.ts @@ -13,7 +13,7 @@ export const GoogleSlidesBlock: BlockConfig = { hideFromToolbar: true, authMode: AuthMode.OAuth, longDescription: - 'Integrate Google Slides into the workflow. Can read, write, create presentations, replace text, add slides, add images, get thumbnails, get page details, delete objects, duplicate objects, reorder slides, create tables, create shapes, and insert text.', + 'Build, edit, and export branded Google Slides presentations end-to-end. Copy a template, replace text and image tokens, embed Sheets charts, style text and shapes with brand fonts and colors, manage tables and layouts, group elements, run atomic batch updates, and export to PDF or PPTX.', docsLink: 'https://docs.sim.ai/tools/google_slides', category: 'tools', integrationType: IntegrationType.Documents, @@ -30,7 +30,13 @@ export const GoogleSlidesBlock: BlockConfig = { { label: 'Read Presentation', id: 'read' }, { label: 'Write to Presentation', id: 'write' }, { label: 'Create Presentation', id: 'create' }, + { label: 'Copy Presentation', id: 'copy_presentation' }, + { label: 'Export Presentation', id: 'export_presentation' }, + { label: 'Batch Update (Raw)', id: 'batch_update' }, { label: 'Replace All Text', id: 'replace_all_text' }, + { label: 'Replace All Shapes With Image', id: 'replace_all_shapes_with_image' }, + { label: 'Replace Image', id: 'replace_image' }, + { label: 'Update Image Properties', id: 'update_image_properties' }, { label: 'Add Slide', id: 'add_slide' }, { label: 'Add Image', id: 'add_image' }, { label: 'Get Thumbnail', id: 'get_thumbnail' }, @@ -40,7 +46,42 @@ export const GoogleSlidesBlock: BlockConfig = { { label: 'Reorder Slides', id: 'reorder_slides' }, { label: 'Create Table', id: 'create_table' }, { label: 'Create Shape', id: 'create_shape' }, + { label: 'Create Line', id: 'create_line' }, { label: 'Insert Text', id: 'insert_text' }, + { label: 'Delete Text', id: 'delete_text' }, + { label: 'Update Text Style', id: 'update_text_style' }, + { label: 'Update Paragraph Style', id: 'update_paragraph_style' }, + { label: 'Create Paragraph Bullets', id: 'create_paragraph_bullets' }, + { label: 'Delete Paragraph Bullets', id: 'delete_paragraph_bullets' }, + { label: 'Update Shape Properties', id: 'update_shape_properties' }, + { label: 'Update Page Properties', id: 'update_page_properties' }, + { label: 'Update Slide Properties', id: 'update_slide_properties' }, + { label: 'Update Alt Text', id: 'update_page_element_alt_text' }, + { label: 'Update Element Transform', id: 'update_page_element_transform' }, + { label: 'Update Z-Order', id: 'update_page_elements_z_order' }, + { label: 'Group Objects', id: 'group_objects' }, + { label: 'Ungroup Objects', id: 'ungroup_objects' }, + { label: 'Update Line Properties', id: 'update_line_properties' }, + { label: 'Update Line Category', id: 'update_line_category' }, + { label: 'Reroute Line', id: 'reroute_line' }, + { label: 'Insert Table Rows', id: 'insert_table_rows' }, + { label: 'Insert Table Columns', id: 'insert_table_columns' }, + { label: 'Delete Table Row', id: 'delete_table_row' }, + { label: 'Delete Table Column', id: 'delete_table_column' }, + { label: 'Merge Table Cells', id: 'merge_table_cells' }, + { label: 'Unmerge Table Cells', id: 'unmerge_table_cells' }, + { label: 'Update Table Cell Properties', id: 'update_table_cell_properties' }, + { label: 'Update Table Border Properties', id: 'update_table_border_properties' }, + { label: 'Update Table Column Properties', id: 'update_table_column_properties' }, + { label: 'Update Table Row Properties', id: 'update_table_row_properties' }, + { label: 'Embed Sheets Chart', id: 'create_sheets_chart' }, + { label: 'Refresh Sheets Chart', id: 'refresh_sheets_chart' }, + { + label: 'Replace All Shapes With Sheets Chart', + id: 'replace_all_shapes_with_sheets_chart', + }, + { label: 'Embed Video', id: 'create_video' }, + { label: 'Update Video Properties', id: 'update_video_properties' }, ], value: () => 'read', }, @@ -95,6 +136,43 @@ export const GoogleSlidesBlock: BlockConfig = { 'create_table', 'create_shape', 'insert_text', + 'batch_update', + 'export_presentation', + 'replace_all_shapes_with_image', + 'replace_image', + 'update_image_properties', + 'update_text_style', + 'update_paragraph_style', + 'delete_text', + 'create_paragraph_bullets', + 'delete_paragraph_bullets', + 'update_shape_properties', + 'update_page_properties', + 'update_slide_properties', + 'update_page_element_alt_text', + 'update_page_element_transform', + 'update_page_elements_z_order', + 'group_objects', + 'ungroup_objects', + 'create_line', + 'update_line_properties', + 'update_line_category', + 'reroute_line', + 'insert_table_rows', + 'insert_table_columns', + 'delete_table_row', + 'delete_table_column', + 'merge_table_cells', + 'unmerge_table_cells', + 'update_table_cell_properties', + 'update_table_border_properties', + 'update_table_column_properties', + 'update_table_row_properties', + 'create_sheets_chart', + 'refresh_sheets_chart', + 'replace_all_shapes_with_sheets_chart', + 'create_video', + 'update_video_properties', ], }, }, @@ -123,6 +201,43 @@ export const GoogleSlidesBlock: BlockConfig = { 'create_table', 'create_shape', 'insert_text', + 'batch_update', + 'export_presentation', + 'replace_all_shapes_with_image', + 'replace_image', + 'update_image_properties', + 'update_text_style', + 'update_paragraph_style', + 'delete_text', + 'create_paragraph_bullets', + 'delete_paragraph_bullets', + 'update_shape_properties', + 'update_page_properties', + 'update_slide_properties', + 'update_page_element_alt_text', + 'update_page_element_transform', + 'update_page_elements_z_order', + 'group_objects', + 'ungroup_objects', + 'create_line', + 'update_line_properties', + 'update_line_category', + 'reroute_line', + 'insert_table_rows', + 'insert_table_columns', + 'delete_table_row', + 'delete_table_column', + 'merge_table_cells', + 'unmerge_table_cells', + 'update_table_cell_properties', + 'update_table_border_properties', + 'update_table_column_properties', + 'update_table_row_properties', + 'create_sheets_chart', + 'refresh_sheets_chart', + 'replace_all_shapes_with_sheets_chart', + 'create_video', + 'update_video_properties', ], }, }, @@ -621,6 +736,1610 @@ Return ONLY the text content - no explanations, no markdown formatting markers, placeholder: 'Zero-based index (default: 0)', condition: { field: 'operation', value: 'insert_text' }, }, + + // ========== Copy Presentation Operation Fields ========== + { + id: 'sourcePresentationSelector', + title: 'Source Presentation', + type: 'file-selector', + canonicalParamId: 'sourcePresentationId', + serviceId: 'google-drive', + selectorKey: 'google.drive', + requiredScopes: [], + mimeType: 'application/vnd.google-apps.presentation', + placeholder: 'Select template presentation to copy', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: 'copy_presentation' }, + required: true, + }, + { + id: 'manualSourcePresentationId', + title: 'Source Presentation ID', + type: 'short-input', + canonicalParamId: 'sourcePresentationId', + placeholder: 'Enter source presentation ID', + dependsOn: ['credential'], + mode: 'advanced', + condition: { field: 'operation', value: 'copy_presentation' }, + required: true, + }, + { + id: 'copyTitle', + title: 'Copy Title', + type: 'short-input', + placeholder: 'Title for the copy (defaults to "Copy of ")', + condition: { field: 'operation', value: 'copy_presentation' }, + }, + { + id: 'copyFolderSelector', + title: 'Destination Folder', + type: 'file-selector', + canonicalParamId: 'copyFolderId', + serviceId: 'google-drive', + selectorKey: 'google.drive', + requiredScopes: [], + mimeType: 'application/vnd.google-apps.folder', + placeholder: 'Select destination folder (optional)', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: 'copy_presentation' }, + }, + { + id: 'manualCopyFolderId', + title: 'Destination Folder ID', + type: 'short-input', + canonicalParamId: 'copyFolderId', + placeholder: 'Folder ID (optional)', + dependsOn: ['credential'], + mode: 'advanced', + condition: { field: 'operation', value: 'copy_presentation' }, + }, + + // ========== Export Presentation Operation Fields ========== + { + id: 'exportFormat', + title: 'Export Format', + type: 'dropdown', + options: [ + { label: 'PDF', id: 'PDF' }, + { label: 'PowerPoint (PPTX)', id: 'PPTX' }, + { label: 'OpenDocument (ODP)', id: 'ODP' }, + { label: 'Plain Text', id: 'TXT' }, + { label: 'PNG (first slide)', id: 'PNG' }, + { label: 'JPEG (first slide)', id: 'JPEG' }, + { label: 'SVG (first slide)', id: 'SVG' }, + ], + value: () => 'PDF', + condition: { field: 'operation', value: 'export_presentation' }, + }, + + // ========== Batch Update (Raw) Operation Fields ========== + { + id: 'requestsJson', + title: 'Requests (JSON Array)', + type: 'long-input', + placeholder: + 'JSON array of Slides API Request objects, e.g. [{"replaceAllText":{...}}, {"updatePageProperties":{...}}]', + condition: { field: 'operation', value: 'batch_update' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Produce a JSON array of Google Slides API Request objects matching the user's intent. Each item must be a valid Request — for example {"replaceAllText": {...}}, {"updateTextStyle": {...}}, {"createSlide": {...}}. Return ONLY the JSON array, no commentary.`, + placeholder: 'Describe the batch update you want to run...', + generationType: 'json-object', + }, + }, + { + id: 'writeControlJson', + title: 'Write Control (JSON)', + type: 'long-input', + placeholder: '{"requiredRevisionId":"..."} or {"targetRevisionId":"..."}', + condition: { field: 'operation', value: 'batch_update' }, + mode: 'advanced', + }, + + // ========== Replace All Shapes With Image Fields ========== + { + id: 'replaceShapesImageUrl', + title: 'Image URL', + type: 'short-input', + placeholder: 'Publicly fetchable image URL (PNG, JPEG, GIF)', + condition: { field: 'operation', value: 'replace_all_shapes_with_image' }, + required: true, + }, + { + id: 'replaceShapesFindText', + title: 'Find Text (Token)', + type: 'short-input', + placeholder: 'Shape text token, e.g. {{cover-image}}', + condition: { field: 'operation', value: 'replace_all_shapes_with_image' }, + required: true, + }, + { + id: 'replaceShapesMatchCase', + title: 'Match Case', + type: 'switch', + condition: { field: 'operation', value: 'replace_all_shapes_with_image' }, + }, + { + id: 'replaceShapesImageMethod', + title: 'Image Fit', + type: 'dropdown', + options: [ + { label: 'Center Inside (preserve aspect)', id: 'CENTER_INSIDE' }, + { label: 'Center Crop (fill, crop)', id: 'CENTER_CROP' }, + ], + value: () => 'CENTER_INSIDE', + condition: { field: 'operation', value: 'replace_all_shapes_with_image' }, + }, + { + id: 'replaceShapesPageObjectIds', + title: 'Limit to Slides (IDs)', + type: 'short-input', + placeholder: 'Comma-separated slide IDs (empty = all)', + condition: { field: 'operation', value: 'replace_all_shapes_with_image' }, + mode: 'advanced', + }, + + // ========== Replace Image Fields ========== + { + id: 'replaceImageObjectId', + title: 'Image Object ID', + type: 'short-input', + placeholder: 'Object ID of the existing image to replace', + condition: { field: 'operation', value: 'replace_image' }, + required: true, + }, + { + id: 'replaceImageUrl', + title: 'New Image URL', + type: 'short-input', + placeholder: 'Publicly fetchable image URL', + condition: { field: 'operation', value: 'replace_image' }, + required: true, + }, + { + id: 'replaceImageMethod', + title: 'Image Fit', + type: 'dropdown', + options: [ + { label: 'Center Inside (preserve aspect)', id: 'CENTER_INSIDE' }, + { label: 'Center Crop (fill, crop)', id: 'CENTER_CROP' }, + ], + value: () => 'CENTER_INSIDE', + condition: { field: 'operation', value: 'replace_image' }, + }, + + // ========== Update Image Properties Fields ========== + { + id: 'imagePropsObjectId', + title: 'Image Object ID', + type: 'short-input', + placeholder: 'Object ID of the image', + condition: { field: 'operation', value: 'update_image_properties' }, + required: true, + }, + { + id: 'imageBrightness', + title: 'Brightness', + type: 'short-input', + placeholder: '-1.0 to 1.0', + condition: { field: 'operation', value: 'update_image_properties' }, + }, + { + id: 'imageContrast', + title: 'Contrast', + type: 'short-input', + placeholder: '-1.0 to 1.0', + condition: { field: 'operation', value: 'update_image_properties' }, + }, + { + id: 'imageTransparency', + title: 'Transparency', + type: 'short-input', + placeholder: '0.0 (opaque) to 1.0 (transparent)', + condition: { field: 'operation', value: 'update_image_properties' }, + }, + { + id: 'imageLinkUrl', + title: 'Link URL', + type: 'short-input', + placeholder: 'Make the image a hyperlink', + condition: { field: 'operation', value: 'update_image_properties' }, + }, + { + id: 'imageOutlineColor', + title: 'Outline Color', + type: 'short-input', + placeholder: 'Hex, e.g. #1A73E8', + condition: { field: 'operation', value: 'update_image_properties' }, + mode: 'advanced', + }, + { + id: 'imageOutlineWeight', + title: 'Outline Weight (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'update_image_properties' }, + mode: 'advanced', + }, + { + id: 'imageOutlineDashStyle', + title: 'Outline Dash Style', + type: 'short-input', + placeholder: 'SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT', + condition: { field: 'operation', value: 'update_image_properties' }, + mode: 'advanced', + }, + { + id: 'imagePropertiesJson', + title: 'Properties JSON (advanced)', + type: 'long-input', + placeholder: 'Raw ImageProperties JSON (merged with the simple fields)', + condition: { field: 'operation', value: 'update_image_properties' }, + mode: 'advanced', + }, + { + id: 'imagePropertiesFields', + title: 'FieldMask (advanced)', + type: 'short-input', + placeholder: 'Comma-separated field mask', + condition: { field: 'operation', value: 'update_image_properties' }, + mode: 'advanced', + }, + + // ========== Text Style Fields ========== + { + id: 'textObjectId', + title: 'Object ID', + type: 'short-input', + placeholder: 'Shape or table object ID containing the text', + condition: { + field: 'operation', + value: [ + 'update_text_style', + 'update_paragraph_style', + 'delete_text', + 'create_paragraph_bullets', + 'delete_paragraph_bullets', + ], + }, + required: true, + }, + { + id: 'textRowIndex', + title: 'Table Cell Row Index', + type: 'short-input', + placeholder: 'Zero-based row (for table cells only)', + condition: { + field: 'operation', + value: [ + 'update_text_style', + 'update_paragraph_style', + 'delete_text', + 'create_paragraph_bullets', + 'delete_paragraph_bullets', + ], + }, + mode: 'advanced', + }, + { + id: 'textColumnIndex', + title: 'Table Cell Column Index', + type: 'short-input', + placeholder: 'Zero-based column (for table cells only)', + condition: { + field: 'operation', + value: [ + 'update_text_style', + 'update_paragraph_style', + 'delete_text', + 'create_paragraph_bullets', + 'delete_paragraph_bullets', + ], + }, + mode: 'advanced', + }, + { + id: 'textRangeType', + title: 'Range Type', + type: 'dropdown', + options: [ + { label: 'All Text', id: 'ALL' }, + { label: 'From Start Index', id: 'FROM_START_INDEX' }, + { label: 'Fixed Range', id: 'FIXED_RANGE' }, + ], + value: () => 'ALL', + condition: { + field: 'operation', + value: [ + 'update_text_style', + 'update_paragraph_style', + 'delete_text', + 'create_paragraph_bullets', + 'delete_paragraph_bullets', + ], + }, + }, + { + id: 'textStartIndex', + title: 'Start Index', + type: 'short-input', + placeholder: 'Required for FROM_START_INDEX or FIXED_RANGE', + condition: { + field: 'operation', + value: [ + 'update_text_style', + 'update_paragraph_style', + 'delete_text', + 'create_paragraph_bullets', + 'delete_paragraph_bullets', + ], + }, + mode: 'advanced', + }, + { + id: 'textEndIndex', + title: 'End Index', + type: 'short-input', + placeholder: 'Required for FIXED_RANGE', + condition: { + field: 'operation', + value: [ + 'update_text_style', + 'update_paragraph_style', + 'delete_text', + 'create_paragraph_bullets', + 'delete_paragraph_bullets', + ], + }, + mode: 'advanced', + }, + + // update_text_style specific + { + id: 'textBold', + title: 'Bold', + type: 'switch', + condition: { field: 'operation', value: 'update_text_style' }, + }, + { + id: 'textItalic', + title: 'Italic', + type: 'switch', + condition: { field: 'operation', value: 'update_text_style' }, + }, + { + id: 'textUnderline', + title: 'Underline', + type: 'switch', + condition: { field: 'operation', value: 'update_text_style' }, + }, + { + id: 'textStrikethrough', + title: 'Strikethrough', + type: 'switch', + condition: { field: 'operation', value: 'update_text_style' }, + mode: 'advanced', + }, + { + id: 'textSmallCaps', + title: 'Small Caps', + type: 'switch', + condition: { field: 'operation', value: 'update_text_style' }, + mode: 'advanced', + }, + { + id: 'textFontFamily', + title: 'Font Family', + type: 'short-input', + placeholder: 'e.g. Inter, Roboto, Arial', + condition: { field: 'operation', value: 'update_text_style' }, + }, + { + id: 'textFontSize', + title: 'Font Size (pt)', + type: 'short-input', + placeholder: 'Numeric, e.g. 14', + condition: { field: 'operation', value: 'update_text_style' }, + }, + { + id: 'textForegroundColor', + title: 'Text Color', + type: 'short-input', + placeholder: 'Hex, e.g. #1A73E8', + condition: { field: 'operation', value: 'update_text_style' }, + }, + { + id: 'textBackgroundColor', + title: 'Text Background Color', + type: 'short-input', + placeholder: 'Hex, e.g. #FFF8E1', + condition: { field: 'operation', value: 'update_text_style' }, + mode: 'advanced', + }, + { + id: 'textLinkUrl', + title: 'Link URL', + type: 'short-input', + placeholder: 'Hyperlink URL for the range', + condition: { field: 'operation', value: 'update_text_style' }, + mode: 'advanced', + }, + { + id: 'textBaselineOffset', + title: 'Baseline Offset', + type: 'dropdown', + options: [ + { label: 'None', id: 'NONE' }, + { label: 'Superscript', id: 'SUPERSCRIPT' }, + { label: 'Subscript', id: 'SUBSCRIPT' }, + ], + condition: { field: 'operation', value: 'update_text_style' }, + mode: 'advanced', + }, + { + id: 'textStyleJson', + title: 'Style JSON (advanced)', + type: 'long-input', + placeholder: 'Raw TextStyle JSON (merged with the simple fields)', + condition: { field: 'operation', value: 'update_text_style' }, + mode: 'advanced', + }, + { + id: 'textStyleFields', + title: 'FieldMask (advanced)', + type: 'short-input', + placeholder: 'Comma-separated field mask', + condition: { field: 'operation', value: 'update_text_style' }, + mode: 'advanced', + }, + + // update_paragraph_style specific + { + id: 'paragraphAlignment', + title: 'Alignment', + type: 'dropdown', + options: [ + { label: 'Start', id: 'START' }, + { label: 'Center', id: 'CENTER' }, + { label: 'End', id: 'END' }, + { label: 'Justified', id: 'JUSTIFIED' }, + ], + condition: { field: 'operation', value: 'update_paragraph_style' }, + }, + { + id: 'paragraphLineSpacing', + title: 'Line Spacing (%)', + type: 'short-input', + placeholder: '100 = single, 200 = double', + condition: { field: 'operation', value: 'update_paragraph_style' }, + }, + { + id: 'paragraphIndentStart', + title: 'Indent Start (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'update_paragraph_style' }, + mode: 'advanced', + }, + { + id: 'paragraphIndentEnd', + title: 'Indent End (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'update_paragraph_style' }, + mode: 'advanced', + }, + { + id: 'paragraphIndentFirstLine', + title: 'First-Line Indent (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'update_paragraph_style' }, + mode: 'advanced', + }, + { + id: 'paragraphSpaceAbove', + title: 'Space Above (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'update_paragraph_style' }, + mode: 'advanced', + }, + { + id: 'paragraphSpaceBelow', + title: 'Space Below (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'update_paragraph_style' }, + mode: 'advanced', + }, + { + id: 'paragraphDirection', + title: 'Direction', + type: 'dropdown', + options: [ + { label: 'Left to Right', id: 'LEFT_TO_RIGHT' }, + { label: 'Right to Left', id: 'RIGHT_TO_LEFT' }, + ], + condition: { field: 'operation', value: 'update_paragraph_style' }, + mode: 'advanced', + }, + { + id: 'paragraphSpacingMode', + title: 'Spacing Mode', + type: 'dropdown', + options: [ + { label: 'Never Collapse', id: 'NEVER_COLLAPSE' }, + { label: 'Collapse Lists', id: 'COLLAPSE_LISTS' }, + ], + condition: { field: 'operation', value: 'update_paragraph_style' }, + mode: 'advanced', + }, + { + id: 'paragraphStyleJson', + title: 'Style JSON (advanced)', + type: 'long-input', + placeholder: 'Raw ParagraphStyle JSON (merged with the simple fields)', + condition: { field: 'operation', value: 'update_paragraph_style' }, + mode: 'advanced', + }, + { + id: 'paragraphStyleFields', + title: 'FieldMask (advanced)', + type: 'short-input', + placeholder: 'Comma-separated, e.g. alignment,lineSpacing,indentStart', + condition: { field: 'operation', value: 'update_paragraph_style' }, + mode: 'advanced', + }, + + // create_paragraph_bullets specific + { + id: 'bulletPreset', + title: 'Bullet Preset', + type: 'dropdown', + options: [ + { label: 'Disc / Circle / Square', id: 'BULLET_DISC_CIRCLE_SQUARE' }, + { label: 'Diamond / Arrow / Disc', id: 'BULLET_DIAMONDX_ARROW3D_SQUARE' }, + { label: 'Checkbox', id: 'BULLET_CHECKBOX' }, + { label: 'Arrow / Diamond / Disc', id: 'BULLET_ARROW_DIAMOND_DISC' }, + { label: 'Star / Circle / Disc', id: 'BULLET_STAR_CIRCLE_DISC' }, + { label: 'Arrow3D / Circle / Square', id: 'BULLET_ARROW3D_CIRCLE_SQUARE' }, + { label: 'Left Triangle / Diamond / Disc', id: 'BULLET_LEFTTRIANGLE_DIAMOND_DISC' }, + { label: 'Numbered Digit/Alpha/Roman', id: 'NUMBERED_DIGIT_ALPHA_ROMAN' }, + { label: 'Numbered Digit/Alpha/Roman (parens)', id: 'NUMBERED_DIGIT_ALPHA_ROMAN_PARENS' }, + { label: 'Numbered Digit Nested', id: 'NUMBERED_DIGIT_NESTED' }, + { label: 'Numbered Upper Alpha / Alpha / Roman', id: 'NUMBERED_UPPERALPHA_ALPHA_ROMAN' }, + { + label: 'Numbered Upper Roman / Upper Alpha / Digit', + id: 'NUMBERED_UPPERROMAN_UPPERALPHA_DIGIT', + }, + { label: 'Numbered Zero-Digit / Alpha / Roman', id: 'NUMBERED_ZERODIGIT_ALPHA_ROMAN' }, + ], + value: () => 'BULLET_DISC_CIRCLE_SQUARE', + condition: { field: 'operation', value: 'create_paragraph_bullets' }, + }, + + // ========== Update Shape Properties Fields ========== + { + id: 'shapePropsObjectId', + title: 'Shape Object ID', + type: 'short-input', + placeholder: 'Object ID of the shape', + condition: { field: 'operation', value: 'update_shape_properties' }, + required: true, + }, + { + id: 'shapeFillColor', + title: 'Fill Color', + type: 'short-input', + placeholder: 'Hex, e.g. #FF6F61', + condition: { field: 'operation', value: 'update_shape_properties' }, + }, + { + id: 'shapeFillAlpha', + title: 'Fill Opacity', + type: 'short-input', + placeholder: '0.0 to 1.0', + condition: { field: 'operation', value: 'update_shape_properties' }, + mode: 'advanced', + }, + { + id: 'shapeFillUnset', + title: 'Clear Fill (inherit)', + type: 'switch', + condition: { field: 'operation', value: 'update_shape_properties' }, + mode: 'advanced', + }, + { + id: 'shapeOutlineColor', + title: 'Outline Color', + type: 'short-input', + placeholder: 'Hex', + condition: { field: 'operation', value: 'update_shape_properties' }, + }, + { + id: 'shapeOutlineWeight', + title: 'Outline Weight (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'update_shape_properties' }, + }, + { + id: 'shapeOutlineDashStyle', + title: 'Outline Dash Style', + type: 'short-input', + placeholder: 'SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT', + condition: { field: 'operation', value: 'update_shape_properties' }, + mode: 'advanced', + }, + { + id: 'shapeOutlineUnset', + title: 'Clear Outline (inherit)', + type: 'switch', + condition: { field: 'operation', value: 'update_shape_properties' }, + mode: 'advanced', + }, + { + id: 'shapeLinkUrl', + title: 'Link URL', + type: 'short-input', + condition: { field: 'operation', value: 'update_shape_properties' }, + mode: 'advanced', + }, + { + id: 'shapeContentAlignment', + title: 'Content Alignment', + type: 'dropdown', + options: [ + { label: 'Top', id: 'TOP' }, + { label: 'Middle', id: 'MIDDLE' }, + { label: 'Bottom', id: 'BOTTOM' }, + ], + condition: { field: 'operation', value: 'update_shape_properties' }, + }, + { + id: 'shapeAutofitType', + title: 'Autofit', + type: 'dropdown', + options: [ + { label: 'None', id: 'NONE' }, + { label: 'Text Autofit', id: 'TEXT_AUTOFIT' }, + { label: 'Shape Autofit', id: 'SHAPE_AUTOFIT' }, + ], + condition: { field: 'operation', value: 'update_shape_properties' }, + mode: 'advanced', + }, + { + id: 'shapePropertiesJson', + title: 'Properties JSON (advanced)', + type: 'long-input', + condition: { field: 'operation', value: 'update_shape_properties' }, + mode: 'advanced', + }, + { + id: 'shapePropertiesFields', + title: 'FieldMask (advanced)', + type: 'short-input', + placeholder: 'Comma-separated, e.g. shapeBackgroundFill,outline,contentAlignment', + condition: { field: 'operation', value: 'update_shape_properties' }, + mode: 'advanced', + }, + + // ========== Update Page Properties Fields ========== + { + id: 'pagePropsObjectId', + title: 'Slide ID', + type: 'short-input', + placeholder: 'Object ID of the slide', + condition: { field: 'operation', value: 'update_page_properties' }, + required: true, + }, + { + id: 'pageBackgroundColor', + title: 'Background Color', + type: 'short-input', + placeholder: 'Hex, e.g. #0B1F3A', + condition: { field: 'operation', value: 'update_page_properties' }, + }, + { + id: 'pageBackgroundAlpha', + title: 'Background Opacity', + type: 'short-input', + placeholder: '0.0 to 1.0', + condition: { field: 'operation', value: 'update_page_properties' }, + mode: 'advanced', + }, + { + id: 'pageBackgroundImageUrl', + title: 'Background Image URL', + type: 'short-input', + placeholder: 'Publicly fetchable image URL', + condition: { field: 'operation', value: 'update_page_properties' }, + }, + { + id: 'pageBackgroundUnset', + title: 'Clear Background (inherit)', + type: 'switch', + condition: { field: 'operation', value: 'update_page_properties' }, + mode: 'advanced', + }, + { + id: 'pagePropertiesJson', + title: 'Properties JSON (advanced)', + type: 'long-input', + condition: { field: 'operation', value: 'update_page_properties' }, + mode: 'advanced', + }, + { + id: 'pagePropertiesFields', + title: 'FieldMask (advanced)', + type: 'short-input', + placeholder: 'Comma-separated, e.g. pageBackgroundFill', + condition: { field: 'operation', value: 'update_page_properties' }, + mode: 'advanced', + }, + + // ========== Update Slide Properties Fields ========== + { + id: 'slidePropsObjectId', + title: 'Slide ID', + type: 'short-input', + placeholder: 'Object ID of the slide', + condition: { field: 'operation', value: 'update_slide_properties' }, + required: true, + }, + { + id: 'slideIsSkipped', + title: 'Skip Slide in Presentation', + type: 'switch', + condition: { field: 'operation', value: 'update_slide_properties' }, + }, + { + id: 'slidePropertiesJson', + title: 'Properties JSON (advanced)', + type: 'long-input', + condition: { field: 'operation', value: 'update_slide_properties' }, + mode: 'advanced', + }, + { + id: 'slidePropertiesFields', + title: 'FieldMask (advanced)', + type: 'short-input', + placeholder: 'Comma-separated, e.g. isSkipped', + condition: { field: 'operation', value: 'update_slide_properties' }, + mode: 'advanced', + }, + + // ========== Update Alt Text Fields ========== + { + id: 'altTextObjectId', + title: 'Element Object ID', + type: 'short-input', + condition: { field: 'operation', value: 'update_page_element_alt_text' }, + required: true, + }, + { + id: 'altTextTitle', + title: 'Accessibility Title', + type: 'short-input', + condition: { field: 'operation', value: 'update_page_element_alt_text' }, + }, + { + id: 'altTextDescription', + title: 'Accessibility Description', + type: 'long-input', + condition: { field: 'operation', value: 'update_page_element_alt_text' }, + }, + + // ========== Update Element Transform Fields ========== + { + id: 'transformObjectId', + title: 'Element Object ID', + type: 'short-input', + condition: { field: 'operation', value: 'update_page_element_transform' }, + required: true, + }, + { + id: 'transformScaleX', + title: 'Scale X', + type: 'short-input', + placeholder: 'Default 1', + condition: { field: 'operation', value: 'update_page_element_transform' }, + }, + { + id: 'transformScaleY', + title: 'Scale Y', + type: 'short-input', + placeholder: 'Default 1', + condition: { field: 'operation', value: 'update_page_element_transform' }, + }, + { + id: 'transformShearX', + title: 'Shear X', + type: 'short-input', + condition: { field: 'operation', value: 'update_page_element_transform' }, + mode: 'advanced', + }, + { + id: 'transformShearY', + title: 'Shear Y', + type: 'short-input', + condition: { field: 'operation', value: 'update_page_element_transform' }, + mode: 'advanced', + }, + { + id: 'transformTranslateX', + title: 'X Position (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'update_page_element_transform' }, + }, + { + id: 'transformTranslateY', + title: 'Y Position (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'update_page_element_transform' }, + }, + { + id: 'transformApplyMode', + title: 'Apply Mode', + type: 'dropdown', + options: [ + { label: 'Absolute (replace)', id: 'ABSOLUTE' }, + { label: 'Relative (multiply)', id: 'RELATIVE' }, + ], + value: () => 'ABSOLUTE', + condition: { field: 'operation', value: 'update_page_element_transform' }, + }, + + // ========== Z-Order Fields ========== + { + id: 'zOrderObjectIds', + title: 'Object IDs', + type: 'short-input', + placeholder: 'Comma-separated element IDs', + condition: { field: 'operation', value: 'update_page_elements_z_order' }, + required: true, + }, + { + id: 'zOrderOperation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Bring to Front', id: 'BRING_TO_FRONT' }, + { label: 'Bring Forward', id: 'BRING_FORWARD' }, + { label: 'Send Backward', id: 'SEND_BACKWARD' }, + { label: 'Send to Back', id: 'SEND_TO_BACK' }, + ], + condition: { field: 'operation', value: 'update_page_elements_z_order' }, + required: true, + }, + + // ========== Group / Ungroup Fields ========== + { + id: 'groupChildrenObjectIds', + title: 'Children Object IDs', + type: 'short-input', + placeholder: 'Comma-separated element IDs (same slide)', + condition: { field: 'operation', value: 'group_objects' }, + required: true, + }, + { + id: 'groupObjectIdInput', + title: 'Group ID (optional)', + type: 'short-input', + placeholder: 'Custom group object ID', + condition: { field: 'operation', value: 'group_objects' }, + mode: 'advanced', + }, + { + id: 'ungroupObjectIds', + title: 'Group Object IDs', + type: 'short-input', + placeholder: 'Comma-separated group IDs', + condition: { field: 'operation', value: 'ungroup_objects' }, + required: true, + }, + + // ========== Create Line Fields ========== + { + id: 'linePageObjectId', + title: 'Slide ID', + type: 'short-input', + placeholder: 'Object ID of the slide', + condition: { field: 'operation', value: 'create_line' }, + required: true, + }, + { + id: 'lineCategory', + title: 'Line Category', + type: 'dropdown', + options: [ + { label: 'Straight', id: 'STRAIGHT' }, + { label: 'Bent', id: 'BENT' }, + { label: 'Curved', id: 'CURVED' }, + ], + value: () => 'STRAIGHT', + condition: { field: 'operation', value: 'create_line' }, + }, + { + id: 'lineWidth', + title: 'Width (pt)', + type: 'short-input', + placeholder: 'Default 200', + condition: { field: 'operation', value: 'create_line' }, + }, + { + id: 'lineHeight', + title: 'Height (pt)', + type: 'short-input', + placeholder: 'Default 1', + condition: { field: 'operation', value: 'create_line' }, + }, + { + id: 'linePositionX', + title: 'X Position (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'create_line' }, + }, + { + id: 'linePositionY', + title: 'Y Position (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'create_line' }, + }, + + // ========== Update Line Properties Fields ========== + { + id: 'linePropsObjectId', + title: 'Line Object ID', + type: 'short-input', + condition: { field: 'operation', value: 'update_line_properties' }, + required: true, + }, + { + id: 'lineColor', + title: 'Line Color', + type: 'short-input', + placeholder: 'Hex', + condition: { field: 'operation', value: 'update_line_properties' }, + }, + { + id: 'lineWeight', + title: 'Line Weight (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'update_line_properties' }, + }, + { + id: 'lineDashStyle', + title: 'Dash Style', + type: 'short-input', + placeholder: 'SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT', + condition: { field: 'operation', value: 'update_line_properties' }, + mode: 'advanced', + }, + { + id: 'lineStartArrow', + title: 'Start Arrow', + type: 'short-input', + placeholder: 'NONE, STEALTH_ARROW, FILL_ARROW, OPEN_ARROW, ...', + condition: { field: 'operation', value: 'update_line_properties' }, + mode: 'advanced', + }, + { + id: 'lineEndArrow', + title: 'End Arrow', + type: 'short-input', + placeholder: 'NONE, STEALTH_ARROW, FILL_ARROW, OPEN_ARROW, ...', + condition: { field: 'operation', value: 'update_line_properties' }, + mode: 'advanced', + }, + { + id: 'lineLinkUrl', + title: 'Link URL', + type: 'short-input', + condition: { field: 'operation', value: 'update_line_properties' }, + mode: 'advanced', + }, + { + id: 'linePropertiesJson', + title: 'Properties JSON (advanced)', + type: 'long-input', + condition: { field: 'operation', value: 'update_line_properties' }, + mode: 'advanced', + }, + { + id: 'linePropertiesFields', + title: 'FieldMask (advanced)', + type: 'short-input', + placeholder: 'Comma-separated, e.g. lineFill,weight,dashStyle', + condition: { field: 'operation', value: 'update_line_properties' }, + mode: 'advanced', + }, + + // ========== Update Line Category Fields ========== + { + id: 'lineCategoryObjectId', + title: 'Line Object ID', + type: 'short-input', + condition: { field: 'operation', value: 'update_line_category' }, + required: true, + }, + { + id: 'newLineCategory', + title: 'Line Category', + type: 'dropdown', + options: [ + { label: 'Straight', id: 'STRAIGHT' }, + { label: 'Bent', id: 'BENT' }, + { label: 'Curved', id: 'CURVED' }, + ], + condition: { field: 'operation', value: 'update_line_category' }, + required: true, + }, + + // ========== Reroute Line Fields ========== + { + id: 'rerouteLineObjectId', + title: 'Line Object ID', + type: 'short-input', + condition: { field: 'operation', value: 'reroute_line' }, + required: true, + }, + + // ========== Table Row/Column Insert/Delete Fields ========== + { + id: 'tableTargetObjectId', + title: 'Table Object ID', + type: 'short-input', + condition: { + field: 'operation', + value: [ + 'insert_table_rows', + 'insert_table_columns', + 'delete_table_row', + 'delete_table_column', + ], + }, + required: true, + }, + { + id: 'tableCellRowIndex', + title: 'Cell Row Index', + type: 'short-input', + placeholder: 'Zero-based', + condition: { + field: 'operation', + value: [ + 'insert_table_rows', + 'insert_table_columns', + 'delete_table_row', + 'delete_table_column', + ], + }, + required: true, + }, + { + id: 'tableCellColumnIndex', + title: 'Cell Column Index', + type: 'short-input', + placeholder: 'Zero-based', + condition: { + field: 'operation', + value: [ + 'insert_table_rows', + 'insert_table_columns', + 'delete_table_row', + 'delete_table_column', + ], + }, + required: true, + }, + { + id: 'tableInsertNumber', + title: 'Number to Insert', + type: 'short-input', + placeholder: 'Minimum 1', + condition: { + field: 'operation', + value: ['insert_table_rows', 'insert_table_columns'], + }, + required: true, + }, + { + id: 'tableInsertBelow', + title: 'Insert Below', + type: 'switch', + condition: { field: 'operation', value: 'insert_table_rows' }, + }, + { + id: 'tableInsertRight', + title: 'Insert Right', + type: 'switch', + condition: { field: 'operation', value: 'insert_table_columns' }, + }, + + // ========== Merge / Unmerge / Cell / Border Table Range Fields ========== + { + id: 'tableRangeObjectId', + title: 'Table Object ID', + type: 'short-input', + condition: { + field: 'operation', + value: [ + 'merge_table_cells', + 'unmerge_table_cells', + 'update_table_cell_properties', + 'update_table_border_properties', + ], + }, + required: true, + }, + { + id: 'tableRangeRowIndex', + title: 'Range Start Row', + type: 'short-input', + placeholder: 'Zero-based', + condition: { + field: 'operation', + value: [ + 'merge_table_cells', + 'unmerge_table_cells', + 'update_table_cell_properties', + 'update_table_border_properties', + ], + }, + required: true, + }, + { + id: 'tableRangeColumnIndex', + title: 'Range Start Column', + type: 'short-input', + placeholder: 'Zero-based', + condition: { + field: 'operation', + value: [ + 'merge_table_cells', + 'unmerge_table_cells', + 'update_table_cell_properties', + 'update_table_border_properties', + ], + }, + required: true, + }, + { + id: 'tableRangeRowSpan', + title: 'Row Span', + type: 'short-input', + placeholder: 'Minimum 1', + condition: { + field: 'operation', + value: [ + 'merge_table_cells', + 'unmerge_table_cells', + 'update_table_cell_properties', + 'update_table_border_properties', + ], + }, + required: true, + }, + { + id: 'tableRangeColumnSpan', + title: 'Column Span', + type: 'short-input', + placeholder: 'Minimum 1', + condition: { + field: 'operation', + value: [ + 'merge_table_cells', + 'unmerge_table_cells', + 'update_table_cell_properties', + 'update_table_border_properties', + ], + }, + required: true, + }, + + // ========== Update Table Cell Properties Fields ========== + { + id: 'tableCellBackgroundColor', + title: 'Cell Background Color', + type: 'short-input', + placeholder: 'Hex, e.g. #F1F3F4', + condition: { field: 'operation', value: 'update_table_cell_properties' }, + }, + { + id: 'tableCellBackgroundAlpha', + title: 'Background Opacity', + type: 'short-input', + placeholder: '0.0 to 1.0', + condition: { field: 'operation', value: 'update_table_cell_properties' }, + mode: 'advanced', + }, + { + id: 'tableCellContentAlignment', + title: 'Content Alignment', + type: 'dropdown', + options: [ + { label: 'Top', id: 'TOP' }, + { label: 'Middle', id: 'MIDDLE' }, + { label: 'Bottom', id: 'BOTTOM' }, + ], + condition: { field: 'operation', value: 'update_table_cell_properties' }, + }, + { + id: 'tableCellPropertiesJson', + title: 'Properties JSON (advanced)', + type: 'long-input', + condition: { field: 'operation', value: 'update_table_cell_properties' }, + mode: 'advanced', + }, + { + id: 'tableCellPropertiesFields', + title: 'FieldMask (advanced)', + type: 'short-input', + placeholder: 'Comma-separated, e.g. tableCellBackgroundFill,contentAlignment', + condition: { field: 'operation', value: 'update_table_cell_properties' }, + mode: 'advanced', + }, + + // ========== Update Table Border Properties Fields ========== + { + id: 'tableBorderPosition', + title: 'Border Position', + type: 'dropdown', + options: [ + { label: 'All', id: 'ALL' }, + { label: 'Bottom', id: 'BOTTOM' }, + { label: 'Inner', id: 'INNER' }, + { label: 'Inner Horizontal', id: 'INNER_HORIZONTAL' }, + { label: 'Inner Vertical', id: 'INNER_VERTICAL' }, + { label: 'Left', id: 'LEFT' }, + { label: 'Outer', id: 'OUTER' }, + { label: 'Right', id: 'RIGHT' }, + { label: 'Top', id: 'TOP' }, + ], + value: () => 'ALL', + condition: { field: 'operation', value: 'update_table_border_properties' }, + }, + { + id: 'tableBorderColor', + title: 'Border Color', + type: 'short-input', + placeholder: 'Hex', + condition: { field: 'operation', value: 'update_table_border_properties' }, + }, + { + id: 'tableBorderWeight', + title: 'Border Weight (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'update_table_border_properties' }, + }, + { + id: 'tableBorderDashStyle', + title: 'Dash Style', + type: 'short-input', + placeholder: 'SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT', + condition: { field: 'operation', value: 'update_table_border_properties' }, + mode: 'advanced', + }, + { + id: 'tableBorderPropertiesJson', + title: 'Properties JSON (advanced)', + type: 'long-input', + condition: { field: 'operation', value: 'update_table_border_properties' }, + mode: 'advanced', + }, + { + id: 'tableBorderPropertiesFields', + title: 'FieldMask (advanced)', + type: 'short-input', + placeholder: 'Comma-separated, e.g. tableBorderFill,weight,dashStyle', + condition: { field: 'operation', value: 'update_table_border_properties' }, + mode: 'advanced', + }, + + // ========== Update Table Column Properties Fields ========== + { + id: 'tableColumnPropsObjectId', + title: 'Table Object ID', + type: 'short-input', + condition: { field: 'operation', value: 'update_table_column_properties' }, + required: true, + }, + { + id: 'tableColumnIndices', + title: 'Column Indices', + type: 'short-input', + placeholder: 'Comma-separated, zero-based (e.g. "0,2,3")', + condition: { field: 'operation', value: 'update_table_column_properties' }, + required: true, + }, + { + id: 'tableColumnWidth', + title: 'Column Width (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'update_table_column_properties' }, + }, + { + id: 'tableColumnPropertiesJson', + title: 'Properties JSON (advanced)', + type: 'long-input', + condition: { field: 'operation', value: 'update_table_column_properties' }, + mode: 'advanced', + }, + { + id: 'tableColumnPropertiesFields', + title: 'FieldMask (advanced)', + type: 'short-input', + placeholder: 'Comma-separated, e.g. columnWidth', + condition: { field: 'operation', value: 'update_table_column_properties' }, + mode: 'advanced', + }, + + // ========== Update Table Row Properties Fields ========== + { + id: 'tableRowPropsObjectId', + title: 'Table Object ID', + type: 'short-input', + condition: { field: 'operation', value: 'update_table_row_properties' }, + required: true, + }, + { + id: 'tableRowIndices', + title: 'Row Indices', + type: 'short-input', + placeholder: 'Comma-separated, zero-based', + condition: { field: 'operation', value: 'update_table_row_properties' }, + required: true, + }, + { + id: 'tableMinRowHeight', + title: 'Minimum Row Height (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'update_table_row_properties' }, + }, + { + id: 'tableRowPropertiesJson', + title: 'Properties JSON (advanced)', + type: 'long-input', + condition: { field: 'operation', value: 'update_table_row_properties' }, + mode: 'advanced', + }, + { + id: 'tableRowPropertiesFields', + title: 'FieldMask (advanced)', + type: 'short-input', + placeholder: 'Comma-separated, e.g. minRowHeight', + condition: { field: 'operation', value: 'update_table_row_properties' }, + mode: 'advanced', + }, + + // ========== Sheets Chart Embed Fields ========== + { + id: 'chartPageObjectId', + title: 'Slide ID', + type: 'short-input', + placeholder: 'Object ID of the slide to embed the chart on', + condition: { field: 'operation', value: 'create_sheets_chart' }, + required: true, + }, + { + id: 'chartSpreadsheetId', + title: 'Spreadsheet ID', + type: 'short-input', + condition: { + field: 'operation', + value: ['create_sheets_chart', 'replace_all_shapes_with_sheets_chart'], + }, + required: true, + }, + { + id: 'chartId', + title: 'Chart ID', + type: 'short-input', + placeholder: 'Numeric chart ID within the spreadsheet', + condition: { + field: 'operation', + value: ['create_sheets_chart', 'replace_all_shapes_with_sheets_chart'], + }, + required: true, + }, + { + id: 'chartLinkingMode', + title: 'Linking Mode', + type: 'dropdown', + options: [ + { label: 'Linked (refreshable)', id: 'LINKED' }, + { label: 'Static Image', id: 'NOT_LINKED_IMAGE' }, + ], + value: () => 'LINKED', + condition: { + field: 'operation', + value: ['create_sheets_chart', 'replace_all_shapes_with_sheets_chart'], + }, + }, + { + id: 'chartWidth', + title: 'Width (pt)', + type: 'short-input', + placeholder: 'Default 400', + condition: { field: 'operation', value: 'create_sheets_chart' }, + }, + { + id: 'chartHeight', + title: 'Height (pt)', + type: 'short-input', + placeholder: 'Default 300', + condition: { field: 'operation', value: 'create_sheets_chart' }, + }, + { + id: 'chartPositionX', + title: 'X Position (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'create_sheets_chart' }, + }, + { + id: 'chartPositionY', + title: 'Y Position (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'create_sheets_chart' }, + }, + + // ========== Refresh Sheets Chart Fields ========== + { + id: 'refreshChartObjectId', + title: 'Chart Object ID', + type: 'short-input', + condition: { field: 'operation', value: 'refresh_sheets_chart' }, + required: true, + }, + + // ========== Replace All Shapes With Sheets Chart Fields ========== + { + id: 'replaceShapesChartFindText', + title: 'Find Text (Token)', + type: 'short-input', + placeholder: 'Shape text token, e.g. {{revenue-chart}}', + condition: { field: 'operation', value: 'replace_all_shapes_with_sheets_chart' }, + required: true, + }, + { + id: 'replaceShapesChartMatchCase', + title: 'Match Case', + type: 'switch', + condition: { field: 'operation', value: 'replace_all_shapes_with_sheets_chart' }, + }, + { + id: 'replaceShapesChartPageObjectIds', + title: 'Limit to Slides (IDs)', + type: 'short-input', + placeholder: 'Comma-separated slide IDs (empty = all)', + condition: { field: 'operation', value: 'replace_all_shapes_with_sheets_chart' }, + mode: 'advanced', + }, + + // ========== Create Video Fields ========== + { + id: 'videoPageObjectId', + title: 'Slide ID', + type: 'short-input', + placeholder: 'Object ID of the slide', + condition: { field: 'operation', value: 'create_video' }, + required: true, + }, + { + id: 'videoSource', + title: 'Source', + type: 'dropdown', + options: [ + { label: 'YouTube', id: 'YOUTUBE' }, + { label: 'Google Drive', id: 'DRIVE' }, + ], + value: () => 'YOUTUBE', + condition: { field: 'operation', value: 'create_video' }, + required: true, + }, + { + id: 'videoId', + title: 'Video ID', + type: 'short-input', + placeholder: 'YouTube video ID or Drive file ID', + condition: { field: 'operation', value: 'create_video' }, + required: true, + }, + { + id: 'videoWidth', + title: 'Width (pt)', + type: 'short-input', + placeholder: 'Default 400', + condition: { field: 'operation', value: 'create_video' }, + }, + { + id: 'videoHeight', + title: 'Height (pt)', + type: 'short-input', + placeholder: 'Default 225', + condition: { field: 'operation', value: 'create_video' }, + }, + { + id: 'videoPositionX', + title: 'X Position (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'create_video' }, + }, + { + id: 'videoPositionY', + title: 'Y Position (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'create_video' }, + }, + + // ========== Update Video Properties Fields ========== + { + id: 'videoPropsObjectId', + title: 'Video Object ID', + type: 'short-input', + condition: { field: 'operation', value: 'update_video_properties' }, + required: true, + }, + { + id: 'videoAutoPlay', + title: 'Auto Play', + type: 'switch', + condition: { field: 'operation', value: 'update_video_properties' }, + }, + { + id: 'videoMute', + title: 'Mute', + type: 'switch', + condition: { field: 'operation', value: 'update_video_properties' }, + }, + { + id: 'videoStart', + title: 'Start (sec)', + type: 'short-input', + condition: { field: 'operation', value: 'update_video_properties' }, + }, + { + id: 'videoEnd', + title: 'End (sec)', + type: 'short-input', + condition: { field: 'operation', value: 'update_video_properties' }, + }, + { + id: 'videoOutlineColor', + title: 'Outline Color', + type: 'short-input', + placeholder: 'Hex', + condition: { field: 'operation', value: 'update_video_properties' }, + mode: 'advanced', + }, + { + id: 'videoOutlineWeight', + title: 'Outline Weight (pt)', + type: 'short-input', + condition: { field: 'operation', value: 'update_video_properties' }, + mode: 'advanced', + }, + { + id: 'videoOutlineDashStyle', + title: 'Outline Dash Style', + type: 'short-input', + placeholder: 'SOLID, DOT, DASH, ...', + condition: { field: 'operation', value: 'update_video_properties' }, + mode: 'advanced', + }, + { + id: 'videoPropertiesJson', + title: 'Properties JSON (advanced)', + type: 'long-input', + condition: { field: 'operation', value: 'update_video_properties' }, + mode: 'advanced', + }, + { + id: 'videoPropertiesFields', + title: 'FieldMask (advanced)', + type: 'short-input', + placeholder: 'Comma-separated, e.g. autoPlay,mute,start,end', + condition: { field: 'operation', value: 'update_video_properties' }, + mode: 'advanced', + }, ], tools: { access: [ @@ -638,6 +2357,44 @@ Return ONLY the text content - no explanations, no markdown formatting markers, 'google_slides_create_table', 'google_slides_create_shape', 'google_slides_insert_text', + 'google_slides_update_text_style', + 'google_slides_update_paragraph_style', + 'google_slides_delete_text', + 'google_slides_create_paragraph_bullets', + 'google_slides_delete_paragraph_bullets', + 'google_slides_replace_all_shapes_with_image', + 'google_slides_replace_image', + 'google_slides_update_image_properties', + 'google_slides_update_shape_properties', + 'google_slides_update_page_properties', + 'google_slides_update_slide_properties', + 'google_slides_update_page_element_alt_text', + 'google_slides_update_page_element_transform', + 'google_slides_update_page_elements_z_order', + 'google_slides_group_objects', + 'google_slides_ungroup_objects', + 'google_slides_create_line', + 'google_slides_update_line_properties', + 'google_slides_update_line_category', + 'google_slides_reroute_line', + 'google_slides_insert_table_rows', + 'google_slides_insert_table_columns', + 'google_slides_delete_table_row', + 'google_slides_delete_table_column', + 'google_slides_merge_table_cells', + 'google_slides_unmerge_table_cells', + 'google_slides_update_table_cell_properties', + 'google_slides_update_table_border_properties', + 'google_slides_update_table_column_properties', + 'google_slides_update_table_row_properties', + 'google_slides_create_sheets_chart', + 'google_slides_refresh_sheets_chart', + 'google_slides_replace_all_shapes_with_sheets_chart', + 'google_slides_create_video', + 'google_slides_update_video_properties', + 'google_slides_batch_update', + 'google_slides_copy_presentation', + 'google_slides_export_presentation', ], config: { tool: (params) => { @@ -670,6 +2427,82 @@ Return ONLY the text content - no explanations, no markdown formatting markers, return 'google_slides_create_shape' case 'insert_text': return 'google_slides_insert_text' + case 'update_text_style': + return 'google_slides_update_text_style' + case 'update_paragraph_style': + return 'google_slides_update_paragraph_style' + case 'delete_text': + return 'google_slides_delete_text' + case 'create_paragraph_bullets': + return 'google_slides_create_paragraph_bullets' + case 'delete_paragraph_bullets': + return 'google_slides_delete_paragraph_bullets' + case 'replace_all_shapes_with_image': + return 'google_slides_replace_all_shapes_with_image' + case 'replace_image': + return 'google_slides_replace_image' + case 'update_image_properties': + return 'google_slides_update_image_properties' + case 'update_shape_properties': + return 'google_slides_update_shape_properties' + case 'update_page_properties': + return 'google_slides_update_page_properties' + case 'update_slide_properties': + return 'google_slides_update_slide_properties' + case 'update_page_element_alt_text': + return 'google_slides_update_page_element_alt_text' + case 'update_page_element_transform': + return 'google_slides_update_page_element_transform' + case 'update_page_elements_z_order': + return 'google_slides_update_page_elements_z_order' + case 'group_objects': + return 'google_slides_group_objects' + case 'ungroup_objects': + return 'google_slides_ungroup_objects' + case 'create_line': + return 'google_slides_create_line' + case 'update_line_properties': + return 'google_slides_update_line_properties' + case 'update_line_category': + return 'google_slides_update_line_category' + case 'reroute_line': + return 'google_slides_reroute_line' + case 'insert_table_rows': + return 'google_slides_insert_table_rows' + case 'insert_table_columns': + return 'google_slides_insert_table_columns' + case 'delete_table_row': + return 'google_slides_delete_table_row' + case 'delete_table_column': + return 'google_slides_delete_table_column' + case 'merge_table_cells': + return 'google_slides_merge_table_cells' + case 'unmerge_table_cells': + return 'google_slides_unmerge_table_cells' + case 'update_table_cell_properties': + return 'google_slides_update_table_cell_properties' + case 'update_table_border_properties': + return 'google_slides_update_table_border_properties' + case 'update_table_column_properties': + return 'google_slides_update_table_column_properties' + case 'update_table_row_properties': + return 'google_slides_update_table_row_properties' + case 'create_sheets_chart': + return 'google_slides_create_sheets_chart' + case 'refresh_sheets_chart': + return 'google_slides_refresh_sheets_chart' + case 'replace_all_shapes_with_sheets_chart': + return 'google_slides_replace_all_shapes_with_sheets_chart' + case 'create_video': + return 'google_slides_create_video' + case 'update_video_properties': + return 'google_slides_update_video_properties' + case 'batch_update': + return 'google_slides_batch_update' + case 'copy_presentation': + return 'google_slides_copy_presentation' + case 'export_presentation': + return 'google_slides_export_presentation' default: throw new Error(`Invalid Google Slides operation: ${params.operation}`) } @@ -807,6 +2640,381 @@ Return ONLY the text content - no explanations, no markdown formatting markers, } } + const toNum = (v: unknown): number | undefined => { + if (v === undefined || v === null || v === '') return undefined + const n = typeof v === 'number' ? v : Number.parseFloat(String(v)) + return Number.isFinite(n) ? n : undefined + } + + const TEXT_RANGE_OPS = new Set([ + 'update_text_style', + 'update_paragraph_style', + 'delete_text', + 'create_paragraph_bullets', + 'delete_paragraph_bullets', + ]) + if (TEXT_RANGE_OPS.has(params.operation as string)) { + result.objectId = params.textObjectId + const rowIdx = toNum(params.textRowIndex) + const colIdx = toNum(params.textColumnIndex) + if (rowIdx !== undefined) result.rowIndex = rowIdx + if (colIdx !== undefined) result.columnIndex = colIdx + if (params.textRangeType) result.rangeType = params.textRangeType + const startIdx = toNum(params.textStartIndex) + const endIdx = toNum(params.textEndIndex) + if (startIdx !== undefined) result.startIndex = startIdx + if (endIdx !== undefined) result.endIndex = endIdx + } + + if (params.operation === 'update_text_style') { + if (params.textBold !== undefined) result.bold = params.textBold + if (params.textItalic !== undefined) result.italic = params.textItalic + if (params.textUnderline !== undefined) result.underline = params.textUnderline + if (params.textStrikethrough !== undefined) + result.strikethrough = params.textStrikethrough + if (params.textSmallCaps !== undefined) result.smallCaps = params.textSmallCaps + if (params.textFontFamily) result.fontFamily = params.textFontFamily + const fontSize = toNum(params.textFontSize) + if (fontSize !== undefined) result.fontSize = fontSize + if (params.textForegroundColor) result.foregroundColor = params.textForegroundColor + if (params.textBackgroundColor) result.backgroundColor = params.textBackgroundColor + if (params.textLinkUrl) result.linkUrl = params.textLinkUrl + if (params.textBaselineOffset) result.baselineOffset = params.textBaselineOffset + if (params.textStyleJson) result.styleJson = params.textStyleJson + if (params.textStyleFields) result.fields = params.textStyleFields + } + + if (params.operation === 'update_paragraph_style') { + if (params.paragraphAlignment) result.alignment = params.paragraphAlignment + const ls = toNum(params.paragraphLineSpacing) + if (ls !== undefined) result.lineSpacing = ls + const indentStart = toNum(params.paragraphIndentStart) + if (indentStart !== undefined) result.indentStart = indentStart + const indentEnd = toNum(params.paragraphIndentEnd) + if (indentEnd !== undefined) result.indentEnd = indentEnd + const indentFirst = toNum(params.paragraphIndentFirstLine) + if (indentFirst !== undefined) result.indentFirstLine = indentFirst + const spaceAbove = toNum(params.paragraphSpaceAbove) + if (spaceAbove !== undefined) result.spaceAbove = spaceAbove + const spaceBelow = toNum(params.paragraphSpaceBelow) + if (spaceBelow !== undefined) result.spaceBelow = spaceBelow + if (params.paragraphDirection) result.direction = params.paragraphDirection + if (params.paragraphSpacingMode) result.spacingMode = params.paragraphSpacingMode + if (params.paragraphStyleJson) result.styleJson = params.paragraphStyleJson + if (params.paragraphStyleFields) result.fields = params.paragraphStyleFields + } + + if (params.operation === 'create_paragraph_bullets' && params.bulletPreset) { + result.bulletPreset = params.bulletPreset + } + + if (params.operation === 'replace_all_shapes_with_image') { + result.imageUrl = params.replaceShapesImageUrl + result.findText = params.replaceShapesFindText + if (params.replaceShapesMatchCase !== undefined) + result.matchCase = params.replaceShapesMatchCase + if (params.replaceShapesImageMethod) + result.imageReplaceMethod = params.replaceShapesImageMethod + if (params.replaceShapesPageObjectIds) + result.pageObjectIds = params.replaceShapesPageObjectIds + } + + if (params.operation === 'replace_image') { + result.imageObjectId = params.replaceImageObjectId + result.imageUrl = params.replaceImageUrl + if (params.replaceImageMethod) result.imageReplaceMethod = params.replaceImageMethod + } + + if (params.operation === 'update_image_properties') { + result.objectId = params.imagePropsObjectId + const brightness = toNum(params.imageBrightness) + if (brightness !== undefined) result.brightness = brightness + const contrast = toNum(params.imageContrast) + if (contrast !== undefined) result.contrast = contrast + const transparency = toNum(params.imageTransparency) + if (transparency !== undefined) result.transparency = transparency + if (params.imageLinkUrl) result.linkUrl = params.imageLinkUrl + if (params.imageOutlineColor) result.outlineColor = params.imageOutlineColor + const outlineWeight = toNum(params.imageOutlineWeight) + if (outlineWeight !== undefined) result.outlineWeight = outlineWeight + if (params.imageOutlineDashStyle) result.outlineDashStyle = params.imageOutlineDashStyle + if (params.imagePropertiesJson) result.propertiesJson = params.imagePropertiesJson + if (params.imagePropertiesFields) result.fields = params.imagePropertiesFields + } + + if (params.operation === 'update_shape_properties') { + result.objectId = params.shapePropsObjectId + if (params.shapeFillColor) result.fillColor = params.shapeFillColor + const fillAlpha = toNum(params.shapeFillAlpha) + if (fillAlpha !== undefined) result.fillAlpha = fillAlpha + if (params.shapeFillUnset !== undefined) result.fillUnset = params.shapeFillUnset + if (params.shapeOutlineColor) result.outlineColor = params.shapeOutlineColor + const shapeOutlineWeight = toNum(params.shapeOutlineWeight) + if (shapeOutlineWeight !== undefined) result.outlineWeight = shapeOutlineWeight + if (params.shapeOutlineDashStyle) result.outlineDashStyle = params.shapeOutlineDashStyle + if (params.shapeOutlineUnset !== undefined) result.outlineUnset = params.shapeOutlineUnset + if (params.shapeLinkUrl) result.linkUrl = params.shapeLinkUrl + if (params.shapeContentAlignment) result.contentAlignment = params.shapeContentAlignment + if (params.shapeAutofitType) result.autofitType = params.shapeAutofitType + if (params.shapePropertiesJson) result.propertiesJson = params.shapePropertiesJson + if (params.shapePropertiesFields) result.fields = params.shapePropertiesFields + } + + if (params.operation === 'update_page_properties') { + result.objectId = params.pagePropsObjectId + if (params.pageBackgroundColor) result.backgroundColor = params.pageBackgroundColor + const bgAlpha = toNum(params.pageBackgroundAlpha) + if (bgAlpha !== undefined) result.backgroundAlpha = bgAlpha + if (params.pageBackgroundImageUrl) + result.backgroundImageUrl = params.pageBackgroundImageUrl + if (params.pageBackgroundUnset !== undefined) + result.backgroundUnset = params.pageBackgroundUnset + if (params.pagePropertiesJson) result.propertiesJson = params.pagePropertiesJson + if (params.pagePropertiesFields) result.fields = params.pagePropertiesFields + } + + if (params.operation === 'update_slide_properties') { + result.objectId = params.slidePropsObjectId + if (params.slideIsSkipped !== undefined) result.isSkipped = params.slideIsSkipped + if (params.slidePropertiesJson) result.propertiesJson = params.slidePropertiesJson + if (params.slidePropertiesFields) result.fields = params.slidePropertiesFields + } + + if (params.operation === 'update_page_element_alt_text') { + result.objectId = params.altTextObjectId + if (params.altTextTitle !== undefined) result.title = params.altTextTitle + if (params.altTextDescription !== undefined) + result.description = params.altTextDescription + } + + if (params.operation === 'update_page_element_transform') { + result.objectId = params.transformObjectId + const sx = toNum(params.transformScaleX) + const sy = toNum(params.transformScaleY) + const shx = toNum(params.transformShearX) + const shy = toNum(params.transformShearY) + const tx = toNum(params.transformTranslateX) + const ty = toNum(params.transformTranslateY) + if (sx !== undefined) result.scaleX = sx + if (sy !== undefined) result.scaleY = sy + if (shx !== undefined) result.shearX = shx + if (shy !== undefined) result.shearY = shy + if (tx !== undefined) result.translateX = tx + if (ty !== undefined) result.translateY = ty + if (params.transformApplyMode) result.applyMode = params.transformApplyMode + } + + if (params.operation === 'update_page_elements_z_order') { + result.objectIds = params.zOrderObjectIds + // Always overwrite — even when zOrderOperation is empty — so the block-level + // operation name 'update_page_elements_z_order' can never leak through as the + // z-order enum value and produce a confusing API error. + result.operation = params.zOrderOperation || undefined + } + + if (params.operation === 'group_objects') { + result.childrenObjectIds = params.groupChildrenObjectIds + if (params.groupObjectIdInput) result.groupObjectId = params.groupObjectIdInput + } + + if (params.operation === 'ungroup_objects') { + result.objectIds = params.ungroupObjectIds + } + + if (params.operation === 'create_line') { + result.pageObjectId = params.linePageObjectId + if (params.lineCategory) result.lineCategory = params.lineCategory + const lw = toNum(params.lineWidth) + const lh = toNum(params.lineHeight) + const lpx = toNum(params.linePositionX) + const lpy = toNum(params.linePositionY) + if (lw !== undefined) result.width = lw + if (lh !== undefined) result.height = lh + if (lpx !== undefined) result.positionX = lpx + if (lpy !== undefined) result.positionY = lpy + } + + if (params.operation === 'update_line_properties') { + result.objectId = params.linePropsObjectId + if (params.lineColor) result.lineColor = params.lineColor + const lineWeight = toNum(params.lineWeight) + if (lineWeight !== undefined) result.lineWeight = lineWeight + if (params.lineDashStyle) result.dashStyle = params.lineDashStyle + if (params.lineStartArrow) result.startArrow = params.lineStartArrow + if (params.lineEndArrow) result.endArrow = params.lineEndArrow + if (params.lineLinkUrl) result.linkUrl = params.lineLinkUrl + if (params.linePropertiesJson) result.propertiesJson = params.linePropertiesJson + if (params.linePropertiesFields) result.fields = params.linePropertiesFields + } + + if (params.operation === 'update_line_category') { + result.objectId = params.lineCategoryObjectId + if (params.newLineCategory) result.lineCategory = params.newLineCategory + } + + if (params.operation === 'reroute_line') { + result.objectId = params.rerouteLineObjectId + } + + const TABLE_CELL_REF_OPS = new Set([ + 'insert_table_rows', + 'insert_table_columns', + 'delete_table_row', + 'delete_table_column', + ]) + if (TABLE_CELL_REF_OPS.has(params.operation as string)) { + result.tableObjectId = params.tableTargetObjectId + const rIdx = toNum(params.tableCellRowIndex) + const cIdx = toNum(params.tableCellColumnIndex) + if (rIdx !== undefined) result.rowIndex = rIdx + if (cIdx !== undefined) result.columnIndex = cIdx + } + if ( + params.operation === 'insert_table_rows' || + params.operation === 'insert_table_columns' + ) { + const n = toNum(params.tableInsertNumber) + if (n !== undefined) result.number = n + } + if (params.operation === 'insert_table_rows' && params.tableInsertBelow !== undefined) { + result.insertBelow = params.tableInsertBelow + } + if (params.operation === 'insert_table_columns' && params.tableInsertRight !== undefined) { + result.insertRight = params.tableInsertRight + } + + const TABLE_RANGE_OPS = new Set([ + 'merge_table_cells', + 'unmerge_table_cells', + 'update_table_cell_properties', + 'update_table_border_properties', + ]) + if (TABLE_RANGE_OPS.has(params.operation as string)) { + result.objectId = params.tableRangeObjectId + const rIdx = toNum(params.tableRangeRowIndex) + const cIdx = toNum(params.tableRangeColumnIndex) + const rSpan = toNum(params.tableRangeRowSpan) + const cSpan = toNum(params.tableRangeColumnSpan) + if (rIdx !== undefined) result.rowIndex = rIdx + if (cIdx !== undefined) result.columnIndex = cIdx + if (rSpan !== undefined) result.rowSpan = rSpan + if (cSpan !== undefined) result.columnSpan = cSpan + } + + if (params.operation === 'update_table_cell_properties') { + if (params.tableCellBackgroundColor) + result.backgroundColor = params.tableCellBackgroundColor + const cellAlpha = toNum(params.tableCellBackgroundAlpha) + if (cellAlpha !== undefined) result.backgroundAlpha = cellAlpha + if (params.tableCellContentAlignment) + result.contentAlignment = params.tableCellContentAlignment + if (params.tableCellPropertiesJson) result.propertiesJson = params.tableCellPropertiesJson + if (params.tableCellPropertiesFields) result.fields = params.tableCellPropertiesFields + } + + if (params.operation === 'update_table_border_properties') { + if (params.tableBorderPosition) result.borderPosition = params.tableBorderPosition + if (params.tableBorderColor) result.borderColor = params.tableBorderColor + const borderWeight = toNum(params.tableBorderWeight) + if (borderWeight !== undefined) result.borderWeight = borderWeight + if (params.tableBorderDashStyle) result.dashStyle = params.tableBorderDashStyle + if (params.tableBorderPropertiesJson) + result.propertiesJson = params.tableBorderPropertiesJson + if (params.tableBorderPropertiesFields) result.fields = params.tableBorderPropertiesFields + } + + if (params.operation === 'update_table_column_properties') { + result.objectId = params.tableColumnPropsObjectId + if (params.tableColumnIndices) result.columnIndices = params.tableColumnIndices + const colWidth = toNum(params.tableColumnWidth) + if (colWidth !== undefined) result.columnWidth = colWidth + if (params.tableColumnPropertiesJson) + result.propertiesJson = params.tableColumnPropertiesJson + if (params.tableColumnPropertiesFields) result.fields = params.tableColumnPropertiesFields + } + + if (params.operation === 'update_table_row_properties') { + result.objectId = params.tableRowPropsObjectId + if (params.tableRowIndices) result.rowIndices = params.tableRowIndices + const minHeight = toNum(params.tableMinRowHeight) + if (minHeight !== undefined) result.minRowHeight = minHeight + if (params.tableRowPropertiesJson) result.propertiesJson = params.tableRowPropertiesJson + if (params.tableRowPropertiesFields) result.fields = params.tableRowPropertiesFields + } + + if (params.operation === 'create_sheets_chart') { + result.pageObjectId = params.chartPageObjectId + if (params.chartSpreadsheetId) result.spreadsheetId = params.chartSpreadsheetId + const cId = toNum(params.chartId) + if (cId !== undefined) result.chartId = cId + if (params.chartLinkingMode) result.linkingMode = params.chartLinkingMode + const cw = toNum(params.chartWidth) + const ch = toNum(params.chartHeight) + const cpx = toNum(params.chartPositionX) + const cpy = toNum(params.chartPositionY) + if (cw !== undefined) result.width = cw + if (ch !== undefined) result.height = ch + if (cpx !== undefined) result.positionX = cpx + if (cpy !== undefined) result.positionY = cpy + } + + if (params.operation === 'refresh_sheets_chart') { + result.objectId = params.refreshChartObjectId + } + + if (params.operation === 'replace_all_shapes_with_sheets_chart') { + if (params.chartSpreadsheetId) result.spreadsheetId = params.chartSpreadsheetId + const cId = toNum(params.chartId) + if (cId !== undefined) result.chartId = cId + result.findText = params.replaceShapesChartFindText + if (params.replaceShapesChartMatchCase !== undefined) + result.matchCase = params.replaceShapesChartMatchCase + if (params.chartLinkingMode) result.linkingMode = params.chartLinkingMode + if (params.replaceShapesChartPageObjectIds) + result.pageObjectIds = params.replaceShapesChartPageObjectIds + } + + if (params.operation === 'create_video') { + result.pageObjectId = params.videoPageObjectId + if (params.videoSource) result.source = params.videoSource + const vw = toNum(params.videoWidth) + const vh = toNum(params.videoHeight) + const vpx = toNum(params.videoPositionX) + const vpy = toNum(params.videoPositionY) + if (vw !== undefined) result.width = vw + if (vh !== undefined) result.height = vh + if (vpx !== undefined) result.positionX = vpx + if (vpy !== undefined) result.positionY = vpy + } + + if (params.operation === 'update_video_properties') { + result.objectId = params.videoPropsObjectId + if (params.videoAutoPlay !== undefined) result.autoPlay = params.videoAutoPlay + if (params.videoMute !== undefined) result.mute = params.videoMute + const vStart = toNum(params.videoStart) + const vEnd = toNum(params.videoEnd) + if (vStart !== undefined) result.start = vStart + if (vEnd !== undefined) result.end = vEnd + if (params.videoOutlineColor) result.outlineColor = params.videoOutlineColor + const voWeight = toNum(params.videoOutlineWeight) + if (voWeight !== undefined) result.outlineWeight = voWeight + if (params.videoOutlineDashStyle) result.outlineDashStyle = params.videoOutlineDashStyle + if (params.videoPropertiesJson) result.propertiesJson = params.videoPropertiesJson + if (params.videoPropertiesFields) result.fields = params.videoPropertiesFields + } + + if (params.operation === 'batch_update') { + if (params.requestsJson) result.requests = params.requestsJson + if (params.writeControlJson) result.writeControl = params.writeControlJson + } + + if (params.operation === 'copy_presentation') { + if (params.sourcePresentationId) result.sourcePresentationId = params.sourcePresentationId + if (params.copyTitle) result.title = params.copyTitle + if (params.copyFolderId) result.folderId = params.copyFolderId + result.presentationId = undefined + } + return result }, }, @@ -874,6 +3082,244 @@ Return ONLY the text content - no explanations, no markdown formatting markers, insertTextObjectId: { type: 'string', description: 'Object ID for text insertion' }, insertTextContent: { type: 'string', description: 'Text to insert' }, insertTextIndex: { type: 'number', description: 'Insertion index' }, + + // Copy presentation operation + sourcePresentationId: { type: 'string', description: 'Source/template presentation ID' }, + copyTitle: { type: 'string', description: 'Title for the copy' }, + copyFolderId: { type: 'string', description: 'Destination folder ID for the copy' }, + + // Export presentation operation + exportFormat: { type: 'string', description: 'Export format (PDF, PPTX, ODP, etc.)' }, + + // Batch update (raw) + requestsJson: { type: 'string', description: 'JSON array of raw Slides API Request objects' }, + writeControlJson: { type: 'string', description: 'WriteControl JSON object' }, + + // Replace all shapes with image + replaceShapesImageUrl: { type: 'string', description: 'Image URL to insert' }, + replaceShapesFindText: { type: 'string', description: 'Text token of shapes to replace' }, + replaceShapesMatchCase: { type: 'boolean', description: 'Match case' }, + replaceShapesImageMethod: { type: 'string', description: 'Image fit method' }, + replaceShapesPageObjectIds: { type: 'string', description: 'Slide IDs to limit to' }, + + // Replace image + replaceImageObjectId: { type: 'string', description: 'Image object ID to replace' }, + replaceImageUrl: { type: 'string', description: 'New image URL' }, + replaceImageMethod: { type: 'string', description: 'Image fit method' }, + + // Update image properties + imagePropsObjectId: { type: 'string', description: 'Image object ID' }, + imageBrightness: { type: 'number', description: 'Brightness -1.0 to 1.0' }, + imageContrast: { type: 'number', description: 'Contrast -1.0 to 1.0' }, + imageTransparency: { type: 'number', description: 'Transparency 0.0 to 1.0' }, + imageLinkUrl: { type: 'string', description: 'Hyperlink URL' }, + imageOutlineColor: { type: 'string', description: 'Outline color (hex)' }, + imageOutlineWeight: { type: 'number', description: 'Outline weight (pt)' }, + imageOutlineDashStyle: { type: 'string', description: 'Outline dash style' }, + imagePropertiesJson: { type: 'string', description: 'Raw ImageProperties JSON' }, + imagePropertiesFields: { type: 'string', description: 'FieldMask' }, + + // Shared text range targeting + textObjectId: { type: 'string', description: 'Object ID for text styling target' }, + textRowIndex: { type: 'number', description: 'Table cell row index' }, + textColumnIndex: { type: 'number', description: 'Table cell column index' }, + textRangeType: { type: 'string', description: 'Range type (ALL/FROM_START_INDEX/FIXED_RANGE)' }, + textStartIndex: { type: 'number', description: 'Range start index' }, + textEndIndex: { type: 'number', description: 'Range end index' }, + + // Update text style + textBold: { type: 'boolean', description: 'Bold' }, + textItalic: { type: 'boolean', description: 'Italic' }, + textUnderline: { type: 'boolean', description: 'Underline' }, + textStrikethrough: { type: 'boolean', description: 'Strikethrough' }, + textSmallCaps: { type: 'boolean', description: 'Small caps' }, + textFontFamily: { type: 'string', description: 'Font family' }, + textFontSize: { type: 'number', description: 'Font size in points' }, + textForegroundColor: { type: 'string', description: 'Text color (hex)' }, + textBackgroundColor: { type: 'string', description: 'Text background color (hex)' }, + textLinkUrl: { type: 'string', description: 'Text hyperlink URL' }, + textBaselineOffset: { type: 'string', description: 'Baseline offset' }, + textStyleJson: { type: 'string', description: 'Raw TextStyle JSON' }, + textStyleFields: { type: 'string', description: 'FieldMask' }, + + // Update paragraph style + paragraphAlignment: { type: 'string', description: 'Paragraph alignment' }, + paragraphLineSpacing: { type: 'number', description: 'Line spacing percent' }, + paragraphIndentStart: { type: 'number', description: 'Indent start (pt)' }, + paragraphIndentEnd: { type: 'number', description: 'Indent end (pt)' }, + paragraphIndentFirstLine: { type: 'number', description: 'First-line indent (pt)' }, + paragraphSpaceAbove: { type: 'number', description: 'Space above (pt)' }, + paragraphSpaceBelow: { type: 'number', description: 'Space below (pt)' }, + paragraphDirection: { type: 'string', description: 'Text direction' }, + paragraphSpacingMode: { type: 'string', description: 'Paragraph spacing mode' }, + paragraphStyleJson: { type: 'string', description: 'Raw ParagraphStyle JSON' }, + paragraphStyleFields: { type: 'string', description: 'FieldMask' }, + + // Bullets + bulletPreset: { type: 'string', description: 'Bullet preset' }, + + // Update shape properties + shapePropsObjectId: { type: 'string', description: 'Shape object ID' }, + shapeFillColor: { type: 'string', description: 'Shape fill color (hex)' }, + shapeFillAlpha: { type: 'number', description: 'Shape fill opacity' }, + shapeFillUnset: { type: 'boolean', description: 'Clear shape fill' }, + shapeOutlineColor: { type: 'string', description: 'Shape outline color (hex)' }, + shapeOutlineWeight: { type: 'number', description: 'Shape outline weight (pt)' }, + shapeOutlineDashStyle: { type: 'string', description: 'Shape outline dash style' }, + shapeOutlineUnset: { type: 'boolean', description: 'Clear shape outline' }, + shapeLinkUrl: { type: 'string', description: 'Shape hyperlink URL' }, + shapeContentAlignment: { type: 'string', description: 'Shape content alignment' }, + shapeAutofitType: { type: 'string', description: 'Shape autofit type' }, + shapePropertiesJson: { type: 'string', description: 'Raw ShapeProperties JSON' }, + shapePropertiesFields: { type: 'string', description: 'FieldMask' }, + + // Update page properties + pagePropsObjectId: { type: 'string', description: 'Slide object ID' }, + pageBackgroundColor: { type: 'string', description: 'Slide background color (hex)' }, + pageBackgroundAlpha: { type: 'number', description: 'Slide background opacity' }, + pageBackgroundImageUrl: { type: 'string', description: 'Slide background image URL' }, + pageBackgroundUnset: { type: 'boolean', description: 'Clear slide background' }, + pagePropertiesJson: { type: 'string', description: 'Raw PageProperties JSON' }, + pagePropertiesFields: { type: 'string', description: 'FieldMask' }, + + // Update slide properties + slidePropsObjectId: { type: 'string', description: 'Slide object ID' }, + slideIsSkipped: { type: 'boolean', description: 'Whether the slide is skipped' }, + slidePropertiesJson: { type: 'string', description: 'Raw SlideProperties JSON' }, + slidePropertiesFields: { type: 'string', description: 'FieldMask' }, + + // Alt text + altTextObjectId: { type: 'string', description: 'Element object ID' }, + altTextTitle: { type: 'string', description: 'Accessibility title' }, + altTextDescription: { type: 'string', description: 'Accessibility description' }, + + // Transform + transformObjectId: { type: 'string', description: 'Element object ID' }, + transformScaleX: { type: 'number', description: 'Scale X' }, + transformScaleY: { type: 'number', description: 'Scale Y' }, + transformShearX: { type: 'number', description: 'Shear X' }, + transformShearY: { type: 'number', description: 'Shear Y' }, + transformTranslateX: { type: 'number', description: 'X position (pt)' }, + transformTranslateY: { type: 'number', description: 'Y position (pt)' }, + transformApplyMode: { type: 'string', description: 'Apply mode' }, + + // Z-order + zOrderObjectIds: { type: 'string', description: 'Comma-separated element IDs' }, + zOrderOperation: { type: 'string', description: 'Z-order operation' }, + + // Group / ungroup + groupChildrenObjectIds: { type: 'string', description: 'Children object IDs' }, + groupObjectIdInput: { type: 'string', description: 'Group object ID' }, + ungroupObjectIds: { type: 'string', description: 'Group object IDs to ungroup' }, + + // Create line + linePageObjectId: { type: 'string', description: 'Slide object ID' }, + lineCategory: { type: 'string', description: 'Line category' }, + lineWidth: { type: 'number', description: 'Line width (pt)' }, + lineHeight: { type: 'number', description: 'Line height (pt)' }, + linePositionX: { type: 'number', description: 'Line X position (pt)' }, + linePositionY: { type: 'number', description: 'Line Y position (pt)' }, + + // Update line properties + linePropsObjectId: { type: 'string', description: 'Line object ID' }, + lineColor: { type: 'string', description: 'Line color (hex)' }, + lineWeight: { type: 'number', description: 'Line weight (pt)' }, + lineDashStyle: { type: 'string', description: 'Line dash style' }, + lineStartArrow: { type: 'string', description: 'Line start arrow' }, + lineEndArrow: { type: 'string', description: 'Line end arrow' }, + lineLinkUrl: { type: 'string', description: 'Line hyperlink URL' }, + linePropertiesJson: { type: 'string', description: 'Raw LineProperties JSON' }, + linePropertiesFields: { type: 'string', description: 'FieldMask' }, + + // Update line category + lineCategoryObjectId: { type: 'string', description: 'Line object ID' }, + newLineCategory: { type: 'string', description: 'New line category' }, + + // Reroute line + rerouteLineObjectId: { type: 'string', description: 'Line object ID' }, + + // Table cell-reference ops + tableTargetObjectId: { type: 'string', description: 'Table object ID' }, + tableCellRowIndex: { type: 'number', description: 'Cell row index' }, + tableCellColumnIndex: { type: 'number', description: 'Cell column index' }, + tableInsertNumber: { type: 'number', description: 'Number of rows/columns to insert' }, + tableInsertBelow: { type: 'boolean', description: 'Insert below reference cell' }, + tableInsertRight: { type: 'boolean', description: 'Insert to the right of reference cell' }, + + // Table range ops + tableRangeObjectId: { type: 'string', description: 'Table object ID' }, + tableRangeRowIndex: { type: 'number', description: 'Range start row' }, + tableRangeColumnIndex: { type: 'number', description: 'Range start column' }, + tableRangeRowSpan: { type: 'number', description: 'Row span' }, + tableRangeColumnSpan: { type: 'number', description: 'Column span' }, + + // Update table cell properties + tableCellBackgroundColor: { type: 'string', description: 'Cell background color (hex)' }, + tableCellBackgroundAlpha: { type: 'number', description: 'Cell background opacity' }, + tableCellContentAlignment: { type: 'string', description: 'Cell content alignment' }, + tableCellPropertiesJson: { type: 'string', description: 'Raw TableCellProperties JSON' }, + tableCellPropertiesFields: { type: 'string', description: 'FieldMask' }, + + // Update table border properties + tableBorderPosition: { type: 'string', description: 'Border position' }, + tableBorderColor: { type: 'string', description: 'Border color (hex)' }, + tableBorderWeight: { type: 'number', description: 'Border weight (pt)' }, + tableBorderDashStyle: { type: 'string', description: 'Border dash style' }, + tableBorderPropertiesJson: { type: 'string', description: 'Raw TableBorderProperties JSON' }, + tableBorderPropertiesFields: { type: 'string', description: 'FieldMask' }, + + // Update table column properties + tableColumnPropsObjectId: { type: 'string', description: 'Table object ID' }, + tableColumnIndices: { type: 'string', description: 'Comma-separated column indices' }, + tableColumnWidth: { type: 'number', description: 'Column width (pt)' }, + tableColumnPropertiesJson: { type: 'string', description: 'Raw TableColumnProperties JSON' }, + tableColumnPropertiesFields: { type: 'string', description: 'FieldMask' }, + + // Update table row properties + tableRowPropsObjectId: { type: 'string', description: 'Table object ID' }, + tableRowIndices: { type: 'string', description: 'Comma-separated row indices' }, + tableMinRowHeight: { type: 'number', description: 'Minimum row height (pt)' }, + tableRowPropertiesJson: { type: 'string', description: 'Raw TableRowProperties JSON' }, + tableRowPropertiesFields: { type: 'string', description: 'FieldMask' }, + + // Sheets chart + chartPageObjectId: { type: 'string', description: 'Slide object ID' }, + chartSpreadsheetId: { type: 'string', description: 'Spreadsheet ID' }, + chartId: { type: 'number', description: 'Chart ID' }, + chartLinkingMode: { type: 'string', description: 'Chart linking mode' }, + chartWidth: { type: 'number', description: 'Chart width (pt)' }, + chartHeight: { type: 'number', description: 'Chart height (pt)' }, + chartPositionX: { type: 'number', description: 'Chart X position (pt)' }, + chartPositionY: { type: 'number', description: 'Chart Y position (pt)' }, + + // Refresh sheets chart + refreshChartObjectId: { type: 'string', description: 'Chart object ID' }, + + // Replace all shapes with sheets chart + replaceShapesChartFindText: { type: 'string', description: 'Text token to replace' }, + replaceShapesChartMatchCase: { type: 'boolean', description: 'Match case' }, + replaceShapesChartPageObjectIds: { type: 'string', description: 'Slide IDs to limit to' }, + + // Create video + videoPageObjectId: { type: 'string', description: 'Slide object ID' }, + videoSource: { type: 'string', description: 'Video source (YOUTUBE or DRIVE)' }, + videoId: { type: 'string', description: 'Video ID' }, + videoWidth: { type: 'number', description: 'Video width (pt)' }, + videoHeight: { type: 'number', description: 'Video height (pt)' }, + videoPositionX: { type: 'number', description: 'Video X position (pt)' }, + videoPositionY: { type: 'number', description: 'Video Y position (pt)' }, + + // Update video properties + videoPropsObjectId: { type: 'string', description: 'Video object ID' }, + videoAutoPlay: { type: 'boolean', description: 'Auto play' }, + videoMute: { type: 'boolean', description: 'Mute' }, + videoStart: { type: 'number', description: 'Playback start (sec)' }, + videoEnd: { type: 'number', description: 'Playback end (sec)' }, + videoOutlineColor: { type: 'string', description: 'Outline color (hex)' }, + videoOutlineWeight: { type: 'number', description: 'Outline weight (pt)' }, + videoOutlineDashStyle: { type: 'string', description: 'Outline dash style' }, + videoPropertiesJson: { type: 'string', description: 'Raw VideoProperties JSON' }, + videoPropertiesFields: { type: 'string', description: 'FieldMask' }, }, outputs: { // Read operation @@ -912,6 +3358,61 @@ Return ONLY the text content - no explanations, no markdown formatting markers, // Insert text operation inserted: { type: 'boolean', description: 'Whether text was inserted' }, text: { type: 'string', description: 'Text that was inserted' }, + + // Generic update outputs (text style, paragraph style, shape/page/slide props, image/line/video props, table props) + updated: { type: 'boolean', description: 'Whether the operation updated the target' }, + fields: { type: 'string', description: 'FieldMask that was applied' }, + + // Copy presentation + presentationId: { type: 'string', description: 'New presentation ID (copy)' }, + title: { type: 'string', description: 'Presentation title' }, + + // Export presentation + contentBase64: { type: 'string', description: 'Base64-encoded exported content' }, + mimeType: { type: 'string', description: 'MIME type of the exported content' }, + sizeBytes: { type: 'number', description: 'Size of the exported content in bytes' }, + + // Batch update (raw) + replies: { type: 'json', description: 'Array of reply objects from batchUpdate' }, + writeControl: { type: 'json', description: 'WriteControl from batchUpdate response' }, + + // Image / line / video object IDs + imageObjectId: { type: 'string', description: 'Image object ID' }, + lineId: { type: 'string', description: 'Line object ID' }, + videoObjectId: { type: 'string', description: 'Video object ID' }, + chartObjectId: { type: 'string', description: 'Sheets chart object ID' }, + + // Replace image + replaced: { type: 'boolean', description: 'Whether the image was replaced' }, + + // Group / ungroup + grouped: { type: 'boolean', description: 'Whether objects were grouped' }, + ungrouped: { type: 'boolean', description: 'Whether objects were ungrouped' }, + groupObjectId: { type: 'string', description: 'Object ID of the resulting group' }, + childrenObjectIds: { type: 'json', description: 'Children IDs of the group' }, + + // Z-order + reordered: { type: 'boolean', description: 'Whether the z-order was changed' }, + objectIds: { type: 'json', description: 'Object IDs affected by the operation' }, + + // Table extension + tableObjectId: { type: 'string', description: 'Table object ID affected' }, + number: { type: 'number', description: 'Number of rows/columns inserted' }, + merged: { type: 'boolean', description: 'Whether cells were merged' }, + unmerged: { type: 'boolean', description: 'Whether cells were unmerged' }, + + // Sheets chart + refreshed: { type: 'boolean', description: 'Whether the chart was refreshed' }, + + // Line reroute + rerouted: { type: 'boolean', description: 'Whether the line was rerouted' }, + + // Paragraph bullets + created: { type: 'boolean', description: 'Whether bullets were created' }, + + // Bullets / shape / line categories returned + lineCategory: { type: 'string', description: 'Line category created or updated' }, + shapeType: { type: 'string', description: 'Shape type created' }, }, } diff --git a/apps/sim/tools/google_slides/batch_update.ts b/apps/sim/tools/google_slides/batch_update.ts new file mode 100644 index 00000000000..a935614a936 --- /dev/null +++ b/apps/sim/tools/google_slides/batch_update.ts @@ -0,0 +1,139 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesBatchUpdateTool') + +interface BatchUpdateParams { + accessToken: string + presentationId: string + requests: string + writeControl?: string +} + +interface BatchUpdateResponse { + success: boolean + output: { + replies: unknown[] + writeControl: unknown + metadata: { presentationId: string; url: string; requestCount: number } + } +} + +export const batchUpdateTool: ToolConfig = { + id: 'google_slides_batch_update', + name: 'Batch Update Google Slides (Raw)', + description: + 'Run a raw Slides API batchUpdate with a list of Request objects. Use this when the higher-level tools do not cover an operation, or to bundle multiple operations into a single atomic batch (all-or-nothing).', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + requests: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON array of Slides API Request objects. Example: [{"replaceAllText":{"containsText":{"text":"{{title}}"},"replaceText":"Q3 Review"}}, {"updatePageProperties":{"objectId":"slide_1","pageProperties":{"pageBackgroundFill":{"solidFill":{"color":{"rgbColor":{"red":0.043,"green":0.122,"blue":0.231}}}}},"fields":"pageBackgroundFill"}}]', + }, + writeControl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Optional JSON WriteControl object for optimistic concurrency, e.g. {"requiredRevisionId":"..."}', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const raw = params.requests + if (!raw) throw new Error('Requests JSON is required') + + let requests: unknown + try { + requests = typeof raw === 'string' ? JSON.parse(raw) : raw + } catch (e) { + throw new Error(`Invalid requests JSON: ${(e as Error).message}`) + } + if (!Array.isArray(requests)) { + throw new Error('Requests must be a JSON array of Request objects') + } + if (requests.length === 0) { + throw new Error('Requests array must contain at least one Request') + } + + const body: Record = { requests } + + if (params.writeControl?.trim()) { + try { + const wc = JSON.parse(params.writeControl) + if (wc && typeof wc === 'object') body.writeControl = wc + } catch (e) { + logger.warn('Invalid writeControl JSON, ignoring:', { error: e }) + } + } + + return body + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Batch update failed') + } + const presentationId = params?.presentationId?.trim() || '' + const replies: unknown[] = Array.isArray(data.replies) ? data.replies : [] + return { + success: true, + output: { + replies, + writeControl: data.writeControl ?? null, + metadata: { + presentationId, + url: presentationUrl(presentationId), + requestCount: replies.length, + }, + }, + } + }, + + outputs: { + replies: { + type: 'array', + description: 'Array of reply objects, one per request (parallel-indexed)', + items: { type: 'json' }, + }, + writeControl: { + type: 'json', + description: 'WriteControl returned by the server (revision tracking)', + }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + requestCount: { type: 'number', description: 'Number of replies returned' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/copy_presentation.ts b/apps/sim/tools/google_slides/copy_presentation.ts new file mode 100644 index 00000000000..9c258982916 --- /dev/null +++ b/apps/sim/tools/google_slides/copy_presentation.ts @@ -0,0 +1,128 @@ +import { createLogger } from '@sim/logger' +import { presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesCopyPresentationTool') + +interface CopyPresentationParams { + accessToken: string + sourcePresentationId: string + title?: string + folderId?: string +} + +interface CopyPresentationResponse { + success: boolean + output: { + presentationId: string + title: string + metadata: { + sourcePresentationId: string + presentationId: string + title: string + mimeType: string + url: string + } + } +} + +const PRESENTATION_MIME = 'application/vnd.google-apps.presentation' + +export const copyPresentationTool: ToolConfig = { + id: 'google_slides_copy_presentation', + name: 'Copy Google Slides Presentation', + description: + 'Copy a template presentation in Drive to a new file. Use this before merging data so the original template is never modified.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides / Drive API', + }, + sourcePresentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Drive file ID of the source/template presentation', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Title for the copy. Defaults to "Copy of ".', + }, + folderId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Drive folder ID where the copy should be placed', + }, + }, + + request: { + url: (params) => { + const sourceId = params.sourcePresentationId?.trim() + if (!sourceId) throw new Error('Source presentation ID is required') + return `https://www.googleapis.com/drive/v3/files/${sourceId}/copy?supportsAllDrives=true` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) throw new Error('Access token is required') + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const body: Record = {} + if (params.title?.trim()) body.name = params.title.trim() + if (params.folderId?.trim()) body.parents = [params.folderId.trim()] + return body + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Drive API error during copy:', { data }) + throw new Error(data.error?.message || 'Failed to copy presentation') + } + const presentationId: string = data.id + const title: string = data.name || 'Untitled Presentation' + return { + success: true, + output: { + presentationId, + title, + metadata: { + sourcePresentationId: params?.sourcePresentationId?.trim() || '', + presentationId, + title, + mimeType: PRESENTATION_MIME, + url: presentationUrl(presentationId), + }, + }, + } + }, + + outputs: { + presentationId: { type: 'string', description: 'ID of the new copied presentation' }, + title: { type: 'string', description: 'Title of the new presentation' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + sourcePresentationId: { type: 'string', description: 'Source/template presentation ID' }, + presentationId: { type: 'string', description: 'New presentation ID' }, + title: { type: 'string', description: 'New presentation title' }, + mimeType: { type: 'string', description: 'MIME type of the presentation' }, + url: { type: 'string', description: 'URL to the new presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/create_line.ts b/apps/sim/tools/google_slides/create_line.ts new file mode 100644 index 00000000000..9c86d04e72b --- /dev/null +++ b/apps/sim/tools/google_slides/create_line.ts @@ -0,0 +1,157 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + buildElementProperties, + generateObjectId, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesCreateLineTool') + +interface CreateLineParams { + accessToken: string + presentationId: string + pageObjectId: string + lineCategory?: 'STRAIGHT' | 'BENT' | 'CURVED' + width?: number + height?: number + positionX?: number + positionY?: number +} + +interface CreateLineResponse { + success: boolean + output: { + lineId: string + lineCategory: string + metadata: { presentationId: string; pageObjectId: string; url: string } + } +} + +export const createLineTool: ToolConfig = { + id: 'google_slides_create_line', + name: 'Create Line in Google Slides', + description: 'Create a line or connector on a slide.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + pageObjectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the slide to add the line to', + }, + lineCategory: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'STRAIGHT (default), BENT, or CURVED', + }, + width: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Line width in points (default 200)', + }, + height: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Line height in points (default 0 — horizontal line)', + }, + positionX: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'X position in points (default 100)', + }, + positionY: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Y position in points (default 100)', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const pageObjectId = params.pageObjectId?.trim() + if (!pageObjectId) throw new Error('Page Object ID is required') + + const objectId = generateObjectId('line') + const elementProperties = buildElementProperties({ + pageObjectId, + width: params.width, + height: params.height ?? 1, + positionX: params.positionX, + positionY: params.positionY, + defaultWidth: 200, + defaultHeight: 1, + }) + + return { + requests: [ + { + createLine: { + objectId, + lineCategory: params.lineCategory || 'STRAIGHT', + elementProperties, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to create line') + } + const lineId = data.replies?.[0]?.createLine?.objectId ?? '' + const presentationId = params?.presentationId?.trim() || '' + const pageObjectId = params?.pageObjectId?.trim() || '' + return { + success: true, + output: { + lineId, + lineCategory: params?.lineCategory || 'STRAIGHT', + metadata: { presentationId, pageObjectId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + lineId: { type: 'string', description: 'Object ID of the new line' }, + lineCategory: { type: 'string', description: 'Line category created' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + pageObjectId: { type: 'string', description: 'The slide ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/create_paragraph_bullets.ts b/apps/sim/tools/google_slides/create_paragraph_bullets.ts new file mode 100644 index 00000000000..05582db7b54 --- /dev/null +++ b/apps/sim/tools/google_slides/create_paragraph_bullets.ts @@ -0,0 +1,160 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + buildCellLocation, + buildTextRange, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesCreateParagraphBulletsTool') + +interface CreateParagraphBulletsParams { + accessToken: string + presentationId: string + objectId: string + rowIndex?: number + columnIndex?: number + rangeType?: 'ALL' | 'FROM_START_INDEX' | 'FIXED_RANGE' + startIndex?: number + endIndex?: number + bulletPreset?: string +} + +interface CreateParagraphBulletsResponse { + success: boolean + output: { + created: boolean + objectId: string + metadata: { presentationId: string; url: string } + } +} + +export const createParagraphBulletsTool: ToolConfig< + CreateParagraphBulletsParams, + CreateParagraphBulletsResponse +> = { + id: 'google_slides_create_paragraph_bullets', + name: 'Create Paragraph Bullets in Google Slides', + description: + 'Convert paragraphs in a shape or table cell into a bulleted or numbered list using a Google Slides bullet preset.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the shape or table containing the text', + }, + rowIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'When targeting a table cell, the zero-based row index', + }, + columnIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'When targeting a table cell, the zero-based column index', + }, + rangeType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Range to apply bullets to: ALL (default), FROM_START_INDEX, or FIXED_RANGE', + }, + startIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start index for FROM_START_INDEX or FIXED_RANGE', + }, + endIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'End index for FIXED_RANGE', + }, + bulletPreset: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Bullet preset (e.g. BULLET_DISC_CIRCLE_SQUARE, BULLET_ARROW_DIAMOND_DISC, NUMBERED_DIGIT_ALPHA_ROMAN, NUMBERED_DIGIT_ALPHA_ROMAN_PARENS, NUMBERED_DIGIT_NESTED). Defaults to BULLET_DISC_CIRCLE_SQUARE.', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + + const createRequest: Record = { + objectId, + textRange: buildTextRange({ + rangeType: params.rangeType, + startIndex: params.startIndex, + endIndex: params.endIndex, + }), + bulletPreset: params.bulletPreset?.trim() || 'BULLET_DISC_CIRCLE_SQUARE', + } + const cellLocation = buildCellLocation({ + rowIndex: params.rowIndex, + columnIndex: params.columnIndex, + }) + if (cellLocation) createRequest.cellLocation = cellLocation + + return { requests: [{ createParagraphBullets: createRequest }] } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to create paragraph bullets') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + created: true, + objectId: params?.objectId?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + created: { type: 'boolean', description: 'Whether bullets were created' }, + objectId: { type: 'string', description: 'The object where bullets were created' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/create_sheets_chart.ts b/apps/sim/tools/google_slides/create_sheets_chart.ts new file mode 100644 index 00000000000..6452618a4a9 --- /dev/null +++ b/apps/sim/tools/google_slides/create_sheets_chart.ts @@ -0,0 +1,175 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + buildElementProperties, + generateObjectId, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesCreateSheetsChartTool') + +interface CreateSheetsChartParams { + accessToken: string + presentationId: string + pageObjectId: string + spreadsheetId: string + chartId: number + linkingMode?: 'LINKED' | 'NOT_LINKED_IMAGE' + width?: number + height?: number + positionX?: number + positionY?: number +} + +interface CreateSheetsChartResponse { + success: boolean + output: { + chartObjectId: string + metadata: { presentationId: string; pageObjectId: string; url: string } + } +} + +export const createSheetsChartTool: ToolConfig = + { + id: 'google_slides_create_sheets_chart', + name: 'Embed Google Sheets Chart in Slides', + description: + 'Embed a chart from a Google Sheets spreadsheet onto a slide. LINKED charts can be refreshed; NOT_LINKED_IMAGE inserts a static image of the chart.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + pageObjectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the slide to add the chart to', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Sheets spreadsheet ID containing the chart', + }, + chartId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Numeric chart ID within the spreadsheet', + }, + linkingMode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LINKED (default) or NOT_LINKED_IMAGE', + }, + width: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Width in points (default 400)', + }, + height: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Height in points (default 300)', + }, + positionX: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'X position in points (default 100)', + }, + positionY: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Y position in points (default 100)', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const pageObjectId = params.pageObjectId?.trim() + const spreadsheetId = params.spreadsheetId?.trim() + if (!pageObjectId) throw new Error('Page Object ID is required') + if (!spreadsheetId) throw new Error('Spreadsheet ID is required') + if (params.chartId === undefined) throw new Error('Chart ID is required') + + const objectId = generateObjectId('chart') + const elementProperties = buildElementProperties({ + pageObjectId, + width: params.width, + height: params.height, + positionX: params.positionX, + positionY: params.positionY, + defaultWidth: 400, + defaultHeight: 300, + }) + + return { + requests: [ + { + createSheetsChart: { + objectId, + spreadsheetId, + chartId: params.chartId, + linkingMode: params.linkingMode || 'LINKED', + elementProperties, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to create sheets chart') + } + const chartObjectId = data.replies?.[0]?.createSheetsChart?.objectId ?? '' + const presentationId = params?.presentationId?.trim() || '' + const pageObjectId = params?.pageObjectId?.trim() || '' + return { + success: true, + output: { + chartObjectId, + metadata: { presentationId, pageObjectId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + chartObjectId: { type: 'string', description: 'Object ID of the inserted chart' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + pageObjectId: { type: 'string', description: 'The slide ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, + } diff --git a/apps/sim/tools/google_slides/create_video.ts b/apps/sim/tools/google_slides/create_video.ts new file mode 100644 index 00000000000..bb15566904d --- /dev/null +++ b/apps/sim/tools/google_slides/create_video.ts @@ -0,0 +1,165 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + buildElementProperties, + generateObjectId, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesCreateVideoTool') + +interface CreateVideoParams { + accessToken: string + presentationId: string + pageObjectId: string + source: 'YOUTUBE' | 'DRIVE' + videoId: string + width?: number + height?: number + positionX?: number + positionY?: number +} + +interface CreateVideoResponse { + success: boolean + output: { + videoObjectId: string + metadata: { presentationId: string; pageObjectId: string; url: string } + } +} + +export const createVideoTool: ToolConfig = { + id: 'google_slides_create_video', + name: 'Embed Video in Google Slides', + description: 'Embed a YouTube or Google Drive video on a slide.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + pageObjectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the slide to add the video to', + }, + source: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'YOUTUBE or DRIVE', + }, + videoId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'YouTube video ID or Drive file ID', + }, + width: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Width in points (default 400)', + }, + height: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Height in points (default 225)', + }, + positionX: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'X position in points (default 100)', + }, + positionY: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Y position in points (default 100)', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const pageObjectId = params.pageObjectId?.trim() + const videoId = params.videoId?.trim() + if (!pageObjectId) throw new Error('Page Object ID is required') + if (!videoId) throw new Error('Video ID is required') + if (!params.source) throw new Error('Source is required (YOUTUBE or DRIVE)') + + const objectId = generateObjectId('video') + const elementProperties = buildElementProperties({ + pageObjectId, + width: params.width, + height: params.height, + positionX: params.positionX, + positionY: params.positionY, + defaultWidth: 400, + defaultHeight: 225, + }) + + return { + requests: [ + { + createVideo: { + objectId, + source: params.source, + id: videoId, + elementProperties, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to create video') + } + const videoObjectId = data.replies?.[0]?.createVideo?.objectId ?? '' + const presentationId = params?.presentationId?.trim() || '' + const pageObjectId = params?.pageObjectId?.trim() || '' + return { + success: true, + output: { + videoObjectId, + metadata: { presentationId, pageObjectId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + videoObjectId: { type: 'string', description: 'Object ID of the inserted video' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + pageObjectId: { type: 'string', description: 'The slide ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/delete_paragraph_bullets.ts b/apps/sim/tools/google_slides/delete_paragraph_bullets.ts new file mode 100644 index 00000000000..0364059cfde --- /dev/null +++ b/apps/sim/tools/google_slides/delete_paragraph_bullets.ts @@ -0,0 +1,150 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + buildCellLocation, + buildTextRange, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesDeleteParagraphBulletsTool') + +interface DeleteParagraphBulletsParams { + accessToken: string + presentationId: string + objectId: string + rowIndex?: number + columnIndex?: number + rangeType?: 'ALL' | 'FROM_START_INDEX' | 'FIXED_RANGE' + startIndex?: number + endIndex?: number +} + +interface DeleteParagraphBulletsResponse { + success: boolean + output: { + deleted: boolean + objectId: string + metadata: { presentationId: string; url: string } + } +} + +export const deleteParagraphBulletsTool: ToolConfig< + DeleteParagraphBulletsParams, + DeleteParagraphBulletsResponse +> = { + id: 'google_slides_delete_paragraph_bullets', + name: 'Delete Paragraph Bullets in Google Slides', + description: 'Remove bullets/numbering from paragraphs in a shape or table cell.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the shape or table containing the text', + }, + rowIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'When targeting a table cell, the zero-based row index', + }, + columnIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'When targeting a table cell, the zero-based column index', + }, + rangeType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Range to clear bullets from: ALL (default), FROM_START_INDEX, or FIXED_RANGE', + }, + startIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start index for FROM_START_INDEX or FIXED_RANGE', + }, + endIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'End index for FIXED_RANGE', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + + const deleteRequest: Record = { + objectId, + textRange: buildTextRange({ + rangeType: params.rangeType, + startIndex: params.startIndex, + endIndex: params.endIndex, + }), + } + const cellLocation = buildCellLocation({ + rowIndex: params.rowIndex, + columnIndex: params.columnIndex, + }) + if (cellLocation) deleteRequest.cellLocation = cellLocation + + return { requests: [{ deleteParagraphBullets: deleteRequest }] } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to delete paragraph bullets') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + deleted: true, + objectId: params?.objectId?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether bullets were deleted' }, + objectId: { type: 'string', description: 'The object whose bullets were deleted' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/delete_table_column.ts b/apps/sim/tools/google_slides/delete_table_column.ts new file mode 100644 index 00000000000..14a08c1eb77 --- /dev/null +++ b/apps/sim/tools/google_slides/delete_table_column.ts @@ -0,0 +1,116 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesDeleteTableColumnTool') + +interface DeleteTableColumnParams { + accessToken: string + presentationId: string + tableObjectId: string + rowIndex: number + columnIndex: number +} + +interface DeleteTableColumnResponse { + success: boolean + output: { + deleted: boolean + tableObjectId: string + metadata: { presentationId: string; url: string } + } +} + +export const deleteTableColumnTool: ToolConfig = + { + id: 'google_slides_delete_table_column', + name: 'Delete Table Column in Google Slides', + description: 'Delete the column containing the reference cell from a table.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + tableObjectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the table', + }, + rowIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based row index of any cell in the column', + }, + columnIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based column index identifying the column to delete', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const tableObjectId = params.tableObjectId?.trim() + if (!tableObjectId) throw new Error('Table object ID is required') + + return { + requests: [ + { + deleteTableColumn: { + tableObjectId, + cellLocation: { rowIndex: params.rowIndex, columnIndex: params.columnIndex }, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to delete table column') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + deleted: true, + tableObjectId: params?.tableObjectId?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the column was deleted' }, + tableObjectId: { type: 'string', description: 'The table updated' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, + } diff --git a/apps/sim/tools/google_slides/delete_table_row.ts b/apps/sim/tools/google_slides/delete_table_row.ts new file mode 100644 index 00000000000..62f662b4976 --- /dev/null +++ b/apps/sim/tools/google_slides/delete_table_row.ts @@ -0,0 +1,115 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesDeleteTableRowTool') + +interface DeleteTableRowParams { + accessToken: string + presentationId: string + tableObjectId: string + rowIndex: number + columnIndex: number +} + +interface DeleteTableRowResponse { + success: boolean + output: { + deleted: boolean + tableObjectId: string + metadata: { presentationId: string; url: string } + } +} + +export const deleteTableRowTool: ToolConfig = { + id: 'google_slides_delete_table_row', + name: 'Delete Table Row in Google Slides', + description: 'Delete the row containing the reference cell from a table.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + tableObjectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the table', + }, + rowIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based row index identifying the row to delete', + }, + columnIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based column index of any cell in the row', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const tableObjectId = params.tableObjectId?.trim() + if (!tableObjectId) throw new Error('Table object ID is required') + + return { + requests: [ + { + deleteTableRow: { + tableObjectId, + cellLocation: { rowIndex: params.rowIndex, columnIndex: params.columnIndex }, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to delete table row') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + deleted: true, + tableObjectId: params?.tableObjectId?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the row was deleted' }, + tableObjectId: { type: 'string', description: 'The table updated' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/delete_text.ts b/apps/sim/tools/google_slides/delete_text.ts new file mode 100644 index 00000000000..d618b2d3018 --- /dev/null +++ b/apps/sim/tools/google_slides/delete_text.ts @@ -0,0 +1,148 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + buildCellLocation, + buildTextRange, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesDeleteTextTool') + +interface DeleteTextParams { + accessToken: string + presentationId: string + objectId: string + rowIndex?: number + columnIndex?: number + rangeType?: 'ALL' | 'FROM_START_INDEX' | 'FIXED_RANGE' + startIndex?: number + endIndex?: number +} + +interface DeleteTextResponse { + success: boolean + output: { + deleted: boolean + objectId: string + metadata: { presentationId: string; url: string } + } +} + +export const deleteTextTool: ToolConfig = { + id: 'google_slides_delete_text', + name: 'Delete Text in Google Slides', + description: + 'Delete text from a shape or table cell. Use range type ALL to clear all text, or FIXED_RANGE / FROM_START_INDEX to delete a specific span.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the shape or table containing the text', + }, + rowIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'When targeting a table cell, the zero-based row index', + }, + columnIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'When targeting a table cell, the zero-based column index', + }, + rangeType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Range to delete: ALL (default), FROM_START_INDEX, or FIXED_RANGE', + }, + startIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start index for FROM_START_INDEX or FIXED_RANGE', + }, + endIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'End index for FIXED_RANGE', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + + const deleteRequest: Record = { + objectId, + textRange: buildTextRange({ + rangeType: params.rangeType, + startIndex: params.startIndex, + endIndex: params.endIndex, + }), + } + const cellLocation = buildCellLocation({ + rowIndex: params.rowIndex, + columnIndex: params.columnIndex, + }) + if (cellLocation) deleteRequest.cellLocation = cellLocation + + return { requests: [{ deleteText: deleteRequest }] } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to delete text') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + deleted: true, + objectId: params?.objectId?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the text was deleted' }, + objectId: { type: 'string', description: 'The object whose text was deleted' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/export_presentation.ts b/apps/sim/tools/google_slides/export_presentation.ts new file mode 100644 index 00000000000..78d8cea1145 --- /dev/null +++ b/apps/sim/tools/google_slides/export_presentation.ts @@ -0,0 +1,137 @@ +import { createLogger } from '@sim/logger' +import { presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesExportPresentationTool') + +interface ExportPresentationParams { + accessToken: string + presentationId: string + exportFormat?: 'PDF' | 'PPTX' | 'ODP' | 'TXT' | 'PNG' | 'JPEG' | 'SVG' +} + +interface ExportPresentationResponse { + success: boolean + output: { + contentBase64: string + mimeType: string + sizeBytes: number + metadata: { presentationId: string; url: string; exportFormat: string } + } +} + +const FORMAT_TO_MIME: Record = { + PDF: 'application/pdf', + PPTX: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + ODP: 'application/vnd.oasis.opendocument.presentation', + TXT: 'text/plain', + PNG: 'image/png', + JPEG: 'image/jpeg', + SVG: 'image/svg+xml', +} + +export const exportPresentationTool: ToolConfig< + ExportPresentationParams, + ExportPresentationResponse +> = { + id: 'google_slides_export_presentation', + name: 'Export Google Slides Presentation', + description: + 'Export a presentation to PDF, PPTX, ODP, TXT, PNG, JPEG, or SVG via the Drive export endpoint. Returns the file content base64-encoded.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides / Drive API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + exportFormat: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Format: PDF (default), PPTX, ODP, TXT, PNG, JPEG, or SVG', + }, + }, + + request: { + url: (params) => { + const presentationId = params.presentationId?.trim() + if (!presentationId) throw new Error('Presentation ID is required') + const format = (params.exportFormat || 'PDF').toUpperCase() + const mime = FORMAT_TO_MIME[format] + if (!mime) throw new Error(`Unsupported export format: ${format}`) + return `https://www.googleapis.com/drive/v3/files/${presentationId}/export?mimeType=${encodeURIComponent(mime)}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) throw new Error('Access token is required') + return { Authorization: `Bearer ${params.accessToken}` } + }, + }, + + transformResponse: async (response: Response, params) => { + if (!response.ok) { + let errorMessage = `Failed to export presentation (status ${response.status})` + try { + const data = await response.json() + errorMessage = data.error?.message || errorMessage + logger.error('Drive API error during export:', { data }) + } catch { + // Body wasn't JSON — fall through with default error message. + } + throw new Error(errorMessage) + } + + const buffer = await response.arrayBuffer() + const bytes = new Uint8Array(buffer) + let binary = '' + for (let i = 0; i < bytes.length; i += 1) { + binary += String.fromCharCode(bytes[i]) + } + const contentBase64 = + typeof btoa === 'function' ? btoa(binary) : Buffer.from(buffer).toString('base64') + + const presentationId = params?.presentationId?.trim() || '' + const format = (params?.exportFormat || 'PDF').toUpperCase() + const mime = FORMAT_TO_MIME[format] ?? 'application/octet-stream' + + return { + success: true, + output: { + contentBase64, + mimeType: mime, + sizeBytes: bytes.length, + metadata: { + presentationId, + url: presentationUrl(presentationId), + exportFormat: format, + }, + }, + } + }, + + outputs: { + contentBase64: { type: 'string', description: 'Base64-encoded exported file content' }, + mimeType: { type: 'string', description: 'MIME type of the exported content' }, + sizeBytes: { type: 'number', description: 'Size of the exported content in bytes' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + exportFormat: { type: 'string', description: 'Export format used' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/group_objects.ts b/apps/sim/tools/google_slides/group_objects.ts new file mode 100644 index 00000000000..1091c16ac6a --- /dev/null +++ b/apps/sim/tools/google_slides/group_objects.ts @@ -0,0 +1,131 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + generateObjectId, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesGroupObjectsTool') + +interface GroupObjectsParams { + accessToken: string + presentationId: string + childrenObjectIds: string + groupObjectId?: string +} + +interface GroupObjectsResponse { + success: boolean + output: { + grouped: boolean + groupObjectId: string + childrenObjectIds: string[] + metadata: { presentationId: string; url: string } + } +} + +export const groupObjectsTool: ToolConfig = { + id: 'google_slides_group_objects', + name: 'Group Objects in Google Slides', + description: 'Group two or more page elements on the same slide into a single object group.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + childrenObjectIds: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Comma-separated object IDs of the elements to group (must be on the same slide)', + }, + groupObjectId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional object ID to assign to the new group', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const children = (params.childrenObjectIds || '') + .split(',') + .map((id) => id.trim()) + .filter((id) => id.length > 0) + if (children.length < 2) throw new Error('At least two child object IDs are required') + + const groupObjectId = params.groupObjectId?.trim() || generateObjectId('group') + + return { + requests: [ + { + groupObjects: { + groupObjectId, + childrenObjectIds: children, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to group objects') + } + const groupObjectId = data.replies?.[0]?.groupObjects?.objectId ?? params?.groupObjectId ?? '' + const presentationId = params?.presentationId?.trim() || '' + const children = (params?.childrenObjectIds || '') + .split(',') + .map((id) => id.trim()) + .filter((id) => id.length > 0) + return { + success: true, + output: { + grouped: true, + groupObjectId, + childrenObjectIds: children, + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + grouped: { type: 'boolean', description: 'Whether the objects were grouped' }, + groupObjectId: { type: 'string', description: 'Object ID of the new group' }, + childrenObjectIds: { + type: 'array', + description: 'IDs of the grouped children', + items: { type: 'string' }, + }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/index.ts b/apps/sim/tools/google_slides/index.ts index 5245d87f919..26667bab8b4 100644 --- a/apps/sim/tools/google_slides/index.ts +++ b/apps/sim/tools/google_slides/index.ts @@ -1,16 +1,54 @@ import { addImageTool } from '@/tools/google_slides/add_image' import { addSlideTool } from '@/tools/google_slides/add_slide' +import { batchUpdateTool } from '@/tools/google_slides/batch_update' +import { copyPresentationTool } from '@/tools/google_slides/copy_presentation' import { createTool } from '@/tools/google_slides/create' +import { createLineTool } from '@/tools/google_slides/create_line' +import { createParagraphBulletsTool } from '@/tools/google_slides/create_paragraph_bullets' import { createShapeTool } from '@/tools/google_slides/create_shape' +import { createSheetsChartTool } from '@/tools/google_slides/create_sheets_chart' import { createTableTool } from '@/tools/google_slides/create_table' +import { createVideoTool } from '@/tools/google_slides/create_video' import { deleteObjectTool } from '@/tools/google_slides/delete_object' +import { deleteParagraphBulletsTool } from '@/tools/google_slides/delete_paragraph_bullets' +import { deleteTableColumnTool } from '@/tools/google_slides/delete_table_column' +import { deleteTableRowTool } from '@/tools/google_slides/delete_table_row' +import { deleteTextTool } from '@/tools/google_slides/delete_text' import { duplicateObjectTool } from '@/tools/google_slides/duplicate_object' +import { exportPresentationTool } from '@/tools/google_slides/export_presentation' import { getPageTool } from '@/tools/google_slides/get_page' import { getThumbnailTool } from '@/tools/google_slides/get_thumbnail' +import { groupObjectsTool } from '@/tools/google_slides/group_objects' +import { insertTableColumnsTool } from '@/tools/google_slides/insert_table_columns' +import { insertTableRowsTool } from '@/tools/google_slides/insert_table_rows' import { insertTextTool } from '@/tools/google_slides/insert_text' +import { mergeTableCellsTool } from '@/tools/google_slides/merge_table_cells' import { readTool } from '@/tools/google_slides/read' +import { refreshSheetsChartTool } from '@/tools/google_slides/refresh_sheets_chart' +import { replaceAllShapesWithImageTool } from '@/tools/google_slides/replace_all_shapes_with_image' +import { replaceAllShapesWithSheetsChartTool } from '@/tools/google_slides/replace_all_shapes_with_sheets_chart' import { replaceAllTextTool } from '@/tools/google_slides/replace_all_text' +import { replaceImageTool } from '@/tools/google_slides/replace_image' +import { rerouteLineTool } from '@/tools/google_slides/reroute_line' +import { ungroupObjectsTool } from '@/tools/google_slides/ungroup_objects' +import { unmergeTableCellsTool } from '@/tools/google_slides/unmerge_table_cells' +import { updateImagePropertiesTool } from '@/tools/google_slides/update_image_properties' +import { updateLineCategoryTool } from '@/tools/google_slides/update_line_category' +import { updateLinePropertiesTool } from '@/tools/google_slides/update_line_properties' +import { updatePageElementAltTextTool } from '@/tools/google_slides/update_page_element_alt_text' +import { updatePageElementTransformTool } from '@/tools/google_slides/update_page_element_transform' +import { updatePageElementsZOrderTool } from '@/tools/google_slides/update_page_elements_z_order' +import { updatePagePropertiesTool } from '@/tools/google_slides/update_page_properties' +import { updateParagraphStyleTool } from '@/tools/google_slides/update_paragraph_style' +import { updateShapePropertiesTool } from '@/tools/google_slides/update_shape_properties' +import { updateSlidePropertiesTool } from '@/tools/google_slides/update_slide_properties' import { updateSlidesPositionTool } from '@/tools/google_slides/update_slides_position' +import { updateTableBorderPropertiesTool } from '@/tools/google_slides/update_table_border_properties' +import { updateTableCellPropertiesTool } from '@/tools/google_slides/update_table_cell_properties' +import { updateTableColumnPropertiesTool } from '@/tools/google_slides/update_table_column_properties' +import { updateTableRowPropertiesTool } from '@/tools/google_slides/update_table_row_properties' +import { updateTextStyleTool } from '@/tools/google_slides/update_text_style' +import { updateVideoPropertiesTool } from '@/tools/google_slides/update_video_properties' import { writeTool } from '@/tools/google_slides/write' export const googleSlidesReadTool = readTool @@ -27,3 +65,50 @@ export const googleSlidesUpdateSlidesPositionTool = updateSlidesPositionTool export const googleSlidesCreateTableTool = createTableTool export const googleSlidesCreateShapeTool = createShapeTool export const googleSlidesInsertTextTool = insertTextTool + +export const googleSlidesUpdateTextStyleTool = updateTextStyleTool +export const googleSlidesUpdateParagraphStyleTool = updateParagraphStyleTool +export const googleSlidesDeleteTextTool = deleteTextTool +export const googleSlidesCreateParagraphBulletsTool = createParagraphBulletsTool +export const googleSlidesDeleteParagraphBulletsTool = deleteParagraphBulletsTool + +export const googleSlidesReplaceAllShapesWithImageTool = replaceAllShapesWithImageTool +export const googleSlidesReplaceImageTool = replaceImageTool +export const googleSlidesUpdateImagePropertiesTool = updateImagePropertiesTool + +export const googleSlidesUpdateShapePropertiesTool = updateShapePropertiesTool +export const googleSlidesUpdatePagePropertiesTool = updatePagePropertiesTool +export const googleSlidesUpdateSlidePropertiesTool = updateSlidePropertiesTool +export const googleSlidesUpdatePageElementAltTextTool = updatePageElementAltTextTool + +export const googleSlidesUpdatePageElementTransformTool = updatePageElementTransformTool +export const googleSlidesUpdatePageElementsZOrderTool = updatePageElementsZOrderTool +export const googleSlidesGroupObjectsTool = groupObjectsTool +export const googleSlidesUngroupObjectsTool = ungroupObjectsTool + +export const googleSlidesCreateLineTool = createLineTool +export const googleSlidesUpdateLinePropertiesTool = updateLinePropertiesTool +export const googleSlidesUpdateLineCategoryTool = updateLineCategoryTool +export const googleSlidesRerouteLineTool = rerouteLineTool + +export const googleSlidesInsertTableRowsTool = insertTableRowsTool +export const googleSlidesInsertTableColumnsTool = insertTableColumnsTool +export const googleSlidesDeleteTableRowTool = deleteTableRowTool +export const googleSlidesDeleteTableColumnTool = deleteTableColumnTool +export const googleSlidesMergeTableCellsTool = mergeTableCellsTool +export const googleSlidesUnmergeTableCellsTool = unmergeTableCellsTool +export const googleSlidesUpdateTableCellPropertiesTool = updateTableCellPropertiesTool +export const googleSlidesUpdateTableBorderPropertiesTool = updateTableBorderPropertiesTool +export const googleSlidesUpdateTableColumnPropertiesTool = updateTableColumnPropertiesTool +export const googleSlidesUpdateTableRowPropertiesTool = updateTableRowPropertiesTool + +export const googleSlidesCreateSheetsChartTool = createSheetsChartTool +export const googleSlidesRefreshSheetsChartTool = refreshSheetsChartTool +export const googleSlidesReplaceAllShapesWithSheetsChartTool = replaceAllShapesWithSheetsChartTool + +export const googleSlidesCreateVideoTool = createVideoTool +export const googleSlidesUpdateVideoPropertiesTool = updateVideoPropertiesTool + +export const googleSlidesBatchUpdateTool = batchUpdateTool +export const googleSlidesCopyPresentationTool = copyPresentationTool +export const googleSlidesExportPresentationTool = exportPresentationTool diff --git a/apps/sim/tools/google_slides/insert_table_columns.ts b/apps/sim/tools/google_slides/insert_table_columns.ts new file mode 100644 index 00000000000..ec7b0c27fce --- /dev/null +++ b/apps/sim/tools/google_slides/insert_table_columns.ts @@ -0,0 +1,139 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesInsertTableColumnsTool') + +interface InsertTableColumnsParams { + accessToken: string + presentationId: string + tableObjectId: string + rowIndex: number + columnIndex: number + number: number + insertRight?: boolean +} + +interface InsertTableColumnsResponse { + success: boolean + output: { + inserted: boolean + tableObjectId: string + number: number + metadata: { presentationId: string; url: string } + } +} + +export const insertTableColumnsTool: ToolConfig< + InsertTableColumnsParams, + InsertTableColumnsResponse +> = { + id: 'google_slides_insert_table_columns', + name: 'Insert Table Columns in Google Slides', + description: 'Insert one or more columns into a table, left or right of a reference cell.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + tableObjectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the table', + }, + rowIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based row index of the reference cell', + }, + columnIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based column index of the reference cell', + }, + number: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Number of columns to insert (minimum 1)', + }, + insertRight: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Insert to the right of the reference column instead of left (default false)', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const tableObjectId = params.tableObjectId?.trim() + if (!tableObjectId) throw new Error('Table object ID is required') + const number = params.number + if (!number || number < 1) throw new Error('Number of columns must be at least 1') + + return { + requests: [ + { + insertTableColumns: { + tableObjectId, + cellLocation: { rowIndex: params.rowIndex, columnIndex: params.columnIndex }, + insertRight: params.insertRight ?? false, + number, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to insert table columns') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + inserted: true, + tableObjectId: params?.tableObjectId?.trim() || '', + number: params?.number ?? 0, + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + inserted: { type: 'boolean', description: 'Whether columns were inserted' }, + tableObjectId: { type: 'string', description: 'The table updated' }, + number: { type: 'number', description: 'Number of columns inserted' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/insert_table_rows.ts b/apps/sim/tools/google_slides/insert_table_rows.ts new file mode 100644 index 00000000000..048e3804089 --- /dev/null +++ b/apps/sim/tools/google_slides/insert_table_rows.ts @@ -0,0 +1,136 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesInsertTableRowsTool') + +interface InsertTableRowsParams { + accessToken: string + presentationId: string + tableObjectId: string + rowIndex: number + columnIndex: number + number: number + insertBelow?: boolean +} + +interface InsertTableRowsResponse { + success: boolean + output: { + inserted: boolean + tableObjectId: string + number: number + metadata: { presentationId: string; url: string } + } +} + +export const insertTableRowsTool: ToolConfig = { + id: 'google_slides_insert_table_rows', + name: 'Insert Table Rows in Google Slides', + description: 'Insert one or more rows into a table, above or below a reference cell.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + tableObjectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the table', + }, + rowIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based row index of the reference cell', + }, + columnIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based column index of the reference cell', + }, + number: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Number of rows to insert (minimum 1)', + }, + insertBelow: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Insert below the reference row instead of above (default false)', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const tableObjectId = params.tableObjectId?.trim() + if (!tableObjectId) throw new Error('Table object ID is required') + const number = params.number + if (!number || number < 1) throw new Error('Number of rows must be at least 1') + + return { + requests: [ + { + insertTableRows: { + tableObjectId, + cellLocation: { rowIndex: params.rowIndex, columnIndex: params.columnIndex }, + insertBelow: params.insertBelow ?? false, + number, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to insert table rows') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + inserted: true, + tableObjectId: params?.tableObjectId?.trim() || '', + number: params?.number ?? 0, + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + inserted: { type: 'boolean', description: 'Whether rows were inserted' }, + tableObjectId: { type: 'string', description: 'The table updated' }, + number: { type: 'number', description: 'Number of rows inserted' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/merge_table_cells.ts b/apps/sim/tools/google_slides/merge_table_cells.ts new file mode 100644 index 00000000000..31259196184 --- /dev/null +++ b/apps/sim/tools/google_slides/merge_table_cells.ts @@ -0,0 +1,137 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesMergeTableCellsTool') + +interface MergeTableCellsParams { + accessToken: string + presentationId: string + objectId: string + rowIndex: number + columnIndex: number + rowSpan: number + columnSpan: number +} + +interface MergeTableCellsResponse { + success: boolean + output: { + merged: boolean + objectId: string + metadata: { presentationId: string; url: string } + } +} + +export const mergeTableCellsTool: ToolConfig = { + id: 'google_slides_merge_table_cells', + name: 'Merge Table Cells in Google Slides', + description: + 'Merge a rectangular range of table cells into a single cell. The range starts at (rowIndex, columnIndex) and covers rowSpan × columnSpan cells.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the table', + }, + rowIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based row index of the top-left cell', + }, + columnIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based column index of the top-left cell', + }, + rowSpan: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Number of rows to merge (minimum 1)', + }, + columnSpan: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Number of columns to merge (minimum 1)', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Table object ID is required') + if (!params.rowSpan || params.rowSpan < 1) throw new Error('rowSpan must be at least 1') + if (!params.columnSpan || params.columnSpan < 1) + throw new Error('columnSpan must be at least 1') + + return { + requests: [ + { + mergeTableCells: { + objectId, + tableRange: { + location: { rowIndex: params.rowIndex, columnIndex: params.columnIndex }, + rowSpan: params.rowSpan, + columnSpan: params.columnSpan, + }, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to merge table cells') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + merged: true, + objectId: params?.objectId?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + merged: { type: 'boolean', description: 'Whether the cells were merged' }, + objectId: { type: 'string', description: 'The table updated' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/refresh_sheets_chart.ts b/apps/sim/tools/google_slides/refresh_sheets_chart.ts new file mode 100644 index 00000000000..4b9aeb839b3 --- /dev/null +++ b/apps/sim/tools/google_slides/refresh_sheets_chart.ts @@ -0,0 +1,95 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesRefreshSheetsChartTool') + +interface RefreshSheetsChartParams { + accessToken: string + presentationId: string + objectId: string +} + +interface RefreshSheetsChartResponse { + success: boolean + output: { + refreshed: boolean + objectId: string + metadata: { presentationId: string; url: string } + } +} + +export const refreshSheetsChartTool: ToolConfig< + RefreshSheetsChartParams, + RefreshSheetsChartResponse +> = { + id: 'google_slides_refresh_sheets_chart', + name: 'Refresh Sheets Chart in Slides', + description: + 'Refresh an embedded linked Sheets chart so it reflects the latest spreadsheet data.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the embedded chart to refresh', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + return { requests: [{ refreshSheetsChart: { objectId } }] } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to refresh sheets chart') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + refreshed: true, + objectId: params?.objectId?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + refreshed: { type: 'boolean', description: 'Whether the chart was refreshed' }, + objectId: { type: 'string', description: 'The chart object refreshed' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/replace_all_shapes_with_image.ts b/apps/sim/tools/google_slides/replace_all_shapes_with_image.ts new file mode 100644 index 00000000000..3ce1d4059db --- /dev/null +++ b/apps/sim/tools/google_slides/replace_all_shapes_with_image.ts @@ -0,0 +1,151 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesReplaceAllShapesWithImageTool') + +interface ReplaceAllShapesWithImageParams { + accessToken: string + presentationId: string + imageUrl: string + findText: string + matchCase?: boolean + imageReplaceMethod?: 'CENTER_INSIDE' | 'CENTER_CROP' + pageObjectIds?: string +} + +interface ReplaceAllShapesWithImageResponse { + success: boolean + output: { + occurrencesChanged: number + metadata: { presentationId: string; url: string; imageUrl: string; findText: string } + } +} + +export const replaceAllShapesWithImageTool: ToolConfig< + ReplaceAllShapesWithImageParams, + ReplaceAllShapesWithImageResponse +> = { + id: 'google_slides_replace_all_shapes_with_image', + name: 'Replace All Shapes With Image in Google Slides', + description: + "Find every shape whose text matches the given token (e.g. {{cover-image}}) and replace it with an image, preserving the shape's position and bounds.", + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + imageUrl: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + "Publicly fetchable image URL (PNG, JPEG, or GIF; max 50 MB and accessible to Google's servers)", + }, + findText: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Text content of shapes to replace (e.g. {{cover-image}})', + }, + matchCase: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Case-sensitive match (default: true)', + }, + imageReplaceMethod: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'How the image fits the shape: CENTER_INSIDE (preserve aspect, fit inside) or CENTER_CROP (fill, crop overflow). Default: CENTER_INSIDE.', + }, + pageObjectIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated slide IDs to limit replacement to specific slides', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const imageUrl = params.imageUrl?.trim() + const findText = params.findText + if (!imageUrl) throw new Error('Image URL is required') + if (!findText) throw new Error('Find text is required') + + const request: Record = { + imageUrl, + containsText: { + text: findText, + matchCase: params.matchCase !== false, + }, + imageReplaceMethod: params.imageReplaceMethod || 'CENTER_INSIDE', + } + if (params.pageObjectIds?.trim()) { + request.pageObjectIds = params.pageObjectIds + .split(',') + .map((id) => id.trim()) + .filter((id) => id.length > 0) + } + + return { requests: [{ replaceAllShapesWithImage: request }] } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to replace shapes with image') + } + const occurrencesChanged = data.replies?.[0]?.replaceAllShapesWithImage?.occurrencesChanged ?? 0 + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + occurrencesChanged, + metadata: { + presentationId, + url: presentationUrl(presentationId), + imageUrl: params?.imageUrl?.trim() || '', + findText: params?.findText || '', + }, + }, + } + }, + + outputs: { + occurrencesChanged: { + type: 'number', + description: 'Number of shapes that were replaced with the image', + }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + imageUrl: { type: 'string', description: 'The image URL inserted' }, + findText: { type: 'string', description: 'The matched text token' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/replace_all_shapes_with_sheets_chart.ts b/apps/sim/tools/google_slides/replace_all_shapes_with_sheets_chart.ts new file mode 100644 index 00000000000..4455fdff668 --- /dev/null +++ b/apps/sim/tools/google_slides/replace_all_shapes_with_sheets_chart.ts @@ -0,0 +1,164 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesReplaceAllShapesWithSheetsChartTool') + +interface ReplaceAllShapesWithSheetsChartParams { + accessToken: string + presentationId: string + spreadsheetId: string + chartId: number + findText: string + matchCase?: boolean + linkingMode?: 'LINKED' | 'NOT_LINKED_IMAGE' + pageObjectIds?: string +} + +interface ReplaceAllShapesWithSheetsChartResponse { + success: boolean + output: { + occurrencesChanged: number + metadata: { + presentationId: string + url: string + findText: string + spreadsheetId: string + chartId: number + } + } +} + +export const replaceAllShapesWithSheetsChartTool: ToolConfig< + ReplaceAllShapesWithSheetsChartParams, + ReplaceAllShapesWithSheetsChartResponse +> = { + id: 'google_slides_replace_all_shapes_with_sheets_chart', + name: 'Replace All Shapes With Sheets Chart in Slides', + description: + "Find every shape matching a text token (e.g. {{revenue-chart}}) and replace each with the same embedded Sheets chart, preserving the shape's position and bounds.", + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Sheets spreadsheet ID containing the chart', + }, + chartId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Numeric chart ID within the spreadsheet', + }, + findText: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Text content of shapes to replace (e.g. {{revenue-chart}})', + }, + matchCase: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Case-sensitive match (default true)', + }, + linkingMode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LINKED (default) or NOT_LINKED_IMAGE', + }, + pageObjectIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated slide IDs to limit replacement to specific slides', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) throw new Error('Spreadsheet ID is required') + if (params.chartId === undefined) throw new Error('Chart ID is required') + const findText = params.findText + if (!findText) throw new Error('Find text is required') + + const request: Record = { + spreadsheetId, + chartId: params.chartId, + linkingMode: params.linkingMode || 'LINKED', + containsText: { text: findText, matchCase: params.matchCase !== false }, + } + if (params.pageObjectIds?.trim()) { + request.pageObjectIds = params.pageObjectIds + .split(',') + .map((id) => id.trim()) + .filter((id) => id.length > 0) + } + + return { requests: [{ replaceAllShapesWithSheetsChart: request }] } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to replace shapes with sheets chart') + } + const occurrencesChanged = + data.replies?.[0]?.replaceAllShapesWithSheetsChart?.occurrencesChanged ?? 0 + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + occurrencesChanged, + metadata: { + presentationId, + url: presentationUrl(presentationId), + findText: params?.findText || '', + spreadsheetId: params?.spreadsheetId?.trim() || '', + chartId: params?.chartId ?? 0, + }, + }, + } + }, + + outputs: { + occurrencesChanged: { + type: 'number', + description: 'Number of shapes replaced with the chart', + }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + findText: { type: 'string', description: 'The matched text token' }, + spreadsheetId: { type: 'string', description: 'Source spreadsheet ID' }, + chartId: { type: 'number', description: 'Source chart ID' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/replace_image.ts b/apps/sim/tools/google_slides/replace_image.ts new file mode 100644 index 00000000000..b66d7f27654 --- /dev/null +++ b/apps/sim/tools/google_slides/replace_image.ts @@ -0,0 +1,125 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesReplaceImageTool') + +interface ReplaceImageParams { + accessToken: string + presentationId: string + imageObjectId: string + imageUrl: string + imageReplaceMethod?: 'CENTER_INSIDE' | 'CENTER_CROP' +} + +interface ReplaceImageResponse { + success: boolean + output: { + replaced: boolean + imageObjectId: string + metadata: { presentationId: string; url: string; imageUrl: string } + } +} + +export const replaceImageTool: ToolConfig = { + id: 'google_slides_replace_image', + name: 'Replace Image in Google Slides', + description: + "Replace the source of an existing image with a new image URL, preserving the image's position, size, and properties.", + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + imageObjectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the existing image to replace', + }, + imageUrl: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New publicly fetchable image URL (PNG, JPEG, or GIF, max 50 MB)', + }, + imageReplaceMethod: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'CENTER_INSIDE (preserve aspect) or CENTER_CROP (fill, crop overflow). Default: CENTER_INSIDE.', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const imageObjectId = params.imageObjectId?.trim() + const imageUrl = params.imageUrl?.trim() + if (!imageObjectId) throw new Error('Image object ID is required') + if (!imageUrl) throw new Error('Image URL is required') + + return { + requests: [ + { + replaceImage: { + imageObjectId, + url: imageUrl, + imageReplaceMethod: params.imageReplaceMethod || 'CENTER_INSIDE', + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to replace image') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + replaced: true, + imageObjectId: params?.imageObjectId?.trim() || '', + metadata: { + presentationId, + url: presentationUrl(presentationId), + imageUrl: params?.imageUrl?.trim() || '', + }, + }, + } + }, + + outputs: { + replaced: { type: 'boolean', description: 'Whether the image was replaced' }, + imageObjectId: { type: 'string', description: 'The image object that was replaced' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + imageUrl: { type: 'string', description: 'The new image URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/reroute_line.ts b/apps/sim/tools/google_slides/reroute_line.ts new file mode 100644 index 00000000000..d9106d8917a --- /dev/null +++ b/apps/sim/tools/google_slides/reroute_line.ts @@ -0,0 +1,92 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesRerouteLineTool') + +interface RerouteLineParams { + accessToken: string + presentationId: string + objectId: string +} + +interface RerouteLineResponse { + success: boolean + output: { + rerouted: boolean + objectId: string + metadata: { presentationId: string; url: string } + } +} + +export const rerouteLineTool: ToolConfig = { + id: 'google_slides_reroute_line', + name: 'Reroute Line in Google Slides', + description: + 'Reroute a connector line so it efficiently connects its endpoint shapes — useful after moving the shapes the line connects.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the connector line to reroute', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + return { requests: [{ rerouteLine: { objectId } }] } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to reroute line') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + rerouted: true, + objectId: params?.objectId?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + rerouted: { type: 'boolean', description: 'Whether the line was rerouted' }, + objectId: { type: 'string', description: 'The line object rerouted' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/ungroup_objects.ts b/apps/sim/tools/google_slides/ungroup_objects.ts new file mode 100644 index 00000000000..c03d6775a05 --- /dev/null +++ b/apps/sim/tools/google_slides/ungroup_objects.ts @@ -0,0 +1,103 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUngroupObjectsTool') + +interface UngroupObjectsParams { + accessToken: string + presentationId: string + objectIds: string +} + +interface UngroupObjectsResponse { + success: boolean + output: { + ungrouped: boolean + objectIds: string[] + metadata: { presentationId: string; url: string } + } +} + +export const ungroupObjectsTool: ToolConfig = { + id: 'google_slides_ungroup_objects', + name: 'Ungroup Objects in Google Slides', + description: 'Ungroup one or more object groups, releasing their children back to the slide.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectIds: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comma-separated object IDs of the groups to ungroup', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectIds = (params.objectIds || '') + .split(',') + .map((id) => id.trim()) + .filter((id) => id.length > 0) + if (objectIds.length === 0) throw new Error('At least one group object ID is required') + + return { requests: [{ ungroupObjects: { objectIds } }] } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to ungroup objects') + } + const presentationId = params?.presentationId?.trim() || '' + const objectIds = (params?.objectIds || '') + .split(',') + .map((id) => id.trim()) + .filter((id) => id.length > 0) + return { + success: true, + output: { + ungrouped: true, + objectIds, + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + ungrouped: { type: 'boolean', description: 'Whether the objects were ungrouped' }, + objectIds: { + type: 'array', + description: 'Group IDs that were ungrouped', + items: { type: 'string' }, + }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/unmerge_table_cells.ts b/apps/sim/tools/google_slides/unmerge_table_cells.ts new file mode 100644 index 00000000000..af580c6e39d --- /dev/null +++ b/apps/sim/tools/google_slides/unmerge_table_cells.ts @@ -0,0 +1,137 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUnmergeTableCellsTool') + +interface UnmergeTableCellsParams { + accessToken: string + presentationId: string + objectId: string + rowIndex: number + columnIndex: number + rowSpan: number + columnSpan: number +} + +interface UnmergeTableCellsResponse { + success: boolean + output: { + unmerged: boolean + objectId: string + metadata: { presentationId: string; url: string } + } +} + +export const unmergeTableCellsTool: ToolConfig = + { + id: 'google_slides_unmerge_table_cells', + name: 'Unmerge Table Cells in Google Slides', + description: 'Unmerge any merged cells within the given table range.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the table', + }, + rowIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based row index of the top-left cell of the range', + }, + columnIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based column index of the top-left cell of the range', + }, + rowSpan: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Number of rows in the range (minimum 1)', + }, + columnSpan: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Number of columns in the range (minimum 1)', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Table object ID is required') + if (!params.rowSpan || params.rowSpan < 1) throw new Error('rowSpan must be at least 1') + if (!params.columnSpan || params.columnSpan < 1) + throw new Error('columnSpan must be at least 1') + + return { + requests: [ + { + unmergeTableCells: { + objectId, + tableRange: { + location: { rowIndex: params.rowIndex, columnIndex: params.columnIndex }, + rowSpan: params.rowSpan, + columnSpan: params.columnSpan, + }, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to unmerge table cells') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + unmerged: true, + objectId: params?.objectId?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + unmerged: { type: 'boolean', description: 'Whether the cells were unmerged' }, + objectId: { type: 'string', description: 'The table updated' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, + } diff --git a/apps/sim/tools/google_slides/update_image_properties.ts b/apps/sim/tools/google_slides/update_image_properties.ts new file mode 100644 index 00000000000..25550b51f5e --- /dev/null +++ b/apps/sim/tools/google_slides/update_image_properties.ts @@ -0,0 +1,267 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + hexToOpaqueColor, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdateImagePropertiesTool') + +interface UpdateImagePropertiesParams { + accessToken: string + presentationId: string + objectId: string + brightness?: number + contrast?: number + transparency?: number + linkUrl?: string + outlineColor?: string + outlineWeight?: number + outlineDashStyle?: string + cropLeftOffset?: number + cropRightOffset?: number + cropTopOffset?: number + cropBottomOffset?: number + cropAngle?: number + propertiesJson?: string + fields?: string +} + +interface UpdateImagePropertiesResponse { + success: boolean + output: { + updated: boolean + objectId: string + fields: string + metadata: { presentationId: string; url: string } + } +} + +export const updateImagePropertiesTool: ToolConfig< + UpdateImagePropertiesParams, + UpdateImagePropertiesResponse +> = { + id: 'google_slides_update_image_properties', + name: 'Update Image Properties in Google Slides', + description: + 'Update image properties — brightness, contrast, transparency, crop, outline, link — on an existing image.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the image to update', + }, + brightness: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Brightness adjustment between -1.0 and 1.0', + }, + contrast: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Contrast adjustment between -1.0 and 1.0', + }, + transparency: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Transparency between 0.0 (opaque) and 1.0 (fully transparent)', + }, + linkUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Make the image a hyperlink to this URL', + }, + outlineColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Outline color as hex (e.g. #1A73E8)', + }, + outlineWeight: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Outline weight in points', + }, + outlineDashStyle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Outline dash style: SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT', + }, + cropLeftOffset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Crop offset from left edge (0.0 to 1.0)', + }, + cropRightOffset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Crop offset from right edge (0.0 to 1.0)', + }, + cropTopOffset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Crop offset from top edge (0.0 to 1.0)', + }, + cropBottomOffset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Crop offset from bottom edge (0.0 to 1.0)', + }, + cropAngle: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Crop rotation angle in radians (clockwise)', + }, + propertiesJson: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: raw ImageProperties JSON merged with the simple fields above', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + + const props: Record = {} + const fieldList: string[] = [] + + if (params.brightness !== undefined) { + props.brightness = params.brightness + fieldList.push('brightness') + } + if (params.contrast !== undefined) { + props.contrast = params.contrast + fieldList.push('contrast') + } + if (params.transparency !== undefined) { + props.transparency = params.transparency + fieldList.push('transparency') + } + if (params.linkUrl) { + props.link = { url: params.linkUrl } + fieldList.push('link') + } + const outline: Record = {} + const outlineColor = hexToOpaqueColor(params.outlineColor) + if (outlineColor) { + outline.outlineFill = { solidFill: { color: outlineColor } } + } + if (params.outlineWeight !== undefined) { + outline.weight = { magnitude: params.outlineWeight, unit: 'PT' } + } + if (params.outlineDashStyle) { + outline.dashStyle = params.outlineDashStyle + } + if (Object.keys(outline).length > 0) { + props.outline = outline + fieldList.push('outline') + } + + const crop: Record = {} + if (params.cropLeftOffset !== undefined) crop.leftOffset = params.cropLeftOffset + if (params.cropRightOffset !== undefined) crop.rightOffset = params.cropRightOffset + if (params.cropTopOffset !== undefined) crop.topOffset = params.cropTopOffset + if (params.cropBottomOffset !== undefined) crop.bottomOffset = params.cropBottomOffset + if (params.cropAngle !== undefined) crop.angle = params.cropAngle + if (Object.keys(crop).length > 0) { + props.cropProperties = crop + fieldList.push('cropProperties') + } + + if (params.propertiesJson?.trim()) { + try { + const extra = JSON.parse(params.propertiesJson) + if (extra && typeof extra === 'object') { + Object.assign(props, extra) + } + } catch (e) { + logger.warn('Invalid propertiesJson, ignoring:', { error: e }) + } + } + + const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*') + + return { + requests: [ + { + updateImageProperties: { objectId, imageProperties: props, fields }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update image properties') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + fields: params?.fields?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether the image properties were updated' }, + objectId: { type: 'string', description: 'The image object updated' }, + fields: { type: 'string', description: 'FieldMask applied' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_line_category.ts b/apps/sim/tools/google_slides/update_line_category.ts new file mode 100644 index 00000000000..59bab13bfba --- /dev/null +++ b/apps/sim/tools/google_slides/update_line_category.ts @@ -0,0 +1,115 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdateLineCategoryTool') + +interface UpdateLineCategoryParams { + accessToken: string + presentationId: string + objectId: string + lineCategory: 'STRAIGHT' | 'BENT' | 'CURVED' +} + +interface UpdateLineCategoryResponse { + success: boolean + output: { + updated: boolean + objectId: string + lineCategory: string + metadata: { presentationId: string; url: string } + } +} + +export const updateLineCategoryTool: ToolConfig< + UpdateLineCategoryParams, + UpdateLineCategoryResponse +> = { + id: 'google_slides_update_line_category', + name: 'Update Line Category in Google Slides', + description: "Change a connector line's category (STRAIGHT, BENT, or CURVED).", + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the connector line', + }, + lineCategory: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New line category: STRAIGHT, BENT, or CURVED', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + if (!params.lineCategory) throw new Error('Line category is required') + + return { + requests: [ + { + updateLineCategory: { + objectId, + lineCategory: params.lineCategory, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update line category') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + lineCategory: params?.lineCategory ?? '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether the line category was updated' }, + objectId: { type: 'string', description: 'The line object updated' }, + lineCategory: { type: 'string', description: 'New line category' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_line_properties.ts b/apps/sim/tools/google_slides/update_line_properties.ts new file mode 100644 index 00000000000..40196697962 --- /dev/null +++ b/apps/sim/tools/google_slides/update_line_properties.ts @@ -0,0 +1,201 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + hexToOpaqueColor, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdateLinePropertiesTool') + +interface UpdateLinePropertiesParams { + accessToken: string + presentationId: string + objectId: string + lineColor?: string + lineWeight?: number + dashStyle?: string + startArrow?: string + endArrow?: string + linkUrl?: string + propertiesJson?: string + fields?: string +} + +interface UpdateLinePropertiesResponse { + success: boolean + output: { + updated: boolean + objectId: string + fields: string + metadata: { presentationId: string; url: string } + } +} + +export const updateLinePropertiesTool: ToolConfig< + UpdateLinePropertiesParams, + UpdateLinePropertiesResponse +> = { + id: 'google_slides_update_line_properties', + name: 'Update Line Properties in Google Slides', + description: 'Update line appearance — color, weight, dash style, arrows, link.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the line', + }, + lineColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Line color as hex', + }, + lineWeight: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Line weight in points', + }, + dashStyle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Dash style: SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT', + }, + startArrow: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Start arrow style: NONE, STEALTH_ARROW, FILL_ARROW, FILL_CIRCLE, FILL_SQUARE, FILL_DIAMOND, OPEN_ARROW, OPEN_CIRCLE, OPEN_SQUARE, OPEN_DIAMOND', + }, + endArrow: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'End arrow style (same values as startArrow)', + }, + linkUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Hyperlink URL', + }, + propertiesJson: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: raw LineProperties JSON merged with the simple fields above', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + + const props: Record = {} + const fieldList: string[] = [] + + const color = hexToOpaqueColor(params.lineColor) + if (color) { + props.lineFill = { solidFill: { color } } + fieldList.push('lineFill') + } + if (params.lineWeight !== undefined) { + props.weight = { magnitude: params.lineWeight, unit: 'PT' } + fieldList.push('weight') + } + if (params.dashStyle) { + props.dashStyle = params.dashStyle + fieldList.push('dashStyle') + } + if (params.startArrow) { + props.startArrow = params.startArrow + fieldList.push('startArrow') + } + if (params.endArrow) { + props.endArrow = params.endArrow + fieldList.push('endArrow') + } + if (params.linkUrl) { + props.link = { url: params.linkUrl } + fieldList.push('link') + } + if (params.propertiesJson?.trim()) { + try { + const extra = JSON.parse(params.propertiesJson) + if (extra && typeof extra === 'object') Object.assign(props, extra) + } catch (e) { + logger.warn('Invalid propertiesJson, ignoring:', { error: e }) + } + } + + const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*') + + return { + requests: [{ updateLineProperties: { objectId, lineProperties: props, fields } }], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update line properties') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + fields: params?.fields?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether the line properties were updated' }, + objectId: { type: 'string', description: 'The line object updated' }, + fields: { type: 'string', description: 'FieldMask applied' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_page_element_alt_text.ts b/apps/sim/tools/google_slides/update_page_element_alt_text.ts new file mode 100644 index 00000000000..658eccbfb31 --- /dev/null +++ b/apps/sim/tools/google_slides/update_page_element_alt_text.ts @@ -0,0 +1,114 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdatePageElementAltTextTool') + +interface UpdatePageElementAltTextParams { + accessToken: string + presentationId: string + objectId: string + title?: string + description?: string +} + +interface UpdatePageElementAltTextResponse { + success: boolean + output: { + updated: boolean + objectId: string + metadata: { presentationId: string; url: string } + } +} + +export const updatePageElementAltTextTool: ToolConfig< + UpdatePageElementAltTextParams, + UpdatePageElementAltTextResponse +> = { + id: 'google_slides_update_page_element_alt_text', + name: 'Update Alt Text in Google Slides', + description: + 'Set the accessibility title and/or description (alt text) of a page element such as an image, shape, or group.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the page element', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Accessibility title for the element', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Accessibility description (alt text) for the element', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + + const updateRequest: Record = { objectId } + if (params.title !== undefined) updateRequest.title = params.title + if (params.description !== undefined) updateRequest.description = params.description + + return { requests: [{ updatePageElementAltText: updateRequest }] } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update alt text') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether alt text was updated' }, + objectId: { type: 'string', description: 'The element updated' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_page_element_transform.ts b/apps/sim/tools/google_slides/update_page_element_transform.ts new file mode 100644 index 00000000000..68d143d7284 --- /dev/null +++ b/apps/sim/tools/google_slides/update_page_element_transform.ts @@ -0,0 +1,171 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + PT_TO_EMU, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdatePageElementTransformTool') + +interface UpdatePageElementTransformParams { + accessToken: string + presentationId: string + objectId: string + scaleX?: number + scaleY?: number + shearX?: number + shearY?: number + translateX?: number + translateY?: number + applyMode?: 'ABSOLUTE' | 'RELATIVE' +} + +interface UpdatePageElementTransformResponse { + success: boolean + output: { + updated: boolean + objectId: string + metadata: { presentationId: string; url: string } + } +} + +export const updatePageElementTransformTool: ToolConfig< + UpdatePageElementTransformParams, + UpdatePageElementTransformResponse +> = { + id: 'google_slides_update_page_element_transform', + name: 'Update Page Element Transform in Google Slides', + description: + 'Move, resize, scale, or shear a page element. Translate is specified in points; applyMode controls whether the transform is absolute (default) or relative (multiplied with the current transform).', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the page element to transform', + }, + scaleX: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Horizontal scale factor (default 1)', + }, + scaleY: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Vertical scale factor (default 1)', + }, + shearX: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Horizontal shear factor (default 0)', + }, + shearY: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Vertical shear factor (default 0)', + }, + translateX: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'X position in points (absolute) or delta (relative)', + }, + translateY: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Y position in points (absolute) or delta (relative)', + }, + applyMode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'ABSOLUTE replaces the current transform; RELATIVE multiplies with it. Default ABSOLUTE.', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + + const transform: Record = { + unit: 'EMU', + } + transform.scaleX = params.scaleX ?? 1 + transform.scaleY = params.scaleY ?? 1 + if (params.shearX !== undefined) transform.shearX = params.shearX + if (params.shearY !== undefined) transform.shearY = params.shearY + if (params.translateX !== undefined) transform.translateX = params.translateX * PT_TO_EMU + if (params.translateY !== undefined) transform.translateY = params.translateY * PT_TO_EMU + + return { + requests: [ + { + updatePageElementTransform: { + objectId, + transform, + applyMode: params.applyMode || 'ABSOLUTE', + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update transform') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether the transform was updated' }, + objectId: { type: 'string', description: 'The element transformed' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_page_elements_z_order.ts b/apps/sim/tools/google_slides/update_page_elements_z_order.ts new file mode 100644 index 00000000000..a894aeea83f --- /dev/null +++ b/apps/sim/tools/google_slides/update_page_elements_z_order.ts @@ -0,0 +1,122 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdatePageElementsZOrderTool') + +interface UpdatePageElementsZOrderParams { + accessToken: string + presentationId: string + objectIds: string + operation: 'BRING_TO_FRONT' | 'BRING_FORWARD' | 'SEND_BACKWARD' | 'SEND_TO_BACK' +} + +interface UpdatePageElementsZOrderResponse { + success: boolean + output: { + reordered: boolean + objectIds: string[] + operation: string + metadata: { presentationId: string; url: string } + } +} + +export const updatePageElementsZOrderTool: ToolConfig< + UpdatePageElementsZOrderParams, + UpdatePageElementsZOrderResponse +> = { + id: 'google_slides_update_page_elements_z_order', + name: 'Update Z-Order in Google Slides', + description: 'Bring elements to front, send to back, or step them one layer forward/backward.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectIds: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comma-separated object IDs of the elements to reorder', + }, + operation: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BRING_TO_FRONT, BRING_FORWARD, SEND_BACKWARD, or SEND_TO_BACK', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectIds = (params.objectIds || '') + .split(',') + .map((id) => id.trim()) + .filter((id) => id.length > 0) + if (objectIds.length === 0) throw new Error('At least one object ID is required') + if (!params.operation) throw new Error('Operation is required') + + return { + requests: [ + { + updatePageElementsZOrder: { + pageElementObjectIds: objectIds, + operation: params.operation, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update z-order') + } + const presentationId = params?.presentationId?.trim() || '' + const objectIds = (params?.objectIds || '') + .split(',') + .map((id) => id.trim()) + .filter((id) => id.length > 0) + return { + success: true, + output: { + reordered: true, + objectIds, + operation: params?.operation ?? '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + reordered: { type: 'boolean', description: 'Whether the z-order was changed' }, + objectIds: { type: 'array', description: 'Elements reordered', items: { type: 'string' } }, + operation: { type: 'string', description: 'Operation applied' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_page_properties.ts b/apps/sim/tools/google_slides/update_page_properties.ts new file mode 100644 index 00000000000..b963a9ddc63 --- /dev/null +++ b/apps/sim/tools/google_slides/update_page_properties.ts @@ -0,0 +1,185 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + hexToOpaqueColor, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdatePagePropertiesTool') + +interface UpdatePagePropertiesParams { + accessToken: string + presentationId: string + objectId: string + backgroundColor?: string + backgroundAlpha?: number + backgroundImageUrl?: string + backgroundUnset?: boolean + propertiesJson?: string + fields?: string +} + +interface UpdatePagePropertiesResponse { + success: boolean + output: { + updated: boolean + objectId: string + fields: string + metadata: { presentationId: string; url: string } + } +} + +export const updatePagePropertiesTool: ToolConfig< + UpdatePagePropertiesParams, + UpdatePagePropertiesResponse +> = { + id: 'google_slides_update_page_properties', + name: 'Update Page Properties in Google Slides', + description: + 'Update slide/page background — solid color or stretched picture — and other page properties.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the slide/page to update', + }, + backgroundColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Solid background color as hex (e.g. #0B1F3A)', + }, + backgroundAlpha: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Background fill opacity between 0.0 and 1.0', + }, + backgroundImageUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Publicly fetchable image URL to use as a stretched picture background', + }, + backgroundUnset: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'When true, removes the background so the slide inherits its layout background', + }, + propertiesJson: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: raw PageProperties JSON merged with the simple fields above', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + + const props: Record = {} + const fieldList: string[] = [] + + if (params.backgroundUnset) { + props.pageBackgroundFill = { propertyState: 'NOT_RENDERED' } + fieldList.push('pageBackgroundFill.propertyState') + } else if (params.backgroundImageUrl?.trim()) { + props.pageBackgroundFill = { + stretchedPictureFill: { contentUrl: params.backgroundImageUrl.trim() }, + propertyState: 'RENDERED', + } + fieldList.push('pageBackgroundFill') + } else { + const bg = hexToOpaqueColor(params.backgroundColor) + if (bg) { + props.pageBackgroundFill = { + solidFill: { + color: bg, + ...(params.backgroundAlpha !== undefined ? { alpha: params.backgroundAlpha } : {}), + }, + propertyState: 'RENDERED', + } + fieldList.push('pageBackgroundFill') + } + } + + if (params.propertiesJson?.trim()) { + try { + const extra = JSON.parse(params.propertiesJson) + if (extra && typeof extra === 'object') Object.assign(props, extra) + } catch (e) { + logger.warn('Invalid propertiesJson, ignoring:', { error: e }) + } + } + + const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*') + + return { + requests: [{ updatePageProperties: { objectId, pageProperties: props, fields } }], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update page properties') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + fields: params?.fields?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether the page properties were updated' }, + objectId: { type: 'string', description: 'The page object updated' }, + fields: { type: 'string', description: 'FieldMask applied' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_paragraph_style.ts b/apps/sim/tools/google_slides/update_paragraph_style.ts new file mode 100644 index 00000000000..349c3535f84 --- /dev/null +++ b/apps/sim/tools/google_slides/update_paragraph_style.ts @@ -0,0 +1,288 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + buildCellLocation, + buildTextRange, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdateParagraphStyleTool') + +interface UpdateParagraphStyleParams { + accessToken: string + presentationId: string + objectId: string + rowIndex?: number + columnIndex?: number + rangeType?: 'ALL' | 'FROM_START_INDEX' | 'FIXED_RANGE' + startIndex?: number + endIndex?: number + alignment?: 'START' | 'CENTER' | 'END' | 'JUSTIFIED' + lineSpacing?: number + indentStart?: number + indentEnd?: number + indentFirstLine?: number + spaceAbove?: number + spaceBelow?: number + direction?: 'LEFT_TO_RIGHT' | 'RIGHT_TO_LEFT' + spacingMode?: 'NEVER_COLLAPSE' | 'COLLAPSE_LISTS' + styleJson?: string + fields?: string +} + +interface UpdateParagraphStyleResponse { + success: boolean + output: { + updated: boolean + objectId: string + fields: string + metadata: { presentationId: string; url: string } + } +} + +export const updateParagraphStyleTool: ToolConfig< + UpdateParagraphStyleParams, + UpdateParagraphStyleResponse +> = { + id: 'google_slides_update_paragraph_style', + name: 'Update Paragraph Style in Google Slides', + description: + 'Update paragraph styling — alignment, line spacing, indents, space above/below — for text in a shape or table cell.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the shape or table containing the text', + }, + rowIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'When targeting a table cell, the zero-based row index', + }, + columnIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'When targeting a table cell, the zero-based column index', + }, + rangeType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Range to style: ALL (default), FROM_START_INDEX, or FIXED_RANGE', + }, + startIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start index for FROM_START_INDEX or FIXED_RANGE', + }, + endIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'End index for FIXED_RANGE', + }, + alignment: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Text alignment: START, CENTER, END, or JUSTIFIED', + }, + lineSpacing: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Line spacing as a percentage (100 = single, 200 = double)', + }, + indentStart: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start-edge indent in points', + }, + indentEnd: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'End-edge indent in points', + }, + indentFirstLine: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'First-line indent in points', + }, + spaceAbove: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Space above the paragraph in points', + }, + spaceBelow: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Space below the paragraph in points', + }, + direction: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Text direction: LEFT_TO_RIGHT or RIGHT_TO_LEFT', + }, + spacingMode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Spacing mode: NEVER_COLLAPSE or COLLAPSE_LISTS', + }, + styleJson: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: raw ParagraphStyle JSON merged with the simple fields above', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + + const style: Record = {} + const fieldList: string[] = [] + + const ptDim = (pt: number) => ({ magnitude: pt, unit: 'PT' }) + + if (params.alignment) { + style.alignment = params.alignment + fieldList.push('alignment') + } + if (params.lineSpacing !== undefined) { + style.lineSpacing = params.lineSpacing + fieldList.push('lineSpacing') + } + if (params.indentStart !== undefined) { + style.indentStart = ptDim(params.indentStart) + fieldList.push('indentStart') + } + if (params.indentEnd !== undefined) { + style.indentEnd = ptDim(params.indentEnd) + fieldList.push('indentEnd') + } + if (params.indentFirstLine !== undefined) { + style.indentFirstLine = ptDim(params.indentFirstLine) + fieldList.push('indentFirstLine') + } + if (params.spaceAbove !== undefined) { + style.spaceAbove = ptDim(params.spaceAbove) + fieldList.push('spaceAbove') + } + if (params.spaceBelow !== undefined) { + style.spaceBelow = ptDim(params.spaceBelow) + fieldList.push('spaceBelow') + } + if (params.direction) { + style.direction = params.direction + fieldList.push('direction') + } + if (params.spacingMode) { + style.spacingMode = params.spacingMode + fieldList.push('spacingMode') + } + + if (params.styleJson?.trim()) { + try { + const extra = JSON.parse(params.styleJson) + if (extra && typeof extra === 'object') { + Object.assign(style, extra) + } + } catch (e) { + logger.warn('Invalid styleJson, ignoring:', { error: e }) + } + } + + const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*') + + const updateRequest: Record = { + objectId, + style, + textRange: buildTextRange({ + rangeType: params.rangeType, + startIndex: params.startIndex, + endIndex: params.endIndex, + }), + fields, + } + const cellLocation = buildCellLocation({ + rowIndex: params.rowIndex, + columnIndex: params.columnIndex, + }) + if (cellLocation) updateRequest.cellLocation = cellLocation + + return { requests: [{ updateParagraphStyle: updateRequest }] } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update paragraph style') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + fields: params?.fields?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether the paragraph style was updated' }, + objectId: { type: 'string', description: 'The object whose paragraph was styled' }, + fields: { type: 'string', description: 'FieldMask applied' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_shape_properties.ts b/apps/sim/tools/google_slides/update_shape_properties.ts new file mode 100644 index 00000000000..1b42d449a49 --- /dev/null +++ b/apps/sim/tools/google_slides/update_shape_properties.ts @@ -0,0 +1,246 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + hexToOpaqueColor, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdateShapePropertiesTool') + +interface UpdateShapePropertiesParams { + accessToken: string + presentationId: string + objectId: string + fillColor?: string + fillAlpha?: number + fillUnset?: boolean + outlineColor?: string + outlineWeight?: number + outlineDashStyle?: string + outlineUnset?: boolean + linkUrl?: string + contentAlignment?: 'TOP' | 'MIDDLE' | 'BOTTOM' + autofitType?: 'NONE' | 'TEXT_AUTOFIT' | 'SHAPE_AUTOFIT' + propertiesJson?: string + fields?: string +} + +interface UpdateShapePropertiesResponse { + success: boolean + output: { + updated: boolean + objectId: string + fields: string + metadata: { presentationId: string; url: string } + } +} + +export const updateShapePropertiesTool: ToolConfig< + UpdateShapePropertiesParams, + UpdateShapePropertiesResponse +> = { + id: 'google_slides_update_shape_properties', + name: 'Update Shape Properties in Google Slides', + description: + "Update a shape's appearance — background fill color, outline, link, content alignment, autofit. Pass only the properties you want to change.", + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the shape to update', + }, + fillColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Solid background fill color as hex (e.g. #FF6F61)', + }, + fillAlpha: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Fill opacity between 0.0 (transparent) and 1.0 (opaque)', + }, + fillUnset: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'When true, removes any fill so the shape inherits its layout/master fill', + }, + outlineColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Outline color as hex', + }, + outlineWeight: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Outline weight in points', + }, + outlineDashStyle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Outline dash style: SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT', + }, + outlineUnset: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'When true, removes any outline so the shape inherits its layout/master outline', + }, + linkUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Make the shape a hyperlink to this URL', + }, + contentAlignment: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Vertical alignment of shape contents: TOP, MIDDLE, or BOTTOM', + }, + autofitType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Autofit behavior: NONE, TEXT_AUTOFIT, or SHAPE_AUTOFIT', + }, + propertiesJson: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: raw ShapeProperties JSON merged with the simple fields above', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + + const props: Record = {} + const fieldList: string[] = [] + + const fill = hexToOpaqueColor(params.fillColor) + if (params.fillUnset) { + props.shapeBackgroundFill = { propertyState: 'NOT_RENDERED' } + fieldList.push('shapeBackgroundFill.propertyState') + } else if (fill) { + props.shapeBackgroundFill = { + solidFill: { + color: fill, + ...(params.fillAlpha !== undefined ? { alpha: params.fillAlpha } : {}), + }, + propertyState: 'RENDERED', + } + fieldList.push('shapeBackgroundFill') + } + + const outlineColor = hexToOpaqueColor(params.outlineColor) + if (params.outlineUnset) { + props.outline = { propertyState: 'NOT_RENDERED' } + fieldList.push('outline.propertyState') + } else if (outlineColor || params.outlineWeight !== undefined || params.outlineDashStyle) { + const outline: Record = { propertyState: 'RENDERED' } + if (outlineColor) outline.outlineFill = { solidFill: { color: outlineColor } } + if (params.outlineWeight !== undefined) + outline.weight = { magnitude: params.outlineWeight, unit: 'PT' } + if (params.outlineDashStyle) outline.dashStyle = params.outlineDashStyle + props.outline = outline + fieldList.push('outline') + } + + if (params.linkUrl) { + props.link = { url: params.linkUrl } + fieldList.push('link') + } + if (params.contentAlignment) { + props.contentAlignment = params.contentAlignment + fieldList.push('contentAlignment') + } + if (params.autofitType) { + props.autofit = { autofitType: params.autofitType } + fieldList.push('autofit.autofitType') + } + + if (params.propertiesJson?.trim()) { + try { + const extra = JSON.parse(params.propertiesJson) + if (extra && typeof extra === 'object') Object.assign(props, extra) + } catch (e) { + logger.warn('Invalid propertiesJson, ignoring:', { error: e }) + } + } + + const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*') + + return { + requests: [{ updateShapeProperties: { objectId, shapeProperties: props, fields } }], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update shape properties') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + fields: params?.fields?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether the shape properties were updated' }, + objectId: { type: 'string', description: 'The shape object updated' }, + fields: { type: 'string', description: 'FieldMask applied' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_slide_properties.ts b/apps/sim/tools/google_slides/update_slide_properties.ts new file mode 100644 index 00000000000..b9a9e5111c4 --- /dev/null +++ b/apps/sim/tools/google_slides/update_slide_properties.ts @@ -0,0 +1,140 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdateSlidePropertiesTool') + +interface UpdateSlidePropertiesParams { + accessToken: string + presentationId: string + objectId: string + isSkipped?: boolean + propertiesJson?: string + fields?: string +} + +interface UpdateSlidePropertiesResponse { + success: boolean + output: { + updated: boolean + objectId: string + fields: string + metadata: { presentationId: string; url: string } + } +} + +export const updateSlidePropertiesTool: ToolConfig< + UpdateSlidePropertiesParams, + UpdateSlidePropertiesResponse +> = { + id: 'google_slides_update_slide_properties', + name: 'Update Slide Properties in Google Slides', + description: + 'Update slide-specific properties such as whether the slide is skipped during presentation.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the slide to update', + }, + isSkipped: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the slide is skipped in presentation mode', + }, + propertiesJson: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: raw SlideProperties JSON merged with the simple fields above', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + + const props: Record = {} + const fieldList: string[] = [] + + if (params.isSkipped !== undefined) { + props.isSkipped = params.isSkipped + fieldList.push('isSkipped') + } + if (params.propertiesJson?.trim()) { + try { + const extra = JSON.parse(params.propertiesJson) + if (extra && typeof extra === 'object') Object.assign(props, extra) + } catch (e) { + logger.warn('Invalid propertiesJson, ignoring:', { error: e }) + } + } + + const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*') + + return { + requests: [{ updateSlideProperties: { objectId, slideProperties: props, fields } }], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update slide properties') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + fields: params?.fields?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether the slide properties were updated' }, + objectId: { type: 'string', description: 'The slide object updated' }, + fields: { type: 'string', description: 'FieldMask applied' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_table_border_properties.ts b/apps/sim/tools/google_slides/update_table_border_properties.ts new file mode 100644 index 00000000000..e401d6836cf --- /dev/null +++ b/apps/sim/tools/google_slides/update_table_border_properties.ts @@ -0,0 +1,227 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + hexToOpaqueColor, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdateTableBorderPropertiesTool') + +interface UpdateTableBorderPropertiesParams { + accessToken: string + presentationId: string + objectId: string + rowIndex: number + columnIndex: number + rowSpan: number + columnSpan: number + borderPosition?: + | 'ALL' + | 'BOTTOM' + | 'INNER' + | 'INNER_HORIZONTAL' + | 'INNER_VERTICAL' + | 'LEFT' + | 'OUTER' + | 'RIGHT' + | 'TOP' + borderColor?: string + borderWeight?: number + dashStyle?: string + propertiesJson?: string + fields?: string +} + +interface UpdateTableBorderPropertiesResponse { + success: boolean + output: { + updated: boolean + objectId: string + fields: string + metadata: { presentationId: string; url: string } + } +} + +export const updateTableBorderPropertiesTool: ToolConfig< + UpdateTableBorderPropertiesParams, + UpdateTableBorderPropertiesResponse +> = { + id: 'google_slides_update_table_border_properties', + name: 'Update Table Border Properties in Google Slides', + description: + 'Update border color, weight, and dash style for a position (e.g. ALL, INNER, OUTER) in a table range.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the table', + }, + rowIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based row index of the top-left cell of the range', + }, + columnIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based column index of the top-left cell of the range', + }, + rowSpan: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Number of rows in the range (minimum 1)', + }, + columnSpan: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Number of columns in the range (minimum 1)', + }, + borderPosition: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Which borders to update: ALL (default), BOTTOM, INNER, INNER_HORIZONTAL, INNER_VERTICAL, LEFT, OUTER, RIGHT, TOP', + }, + borderColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Border color as hex', + }, + borderWeight: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Border weight in points', + }, + dashStyle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Dash style: SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT', + }, + propertiesJson: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: raw TableBorderProperties JSON merged with the simple fields above', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Table object ID is required') + + const props: Record = {} + const fieldList: string[] = [] + + const color = hexToOpaqueColor(params.borderColor) + if (color) { + props.tableBorderFill = { solidFill: { color } } + fieldList.push('tableBorderFill') + } + if (params.borderWeight !== undefined) { + props.weight = { magnitude: params.borderWeight, unit: 'PT' } + fieldList.push('weight') + } + if (params.dashStyle) { + props.dashStyle = params.dashStyle + fieldList.push('dashStyle') + } + if (params.propertiesJson?.trim()) { + try { + const extra = JSON.parse(params.propertiesJson) + if (extra && typeof extra === 'object') Object.assign(props, extra) + } catch (e) { + logger.warn('Invalid propertiesJson, ignoring:', { error: e }) + } + } + + const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*') + + return { + requests: [ + { + updateTableBorderProperties: { + objectId, + tableRange: { + location: { rowIndex: params.rowIndex, columnIndex: params.columnIndex }, + rowSpan: params.rowSpan, + columnSpan: params.columnSpan, + }, + borderPosition: params.borderPosition || 'ALL', + tableBorderProperties: props, + fields, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update table border properties') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + fields: params?.fields?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether the border properties were updated' }, + objectId: { type: 'string', description: 'The table updated' }, + fields: { type: 'string', description: 'FieldMask applied' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_table_cell_properties.ts b/apps/sim/tools/google_slides/update_table_cell_properties.ts new file mode 100644 index 00000000000..1bdc2520b0e --- /dev/null +++ b/apps/sim/tools/google_slides/update_table_cell_properties.ts @@ -0,0 +1,210 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + hexToOpaqueColor, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdateTableCellPropertiesTool') + +interface UpdateTableCellPropertiesParams { + accessToken: string + presentationId: string + objectId: string + rowIndex: number + columnIndex: number + rowSpan: number + columnSpan: number + backgroundColor?: string + backgroundAlpha?: number + contentAlignment?: 'TOP' | 'MIDDLE' | 'BOTTOM' + propertiesJson?: string + fields?: string +} + +interface UpdateTableCellPropertiesResponse { + success: boolean + output: { + updated: boolean + objectId: string + fields: string + metadata: { presentationId: string; url: string } + } +} + +export const updateTableCellPropertiesTool: ToolConfig< + UpdateTableCellPropertiesParams, + UpdateTableCellPropertiesResponse +> = { + id: 'google_slides_update_table_cell_properties', + name: 'Update Table Cell Properties in Google Slides', + description: 'Update background fill and content alignment for a range of table cells.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the table', + }, + rowIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based row index of the top-left cell of the range', + }, + columnIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Zero-based column index of the top-left cell of the range', + }, + rowSpan: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Number of rows in the range (minimum 1)', + }, + columnSpan: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Number of columns in the range (minimum 1)', + }, + backgroundColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cell background color as hex (e.g. #F1F3F4)', + }, + backgroundAlpha: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Background fill opacity between 0.0 and 1.0', + }, + contentAlignment: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Vertical alignment of cell content: TOP, MIDDLE, or BOTTOM', + }, + propertiesJson: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: raw TableCellProperties JSON merged with the simple fields above', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Table object ID is required') + + const props: Record = {} + const fieldList: string[] = [] + + const bg = hexToOpaqueColor(params.backgroundColor) + if (bg) { + props.tableCellBackgroundFill = { + solidFill: { + color: bg, + ...(params.backgroundAlpha !== undefined ? { alpha: params.backgroundAlpha } : {}), + }, + propertyState: 'RENDERED', + } + fieldList.push('tableCellBackgroundFill') + } + if (params.contentAlignment) { + props.contentAlignment = params.contentAlignment + fieldList.push('contentAlignment') + } + if (params.propertiesJson?.trim()) { + try { + const extra = JSON.parse(params.propertiesJson) + if (extra && typeof extra === 'object') Object.assign(props, extra) + } catch (e) { + logger.warn('Invalid propertiesJson, ignoring:', { error: e }) + } + } + + const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*') + + return { + requests: [ + { + updateTableCellProperties: { + objectId, + tableRange: { + location: { rowIndex: params.rowIndex, columnIndex: params.columnIndex }, + rowSpan: params.rowSpan, + columnSpan: params.columnSpan, + }, + tableCellProperties: props, + fields, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update table cell properties') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + fields: params?.fields?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether the cell properties were updated' }, + objectId: { type: 'string', description: 'The table updated' }, + fields: { type: 'string', description: 'FieldMask applied' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_table_column_properties.ts b/apps/sim/tools/google_slides/update_table_column_properties.ts new file mode 100644 index 00000000000..bb63f24980c --- /dev/null +++ b/apps/sim/tools/google_slides/update_table_column_properties.ts @@ -0,0 +1,158 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdateTableColumnPropertiesTool') + +interface UpdateTableColumnPropertiesParams { + accessToken: string + presentationId: string + objectId: string + columnIndices: string + columnWidth?: number + propertiesJson?: string + fields?: string +} + +interface UpdateTableColumnPropertiesResponse { + success: boolean + output: { + updated: boolean + objectId: string + fields: string + metadata: { presentationId: string; url: string } + } +} + +export const updateTableColumnPropertiesTool: ToolConfig< + UpdateTableColumnPropertiesParams, + UpdateTableColumnPropertiesResponse +> = { + id: 'google_slides_update_table_column_properties', + name: 'Update Table Column Properties in Google Slides', + description: 'Update column widths and other column-level table properties.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the table', + }, + columnIndices: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comma-separated zero-based column indices to update (e.g. "0,2,3")', + }, + columnWidth: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Column width in points', + }, + propertiesJson: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: raw TableColumnProperties JSON merged with the simple fields above', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Table object ID is required') + const columnIndices = (params.columnIndices || '') + .split(',') + .map((s) => Number.parseInt(s.trim(), 10)) + .filter((n) => Number.isFinite(n) && n >= 0) + if (columnIndices.length === 0) throw new Error('At least one column index is required') + + const props: Record = {} + const fieldList: string[] = [] + if (params.columnWidth !== undefined) { + props.columnWidth = { magnitude: params.columnWidth, unit: 'PT' } + fieldList.push('columnWidth') + } + if (params.propertiesJson?.trim()) { + try { + const extra = JSON.parse(params.propertiesJson) + if (extra && typeof extra === 'object') Object.assign(props, extra) + } catch (e) { + logger.warn('Invalid propertiesJson, ignoring:', { error: e }) + } + } + const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*') + + return { + requests: [ + { + updateTableColumnProperties: { + objectId, + columnIndices, + tableColumnProperties: props, + fields, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update table column properties') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + fields: params?.fields?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether the column properties were updated' }, + objectId: { type: 'string', description: 'The table updated' }, + fields: { type: 'string', description: 'FieldMask applied' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_table_row_properties.ts b/apps/sim/tools/google_slides/update_table_row_properties.ts new file mode 100644 index 00000000000..8a60deb9f40 --- /dev/null +++ b/apps/sim/tools/google_slides/update_table_row_properties.ts @@ -0,0 +1,158 @@ +import { createLogger } from '@sim/logger' +import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdateTableRowPropertiesTool') + +interface UpdateTableRowPropertiesParams { + accessToken: string + presentationId: string + objectId: string + rowIndices: string + minRowHeight?: number + propertiesJson?: string + fields?: string +} + +interface UpdateTableRowPropertiesResponse { + success: boolean + output: { + updated: boolean + objectId: string + fields: string + metadata: { presentationId: string; url: string } + } +} + +export const updateTableRowPropertiesTool: ToolConfig< + UpdateTableRowPropertiesParams, + UpdateTableRowPropertiesResponse +> = { + id: 'google_slides_update_table_row_properties', + name: 'Update Table Row Properties in Google Slides', + description: 'Update minimum row heights and other row-level table properties.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the table', + }, + rowIndices: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comma-separated zero-based row indices to update (e.g. "0,2,3")', + }, + minRowHeight: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Minimum row height in points', + }, + propertiesJson: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: raw TableRowProperties JSON merged with the simple fields above', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Table object ID is required') + const rowIndices = (params.rowIndices || '') + .split(',') + .map((s) => Number.parseInt(s.trim(), 10)) + .filter((n) => Number.isFinite(n) && n >= 0) + if (rowIndices.length === 0) throw new Error('At least one row index is required') + + const props: Record = {} + const fieldList: string[] = [] + if (params.minRowHeight !== undefined) { + props.minRowHeight = { magnitude: params.minRowHeight, unit: 'PT' } + fieldList.push('minRowHeight') + } + if (params.propertiesJson?.trim()) { + try { + const extra = JSON.parse(params.propertiesJson) + if (extra && typeof extra === 'object') Object.assign(props, extra) + } catch (e) { + logger.warn('Invalid propertiesJson, ignoring:', { error: e }) + } + } + const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*') + + return { + requests: [ + { + updateTableRowProperties: { + objectId, + rowIndices, + tableRowProperties: props, + fields, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update table row properties') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + fields: params?.fields?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether the row properties were updated' }, + objectId: { type: 'string', description: 'The table updated' }, + fields: { type: 'string', description: 'FieldMask applied' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_text_style.ts b/apps/sim/tools/google_slides/update_text_style.ts new file mode 100644 index 00000000000..4ebb8dee746 --- /dev/null +++ b/apps/sim/tools/google_slides/update_text_style.ts @@ -0,0 +1,311 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + buildCellLocation, + buildTextRange, + hexToOpaqueColor, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdateTextStyleTool') + +interface UpdateTextStyleParams { + accessToken: string + presentationId: string + objectId: string + rowIndex?: number + columnIndex?: number + rangeType?: 'ALL' | 'FROM_START_INDEX' | 'FIXED_RANGE' + startIndex?: number + endIndex?: number + bold?: boolean + italic?: boolean + underline?: boolean + strikethrough?: boolean + smallCaps?: boolean + fontFamily?: string + fontSize?: number + foregroundColor?: string + backgroundColor?: string + linkUrl?: string + baselineOffset?: 'NONE' | 'SUPERSCRIPT' | 'SUBSCRIPT' + styleJson?: string + fields?: string +} + +interface UpdateTextStyleResponse { + success: boolean + output: { + updated: boolean + objectId: string + fields: string + metadata: { presentationId: string; url: string } + } +} + +export const updateTextStyleTool: ToolConfig = { + id: 'google_slides_update_text_style', + name: 'Update Text Style in Google Slides', + description: + 'Update the styling of text in a shape or table cell (bold, italic, font family, font size, foreground/background color, link, etc.). Only the fields you set are applied.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the shape or table containing the text', + }, + rowIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'When targeting a table cell, the zero-based row index', + }, + columnIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'When targeting a table cell, the zero-based column index', + }, + rangeType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Range to style: ALL (default), FROM_START_INDEX, or FIXED_RANGE', + }, + startIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start index for FROM_START_INDEX or FIXED_RANGE', + }, + endIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'End index for FIXED_RANGE', + }, + bold: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the text is bold', + }, + italic: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the text is italic', + }, + underline: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the text is underlined', + }, + strikethrough: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the text has strikethrough', + }, + smallCaps: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the text is rendered in small caps', + }, + fontFamily: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Font family name (must be a font available to Google Slides)', + }, + fontSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Font size in points', + }, + foregroundColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Text color as hex (e.g. #1A73E8)', + }, + backgroundColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Text background color as hex (e.g. #FFF8E1)', + }, + linkUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Convert the range to a hyperlink with this URL', + }, + baselineOffset: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Baseline offset: NONE, SUPERSCRIPT, or SUBSCRIPT', + }, + styleJson: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Advanced: raw TextStyle JSON merged with the simple fields above (overrides them on conflict)', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Advanced: explicit FieldMask. If omitted, the mask is computed from the fields you provided (or "*" when styleJson is used without explicit fields).', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + + const style: Record = {} + const fieldList: string[] = [] + + if (params.bold !== undefined) { + style.bold = params.bold + fieldList.push('bold') + } + if (params.italic !== undefined) { + style.italic = params.italic + fieldList.push('italic') + } + if (params.underline !== undefined) { + style.underline = params.underline + fieldList.push('underline') + } + if (params.strikethrough !== undefined) { + style.strikethrough = params.strikethrough + fieldList.push('strikethrough') + } + if (params.smallCaps !== undefined) { + style.smallCaps = params.smallCaps + fieldList.push('smallCaps') + } + if (params.fontFamily) { + style.fontFamily = params.fontFamily + fieldList.push('fontFamily') + } + if (params.fontSize !== undefined) { + style.fontSize = { magnitude: params.fontSize, unit: 'PT' } + fieldList.push('fontSize') + } + const fg = hexToOpaqueColor(params.foregroundColor) + if (fg) { + style.foregroundColor = { opaqueColor: fg } + fieldList.push('foregroundColor') + } + const bg = hexToOpaqueColor(params.backgroundColor) + if (bg) { + style.backgroundColor = { opaqueColor: bg } + fieldList.push('backgroundColor') + } + if (params.linkUrl) { + style.link = { url: params.linkUrl } + fieldList.push('link') + } + if (params.baselineOffset) { + style.baselineOffset = params.baselineOffset + fieldList.push('baselineOffset') + } + + if (params.styleJson?.trim()) { + try { + const extra = JSON.parse(params.styleJson) + if (extra && typeof extra === 'object') { + Object.assign(style, extra) + } + } catch (e) { + logger.warn('Invalid styleJson, ignoring:', { error: e }) + } + } + + const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*') + + const updateRequest: Record = { + objectId, + style, + textRange: buildTextRange({ + rangeType: params.rangeType, + startIndex: params.startIndex, + endIndex: params.endIndex, + }), + fields, + } + + const cellLocation = buildCellLocation({ + rowIndex: params.rowIndex, + columnIndex: params.columnIndex, + }) + if (cellLocation) updateRequest.cellLocation = cellLocation + + return { requests: [{ updateTextStyle: updateRequest }] } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update text style') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + fields: params?.fields?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether the text style was updated' }, + objectId: { type: 'string', description: 'The object whose text was styled' }, + fields: { type: 'string', description: 'FieldMask applied' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/update_video_properties.ts b/apps/sim/tools/google_slides/update_video_properties.ts new file mode 100644 index 00000000000..5873499244f --- /dev/null +++ b/apps/sim/tools/google_slides/update_video_properties.ts @@ -0,0 +1,208 @@ +import { createLogger } from '@sim/logger' +import { + authJsonHeaders, + batchUpdateUrl, + hexToOpaqueColor, + presentationUrl, +} from '@/tools/google_slides/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesUpdateVideoPropertiesTool') + +interface UpdateVideoPropertiesParams { + accessToken: string + presentationId: string + objectId: string + autoPlay?: boolean + mute?: boolean + start?: number + end?: number + outlineColor?: string + outlineWeight?: number + outlineDashStyle?: string + propertiesJson?: string + fields?: string +} + +interface UpdateVideoPropertiesResponse { + success: boolean + output: { + updated: boolean + objectId: string + fields: string + metadata: { presentationId: string; url: string } + } +} + +export const updateVideoPropertiesTool: ToolConfig< + UpdateVideoPropertiesParams, + UpdateVideoPropertiesResponse +> = { + id: 'google_slides_update_video_properties', + name: 'Update Video Properties in Google Slides', + description: 'Update video playback options (autoPlay, mute, start/end) and outline.', + version: '1.0.0', + + oauth: { required: true, provider: 'google-drive' }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Slides presentation ID', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object ID of the video', + }, + autoPlay: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Play the video automatically when the slide is shown', + }, + mute: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Mute the video', + }, + start: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Playback start time in seconds', + }, + end: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Playback end time in seconds', + }, + outlineColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Outline color as hex', + }, + outlineWeight: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Outline weight in points', + }, + outlineDashStyle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Outline dash style', + }, + propertiesJson: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: raw VideoProperties JSON merged with the simple fields above', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.', + }, + }, + + request: { + url: (params) => batchUpdateUrl(params.presentationId), + method: 'POST', + headers: (params) => authJsonHeaders(params.accessToken), + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) throw new Error('Object ID is required') + + const props: Record = {} + const fieldList: string[] = [] + + if (params.autoPlay !== undefined) { + props.autoPlay = params.autoPlay + fieldList.push('autoPlay') + } + if (params.mute !== undefined) { + props.mute = params.mute + fieldList.push('mute') + } + if (params.start !== undefined) { + props.start = params.start + fieldList.push('start') + } + if (params.end !== undefined) { + props.end = params.end + fieldList.push('end') + } + const outlineColor = hexToOpaqueColor(params.outlineColor) + if (outlineColor || params.outlineWeight !== undefined || params.outlineDashStyle) { + const outline: Record = { propertyState: 'RENDERED' } + if (outlineColor) outline.outlineFill = { solidFill: { color: outlineColor } } + if (params.outlineWeight !== undefined) + outline.weight = { magnitude: params.outlineWeight, unit: 'PT' } + if (params.outlineDashStyle) outline.dashStyle = params.outlineDashStyle + props.outline = outline + fieldList.push('outline') + } + if (params.propertiesJson?.trim()) { + try { + const extra = JSON.parse(params.propertiesJson) + if (extra && typeof extra === 'object') Object.assign(props, extra) + } catch (e) { + logger.warn('Invalid propertiesJson, ignoring:', { error: e }) + } + } + + const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*') + + return { + requests: [{ updateVideoProperties: { objectId, videoProperties: props, fields } }], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update video properties') + } + const presentationId = params?.presentationId?.trim() || '' + return { + success: true, + output: { + updated: true, + objectId: params?.objectId?.trim() || '', + fields: params?.fields?.trim() || '', + metadata: { presentationId, url: presentationUrl(presentationId) }, + }, + } + }, + + outputs: { + updated: { type: 'boolean', description: 'Whether the video properties were updated' }, + objectId: { type: 'string', description: 'The video object updated' }, + fields: { type: 'string', description: 'FieldMask applied' }, + metadata: { + type: 'object', + description: 'Operation metadata', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_slides/utils.ts b/apps/sim/tools/google_slides/utils.ts new file mode 100644 index 00000000000..48dce63bde2 --- /dev/null +++ b/apps/sim/tools/google_slides/utils.ts @@ -0,0 +1,148 @@ +import { generateRandomString } from '@sim/utils/random' + +/** 1 point = 12700 EMU (English Metric Units). */ +export const PT_TO_EMU = 12700 + +export interface OpaqueColor { + rgbColor: { red: number; green: number; blue: number } +} + +export interface TextRangeInput { + rangeType?: 'ALL' | 'FROM_START_INDEX' | 'FIXED_RANGE' + startIndex?: number + endIndex?: number +} + +export interface CellLocationInput { + rowIndex?: number + columnIndex?: number +} + +/** + * Convert a hex color string (`#RRGGBB`, `#RGB`, or bare hex) into the + * Google Slides API's OpaqueColor shape with rgbColor scaled 0-1. + * Returns null when input is empty/invalid. + */ +export function hexToOpaqueColor(input?: string | null): OpaqueColor | null { + if (!input) return null + let hex = input.trim().replace(/^#/, '') + if (hex.length === 3) { + hex = hex + .split('') + .map((c) => c + c) + .join('') + } + if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null + const r = Number.parseInt(hex.slice(0, 2), 16) + const g = Number.parseInt(hex.slice(2, 4), 16) + const b = Number.parseInt(hex.slice(4, 6), 16) + return { + rgbColor: { + red: r / 255, + green: g / 255, + blue: b / 255, + }, + } +} + +/** + * Build a Slides API TextRange. Defaults to range type ALL. + * `FROM_START_INDEX` requires startIndex; `FIXED_RANGE` requires both indices. + */ +export function buildTextRange(input: TextRangeInput | undefined) { + const rangeType = input?.rangeType ?? 'ALL' + if (rangeType === 'FROM_START_INDEX') { + return { type: 'FROM_START_INDEX', startIndex: input?.startIndex ?? 0 } + } + if (rangeType === 'FIXED_RANGE') { + return { + type: 'FIXED_RANGE', + startIndex: input?.startIndex ?? 0, + endIndex: input?.endIndex ?? 0, + } + } + return { type: 'ALL' } +} + +/** + * Build an optional cellLocation if both row and column indices are provided. + * Slides API treats absence as targeting the shape itself (not a table cell). + */ +export function buildCellLocation(input: CellLocationInput | undefined) { + if (input?.rowIndex === undefined || input?.columnIndex === undefined) return undefined + return { rowIndex: input.rowIndex, columnIndex: input.columnIndex } +} + +/** Generate a stable-enough object ID prefixed by kind. */ +export function generateObjectId(kind: string): string { + return `${kind}_${Date.now()}_${generateRandomString(7)}` +} + +/** Convert a points value (or undefined) to an EMU Dimension object. */ +export function ptToEmuDimension(pt: number | undefined, fallbackPt: number) { + return { + magnitude: (pt ?? fallbackPt) * PT_TO_EMU, + unit: 'EMU', + } +} + +/** Build a standard PageElementProperties with size + transform from points. */ +export function buildElementProperties(opts: { + pageObjectId: string + width?: number + height?: number + positionX?: number + positionY?: number + defaultWidth?: number + defaultHeight?: number + defaultX?: number + defaultY?: number +}) { + return { + pageObjectId: opts.pageObjectId, + size: { + width: ptToEmuDimension(opts.width, opts.defaultWidth ?? 200), + height: ptToEmuDimension(opts.height, opts.defaultHeight ?? 100), + }, + transform: { + scaleX: 1, + scaleY: 1, + translateX: (opts.positionX ?? opts.defaultX ?? 100) * PT_TO_EMU, + translateY: (opts.positionY ?? opts.defaultY ?? 100) * PT_TO_EMU, + unit: 'EMU', + }, + } +} + +/** Standard presentation URL for embedding in metadata. */ +export function presentationUrl(presentationId: string): string { + return `https://docs.google.com/presentation/d/${presentationId}/edit` +} + +/** Standard fetch headers for Slides API JSON calls. */ +export function authJsonHeaders(accessToken: string) { + if (!accessToken) throw new Error('Access token is required') + return { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + } +} + +/** Resolve the batchUpdate URL for a given presentation. */ +export function batchUpdateUrl(presentationId: string | undefined): string { + const id = presentationId?.trim() + if (!id) throw new Error('Presentation ID is required') + return `https://slides.googleapis.com/v1/presentations/${id}:batchUpdate` +} + +/** + * Build a FieldMask string from a record of set/unset fields. Keys with + * defined (non-undefined) values are included; others are omitted. Useful + * for UpdateXxxProperties requests where only changed fields should be sent. + */ +export function buildFieldMask(fields: Record): string { + return Object.entries(fields) + .filter(([, v]) => v !== undefined && v !== null && v !== '') + .map(([k]) => k) + .join(',') +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 32b87050aeb..54e16312802 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1106,17 +1106,55 @@ import { import { googleSlidesAddImageTool, googleSlidesAddSlideTool, + googleSlidesBatchUpdateTool, + googleSlidesCopyPresentationTool, + googleSlidesCreateLineTool, + googleSlidesCreateParagraphBulletsTool, googleSlidesCreateShapeTool, + googleSlidesCreateSheetsChartTool, googleSlidesCreateTableTool, googleSlidesCreateTool, + googleSlidesCreateVideoTool, googleSlidesDeleteObjectTool, + googleSlidesDeleteParagraphBulletsTool, + googleSlidesDeleteTableColumnTool, + googleSlidesDeleteTableRowTool, + googleSlidesDeleteTextTool, googleSlidesDuplicateObjectTool, + googleSlidesExportPresentationTool, googleSlidesGetPageTool, googleSlidesGetThumbnailTool, + googleSlidesGroupObjectsTool, + googleSlidesInsertTableColumnsTool, + googleSlidesInsertTableRowsTool, googleSlidesInsertTextTool, + googleSlidesMergeTableCellsTool, googleSlidesReadTool, + googleSlidesRefreshSheetsChartTool, + googleSlidesReplaceAllShapesWithImageTool, + googleSlidesReplaceAllShapesWithSheetsChartTool, googleSlidesReplaceAllTextTool, + googleSlidesReplaceImageTool, + googleSlidesRerouteLineTool, + googleSlidesUngroupObjectsTool, + googleSlidesUnmergeTableCellsTool, + googleSlidesUpdateImagePropertiesTool, + googleSlidesUpdateLineCategoryTool, + googleSlidesUpdateLinePropertiesTool, + googleSlidesUpdatePageElementAltTextTool, + googleSlidesUpdatePageElementsZOrderTool, + googleSlidesUpdatePageElementTransformTool, + googleSlidesUpdatePagePropertiesTool, + googleSlidesUpdateParagraphStyleTool, + googleSlidesUpdateShapePropertiesTool, + googleSlidesUpdateSlidePropertiesTool, googleSlidesUpdateSlidesPositionTool, + googleSlidesUpdateTableBorderPropertiesTool, + googleSlidesUpdateTableCellPropertiesTool, + googleSlidesUpdateTableColumnPropertiesTool, + googleSlidesUpdateTableRowPropertiesTool, + googleSlidesUpdateTextStyleTool, + googleSlidesUpdateVideoPropertiesTool, googleSlidesWriteTool, } from '@/tools/google_slides' import { @@ -4527,6 +4565,45 @@ export const tools: Record = { google_slides_create_table: googleSlidesCreateTableTool, google_slides_create_shape: googleSlidesCreateShapeTool, google_slides_insert_text: googleSlidesInsertTextTool, + google_slides_update_text_style: googleSlidesUpdateTextStyleTool, + google_slides_update_paragraph_style: googleSlidesUpdateParagraphStyleTool, + google_slides_delete_text: googleSlidesDeleteTextTool, + google_slides_create_paragraph_bullets: googleSlidesCreateParagraphBulletsTool, + google_slides_delete_paragraph_bullets: googleSlidesDeleteParagraphBulletsTool, + google_slides_replace_all_shapes_with_image: googleSlidesReplaceAllShapesWithImageTool, + google_slides_replace_image: googleSlidesReplaceImageTool, + google_slides_update_image_properties: googleSlidesUpdateImagePropertiesTool, + google_slides_update_shape_properties: googleSlidesUpdateShapePropertiesTool, + google_slides_update_page_properties: googleSlidesUpdatePagePropertiesTool, + google_slides_update_slide_properties: googleSlidesUpdateSlidePropertiesTool, + google_slides_update_page_element_alt_text: googleSlidesUpdatePageElementAltTextTool, + google_slides_update_page_element_transform: googleSlidesUpdatePageElementTransformTool, + google_slides_update_page_elements_z_order: googleSlidesUpdatePageElementsZOrderTool, + google_slides_group_objects: googleSlidesGroupObjectsTool, + google_slides_ungroup_objects: googleSlidesUngroupObjectsTool, + google_slides_create_line: googleSlidesCreateLineTool, + google_slides_update_line_properties: googleSlidesUpdateLinePropertiesTool, + google_slides_update_line_category: googleSlidesUpdateLineCategoryTool, + google_slides_reroute_line: googleSlidesRerouteLineTool, + google_slides_insert_table_rows: googleSlidesInsertTableRowsTool, + google_slides_insert_table_columns: googleSlidesInsertTableColumnsTool, + google_slides_delete_table_row: googleSlidesDeleteTableRowTool, + google_slides_delete_table_column: googleSlidesDeleteTableColumnTool, + google_slides_merge_table_cells: googleSlidesMergeTableCellsTool, + google_slides_unmerge_table_cells: googleSlidesUnmergeTableCellsTool, + google_slides_update_table_cell_properties: googleSlidesUpdateTableCellPropertiesTool, + google_slides_update_table_border_properties: googleSlidesUpdateTableBorderPropertiesTool, + google_slides_update_table_column_properties: googleSlidesUpdateTableColumnPropertiesTool, + google_slides_update_table_row_properties: googleSlidesUpdateTableRowPropertiesTool, + google_slides_create_sheets_chart: googleSlidesCreateSheetsChartTool, + google_slides_refresh_sheets_chart: googleSlidesRefreshSheetsChartTool, + google_slides_replace_all_shapes_with_sheets_chart: + googleSlidesReplaceAllShapesWithSheetsChartTool, + google_slides_create_video: googleSlidesCreateVideoTool, + google_slides_update_video_properties: googleSlidesUpdateVideoPropertiesTool, + google_slides_batch_update: googleSlidesBatchUpdateTool, + google_slides_copy_presentation: googleSlidesCopyPresentationTool, + google_slides_export_presentation: googleSlidesExportPresentationTool, pdl_person_enrich: pdlPersonEnrichTool, pdl_person_search: pdlPersonSearchTool, pdl_person_identify: pdlPersonIdentifyTool, From 52948d11db953f6d0dcad3fa14d8e62b5a7827a3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 20 May 2026 13:27:47 -0700 Subject: [PATCH 2/3] =?UTF-8?q?fix(google-slides):=20address=20PR=20review?= =?UTF-8?q?=20=E2=80=94=20explicit=20videoId=20mapping,=20fast=20base64=20?= =?UTF-8?q?export,=20remove=20dead=20utility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sim/blocks/blocks/google_slides.ts | 1 + apps/sim/tools/google_slides/export_presentation.ts | 10 ++-------- apps/sim/tools/google_slides/utils.ts | 12 ------------ 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/apps/sim/blocks/blocks/google_slides.ts b/apps/sim/blocks/blocks/google_slides.ts index 4868e327e8e..1457569fa2f 100644 --- a/apps/sim/blocks/blocks/google_slides.ts +++ b/apps/sim/blocks/blocks/google_slides.ts @@ -2977,6 +2977,7 @@ Return ONLY the text content - no explanations, no markdown formatting markers, if (params.operation === 'create_video') { result.pageObjectId = params.videoPageObjectId if (params.videoSource) result.source = params.videoSource + if (params.videoId) result.videoId = params.videoId const vw = toNum(params.videoWidth) const vh = toNum(params.videoHeight) const vpx = toNum(params.videoPositionX) diff --git a/apps/sim/tools/google_slides/export_presentation.ts b/apps/sim/tools/google_slides/export_presentation.ts index 78d8cea1145..34553149411 100644 --- a/apps/sim/tools/google_slides/export_presentation.ts +++ b/apps/sim/tools/google_slides/export_presentation.ts @@ -93,13 +93,7 @@ export const exportPresentationTool: ToolConfig< } const buffer = await response.arrayBuffer() - const bytes = new Uint8Array(buffer) - let binary = '' - for (let i = 0; i < bytes.length; i += 1) { - binary += String.fromCharCode(bytes[i]) - } - const contentBase64 = - typeof btoa === 'function' ? btoa(binary) : Buffer.from(buffer).toString('base64') + const contentBase64 = Buffer.from(buffer).toString('base64') const presentationId = params?.presentationId?.trim() || '' const format = (params?.exportFormat || 'PDF').toUpperCase() @@ -110,7 +104,7 @@ export const exportPresentationTool: ToolConfig< output: { contentBase64, mimeType: mime, - sizeBytes: bytes.length, + sizeBytes: buffer.byteLength, metadata: { presentationId, url: presentationUrl(presentationId), diff --git a/apps/sim/tools/google_slides/utils.ts b/apps/sim/tools/google_slides/utils.ts index 48dce63bde2..011c4939530 100644 --- a/apps/sim/tools/google_slides/utils.ts +++ b/apps/sim/tools/google_slides/utils.ts @@ -134,15 +134,3 @@ export function batchUpdateUrl(presentationId: string | undefined): string { if (!id) throw new Error('Presentation ID is required') return `https://slides.googleapis.com/v1/presentations/${id}:batchUpdate` } - -/** - * Build a FieldMask string from a record of set/unset fields. Keys with - * defined (non-undefined) values are included; others are omitted. Useful - * for UpdateXxxProperties requests where only changed fields should be sent. - */ -export function buildFieldMask(fields: Record): string { - return Object.entries(fields) - .filter(([, v]) => v !== undefined && v !== null && v !== '') - .map(([k]) => k) - .join(',') -} From 2b11ce249aa40d2164f9b5060b2cb336a8ef46d5 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 20 May 2026 13:34:42 -0700 Subject: [PATCH 3/3] fix(google-slides): declare z-order operation output in block outputs map --- apps/sim/blocks/blocks/google_slides.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/blocks/blocks/google_slides.ts b/apps/sim/blocks/blocks/google_slides.ts index 1457569fa2f..2608eb71f13 100644 --- a/apps/sim/blocks/blocks/google_slides.ts +++ b/apps/sim/blocks/blocks/google_slides.ts @@ -3395,6 +3395,7 @@ Return ONLY the text content - no explanations, no markdown formatting markers, // Z-order reordered: { type: 'boolean', description: 'Whether the z-order was changed' }, objectIds: { type: 'json', description: 'Object IDs affected by the operation' }, + operation: { type: 'string', description: 'Z-order operation applied' }, // Table extension tableObjectId: { type: 'string', description: 'Table object ID affected' },