Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,505 changes: 2,504 additions & 1 deletion apps/sim/blocks/blocks/google_slides.ts

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions apps/sim/tools/google_slides/batch_update.ts
Original file line number Diff line number Diff line change
@@ -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' },
},
},
},
}
128 changes: 128 additions & 0 deletions apps/sim/tools/google_slides/copy_presentation.ts
Original file line number Diff line number Diff line change
@@ -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' },
},
},
},
}
Loading
Loading