chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleSlidesAddImageTool')
|
||||
|
||||
interface AddImageParams {
|
||||
accessToken: string
|
||||
presentationId: string
|
||||
pageObjectId: string
|
||||
imageUrl: string
|
||||
width?: number
|
||||
height?: number
|
||||
positionX?: number
|
||||
positionY?: number
|
||||
}
|
||||
|
||||
interface AddImageResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
imageId: string
|
||||
metadata: {
|
||||
presentationId: string
|
||||
pageObjectId: string
|
||||
imageUrl: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EMU (English Metric Units) conversion: 1 inch = 914400 EMU, 1 pt = 12700 EMU
|
||||
const PT_TO_EMU = 12700
|
||||
|
||||
export const addImageTool: ToolConfig<AddImageParams, AddImageResponse> = {
|
||||
id: 'google_slides_add_image',
|
||||
name: 'Add Image to Google Slides',
|
||||
description: 'Insert an image into a specific slide in a Google Slides presentation',
|
||||
version: '1.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: 'The object ID of the slide/page to add the image to',
|
||||
},
|
||||
imageUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The publicly accessible URL of the image (must be PNG, JPEG, or GIF, max 50MB)',
|
||||
},
|
||||
width: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Width of the image in points (default: 300)',
|
||||
},
|
||||
height: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Height of the image in points (default: 200)',
|
||||
},
|
||||
positionX: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'X position from the left edge in points (default: 100)',
|
||||
},
|
||||
positionY: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Y position from the top edge in points (default: 100)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const presentationId = params.presentationId?.trim()
|
||||
if (!presentationId) {
|
||||
throw new Error('Presentation ID is required')
|
||||
}
|
||||
return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`
|
||||
},
|
||||
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 pageObjectId = params.pageObjectId?.trim()
|
||||
const imageUrl = params.imageUrl?.trim()
|
||||
|
||||
if (!pageObjectId) {
|
||||
throw new Error('Page Object ID is required')
|
||||
}
|
||||
if (!imageUrl) {
|
||||
throw new Error('Image URL is required')
|
||||
}
|
||||
|
||||
// Generate a unique object ID for the new image
|
||||
const imageObjectId = `image_${Date.now()}_${generateRandomString(7)}`
|
||||
|
||||
// Convert points to EMU (default sizes if not specified)
|
||||
const widthEmu = (params.width || 300) * PT_TO_EMU
|
||||
const heightEmu = (params.height || 200) * PT_TO_EMU
|
||||
const translateX = (params.positionX || 100) * PT_TO_EMU
|
||||
const translateY = (params.positionY || 100) * PT_TO_EMU
|
||||
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
createImage: {
|
||||
objectId: imageObjectId,
|
||||
url: imageUrl,
|
||||
elementProperties: {
|
||||
pageObjectId: pageObjectId,
|
||||
size: {
|
||||
width: {
|
||||
magnitude: widthEmu,
|
||||
unit: 'EMU',
|
||||
},
|
||||
height: {
|
||||
magnitude: heightEmu,
|
||||
unit: 'EMU',
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX: translateX,
|
||||
translateY: translateY,
|
||||
unit: 'EMU',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
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 add image')
|
||||
}
|
||||
|
||||
// The response contains the created image's object ID
|
||||
const createImageReply = data.replies?.[0]?.createImage
|
||||
const imageId = createImageReply?.objectId || ''
|
||||
|
||||
const presentationId = params?.presentationId?.trim() || ''
|
||||
const pageObjectId = params?.pageObjectId?.trim() || ''
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
imageId,
|
||||
metadata: {
|
||||
presentationId,
|
||||
pageObjectId,
|
||||
imageUrl: params?.imageUrl?.trim() || '',
|
||||
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
imageId: {
|
||||
type: 'string',
|
||||
description: 'The object ID of the newly created image',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata including presentation ID and image URL',
|
||||
properties: {
|
||||
presentationId: {
|
||||
type: 'string',
|
||||
description: 'The presentation ID',
|
||||
},
|
||||
pageObjectId: {
|
||||
type: 'string',
|
||||
description: 'The page object ID where the image was inserted',
|
||||
},
|
||||
imageUrl: {
|
||||
type: 'string',
|
||||
description: 'The source image URL',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL to open the presentation',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleSlidesAddSlideTool')
|
||||
|
||||
interface AddSlideParams {
|
||||
accessToken: string
|
||||
presentationId: string
|
||||
layout?: string
|
||||
insertionIndex?: number
|
||||
placeholderIdMappings?: string
|
||||
}
|
||||
|
||||
interface AddSlideResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
slideId: string
|
||||
metadata: {
|
||||
presentationId: string
|
||||
layout: string
|
||||
insertionIndex?: number
|
||||
url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Predefined layouts available in Google Slides API
|
||||
const PREDEFINED_LAYOUTS = [
|
||||
'BLANK',
|
||||
'CAPTION_ONLY',
|
||||
'TITLE',
|
||||
'TITLE_AND_BODY',
|
||||
'TITLE_AND_TWO_COLUMNS',
|
||||
'TITLE_ONLY',
|
||||
'SECTION_HEADER',
|
||||
'SECTION_TITLE_AND_DESCRIPTION',
|
||||
'ONE_COLUMN_TEXT',
|
||||
'MAIN_POINT',
|
||||
'BIG_NUMBER',
|
||||
] as const
|
||||
|
||||
export const addSlideTool: ToolConfig<AddSlideParams, AddSlideResponse> = {
|
||||
id: 'google_slides_add_slide',
|
||||
name: 'Add Slide to Google Slides',
|
||||
description: 'Add a new slide to a Google Slides presentation with a specified layout',
|
||||
version: '1.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',
|
||||
},
|
||||
layout: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The predefined layout for the slide (BLANK, TITLE, TITLE_AND_BODY, TITLE_ONLY, SECTION_HEADER, etc.). Defaults to BLANK.',
|
||||
},
|
||||
insertionIndex: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The optional zero-based index indicating where to insert the slide. If not specified, the slide is added at the end.',
|
||||
},
|
||||
placeholderIdMappings: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of placeholder mappings to assign custom object IDs to placeholders. Format: [{"layoutPlaceholder":{"type":"TITLE"},"objectId":"custom_title_id"}]',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const presentationId = params.presentationId?.trim()
|
||||
if (!presentationId) {
|
||||
throw new Error('Presentation ID is required')
|
||||
}
|
||||
return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`
|
||||
},
|
||||
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) => {
|
||||
// Generate a unique object ID for the new slide
|
||||
const slideObjectId = `slide_${Date.now()}_${generateRandomString(7)}`
|
||||
|
||||
// Validate and normalize the layout
|
||||
let layout = (params.layout || 'BLANK').toUpperCase()
|
||||
if (!PREDEFINED_LAYOUTS.includes(layout as (typeof PREDEFINED_LAYOUTS)[number])) {
|
||||
logger.warn(`Invalid layout "${params.layout}", defaulting to BLANK`)
|
||||
layout = 'BLANK'
|
||||
}
|
||||
|
||||
const createSlideRequest: Record<string, any> = {
|
||||
objectId: slideObjectId,
|
||||
slideLayoutReference: {
|
||||
predefinedLayout: layout,
|
||||
},
|
||||
}
|
||||
|
||||
// Add insertion index if specified
|
||||
if (params.insertionIndex !== undefined && params.insertionIndex >= 0) {
|
||||
createSlideRequest.insertionIndex = params.insertionIndex
|
||||
}
|
||||
|
||||
// Add placeholder ID mappings if specified (for advanced use cases)
|
||||
if (params.placeholderIdMappings?.trim()) {
|
||||
try {
|
||||
const mappings = JSON.parse(params.placeholderIdMappings)
|
||||
if (Array.isArray(mappings) && mappings.length > 0) {
|
||||
createSlideRequest.placeholderIdMappings = mappings
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('Invalid placeholderIdMappings JSON, ignoring:', e)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
createSlide: createSlideRequest,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
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 add slide')
|
||||
}
|
||||
|
||||
// The response contains the created slide's object ID
|
||||
const createSlideReply = data.replies?.[0]?.createSlide
|
||||
const slideId = createSlideReply?.objectId || ''
|
||||
|
||||
const presentationId = params?.presentationId?.trim() || ''
|
||||
const layout = (params?.layout || 'BLANK').toUpperCase()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
slideId,
|
||||
metadata: {
|
||||
presentationId,
|
||||
layout,
|
||||
insertionIndex: params?.insertionIndex,
|
||||
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
slideId: {
|
||||
type: 'string',
|
||||
description: 'The object ID of the newly created slide',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata including presentation ID, layout, and URL',
|
||||
properties: {
|
||||
presentationId: {
|
||||
type: 'string',
|
||||
description: 'The presentation ID',
|
||||
},
|
||||
layout: {
|
||||
type: 'string',
|
||||
description: 'The layout used for the new slide',
|
||||
},
|
||||
insertionIndex: {
|
||||
type: 'number',
|
||||
description: 'The zero-based index where the slide was inserted',
|
||||
optional: true,
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL to open the presentation',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<BatchUpdateParams, BatchUpdateResponse> = {
|
||||
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<string, unknown> = { 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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<CopyPresentationParams, CopyPresentationResponse> = {
|
||||
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 <source title>".',
|
||||
},
|
||||
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<string, unknown> = {}
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
GoogleSlidesCreateResponse,
|
||||
GoogleSlidesToolParams,
|
||||
} from '@/tools/google_slides/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleSlidesCreateTool')
|
||||
|
||||
export const createTool: ToolConfig<GoogleSlidesToolParams, GoogleSlidesCreateResponse> = {
|
||||
id: 'google_slides_create',
|
||||
name: 'Create Google Slides Presentation',
|
||||
description: 'Create a new Google Slides presentation',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Slides API',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The title of the presentation to create',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The content to add to the first slide',
|
||||
},
|
||||
folderSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Drive folder ID to create the presentation in (e.g., 1ABCxyz...)',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The ID of the folder to create the presentation in (internal use)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => {
|
||||
return 'https://www.googleapis.com/drive/v3/files?supportsAllDrives=true'
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
// Validate access token
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.title) {
|
||||
throw new Error('Title is required')
|
||||
}
|
||||
|
||||
const requestBody: any = {
|
||||
name: params.title,
|
||||
mimeType: 'application/vnd.google-apps.presentation',
|
||||
}
|
||||
|
||||
// Add parent folder if specified (prefer folderSelector over folderId)
|
||||
const folderId = params.folderSelector || params.folderId
|
||||
if (folderId) {
|
||||
requestBody.parents = [folderId]
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
postProcess: async (result, params, executeTool) => {
|
||||
if (!result.success) {
|
||||
return result
|
||||
}
|
||||
|
||||
const presentationId = result.output.metadata.presentationId
|
||||
|
||||
if (params.content && presentationId) {
|
||||
try {
|
||||
const writeParams = {
|
||||
accessToken: params.accessToken,
|
||||
presentationId: presentationId,
|
||||
content: params.content,
|
||||
}
|
||||
|
||||
const writeResult = await executeTool('google_slides_write', writeParams)
|
||||
|
||||
if (!writeResult.success) {
|
||||
logger.warn(
|
||||
'Failed to add content to presentation, but presentation was created:',
|
||||
writeResult.error
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Error adding content to presentation:', { error })
|
||||
// Don't fail the overall operation if adding content fails
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
try {
|
||||
// Get the response data
|
||||
const responseText = await response.text()
|
||||
const data = JSON.parse(responseText)
|
||||
|
||||
const presentationId = data.id
|
||||
const title = data.name
|
||||
|
||||
const metadata = {
|
||||
presentationId,
|
||||
title: title || 'Untitled Presentation',
|
||||
mimeType: 'application/vnd.google-apps.presentation',
|
||||
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Google Slides create - Error processing response:', {
|
||||
error,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Created presentation metadata including ID, title, and URL',
|
||||
properties: {
|
||||
presentationId: {
|
||||
type: 'string',
|
||||
description: 'The presentation ID',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'The presentation title',
|
||||
},
|
||||
mimeType: {
|
||||
type: 'string',
|
||||
description: 'The mime type of the presentation',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL to open the presentation',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<CreateLineParams, CreateLineResponse> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleSlidesCreateShapeTool')
|
||||
|
||||
interface CreateShapeParams {
|
||||
accessToken: string
|
||||
presentationId: string
|
||||
pageObjectId: string
|
||||
shapeType: string
|
||||
width?: number
|
||||
height?: number
|
||||
positionX?: number
|
||||
positionY?: number
|
||||
}
|
||||
|
||||
interface CreateShapeResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
shapeId: string
|
||||
shapeType: string
|
||||
metadata: {
|
||||
presentationId: string
|
||||
pageObjectId: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EMU (English Metric Units) conversion: 1 pt = 12700 EMU
|
||||
const PT_TO_EMU = 12700
|
||||
|
||||
// Common shape types available in Google Slides API
|
||||
const SHAPE_TYPES = [
|
||||
'TEXT_BOX',
|
||||
'RECTANGLE',
|
||||
'ROUND_RECTANGLE',
|
||||
'ELLIPSE',
|
||||
'ARC',
|
||||
'BENT_ARROW',
|
||||
'BENT_UP_ARROW',
|
||||
'BEVEL',
|
||||
'BLOCK_ARC',
|
||||
'BRACE_PAIR',
|
||||
'BRACKET_PAIR',
|
||||
'CAN',
|
||||
'CHEVRON',
|
||||
'CHORD',
|
||||
'CLOUD',
|
||||
'CORNER',
|
||||
'CUBE',
|
||||
'CURVED_DOWN_ARROW',
|
||||
'CURVED_LEFT_ARROW',
|
||||
'CURVED_RIGHT_ARROW',
|
||||
'CURVED_UP_ARROW',
|
||||
'DECAGON',
|
||||
'DIAGONAL_STRIPE',
|
||||
'DIAMOND',
|
||||
'DODECAGON',
|
||||
'DONUT',
|
||||
'DOUBLE_WAVE',
|
||||
'DOWN_ARROW',
|
||||
'DOWN_ARROW_CALLOUT',
|
||||
'FOLDED_CORNER',
|
||||
'FRAME',
|
||||
'HALF_FRAME',
|
||||
'HEART',
|
||||
'HEPTAGON',
|
||||
'HEXAGON',
|
||||
'HOME_PLATE',
|
||||
'HORIZONTAL_SCROLL',
|
||||
'IRREGULAR_SEAL_1',
|
||||
'IRREGULAR_SEAL_2',
|
||||
'LEFT_ARROW',
|
||||
'LEFT_ARROW_CALLOUT',
|
||||
'LEFT_BRACE',
|
||||
'LEFT_BRACKET',
|
||||
'LEFT_RIGHT_ARROW',
|
||||
'LEFT_RIGHT_ARROW_CALLOUT',
|
||||
'LEFT_RIGHT_UP_ARROW',
|
||||
'LEFT_UP_ARROW',
|
||||
'LIGHTNING_BOLT',
|
||||
'MATH_DIVIDE',
|
||||
'MATH_EQUAL',
|
||||
'MATH_MINUS',
|
||||
'MATH_MULTIPLY',
|
||||
'MATH_NOT_EQUAL',
|
||||
'MATH_PLUS',
|
||||
'MOON',
|
||||
'NO_SMOKING',
|
||||
'NOTCHED_RIGHT_ARROW',
|
||||
'OCTAGON',
|
||||
'PARALLELOGRAM',
|
||||
'PENTAGON',
|
||||
'PIE',
|
||||
'PLAQUE',
|
||||
'PLUS',
|
||||
'QUAD_ARROW',
|
||||
'QUAD_ARROW_CALLOUT',
|
||||
'RIBBON',
|
||||
'RIBBON_2',
|
||||
'RIGHT_ARROW',
|
||||
'RIGHT_ARROW_CALLOUT',
|
||||
'RIGHT_BRACE',
|
||||
'RIGHT_BRACKET',
|
||||
'ROUND_1_RECTANGLE',
|
||||
'ROUND_2_DIAGONAL_RECTANGLE',
|
||||
'ROUND_2_SAME_RECTANGLE',
|
||||
'RIGHT_TRIANGLE',
|
||||
'SMILEY_FACE',
|
||||
'SNIP_1_RECTANGLE',
|
||||
'SNIP_2_DIAGONAL_RECTANGLE',
|
||||
'SNIP_2_SAME_RECTANGLE',
|
||||
'SNIP_ROUND_RECTANGLE',
|
||||
'STAR_10',
|
||||
'STAR_12',
|
||||
'STAR_16',
|
||||
'STAR_24',
|
||||
'STAR_32',
|
||||
'STAR_4',
|
||||
'STAR_5',
|
||||
'STAR_6',
|
||||
'STAR_7',
|
||||
'STAR_8',
|
||||
'STRIPED_RIGHT_ARROW',
|
||||
'SUN',
|
||||
'TRAPEZOID',
|
||||
'TRIANGLE',
|
||||
'UP_ARROW',
|
||||
'UP_ARROW_CALLOUT',
|
||||
'UP_DOWN_ARROW',
|
||||
'UTURN_ARROW',
|
||||
'VERTICAL_SCROLL',
|
||||
'WAVE',
|
||||
'WEDGE_ELLIPSE_CALLOUT',
|
||||
'WEDGE_RECTANGLE_CALLOUT',
|
||||
'WEDGE_ROUND_RECTANGLE_CALLOUT',
|
||||
'FLOW_CHART_ALTERNATE_PROCESS',
|
||||
'FLOW_CHART_COLLATE',
|
||||
'FLOW_CHART_CONNECTOR',
|
||||
'FLOW_CHART_DECISION',
|
||||
'FLOW_CHART_DELAY',
|
||||
'FLOW_CHART_DISPLAY',
|
||||
'FLOW_CHART_DOCUMENT',
|
||||
'FLOW_CHART_EXTRACT',
|
||||
'FLOW_CHART_INPUT_OUTPUT',
|
||||
'FLOW_CHART_INTERNAL_STORAGE',
|
||||
'FLOW_CHART_MAGNETIC_DISK',
|
||||
'FLOW_CHART_MAGNETIC_DRUM',
|
||||
'FLOW_CHART_MAGNETIC_TAPE',
|
||||
'FLOW_CHART_MANUAL_INPUT',
|
||||
'FLOW_CHART_MANUAL_OPERATION',
|
||||
'FLOW_CHART_MERGE',
|
||||
'FLOW_CHART_MULTIDOCUMENT',
|
||||
'FLOW_CHART_OFFLINE_STORAGE',
|
||||
'FLOW_CHART_OFFPAGE_CONNECTOR',
|
||||
'FLOW_CHART_ONLINE_STORAGE',
|
||||
'FLOW_CHART_OR',
|
||||
'FLOW_CHART_PREDEFINED_PROCESS',
|
||||
'FLOW_CHART_PREPARATION',
|
||||
'FLOW_CHART_PROCESS',
|
||||
'FLOW_CHART_PUNCHED_CARD',
|
||||
'FLOW_CHART_PUNCHED_TAPE',
|
||||
'FLOW_CHART_SORT',
|
||||
'FLOW_CHART_SUMMING_JUNCTION',
|
||||
'FLOW_CHART_TERMINATOR',
|
||||
'ARROW_EAST',
|
||||
'ARROW_NORTH_EAST',
|
||||
'ARROW_NORTH',
|
||||
'SPEECH',
|
||||
'STARBURST',
|
||||
'TEARDROP',
|
||||
'ELLIPSE_RIBBON',
|
||||
'ELLIPSE_RIBBON_2',
|
||||
'CLOUD_CALLOUT',
|
||||
'CUSTOM',
|
||||
] as const
|
||||
|
||||
export const createShapeTool: ToolConfig<CreateShapeParams, CreateShapeResponse> = {
|
||||
id: 'google_slides_create_shape',
|
||||
name: 'Create Shape in Google Slides',
|
||||
description:
|
||||
'Create a shape (rectangle, ellipse, text box, arrow, etc.) on a slide in a Google Slides 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',
|
||||
},
|
||||
pageObjectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The object ID of the slide/page to add the shape to',
|
||||
},
|
||||
shapeType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The type of shape to create. Common types: TEXT_BOX, RECTANGLE, ROUND_RECTANGLE, ELLIPSE, TRIANGLE, DIAMOND, STAR_5, ARROW_EAST, HEART, CLOUD',
|
||||
},
|
||||
width: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Width of the shape in points (default: 200)',
|
||||
},
|
||||
height: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Height of the shape in points (default: 100)',
|
||||
},
|
||||
positionX: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'X position from the left edge in points (default: 100)',
|
||||
},
|
||||
positionY: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Y position from the top edge in points (default: 100)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const presentationId = params.presentationId?.trim()
|
||||
if (!presentationId) {
|
||||
throw new Error('Presentation ID is required')
|
||||
}
|
||||
return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`
|
||||
},
|
||||
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 pageObjectId = params.pageObjectId?.trim()
|
||||
if (!pageObjectId) {
|
||||
throw new Error('Page Object ID is required')
|
||||
}
|
||||
|
||||
let shapeType = (params.shapeType || 'RECTANGLE').toUpperCase()
|
||||
if (!SHAPE_TYPES.includes(shapeType as (typeof SHAPE_TYPES)[number])) {
|
||||
logger.warn(`Invalid shape type "${params.shapeType}", defaulting to RECTANGLE`)
|
||||
shapeType = 'RECTANGLE'
|
||||
}
|
||||
|
||||
// Generate a unique object ID for the new shape
|
||||
const shapeObjectId = `shape_${Date.now()}_${generateRandomString(7)}`
|
||||
|
||||
// Convert points to EMU
|
||||
const widthEmu = (params.width || 200) * PT_TO_EMU
|
||||
const heightEmu = (params.height || 100) * PT_TO_EMU
|
||||
const translateX = (params.positionX || 100) * PT_TO_EMU
|
||||
const translateY = (params.positionY || 100) * PT_TO_EMU
|
||||
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
createShape: {
|
||||
objectId: shapeObjectId,
|
||||
shapeType,
|
||||
elementProperties: {
|
||||
pageObjectId,
|
||||
size: {
|
||||
width: {
|
||||
magnitude: widthEmu,
|
||||
unit: 'EMU',
|
||||
},
|
||||
height: {
|
||||
magnitude: heightEmu,
|
||||
unit: 'EMU',
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX,
|
||||
translateY,
|
||||
unit: 'EMU',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
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 shape')
|
||||
}
|
||||
|
||||
const createShapeReply = data.replies?.[0]?.createShape
|
||||
const shapeId = createShapeReply?.objectId || ''
|
||||
|
||||
const presentationId = params?.presentationId?.trim() || ''
|
||||
const pageObjectId = params?.pageObjectId?.trim() || ''
|
||||
const shapeType = (params?.shapeType || 'RECTANGLE').toUpperCase()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
shapeId,
|
||||
shapeType,
|
||||
metadata: {
|
||||
presentationId,
|
||||
pageObjectId,
|
||||
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
shapeId: {
|
||||
type: 'string',
|
||||
description: 'The object ID of the newly created shape',
|
||||
},
|
||||
shapeType: {
|
||||
type: 'string',
|
||||
description: 'The type of shape that was created',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Operation metadata including presentation ID and page object ID',
|
||||
properties: {
|
||||
presentationId: { type: 'string', description: 'The presentation ID' },
|
||||
pageObjectId: {
|
||||
type: 'string',
|
||||
description: 'The page object ID where the shape was created',
|
||||
},
|
||||
url: { type: 'string', description: 'URL to the presentation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<CreateSheetsChartParams, CreateSheetsChartResponse> =
|
||||
{
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleSlidesCreateTableTool')
|
||||
|
||||
interface CreateTableParams {
|
||||
accessToken: string
|
||||
presentationId: string
|
||||
pageObjectId: string
|
||||
rows: number
|
||||
columns: number
|
||||
width?: number
|
||||
height?: number
|
||||
positionX?: number
|
||||
positionY?: number
|
||||
}
|
||||
|
||||
interface CreateTableResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
tableId: string
|
||||
rows: number
|
||||
columns: number
|
||||
metadata: {
|
||||
presentationId: string
|
||||
pageObjectId: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EMU (English Metric Units) conversion: 1 pt = 12700 EMU
|
||||
const PT_TO_EMU = 12700
|
||||
|
||||
export const createTableTool: ToolConfig<CreateTableParams, CreateTableResponse> = {
|
||||
id: 'google_slides_create_table',
|
||||
name: 'Create Table in Google Slides',
|
||||
description: 'Create a new table on a slide in a Google Slides 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',
|
||||
},
|
||||
pageObjectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The object ID of the slide/page to add the table to',
|
||||
},
|
||||
rows: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of rows in the table (minimum 1)',
|
||||
},
|
||||
columns: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of columns in the table (minimum 1)',
|
||||
},
|
||||
width: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Width of the table in points (default: 400)',
|
||||
},
|
||||
height: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Height of the table in points (default: 200)',
|
||||
},
|
||||
positionX: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'X position from the left edge in points (default: 100)',
|
||||
},
|
||||
positionY: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Y position from the top edge in points (default: 100)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const presentationId = params.presentationId?.trim()
|
||||
if (!presentationId) {
|
||||
throw new Error('Presentation ID is required')
|
||||
}
|
||||
return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`
|
||||
},
|
||||
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 pageObjectId = params.pageObjectId?.trim()
|
||||
if (!pageObjectId) {
|
||||
throw new Error('Page Object ID is required')
|
||||
}
|
||||
|
||||
const rows = params.rows
|
||||
const columns = params.columns
|
||||
|
||||
if (!rows || rows < 1) {
|
||||
throw new Error('Rows must be at least 1')
|
||||
}
|
||||
if (!columns || columns < 1) {
|
||||
throw new Error('Columns must be at least 1')
|
||||
}
|
||||
|
||||
// Generate a unique object ID for the new table
|
||||
const tableObjectId = `table_${Date.now()}_${generateRandomString(7)}`
|
||||
|
||||
// Convert points to EMU
|
||||
const widthEmu = (params.width || 400) * PT_TO_EMU
|
||||
const heightEmu = (params.height || 200) * PT_TO_EMU
|
||||
const translateX = (params.positionX || 100) * PT_TO_EMU
|
||||
const translateY = (params.positionY || 100) * PT_TO_EMU
|
||||
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
createTable: {
|
||||
objectId: tableObjectId,
|
||||
rows,
|
||||
columns,
|
||||
elementProperties: {
|
||||
pageObjectId,
|
||||
size: {
|
||||
width: {
|
||||
magnitude: widthEmu,
|
||||
unit: 'EMU',
|
||||
},
|
||||
height: {
|
||||
magnitude: heightEmu,
|
||||
unit: 'EMU',
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX,
|
||||
translateY,
|
||||
unit: 'EMU',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
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 table')
|
||||
}
|
||||
|
||||
const createTableReply = data.replies?.[0]?.createTable
|
||||
const tableId = createTableReply?.objectId || ''
|
||||
|
||||
const presentationId = params?.presentationId?.trim() || ''
|
||||
const pageObjectId = params?.pageObjectId?.trim() || ''
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tableId,
|
||||
rows: params?.rows ?? 0,
|
||||
columns: params?.columns ?? 0,
|
||||
metadata: {
|
||||
presentationId,
|
||||
pageObjectId,
|
||||
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tableId: {
|
||||
type: 'string',
|
||||
description: 'The object ID of the newly created table',
|
||||
},
|
||||
rows: {
|
||||
type: 'number',
|
||||
description: 'Number of rows in the table',
|
||||
},
|
||||
columns: {
|
||||
type: 'number',
|
||||
description: 'Number of columns in the table',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Operation metadata including presentation ID and page object ID',
|
||||
properties: {
|
||||
presentationId: { type: 'string', description: 'The presentation ID' },
|
||||
pageObjectId: {
|
||||
type: 'string',
|
||||
description: 'The page object ID where the table was created',
|
||||
},
|
||||
url: { type: 'string', description: 'URL to the presentation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<CreateVideoParams, CreateVideoResponse> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleSlidesDeleteObjectTool')
|
||||
|
||||
interface DeleteObjectParams {
|
||||
accessToken: string
|
||||
presentationId: string
|
||||
objectId: string
|
||||
}
|
||||
|
||||
interface DeleteObjectResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
deleted: boolean
|
||||
objectId: string
|
||||
metadata: {
|
||||
presentationId: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteObjectTool: ToolConfig<DeleteObjectParams, DeleteObjectResponse> = {
|
||||
id: 'google_slides_delete_object',
|
||||
name: 'Delete Object from Google Slides',
|
||||
description:
|
||||
'Delete a page element (shape, image, table, etc.) or an entire slide from a Google Slides 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: 'The object ID of the element or slide to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const presentationId = params.presentationId?.trim()
|
||||
if (!presentationId) {
|
||||
throw new Error('Presentation ID is required')
|
||||
}
|
||||
return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`
|
||||
},
|
||||
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 objectId = params.objectId?.trim()
|
||||
if (!objectId) {
|
||||
throw new Error('Object ID is required')
|
||||
}
|
||||
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
deleteObject: {
|
||||
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 delete object')
|
||||
}
|
||||
|
||||
const presentationId = params?.presentationId?.trim() || ''
|
||||
const objectId = params?.objectId?.trim() || ''
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
objectId,
|
||||
metadata: {
|
||||
presentationId,
|
||||
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the object was successfully deleted',
|
||||
},
|
||||
objectId: {
|
||||
type: 'string',
|
||||
description: 'The object ID that was deleted',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Operation metadata including presentation ID and URL',
|
||||
properties: {
|
||||
presentationId: { type: 'string', description: 'The presentation ID' },
|
||||
url: { type: 'string', description: 'URL to the presentation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<DeleteTableColumnParams, DeleteTableColumnResponse> =
|
||||
{
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<DeleteTableRowParams, DeleteTableRowResponse> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<DeleteTextParams, DeleteTextResponse> = {
|
||||
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<string, unknown> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleSlidesDuplicateObjectTool')
|
||||
|
||||
interface DuplicateObjectParams {
|
||||
accessToken: string
|
||||
presentationId: string
|
||||
objectId: string
|
||||
objectIds?: string
|
||||
}
|
||||
|
||||
interface DuplicateObjectResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
duplicatedObjectId: string
|
||||
metadata: {
|
||||
presentationId: string
|
||||
sourceObjectId: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const duplicateObjectTool: ToolConfig<DuplicateObjectParams, DuplicateObjectResponse> = {
|
||||
id: 'google_slides_duplicate_object',
|
||||
name: 'Duplicate Object in Google Slides',
|
||||
description:
|
||||
'Duplicate an object (slide, shape, image, table, etc.) in a Google Slides 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: 'The object ID of the element or slide to duplicate',
|
||||
},
|
||||
objectIds: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Optional JSON object mapping source object IDs (within the slide being duplicated) to new object IDs for the duplicates. Format: {"sourceId1":"newId1","sourceId2":"newId2"}',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const presentationId = params.presentationId?.trim()
|
||||
if (!presentationId) {
|
||||
throw new Error('Presentation ID is required')
|
||||
}
|
||||
return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`
|
||||
},
|
||||
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 objectId = params.objectId?.trim()
|
||||
if (!objectId) {
|
||||
throw new Error('Object ID is required')
|
||||
}
|
||||
|
||||
const duplicateRequest: Record<string, any> = {
|
||||
objectId,
|
||||
}
|
||||
|
||||
// Parse objectIds mapping if provided
|
||||
if (params.objectIds?.trim()) {
|
||||
try {
|
||||
const mapping = JSON.parse(params.objectIds)
|
||||
if (typeof mapping === 'object' && !Array.isArray(mapping)) {
|
||||
duplicateRequest.objectIds = mapping
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('Invalid objectIds JSON, ignoring:', e)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
duplicateObject: duplicateRequest,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
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 duplicate object')
|
||||
}
|
||||
|
||||
const duplicateReply = data.replies?.[0]?.duplicateObject
|
||||
const duplicatedObjectId = duplicateReply?.objectId ?? ''
|
||||
|
||||
const presentationId = params?.presentationId?.trim() || ''
|
||||
const objectId = params?.objectId?.trim() || ''
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
duplicatedObjectId,
|
||||
metadata: {
|
||||
presentationId,
|
||||
sourceObjectId: objectId,
|
||||
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
duplicatedObjectId: {
|
||||
type: 'string',
|
||||
description: 'The object ID of the newly created duplicate',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Operation metadata including presentation ID and source object ID',
|
||||
properties: {
|
||||
presentationId: { type: 'string', description: 'The presentation ID' },
|
||||
sourceObjectId: {
|
||||
type: 'string',
|
||||
description: 'The original object ID that was duplicated',
|
||||
},
|
||||
url: { type: 'string', description: 'URL to the presentation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import {
|
||||
createMockRequest,
|
||||
hybridAuthMockFns,
|
||||
inputValidationMock,
|
||||
inputValidationMockFns,
|
||||
} from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { mockUploadCopilotFile, mockUploadExecutionFile } = vi.hoisted(() => ({
|
||||
mockUploadCopilotFile: vi.fn(),
|
||||
mockUploadExecutionFile: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)
|
||||
vi.mock('@/lib/uploads/contexts/copilot', () => ({
|
||||
uploadCopilotFile: mockUploadCopilotFile,
|
||||
}))
|
||||
vi.mock('@/lib/uploads/contexts/execution', () => ({
|
||||
uploadExecutionFile: mockUploadExecutionFile,
|
||||
}))
|
||||
|
||||
import { POST } from '@/app/api/tools/google_slides/export-presentation/route'
|
||||
import type { ExportPresentationParams } from '@/tools/google_slides/export_presentation'
|
||||
import { exportPresentationTool } from '@/tools/google_slides/export_presentation'
|
||||
|
||||
describe('Google Slides export presentation tool', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-1',
|
||||
authType: 'internal_jwt',
|
||||
})
|
||||
inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValue({
|
||||
isValid: true,
|
||||
resolvedIP: '93.184.216.34',
|
||||
originalHostname: 'www.googleapis.com',
|
||||
})
|
||||
mockUploadExecutionFile.mockResolvedValue({
|
||||
id: 'file-1',
|
||||
name: 'presentation-1.pdf',
|
||||
size: 7,
|
||||
type: 'application/pdf',
|
||||
url: '/api/files/serve/execution/file-1',
|
||||
key: 'execution/workflow/file-1',
|
||||
context: 'execution',
|
||||
})
|
||||
mockUploadCopilotFile.mockResolvedValue({
|
||||
id: 'copilot-file-1',
|
||||
name: 'presentation-1.pdf',
|
||||
size: 4,
|
||||
type: 'application/pdf',
|
||||
mimeType: 'application/pdf',
|
||||
url: '/api/files/serve/copilot/copilot-file-1',
|
||||
key: 'copilot/copilot-file-1',
|
||||
context: 'copilot',
|
||||
})
|
||||
})
|
||||
|
||||
it('routes exports through the internal API with execution context', () => {
|
||||
const params: ExportPresentationParams = {
|
||||
accessToken: 'token',
|
||||
presentationId: 'presentation-1',
|
||||
exportFormat: 'PDF',
|
||||
_context: {
|
||||
workspaceId: 'workspace-1',
|
||||
workflowId: 'workflow-1',
|
||||
executionId: 'execution-1',
|
||||
},
|
||||
}
|
||||
|
||||
expect(exportPresentationTool.request.url).toBe('/api/tools/google_slides/export-presentation')
|
||||
expect(exportPresentationTool.request.method).toBe('POST')
|
||||
expect(exportPresentationTool.request.body?.(params)).toEqual({
|
||||
accessToken: 'token',
|
||||
presentationId: 'presentation-1',
|
||||
exportFormat: 'PDF',
|
||||
workspaceId: 'workspace-1',
|
||||
workflowId: 'workflow-1',
|
||||
executionId: 'execution-1',
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects presentation IDs that would break export URL structure', async () => {
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
accessToken: 'token',
|
||||
presentationId: 'abc?mimeType=evil',
|
||||
exportFormat: 'PDF',
|
||||
})
|
||||
)
|
||||
const result = (await response.json()) as { success: false; error: string }
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(result.error).toContain('invalid characters')
|
||||
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('stores exports as execution file references and keeps small legacy base64 output', async () => {
|
||||
inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce(
|
||||
new Response('content', {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/pdf' },
|
||||
})
|
||||
)
|
||||
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
accessToken: 'token',
|
||||
presentationId: 'presentation-1',
|
||||
exportFormat: 'PDF',
|
||||
workspaceId: 'workspace-1',
|
||||
workflowId: 'workflow-1',
|
||||
executionId: 'execution-1',
|
||||
})
|
||||
)
|
||||
const result = (await response.json()) as {
|
||||
success: true
|
||||
output: {
|
||||
file: { key: string; context: string; mimeType?: string }
|
||||
contentBase64?: string
|
||||
}
|
||||
}
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).toHaveBeenCalledWith(
|
||||
'https://www.googleapis.com/drive/v3/files/presentation-1/export?mimeType=application%2Fpdf',
|
||||
'93.184.216.34',
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
maxResponseBytes: 10 * 1024 * 1024,
|
||||
})
|
||||
)
|
||||
expect(mockUploadExecutionFile).toHaveBeenCalledWith(
|
||||
{ workspaceId: 'workspace-1', workflowId: 'workflow-1', executionId: 'execution-1' },
|
||||
Buffer.from('content'),
|
||||
'presentation-1.pdf',
|
||||
'application/pdf',
|
||||
'user-1'
|
||||
)
|
||||
expect(result?.output.file).toMatchObject({
|
||||
key: 'execution/workflow/file-1',
|
||||
context: 'execution',
|
||||
mimeType: 'application/pdf',
|
||||
})
|
||||
expect(result.output.contentBase64).toBe(Buffer.from('content').toString('base64'))
|
||||
})
|
||||
|
||||
it('stores exports in copilot storage when execution context is unavailable', async () => {
|
||||
const bytes = Uint8Array.from([0, 255, 1, 254])
|
||||
inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce(
|
||||
new Response(bytes, {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/pdf' },
|
||||
})
|
||||
)
|
||||
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
accessToken: 'token',
|
||||
presentationId: 'presentation-1',
|
||||
exportFormat: 'PDF',
|
||||
})
|
||||
)
|
||||
const result = (await response.json()) as {
|
||||
success: true
|
||||
output: {
|
||||
file: { key: string; context: string; url: string }
|
||||
contentBase64?: string
|
||||
sizeBytes: number
|
||||
}
|
||||
}
|
||||
|
||||
expect(mockUploadExecutionFile).not.toHaveBeenCalled()
|
||||
expect(mockUploadCopilotFile).toHaveBeenCalledWith({
|
||||
buffer: Buffer.from(bytes),
|
||||
fileName: 'presentation-1.pdf',
|
||||
contentType: 'application/pdf',
|
||||
userId: 'user-1',
|
||||
})
|
||||
expect(result.output.file).toMatchObject({
|
||||
key: 'copilot/copilot-file-1',
|
||||
context: 'copilot',
|
||||
url: '/api/files/serve/copilot/copilot-file-1',
|
||||
})
|
||||
expect(result.output.contentBase64).toBe(Buffer.from(bytes).toString('base64'))
|
||||
expect(result.output.sizeBytes).toBe(bytes.byteLength)
|
||||
})
|
||||
|
||||
it('maps internal API responses into tool output', async () => {
|
||||
const response = new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
output: {
|
||||
file: {
|
||||
key: 'copilot/copilot-file-1',
|
||||
context: 'copilot',
|
||||
url: '/api/files/serve/copilot/copilot-file-1',
|
||||
},
|
||||
mimeType: 'application/pdf',
|
||||
sizeBytes: 3,
|
||||
metadata: {
|
||||
presentationId: 'presentation-1',
|
||||
url: 'https://docs.google.com/presentation/d/presentation-1/edit',
|
||||
exportFormat: 'PDF',
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}
|
||||
)
|
||||
|
||||
const result = await exportPresentationTool.transformResponse?.(response)
|
||||
|
||||
expect(result?.output.file?.key).toBe('copilot/copilot-file-1')
|
||||
expect(result?.output.metadata.presentationId).toBe('presentation-1')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ExportPresentationParams {
|
||||
accessToken: string
|
||||
presentationId: string
|
||||
exportFormat?: 'PDF' | 'PPTX' | 'ODP' | 'TXT' | 'PNG' | 'JPEG' | 'SVG'
|
||||
_context?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface ExportPresentationResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
contentBase64?: string
|
||||
file?: UserFile & { mimeType?: string }
|
||||
mimeType: string
|
||||
sizeBytes: number
|
||||
metadata: { presentationId: string; url: string; exportFormat: string }
|
||||
}
|
||||
}
|
||||
|
||||
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. Stores the exported file as an execution file when execution context is available.',
|
||||
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: '/api/tools/google_slides/export-presentation',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => ({
|
||||
accessToken: params.accessToken,
|
||||
presentationId: params.presentationId,
|
||||
exportFormat: params.exportFormat,
|
||||
workspaceId:
|
||||
typeof params._context?.workspaceId === 'string' ? params._context.workspaceId : undefined,
|
||||
workflowId:
|
||||
typeof params._context?.workflowId === 'string' ? params._context.workflowId : undefined,
|
||||
executionId:
|
||||
typeof params._context?.executionId === 'string' ? params._context.executionId : undefined,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok || data.success === false) {
|
||||
throw new Error(data.error || 'Failed to export presentation')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data.output,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'file',
|
||||
description: 'Stored exported presentation file',
|
||||
optional: true,
|
||||
},
|
||||
contentBase64: {
|
||||
type: 'string',
|
||||
description: 'Deprecated legacy inline content. New exports return file.',
|
||||
optional: true,
|
||||
},
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleSlidesGetPageTool')
|
||||
|
||||
interface GetPageParams {
|
||||
accessToken: string
|
||||
presentationId: string
|
||||
pageObjectId: string
|
||||
}
|
||||
|
||||
interface GetPageResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
objectId: string
|
||||
pageType: string
|
||||
pageElements: any[]
|
||||
slideProperties: {
|
||||
layoutObjectId: string | null
|
||||
masterObjectId: string | null
|
||||
notesPage: any | null
|
||||
} | null
|
||||
metadata: {
|
||||
presentationId: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getPageTool: ToolConfig<GetPageParams, GetPageResponse> = {
|
||||
id: 'google_slides_get_page',
|
||||
name: 'Get Slide Page',
|
||||
description:
|
||||
'Get detailed information about a specific slide/page in a Google Slides 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',
|
||||
},
|
||||
pageObjectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The object ID of the slide/page to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const presentationId = params.presentationId?.trim()
|
||||
const pageObjectId = params.pageObjectId?.trim()
|
||||
|
||||
if (!presentationId) {
|
||||
throw new Error('Presentation ID is required')
|
||||
}
|
||||
if (!pageObjectId) {
|
||||
throw new Error('Page Object ID is required')
|
||||
}
|
||||
|
||||
return `https://slides.googleapis.com/v1/presentations/${presentationId}/pages/${pageObjectId}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
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 get page')
|
||||
}
|
||||
|
||||
const presentationId = params?.presentationId?.trim() || ''
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
objectId: data.objectId,
|
||||
pageType: data.pageType ?? 'SLIDE',
|
||||
pageElements: data.pageElements ?? [],
|
||||
slideProperties: data.slideProperties
|
||||
? {
|
||||
layoutObjectId: data.slideProperties.layoutObjectId ?? null,
|
||||
masterObjectId: data.slideProperties.masterObjectId ?? null,
|
||||
notesPage: data.slideProperties.notesPage ?? null,
|
||||
}
|
||||
: null,
|
||||
metadata: {
|
||||
presentationId,
|
||||
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
objectId: {
|
||||
type: 'string',
|
||||
description: 'The object ID of the page',
|
||||
},
|
||||
pageType: {
|
||||
type: 'string',
|
||||
description: 'The type of page (SLIDE, MASTER, LAYOUT, NOTES, NOTES_MASTER)',
|
||||
},
|
||||
pageElements: {
|
||||
type: 'array',
|
||||
description: 'Array of page elements (shapes, images, tables, etc.) on this page',
|
||||
items: {
|
||||
type: 'json',
|
||||
},
|
||||
},
|
||||
slideProperties: {
|
||||
type: 'object',
|
||||
description: 'Properties specific to slides (layout, master, notes)',
|
||||
optional: true,
|
||||
properties: {
|
||||
layoutObjectId: {
|
||||
type: 'string',
|
||||
description: 'Object ID of the layout this slide is based on',
|
||||
},
|
||||
masterObjectId: {
|
||||
type: 'string',
|
||||
description: 'Object ID of the master this slide is based on',
|
||||
},
|
||||
notesPage: {
|
||||
type: 'json',
|
||||
description: 'The notes page associated with the slide',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Operation metadata including presentation ID and URL',
|
||||
properties: {
|
||||
presentationId: { type: 'string', description: 'The presentation ID' },
|
||||
url: { type: 'string', description: 'URL to the presentation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleSlidesGetThumbnailTool')
|
||||
|
||||
interface GetThumbnailParams {
|
||||
accessToken: string
|
||||
presentationId: string
|
||||
pageObjectId: string
|
||||
thumbnailSize?: string
|
||||
mimeType?: string
|
||||
}
|
||||
|
||||
interface GetThumbnailResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
contentUrl: string
|
||||
width: number
|
||||
height: number
|
||||
metadata: {
|
||||
presentationId: string
|
||||
pageObjectId: string
|
||||
thumbnailSize: string
|
||||
mimeType: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Available thumbnail sizes
|
||||
const THUMBNAIL_SIZES = ['SMALL', 'MEDIUM', 'LARGE'] as const
|
||||
|
||||
// Available MIME types for thumbnails
|
||||
const MIME_TYPES = ['PNG'] as const
|
||||
|
||||
export const getThumbnailTool: ToolConfig<GetThumbnailParams, GetThumbnailResponse> = {
|
||||
id: 'google_slides_get_thumbnail',
|
||||
name: 'Get Slide Thumbnail',
|
||||
description: 'Generate a thumbnail image of a specific slide in a Google Slides presentation',
|
||||
version: '1.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: 'The object ID of the slide/page to get a thumbnail for',
|
||||
},
|
||||
thumbnailSize: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The size of the thumbnail: SMALL (200px), MEDIUM (800px), or LARGE (1600px). Defaults to MEDIUM.',
|
||||
},
|
||||
mimeType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The MIME type of the thumbnail image: PNG. Defaults to PNG.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const presentationId = params.presentationId?.trim()
|
||||
const pageObjectId = params.pageObjectId?.trim()
|
||||
|
||||
if (!presentationId) {
|
||||
throw new Error('Presentation ID is required')
|
||||
}
|
||||
if (!pageObjectId) {
|
||||
throw new Error('Page Object ID is required')
|
||||
}
|
||||
|
||||
// Build the URL with query parameters for thumbnail properties
|
||||
let size = (params.thumbnailSize || 'MEDIUM').toUpperCase()
|
||||
if (!THUMBNAIL_SIZES.includes(size as (typeof THUMBNAIL_SIZES)[number])) {
|
||||
size = 'MEDIUM'
|
||||
}
|
||||
|
||||
// Validate and normalize mimeType
|
||||
let mimeType = (params.mimeType || 'PNG').toUpperCase()
|
||||
if (!MIME_TYPES.includes(mimeType as (typeof MIME_TYPES)[number])) {
|
||||
mimeType = 'PNG'
|
||||
}
|
||||
|
||||
// The API uses thumbnailProperties as query parameters
|
||||
let url = `https://slides.googleapis.com/v1/presentations/${presentationId}/pages/${pageObjectId}/thumbnail?thumbnailProperties.thumbnailSize=${size}`
|
||||
|
||||
// Add mimeType if not the default (PNG)
|
||||
if (mimeType !== 'PNG') {
|
||||
url += `&thumbnailProperties.mimeType=${mimeType}`
|
||||
}
|
||||
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
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 get thumbnail')
|
||||
}
|
||||
|
||||
const presentationId = params?.presentationId?.trim() || ''
|
||||
const pageObjectId = params?.pageObjectId?.trim() || ''
|
||||
const thumbnailSize = (params?.thumbnailSize || 'MEDIUM').toUpperCase()
|
||||
const mimeType = (params?.mimeType || 'PNG').toUpperCase()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
contentUrl: data.contentUrl,
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
metadata: {
|
||||
presentationId,
|
||||
pageObjectId,
|
||||
thumbnailSize,
|
||||
mimeType,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
contentUrl: {
|
||||
type: 'string',
|
||||
description: 'URL to the thumbnail image (valid for 30 minutes)',
|
||||
},
|
||||
width: {
|
||||
type: 'number',
|
||||
description: 'Width of the thumbnail in pixels',
|
||||
},
|
||||
height: {
|
||||
type: 'number',
|
||||
description: 'Height of the thumbnail in pixels',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata including presentation ID and page object ID',
|
||||
properties: {
|
||||
presentationId: {
|
||||
type: 'string',
|
||||
description: 'The presentation ID',
|
||||
},
|
||||
pageObjectId: {
|
||||
type: 'string',
|
||||
description: 'The page object ID for the thumbnail',
|
||||
},
|
||||
thumbnailSize: {
|
||||
type: 'string',
|
||||
description: 'The requested thumbnail size',
|
||||
},
|
||||
mimeType: {
|
||||
type: 'string',
|
||||
description: 'The thumbnail MIME type',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GroupObjectsParams, GroupObjectsResponse> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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
|
||||
export const googleSlidesWriteTool = writeTool
|
||||
export const googleSlidesCreateTool = createTool
|
||||
export const googleSlidesReplaceAllTextTool = replaceAllTextTool
|
||||
export const googleSlidesAddSlideTool = addSlideTool
|
||||
export const googleSlidesGetThumbnailTool = getThumbnailTool
|
||||
export const googleSlidesAddImageTool = addImageTool
|
||||
export const googleSlidesGetPageTool = getPageTool
|
||||
export const googleSlidesDeleteObjectTool = deleteObjectTool
|
||||
export const googleSlidesDuplicateObjectTool = duplicateObjectTool
|
||||
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
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<InsertTableRowsParams, InsertTableRowsResponse> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleSlidesInsertTextTool')
|
||||
|
||||
interface InsertTextParams {
|
||||
accessToken: string
|
||||
presentationId: string
|
||||
objectId: string
|
||||
text: string
|
||||
insertionIndex?: number
|
||||
}
|
||||
|
||||
interface InsertTextResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
inserted: boolean
|
||||
objectId: string
|
||||
text: string
|
||||
metadata: {
|
||||
presentationId: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const insertTextTool: ToolConfig<InsertTextParams, InsertTextResponse> = {
|
||||
id: 'google_slides_insert_text',
|
||||
name: 'Insert Text in Google Slides',
|
||||
description:
|
||||
'Insert text into a shape or table cell in a Google Slides presentation. Use this to add text to text boxes, shapes, or 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:
|
||||
'The object ID of the shape or table cell to insert text into. For table cells, use the cell object ID.',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The text to insert',
|
||||
},
|
||||
insertionIndex: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The zero-based index at which to insert the text. If not specified, text is inserted at the beginning (index 0).',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const presentationId = params.presentationId?.trim()
|
||||
if (!presentationId) {
|
||||
throw new Error('Presentation ID is required')
|
||||
}
|
||||
return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`
|
||||
},
|
||||
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 objectId = params.objectId?.trim()
|
||||
if (!objectId) {
|
||||
throw new Error('Object ID is required')
|
||||
}
|
||||
|
||||
if (params.text === undefined || params.text === null) {
|
||||
throw new Error('Text is required')
|
||||
}
|
||||
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
insertText: {
|
||||
objectId,
|
||||
text: params.text,
|
||||
insertionIndex: params.insertionIndex ?? 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
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 text')
|
||||
}
|
||||
|
||||
const presentationId = params?.presentationId?.trim() || ''
|
||||
const objectId = params?.objectId?.trim() || ''
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
inserted: true,
|
||||
objectId,
|
||||
text: params?.text ?? '',
|
||||
metadata: {
|
||||
presentationId,
|
||||
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
inserted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the text was successfully inserted',
|
||||
},
|
||||
objectId: {
|
||||
type: 'string',
|
||||
description: 'The object ID where text was inserted',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'The text that was inserted',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Operation metadata including presentation ID and URL',
|
||||
properties: {
|
||||
presentationId: { type: 'string', description: 'The presentation ID' },
|
||||
url: { type: 'string', description: 'URL to the presentation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<MergeTableCellsParams, MergeTableCellsResponse> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { GoogleSlidesReadResponse, GoogleSlidesToolParams } from '@/tools/google_slides/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const readTool: ToolConfig<GoogleSlidesToolParams, GoogleSlidesReadResponse> = {
|
||||
id: 'google_slides_read',
|
||||
name: 'Read Google Slides Presentation',
|
||||
description: 'Read content from a Google Slides presentation',
|
||||
version: '1.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',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Ensure presentationId is valid
|
||||
const presentationId = params.presentationId?.trim() || params.manualPresentationId?.trim()
|
||||
if (!presentationId) {
|
||||
throw new Error('Presentation ID is required')
|
||||
}
|
||||
|
||||
return `https://slides.googleapis.com/v1/presentations/${presentationId}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => {
|
||||
// Validate access token
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
// Extract slides from the response
|
||||
const slides = data.slides || []
|
||||
|
||||
// Create presentation metadata
|
||||
const metadata = {
|
||||
presentationId: data.presentationId,
|
||||
title: data.title || 'Untitled Presentation',
|
||||
pageSize: data.pageSize,
|
||||
mimeType: 'application/vnd.google-apps.presentation',
|
||||
url: `https://docs.google.com/presentation/d/${data.presentationId}/edit`,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
slides,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
slides: { type: 'json', description: 'Array of slides with their content' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Presentation metadata including ID, title, and URL',
|
||||
properties: {
|
||||
presentationId: {
|
||||
type: 'string',
|
||||
description: 'The presentation ID',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'The presentation title',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'object',
|
||||
description: 'Presentation page size',
|
||||
optional: true,
|
||||
properties: {
|
||||
width: {
|
||||
type: 'json',
|
||||
description: 'Page width as a Dimension object',
|
||||
},
|
||||
height: {
|
||||
type: 'json',
|
||||
description: 'Page height as a Dimension object',
|
||||
},
|
||||
},
|
||||
},
|
||||
mimeType: {
|
||||
type: 'string',
|
||||
description: 'The mime type of the presentation',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL to open the presentation',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleSlidesReplaceAllTextTool')
|
||||
|
||||
interface ReplaceAllTextParams {
|
||||
accessToken: string
|
||||
presentationId: string
|
||||
findText: string
|
||||
replaceText: string
|
||||
matchCase?: boolean
|
||||
pageObjectIds?: string
|
||||
}
|
||||
|
||||
interface ReplaceAllTextResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
occurrencesChanged: number
|
||||
metadata: {
|
||||
presentationId: string
|
||||
findText: string
|
||||
replaceText: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const replaceAllTextTool: ToolConfig<ReplaceAllTextParams, ReplaceAllTextResponse> = {
|
||||
id: 'google_slides_replace_all_text',
|
||||
name: 'Replace All Text in Google Slides',
|
||||
description: 'Find and replace all occurrences of text throughout a Google Slides presentation',
|
||||
version: '1.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',
|
||||
},
|
||||
findText: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The text to find (e.g., {{placeholder}})',
|
||||
},
|
||||
replaceText: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The text to replace with',
|
||||
},
|
||||
matchCase: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the search should be case-sensitive (default: true)',
|
||||
},
|
||||
pageObjectIds: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated list of slide object IDs to limit replacements to specific slides (leave empty for all slides)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const presentationId = params.presentationId?.trim()
|
||||
if (!presentationId) {
|
||||
throw new Error('Presentation ID is required')
|
||||
}
|
||||
return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`
|
||||
},
|
||||
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) => {
|
||||
if (!params.findText) {
|
||||
throw new Error('Find text is required')
|
||||
}
|
||||
if (params.replaceText === undefined || params.replaceText === null) {
|
||||
throw new Error('Replace text is required')
|
||||
}
|
||||
|
||||
const replaceAllTextRequest: Record<string, any> = {
|
||||
containsText: {
|
||||
text: params.findText,
|
||||
matchCase: params.matchCase !== false, // Default to true
|
||||
},
|
||||
replaceText: params.replaceText,
|
||||
}
|
||||
|
||||
// Add pageObjectIds if specified to limit replacements to specific slides
|
||||
if (params.pageObjectIds?.trim()) {
|
||||
replaceAllTextRequest.pageObjectIds = params.pageObjectIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id.length > 0)
|
||||
}
|
||||
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
replaceAllText: replaceAllTextRequest,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
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 text')
|
||||
}
|
||||
|
||||
// The response contains replies array with replaceAllText results
|
||||
const replaceResult = data.replies?.[0]?.replaceAllText
|
||||
const occurrencesChanged = replaceResult?.occurrencesChanged || 0
|
||||
|
||||
const presentationId = params?.presentationId?.trim() || ''
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
occurrencesChanged,
|
||||
metadata: {
|
||||
presentationId,
|
||||
findText: params?.findText || '',
|
||||
replaceText: params?.replaceText || '',
|
||||
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
occurrencesChanged: {
|
||||
type: 'number',
|
||||
description: 'Number of text occurrences that were replaced',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata including presentation ID and URL',
|
||||
properties: {
|
||||
presentationId: {
|
||||
type: 'string',
|
||||
description: 'The presentation ID',
|
||||
},
|
||||
findText: {
|
||||
type: 'string',
|
||||
description: 'The text that was searched for',
|
||||
},
|
||||
replaceText: {
|
||||
type: 'string',
|
||||
description: 'The text that replaced the matches',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL to open the presentation',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<ReplaceImageParams, ReplaceImageResponse> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<RerouteLineParams, RerouteLineResponse> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleSlidesMetadata {
|
||||
presentationId: string
|
||||
title: string
|
||||
pageSize?: {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
mimeType?: string
|
||||
createdTime?: string
|
||||
modifiedTime?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
export interface GoogleSlidesReadResponse extends ToolResponse {
|
||||
output: {
|
||||
slides: any[]
|
||||
metadata: GoogleSlidesMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleSlidesWriteResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedContent: boolean
|
||||
metadata: GoogleSlidesMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleSlidesCreateResponse extends ToolResponse {
|
||||
output: {
|
||||
metadata: GoogleSlidesMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleSlidesToolParams {
|
||||
accessToken: string
|
||||
presentationId?: string
|
||||
manualPresentationId?: string
|
||||
title?: string
|
||||
content?: string
|
||||
slideIndex?: number
|
||||
folderId?: string
|
||||
folderSelector?: string
|
||||
}
|
||||
|
||||
export type GoogleSlidesResponse =
|
||||
| GoogleSlidesReadResponse
|
||||
| GoogleSlidesWriteResponse
|
||||
| GoogleSlidesCreateResponse
|
||||
@@ -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<UngroupObjectsParams, UngroupObjectsResponse> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<UnmergeTableCellsParams, UnmergeTableCellsResponse> =
|
||||
{
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {}
|
||||
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<string, unknown> = {}
|
||||
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<string, unknown> = {}
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {}
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = { 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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {}
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {}
|
||||
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<string, unknown> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {}
|
||||
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<string, unknown> = { 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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {}
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleSlidesUpdateSlidesPositionTool')
|
||||
|
||||
interface UpdateSlidesPositionParams {
|
||||
accessToken: string
|
||||
presentationId: string
|
||||
slideObjectIds: string
|
||||
insertionIndex: number
|
||||
}
|
||||
|
||||
interface UpdateSlidesPositionResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
moved: boolean
|
||||
slideObjectIds: string[]
|
||||
insertionIndex: number
|
||||
metadata: {
|
||||
presentationId: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const updateSlidesPositionTool: ToolConfig<
|
||||
UpdateSlidesPositionParams,
|
||||
UpdateSlidesPositionResponse
|
||||
> = {
|
||||
id: 'google_slides_update_slides_position',
|
||||
name: 'Reorder Slides in Google Slides',
|
||||
description: 'Move one or more slides to a new position in a Google Slides 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',
|
||||
},
|
||||
slideObjectIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated list of slide object IDs to move. The slides will maintain their relative order.',
|
||||
},
|
||||
insertionIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The zero-based index where the slides should be moved. All slides with indices greater than or equal to this will be shifted right.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const presentationId = params.presentationId?.trim()
|
||||
if (!presentationId) {
|
||||
throw new Error('Presentation ID is required')
|
||||
}
|
||||
return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`
|
||||
},
|
||||
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) => {
|
||||
if (!params.slideObjectIds?.trim()) {
|
||||
throw new Error('Slide object IDs are required')
|
||||
}
|
||||
if (params.insertionIndex === undefined || params.insertionIndex < 0) {
|
||||
throw new Error('Insertion index must be a non-negative number')
|
||||
}
|
||||
|
||||
const slideObjectIds = params.slideObjectIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id.length > 0)
|
||||
|
||||
if (slideObjectIds.length === 0) {
|
||||
throw new Error('At least one slide object ID is required')
|
||||
}
|
||||
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
updateSlidesPosition: {
|
||||
slideObjectIds,
|
||||
insertionIndex: params.insertionIndex,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
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 slides position')
|
||||
}
|
||||
|
||||
const presentationId = params?.presentationId?.trim() || ''
|
||||
const slideObjectIds =
|
||||
params?.slideObjectIds
|
||||
?.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id.length > 0) ?? []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
moved: true,
|
||||
slideObjectIds,
|
||||
insertionIndex: params?.insertionIndex ?? 0,
|
||||
metadata: {
|
||||
presentationId,
|
||||
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
moved: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the slides were successfully moved',
|
||||
},
|
||||
slideObjectIds: {
|
||||
type: 'array',
|
||||
description: 'The slide object IDs that were moved',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
insertionIndex: {
|
||||
type: 'number',
|
||||
description: 'The index where the slides were moved to',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Operation metadata including presentation ID and URL',
|
||||
properties: {
|
||||
presentationId: { type: 'string', description: 'The presentation ID' },
|
||||
url: { type: 'string', description: 'URL to the presentation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {}
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {}
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {}
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {}
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<UpdateTextStyleParams, UpdateTextStyleResponse> = {
|
||||
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<string, unknown> = {}
|
||||
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<string, unknown> = {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, unknown> = {}
|
||||
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<string, unknown> = { 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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
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`
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { GoogleSlidesToolParams, GoogleSlidesWriteResponse } from '@/tools/google_slides/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleSlidesWriteTool')
|
||||
|
||||
export const writeTool: ToolConfig<GoogleSlidesToolParams, GoogleSlidesWriteResponse> = {
|
||||
id: 'google_slides_write',
|
||||
name: 'Write to Google Slides Presentation',
|
||||
description: 'Write or update content in a Google Slides presentation',
|
||||
version: '1.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',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The content to write to the slide',
|
||||
},
|
||||
slideIndex: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The index of the slide to write to (defaults to first slide)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Ensure presentationId is valid
|
||||
const presentationId = params.presentationId?.trim() || params.manualPresentationId?.trim()
|
||||
if (!presentationId) {
|
||||
throw new Error('Presentation ID is required')
|
||||
}
|
||||
|
||||
// First, we'll read the presentation to get slide information
|
||||
return `https://slides.googleapis.com/v1/presentations/${presentationId}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => {
|
||||
// Validate access token
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
postProcess: async (result, params, _executeTool) => {
|
||||
if (!result.success) {
|
||||
return result
|
||||
}
|
||||
|
||||
// Validate content
|
||||
if (!params.content) {
|
||||
throw new Error('Content is required')
|
||||
}
|
||||
|
||||
const presentationId = params.presentationId?.trim() || params.manualPresentationId?.trim()
|
||||
|
||||
if (!presentationId) {
|
||||
throw new Error('Presentation ID is required')
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the presentation data from the initial read
|
||||
const presentationData = await fetch(
|
||||
`https://slides.googleapis.com/v1/presentations/${presentationId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
},
|
||||
}
|
||||
).then((res) => res.json())
|
||||
|
||||
const metadata = {
|
||||
presentationId,
|
||||
title: presentationData.title || 'Updated Presentation',
|
||||
mimeType: 'application/vnd.google-apps.presentation',
|
||||
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
|
||||
}
|
||||
|
||||
const slideIndex =
|
||||
typeof params.slideIndex === 'string'
|
||||
? Number.parseInt(params.slideIndex, 10)
|
||||
: (params.slideIndex ?? 0)
|
||||
const slide = presentationData.slides?.[slideIndex]
|
||||
|
||||
if (Number.isNaN(slideIndex) || slideIndex < 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Slide index must be a non-negative number',
|
||||
output: {
|
||||
updatedContent: false,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (!slide) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Slide at index ${slideIndex} not found`,
|
||||
output: {
|
||||
updatedContent: false,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create requests to add content to the slide
|
||||
const textBoxId = `textbox_${Date.now()}`
|
||||
const requests = [
|
||||
{
|
||||
createShape: {
|
||||
objectId: textBoxId,
|
||||
shapeType: 'TEXT_BOX',
|
||||
elementProperties: {
|
||||
pageObjectId: slide.objectId,
|
||||
size: {
|
||||
width: {
|
||||
magnitude: 400,
|
||||
unit: 'PT',
|
||||
},
|
||||
height: {
|
||||
magnitude: 100,
|
||||
unit: 'PT',
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX: 50,
|
||||
translateY: 100,
|
||||
unit: 'PT',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
insertText: {
|
||||
objectId: textBoxId,
|
||||
text: params.content,
|
||||
insertionIndex: 0,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// Make the batchUpdate request
|
||||
const updateResponse = await fetch(
|
||||
`https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ requests }),
|
||||
}
|
||||
)
|
||||
|
||||
if (!updateResponse.ok) {
|
||||
const errorText = await updateResponse.text()
|
||||
logger.error('Failed to update presentation:', { errorText })
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to update presentation',
|
||||
output: {
|
||||
updatedContent: false,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updatedContent: true,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error in postProcess:', { error })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updatedContent: {
|
||||
type: 'boolean',
|
||||
description: 'Indicates if presentation content was updated successfully',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated presentation metadata including ID, title, and URL',
|
||||
properties: {
|
||||
presentationId: {
|
||||
type: 'string',
|
||||
description: 'The presentation ID',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'The presentation title',
|
||||
},
|
||||
mimeType: {
|
||||
type: 'string',
|
||||
description: 'The mime type of the presentation',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL to open the presentation',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
// This is just for the initial read, the actual response comes from postProcess
|
||||
const data = await response.json()
|
||||
|
||||
const metadata = {
|
||||
presentationId: data.presentationId,
|
||||
title: data.title || 'Presentation',
|
||||
mimeType: 'application/vnd.google-apps.presentation',
|
||||
url: `https://docs.google.com/presentation/d/${data.presentationId}/edit`,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updatedContent: false,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user