Skip to content

Commit b6679a9

Browse files
authored
feat(google-slides): complete API surface for branded slide generation (#4678)
* feat(google-slides): complete API surface for branded slide generation * fix(google-slides): address PR review — explicit videoId mapping, fast base64 export, remove dead utility * fix(google-slides): declare z-order operation output in block outputs map
1 parent 7e67855 commit b6679a9

42 files changed

Lines changed: 8917 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/blocks/blocks/google_slides.ts

Lines changed: 2504 additions & 1 deletion
Large diffs are not rendered by default.
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { createLogger } from '@sim/logger'
2+
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
3+
import type { ToolConfig } from '@/tools/types'
4+
5+
const logger = createLogger('GoogleSlidesBatchUpdateTool')
6+
7+
interface BatchUpdateParams {
8+
accessToken: string
9+
presentationId: string
10+
requests: string
11+
writeControl?: string
12+
}
13+
14+
interface BatchUpdateResponse {
15+
success: boolean
16+
output: {
17+
replies: unknown[]
18+
writeControl: unknown
19+
metadata: { presentationId: string; url: string; requestCount: number }
20+
}
21+
}
22+
23+
export const batchUpdateTool: ToolConfig<BatchUpdateParams, BatchUpdateResponse> = {
24+
id: 'google_slides_batch_update',
25+
name: 'Batch Update Google Slides (Raw)',
26+
description:
27+
'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).',
28+
version: '1.0.0',
29+
30+
oauth: { required: true, provider: 'google-drive' },
31+
32+
params: {
33+
accessToken: {
34+
type: 'string',
35+
required: true,
36+
visibility: 'hidden',
37+
description: 'The access token for the Google Slides API',
38+
},
39+
presentationId: {
40+
type: 'string',
41+
required: true,
42+
visibility: 'user-or-llm',
43+
description: 'Google Slides presentation ID',
44+
},
45+
requests: {
46+
type: 'string',
47+
required: true,
48+
visibility: 'user-or-llm',
49+
description:
50+
'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"}}]',
51+
},
52+
writeControl: {
53+
type: 'string',
54+
required: false,
55+
visibility: 'user-or-llm',
56+
description:
57+
'Optional JSON WriteControl object for optimistic concurrency, e.g. {"requiredRevisionId":"..."}',
58+
},
59+
},
60+
61+
request: {
62+
url: (params) => batchUpdateUrl(params.presentationId),
63+
method: 'POST',
64+
headers: (params) => authJsonHeaders(params.accessToken),
65+
body: (params) => {
66+
const raw = params.requests
67+
if (!raw) throw new Error('Requests JSON is required')
68+
69+
let requests: unknown
70+
try {
71+
requests = typeof raw === 'string' ? JSON.parse(raw) : raw
72+
} catch (e) {
73+
throw new Error(`Invalid requests JSON: ${(e as Error).message}`)
74+
}
75+
if (!Array.isArray(requests)) {
76+
throw new Error('Requests must be a JSON array of Request objects')
77+
}
78+
if (requests.length === 0) {
79+
throw new Error('Requests array must contain at least one Request')
80+
}
81+
82+
const body: Record<string, unknown> = { requests }
83+
84+
if (params.writeControl?.trim()) {
85+
try {
86+
const wc = JSON.parse(params.writeControl)
87+
if (wc && typeof wc === 'object') body.writeControl = wc
88+
} catch (e) {
89+
logger.warn('Invalid writeControl JSON, ignoring:', { error: e })
90+
}
91+
}
92+
93+
return body
94+
},
95+
},
96+
97+
transformResponse: async (response: Response, params) => {
98+
const data = await response.json()
99+
if (!response.ok) {
100+
logger.error('Google Slides API error:', { data })
101+
throw new Error(data.error?.message || 'Batch update failed')
102+
}
103+
const presentationId = params?.presentationId?.trim() || ''
104+
const replies: unknown[] = Array.isArray(data.replies) ? data.replies : []
105+
return {
106+
success: true,
107+
output: {
108+
replies,
109+
writeControl: data.writeControl ?? null,
110+
metadata: {
111+
presentationId,
112+
url: presentationUrl(presentationId),
113+
requestCount: replies.length,
114+
},
115+
},
116+
}
117+
},
118+
119+
outputs: {
120+
replies: {
121+
type: 'array',
122+
description: 'Array of reply objects, one per request (parallel-indexed)',
123+
items: { type: 'json' },
124+
},
125+
writeControl: {
126+
type: 'json',
127+
description: 'WriteControl returned by the server (revision tracking)',
128+
},
129+
metadata: {
130+
type: 'object',
131+
description: 'Operation metadata',
132+
properties: {
133+
presentationId: { type: 'string', description: 'The presentation ID' },
134+
url: { type: 'string', description: 'URL to the presentation' },
135+
requestCount: { type: 'number', description: 'Number of replies returned' },
136+
},
137+
},
138+
},
139+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { createLogger } from '@sim/logger'
2+
import { presentationUrl } from '@/tools/google_slides/utils'
3+
import type { ToolConfig } from '@/tools/types'
4+
5+
const logger = createLogger('GoogleSlidesCopyPresentationTool')
6+
7+
interface CopyPresentationParams {
8+
accessToken: string
9+
sourcePresentationId: string
10+
title?: string
11+
folderId?: string
12+
}
13+
14+
interface CopyPresentationResponse {
15+
success: boolean
16+
output: {
17+
presentationId: string
18+
title: string
19+
metadata: {
20+
sourcePresentationId: string
21+
presentationId: string
22+
title: string
23+
mimeType: string
24+
url: string
25+
}
26+
}
27+
}
28+
29+
const PRESENTATION_MIME = 'application/vnd.google-apps.presentation'
30+
31+
export const copyPresentationTool: ToolConfig<CopyPresentationParams, CopyPresentationResponse> = {
32+
id: 'google_slides_copy_presentation',
33+
name: 'Copy Google Slides Presentation',
34+
description:
35+
'Copy a template presentation in Drive to a new file. Use this before merging data so the original template is never modified.',
36+
version: '1.0.0',
37+
38+
oauth: { required: true, provider: 'google-drive' },
39+
40+
params: {
41+
accessToken: {
42+
type: 'string',
43+
required: true,
44+
visibility: 'hidden',
45+
description: 'The access token for the Google Slides / Drive API',
46+
},
47+
sourcePresentationId: {
48+
type: 'string',
49+
required: true,
50+
visibility: 'user-or-llm',
51+
description: 'Drive file ID of the source/template presentation',
52+
},
53+
title: {
54+
type: 'string',
55+
required: false,
56+
visibility: 'user-or-llm',
57+
description: 'Title for the copy. Defaults to "Copy of <source title>".',
58+
},
59+
folderId: {
60+
type: 'string',
61+
required: false,
62+
visibility: 'user-or-llm',
63+
description: 'Drive folder ID where the copy should be placed',
64+
},
65+
},
66+
67+
request: {
68+
url: (params) => {
69+
const sourceId = params.sourcePresentationId?.trim()
70+
if (!sourceId) throw new Error('Source presentation ID is required')
71+
return `https://www.googleapis.com/drive/v3/files/${sourceId}/copy?supportsAllDrives=true`
72+
},
73+
method: 'POST',
74+
headers: (params) => {
75+
if (!params.accessToken) throw new Error('Access token is required')
76+
return {
77+
Authorization: `Bearer ${params.accessToken}`,
78+
'Content-Type': 'application/json',
79+
}
80+
},
81+
body: (params) => {
82+
const body: Record<string, unknown> = {}
83+
if (params.title?.trim()) body.name = params.title.trim()
84+
if (params.folderId?.trim()) body.parents = [params.folderId.trim()]
85+
return body
86+
},
87+
},
88+
89+
transformResponse: async (response: Response, params) => {
90+
const data = await response.json()
91+
if (!response.ok) {
92+
logger.error('Drive API error during copy:', { data })
93+
throw new Error(data.error?.message || 'Failed to copy presentation')
94+
}
95+
const presentationId: string = data.id
96+
const title: string = data.name || 'Untitled Presentation'
97+
return {
98+
success: true,
99+
output: {
100+
presentationId,
101+
title,
102+
metadata: {
103+
sourcePresentationId: params?.sourcePresentationId?.trim() || '',
104+
presentationId,
105+
title,
106+
mimeType: PRESENTATION_MIME,
107+
url: presentationUrl(presentationId),
108+
},
109+
},
110+
}
111+
},
112+
113+
outputs: {
114+
presentationId: { type: 'string', description: 'ID of the new copied presentation' },
115+
title: { type: 'string', description: 'Title of the new presentation' },
116+
metadata: {
117+
type: 'object',
118+
description: 'Operation metadata',
119+
properties: {
120+
sourcePresentationId: { type: 'string', description: 'Source/template presentation ID' },
121+
presentationId: { type: 'string', description: 'New presentation ID' },
122+
title: { type: 'string', description: 'New presentation title' },
123+
mimeType: { type: 'string', description: 'MIME type of the presentation' },
124+
url: { type: 'string', description: 'URL to the new presentation' },
125+
},
126+
},
127+
},
128+
}

0 commit comments

Comments
 (0)