@agent-native/creative-context 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/agent-native.package.json +11 -0
  2. package/dist/actions/continue-context-import.d.ts +16 -1
  3. package/dist/actions/continue-context-import.js +4 -1
  4. package/dist/actions/continue-context-import.js.map +1 -1
  5. package/dist/actions/get-creative-context.d.ts +38 -0
  6. package/dist/actions/get-creative-context.d.ts.map +1 -1
  7. package/dist/actions/get-creative-context.js +59 -23
  8. package/dist/actions/get-creative-context.js.map +1 -1
  9. package/dist/actions/get-google-picker-session.d.ts +10 -0
  10. package/dist/actions/get-google-picker-session.d.ts.map +1 -0
  11. package/dist/actions/get-google-picker-session.js +38 -0
  12. package/dist/actions/get-google-picker-session.js.map +1 -0
  13. package/dist/actions/index.d.ts.map +1 -1
  14. package/dist/actions/index.js +2 -0
  15. package/dist/actions/index.js.map +1 -1
  16. package/dist/actions/start-context-enrichment.d.ts +16 -1
  17. package/dist/actions/start-context-enrichment.js +6 -4
  18. package/dist/actions/start-context-enrichment.js.map +1 -1
  19. package/dist/actions/start-context-import.d.ts +16 -1
  20. package/dist/actions/start-context-import.js +2 -1
  21. package/dist/actions/start-context-import.js.map +1 -1
  22. package/dist/client/CreativeContextPanel.d.ts +1 -59
  23. package/dist/client/CreativeContextPanel.d.ts.map +1 -1
  24. package/dist/client/CreativeContextPanel.js +111 -36
  25. package/dist/client/CreativeContextPanel.js.map +1 -1
  26. package/dist/client/actions.d.ts +8 -0
  27. package/dist/client/actions.d.ts.map +1 -1
  28. package/dist/client/actions.js +4 -0
  29. package/dist/client/actions.js.map +1 -1
  30. package/dist/client/google-slides-picker.d.ts +19 -0
  31. package/dist/client/google-slides-picker.d.ts.map +1 -0
  32. package/dist/client/google-slides-picker.js +106 -0
  33. package/dist/client/google-slides-picker.js.map +1 -0
  34. package/dist/client/index.d.ts +1 -1
  35. package/dist/client/index.d.ts.map +1 -1
  36. package/dist/client/index.js +1 -1
  37. package/dist/client/index.js.map +1 -1
  38. package/dist/client/messages.d.ts +1 -1
  39. package/dist/client/messages.d.ts.map +1 -1
  40. package/dist/client/messages.js +22 -0
  41. package/dist/client/messages.js.map +1 -1
  42. package/dist/connectors/google-slides.d.ts +1 -1
  43. package/dist/connectors/google-slides.d.ts.map +1 -1
  44. package/dist/connectors/google-slides.js +1 -2
  45. package/dist/connectors/google-slides.js.map +1 -1
  46. package/dist/connectors/normalize.d.ts +7 -0
  47. package/dist/connectors/normalize.d.ts.map +1 -1
  48. package/dist/connectors/normalize.js +122 -3
  49. package/dist/connectors/normalize.js.map +1 -1
  50. package/dist/schema/index.d.ts +30 -30
  51. package/dist/server/enrichment.d.ts +1 -0
  52. package/dist/server/enrichment.d.ts.map +1 -1
  53. package/dist/server/enrichment.js +26 -24
  54. package/dist/server/enrichment.js.map +1 -1
  55. package/dist/server/generation-context.d.ts +1 -1
  56. package/dist/server/generation-context.d.ts.map +1 -1
  57. package/dist/server/generation-context.js +1 -1
  58. package/dist/server/generation-context.js.map +1 -1
  59. package/dist/store/content.d.ts +13 -0
  60. package/dist/store/content.d.ts.map +1 -1
  61. package/dist/store/content.js +172 -1
  62. package/dist/store/content.js.map +1 -1
  63. package/dist/store/jobs.d.ts.map +1 -1
  64. package/dist/store/jobs.js +2 -2
  65. package/dist/store/jobs.js.map +1 -1
  66. package/dist/store/purge.d.ts.map +1 -1
  67. package/dist/store/purge.js +9 -8
  68. package/dist/store/purge.js.map +1 -1
  69. package/dist/store/suggestions.d.ts +1 -0
  70. package/dist/store/suggestions.d.ts.map +1 -1
  71. package/dist/store/suggestions.js +56 -38
  72. package/dist/store/suggestions.js.map +1 -1
  73. package/docs/skills/creative-context/SKILL.md +8 -0
  74. package/package.json +3 -3
  75. package/src/actions/continue-context-import.ts +4 -1
  76. package/src/actions/creative-context-a2a.spec.ts +98 -0
  77. package/src/actions/get-creative-context.spec.ts +121 -1
  78. package/src/actions/get-creative-context.ts +78 -29
  79. package/src/actions/get-google-picker-session.spec.ts +62 -0
  80. package/src/actions/get-google-picker-session.ts +45 -0
  81. package/src/actions/index.ts +2 -0
  82. package/src/actions/job-public-serialization.spec.ts +122 -0
  83. package/src/actions/start-context-enrichment.ts +6 -4
  84. package/src/actions/start-context-import.ts +2 -1
  85. package/src/client/CreativeContextPanel.tsx +163 -64
  86. package/src/client/actions.ts +18 -0
  87. package/src/client/google-slides-picker.spec.ts +36 -0
  88. package/src/client/google-slides-picker.ts +146 -0
  89. package/src/client/index.ts +2 -0
  90. package/src/client/messages.ts +35 -0
  91. package/src/client/source-config.spec.ts +24 -10
  92. package/src/connectors/google-slides.ts +1 -2
  93. package/src/connectors/normalize.spec.ts +144 -0
  94. package/src/connectors/normalize.ts +201 -3
  95. package/src/server/enrichment.spec.ts +94 -0
  96. package/src/server/enrichment.ts +27 -24
  97. package/src/server/generation-artifact-access.spec.ts +104 -0
  98. package/src/server/generation-context-a2a.spec.ts +20 -2
  99. package/src/server/generation-context.ts +6 -6
  100. package/src/server/isolated-a2a.spec.ts +21 -1
  101. package/src/store/access.integration.spec.ts +511 -26
  102. package/src/store/content.ts +213 -1
  103. package/src/store/jobs.ts +2 -0
  104. package/src/store/purge.ts +18 -9
  105. package/src/store/suggestions.ts +67 -41
@@ -72,6 +72,7 @@ import {
72
72
  useCreativeContextBrandProfile,
73
73
  useCanonicalLogoCandidates,
74
74
  useCreativeContextConnections,
75
+ useCreativeContextGooglePickerSession,
75
76
  useCreativeContextRootRecommendations,
76
77
  useCreativeContextImportStatus,
77
78
  useCreativeContextPack,
@@ -97,6 +98,7 @@ import {
97
98
  type CreativeContextMode,
98
99
  } from "./application-state.js";
99
100
  import { CreativeContextChip } from "./CreativeContextChip.js";
101
+ import { chooseGoogleSlidesPresentations } from "./google-slides-picker.js";
100
102
 
101
103
  type ConnectorKind =
102
104
  | "google-slides"
@@ -125,7 +127,7 @@ const CONNECTORS: readonly ConnectorDefinition[] = [
125
127
  {
126
128
  kind: "google-slides",
127
129
  label: "Google Slides",
128
- referencePlaceholder: "Folder or shared-drive URL / ID",
130
+ referencePlaceholder: "Presentation URLs or IDs one per line",
129
131
  referenceRequired: false,
130
132
  icon: IconSlideshow,
131
133
  },
@@ -222,6 +224,20 @@ export function selectRenderableLayoutThumbnails<
222
224
  return thumbnails.filter((thumbnail) => thumbnail.hasThumbnail).slice(0, 3);
223
225
  }
224
226
 
227
+ export function mergeRecommendationSelection(
228
+ current: ReadonlySet<string>,
229
+ available: ReadonlySet<string>,
230
+ previouslySeen: ReadonlySet<string>,
231
+ ): Set<string> {
232
+ const next = new Set(
233
+ [...current].filter((externalId) => available.has(externalId)),
234
+ );
235
+ for (const externalId of available) {
236
+ if (!previouslySeen.has(externalId)) next.add(externalId);
237
+ }
238
+ return next;
239
+ }
240
+
225
241
  export function buildCreativeContextSourceConfig(
226
242
  kind: ConnectorKind,
227
243
  reference: string,
@@ -230,20 +246,19 @@ export function buildCreativeContextSourceConfig(
230
246
  ) {
231
247
  const references = splitReferences(reference);
232
248
  if (kind === "google-slides") {
233
- if (!references.length && recommendations.length) {
234
- return {
235
- presentationIds: recommendations.map((item) => item.externalId),
236
- };
237
- }
238
- const container = references[0];
239
- const folderMatch = container?.match(/\/folders\/([^/?#]+)/);
240
- if (container?.startsWith("shared-drive:")) {
241
- return { sharedDriveId: container.slice("shared-drive:".length) };
242
- }
243
- if (container && /^https?:\/\//.test(container)) {
244
- return { folderUrl: container };
245
- }
246
- return container ? { folderId: folderMatch?.[1] ?? container } : {};
249
+ const presentationIds = references.flatMap((value) => {
250
+ const match = value.match(/\/presentation\/d\/([^/?#]+)/);
251
+ const id = match?.[1] ?? (/^https?:\/\//.test(value) ? "" : value);
252
+ return id ? [id] : [];
253
+ });
254
+ return {
255
+ presentationIds: [
256
+ ...new Set([
257
+ ...presentationIds,
258
+ ...recommendations.map((item) => item.externalId),
259
+ ]),
260
+ ],
261
+ };
247
262
  }
248
263
  if (kind === "figma") {
249
264
  const fileUrls: string[] = [];
@@ -828,6 +843,16 @@ export function CreativeContextPanel({
828
843
  const [selectedRecommendationIds, setSelectedRecommendationIds] = useState<
829
844
  Set<string>
830
845
  >(() => new Set());
846
+ const seenRecommendationIdsRef = useRef<Set<string>>(new Set());
847
+ const [pickerRecommendations, setPickerRecommendations] = useState<
848
+ CreativeContextRootRecommendation[]
849
+ >([]);
850
+ const [openingGooglePicker, setOpeningGooglePicker] = useState(false);
851
+ const googlePickerSession = useCreativeContextGooglePickerSession(
852
+ connectorKind === "google-slides" && selectedConnectionId
853
+ ? selectedConnectionId
854
+ : null,
855
+ );
831
856
  const [uploadedFiles, setUploadedFiles] = useState<UploadedContextFile[]>([]);
832
857
  const fileInputRef = useRef<HTMLInputElement>(null);
833
858
  const folderInputRef = useRef<HTMLInputElement>(null);
@@ -880,15 +905,27 @@ export function CreativeContextPanel({
880
905
  );
881
906
  }, [connectionProvider, connectionsQuery.data?.autoSelectedConnectionId]);
882
907
 
908
+ const availableRecommendations = useMemo(() => {
909
+ const byId = new Map<string, CreativeContextRootRecommendation>();
910
+ for (const recommendation of [
911
+ ...(recommendationsQuery.data?.recommendations ?? []),
912
+ ...pickerRecommendations,
913
+ ]) {
914
+ byId.set(recommendation.externalId, recommendation);
915
+ }
916
+ return [...byId.values()];
917
+ }, [pickerRecommendations, recommendationsQuery.data?.recommendations]);
918
+
883
919
  useEffect(() => {
884
- setSelectedRecommendationIds(
885
- new Set(
886
- (recommendationsQuery.data?.recommendations ?? []).map(
887
- (recommendation) => recommendation.externalId,
888
- ),
889
- ),
920
+ const availableIds = new Set(
921
+ availableRecommendations.map(({ externalId }) => externalId),
922
+ );
923
+ const previouslySeen = seenRecommendationIdsRef.current;
924
+ setSelectedRecommendationIds((current) =>
925
+ mergeRecommendationSelection(current, availableIds, previouslySeen),
890
926
  );
891
- }, [recommendationsQuery.data?.recommendations]);
927
+ seenRecommendationIdsRef.current = availableIds;
928
+ }, [availableRecommendations]);
892
929
 
893
930
  const sources = useMemo(
894
931
  () =>
@@ -1078,10 +1115,9 @@ export function CreativeContextPanel({
1078
1115
  setCompletedJobId(null);
1079
1116
  setPublishedMessage(null);
1080
1117
  try {
1081
- const confirmedRecommendations = (
1082
- recommendationsQuery.data?.recommendations ?? []
1083
- ).filter((recommendation) =>
1084
- selectedRecommendationIds.has(recommendation.externalId),
1118
+ const confirmedRecommendations = availableRecommendations.filter(
1119
+ (recommendation) =>
1120
+ selectedRecommendationIds.has(recommendation.externalId),
1085
1121
  );
1086
1122
  const result = await manageSource.mutateAsync({
1087
1123
  operation: "create",
@@ -1116,6 +1152,8 @@ export function CreativeContextPanel({
1116
1152
  setConnectorKind(null);
1117
1153
  setSourceName("");
1118
1154
  setSourceReference("");
1155
+ setPickerRecommendations([]);
1156
+ seenRecommendationIdsRef.current.clear();
1119
1157
  setUploadedFiles([]);
1120
1158
  await sourcesQuery.refetch();
1121
1159
  } catch {
@@ -1123,6 +1161,48 @@ export function CreativeContextPanel({
1123
1161
  }
1124
1162
  }
1125
1163
 
1164
+ async function chooseGoogleSlides() {
1165
+ setSetupError(null);
1166
+ setOpeningGooglePicker(true);
1167
+ try {
1168
+ const session = await googlePickerSession.refetch();
1169
+ if (!session.data) {
1170
+ throw new Error("Google Picker session is unavailable.");
1171
+ }
1172
+ const selections = await chooseGoogleSlidesPresentations(session.data);
1173
+ if (!selections.length) return;
1174
+ const selected = selections.map((selection) => ({
1175
+ ...selection,
1176
+ provider: "google-slides" as const,
1177
+ kind: "presentation" as const,
1178
+ }));
1179
+ setPickerRecommendations((current) => {
1180
+ const byId = new Map(
1181
+ current.map((recommendation) => [
1182
+ recommendation.externalId,
1183
+ recommendation,
1184
+ ]),
1185
+ );
1186
+ for (const recommendation of selected) {
1187
+ byId.set(recommendation.externalId, recommendation);
1188
+ }
1189
+ return [...byId.values()];
1190
+ });
1191
+ setSelectedRecommendationIds((current) => {
1192
+ const next = new Set(current);
1193
+ for (const recommendation of selected) {
1194
+ next.add(recommendation.externalId);
1195
+ }
1196
+ return next;
1197
+ });
1198
+ void recommendationsQuery.refetch();
1199
+ } catch (error) {
1200
+ setSetupError(error instanceof Error ? error.message : String(error));
1201
+ } finally {
1202
+ setOpeningGooglePicker(false);
1203
+ }
1204
+ }
1205
+
1126
1206
  async function confirmPromotion() {
1127
1207
  if (!promotionSourceId || !promotionPreview) return;
1128
1208
  setPromotionMessage(null);
@@ -1605,6 +1685,8 @@ export function CreativeContextPanel({
1605
1685
  setSourceName(connector.label);
1606
1686
  setSourceReference("");
1607
1687
  setUploadedFiles([]);
1688
+ setPickerRecommendations([]);
1689
+ seenRecommendationIdsRef.current.clear();
1608
1690
  setSelectedConnectionId("");
1609
1691
  setSelectedRecommendationIds(new Set());
1610
1692
  setSetupError(null);
@@ -1682,16 +1764,35 @@ export function CreativeContextPanel({
1682
1764
  ) : null}
1683
1765
  {recommendationProvider && selectedConnectionId ? (
1684
1766
  <div className="mb-3 rounded-md border border-border p-3">
1685
- {recommendationsQuery.isLoading ? (
1767
+ {connectorKind === "google-slides" ? (
1768
+ <div className="mb-3 flex flex-wrap items-center justify-between gap-3 border-b border-border/60 pb-3">
1769
+ <p className="max-w-lg text-xs text-muted-foreground">
1770
+ {t("creativeContext.googlePickerDescription")}
1771
+ </p>
1772
+ <Button
1773
+ type="button"
1774
+ variant="outline"
1775
+ size="sm"
1776
+ disabled={openingGooglePicker}
1777
+ onClick={() => void chooseGoogleSlides()}
1778
+ >
1779
+ <IconSlideshow />
1780
+ {openingGooglePicker
1781
+ ? t("creativeContext.loading")
1782
+ : t("creativeContext.choosePresentations")}
1783
+ </Button>
1784
+ </div>
1785
+ ) : null}
1786
+ {recommendationsQuery.isLoading &&
1787
+ !availableRecommendations.length ? (
1686
1788
  <Skeleton className="h-16 w-full" />
1687
- ) : recommendationsQuery.data?.recommendations.length ? (
1789
+ ) : availableRecommendations.length ? (
1688
1790
  <div className="space-y-2">
1689
1791
  <div className="flex items-center justify-between gap-3 text-xs text-muted-foreground">
1690
1792
  <span>
1691
1793
  {t("creativeContext.discoveredItems", {
1692
1794
  count: formatNumber(
1693
- recommendationsQuery.data.recommendations
1694
- .length,
1795
+ availableRecommendations.length,
1695
1796
  ),
1696
1797
  })}
1697
1798
  </span>
@@ -1704,41 +1805,39 @@ export function CreativeContextPanel({
1704
1805
  </span>
1705
1806
  </div>
1706
1807
  <div className="max-h-52 divide-y divide-border/60 overflow-y-auto">
1707
- {recommendationsQuery.data.recommendations.map(
1708
- (recommendation) => (
1709
- <label
1710
- key={recommendation.externalId}
1711
- className="flex cursor-pointer items-start gap-3 py-2"
1712
- >
1713
- <Checkbox
1714
- className="mt-0.5"
1715
- checked={selectedRecommendationIds.has(
1716
- recommendation.externalId,
1717
- )}
1718
- onCheckedChange={(checked) =>
1719
- setSelectedRecommendationIds((current) => {
1720
- const next = new Set(current);
1721
- if (checked) {
1722
- next.add(recommendation.externalId);
1723
- } else {
1724
- next.delete(recommendation.externalId);
1725
- }
1726
- return next;
1727
- })
1728
- }
1729
- />
1730
- <span className="min-w-0">
1731
- <span className="block truncate text-sm">
1732
- {recommendation.title}
1733
- </span>
1734
- <span className="block text-xs text-muted-foreground">
1735
- {recommendation.containerRef ??
1736
- recommendation.kind}
1737
- </span>
1808
+ {availableRecommendations.map((recommendation) => (
1809
+ <label
1810
+ key={recommendation.externalId}
1811
+ className="flex cursor-pointer items-start gap-3 py-2"
1812
+ >
1813
+ <Checkbox
1814
+ className="mt-0.5"
1815
+ checked={selectedRecommendationIds.has(
1816
+ recommendation.externalId,
1817
+ )}
1818
+ onCheckedChange={(checked) =>
1819
+ setSelectedRecommendationIds((current) => {
1820
+ const next = new Set(current);
1821
+ if (checked) {
1822
+ next.add(recommendation.externalId);
1823
+ } else {
1824
+ next.delete(recommendation.externalId);
1825
+ }
1826
+ return next;
1827
+ })
1828
+ }
1829
+ />
1830
+ <span className="min-w-0">
1831
+ <span className="block truncate text-sm">
1832
+ {recommendation.title}
1738
1833
  </span>
1739
- </label>
1740
- ),
1741
- )}
1834
+ <span className="block text-xs text-muted-foreground">
1835
+ {recommendation.containerRef ??
1836
+ recommendation.kind}
1837
+ </span>
1838
+ </span>
1839
+ </label>
1840
+ ))}
1742
1841
  </div>
1743
1842
  </div>
1744
1843
  ) : (
@@ -32,6 +32,7 @@ export const CREATIVE_CONTEXT_ACTIONS = {
32
32
  managePack: "manage-context-pack",
33
33
  recordFeedback: "record-context-feedback",
34
34
  getPack: "get-context-pack",
35
+ googlePickerSession: "get-google-picker-session",
35
36
  reviewItems: "review-context-items",
36
37
  listLogoCandidates: "list-canonical-logo-candidates",
37
38
  proposeLogo: "propose-canonical-logo",
@@ -205,6 +206,13 @@ export interface ListCreativeContextConnectionsResult {
205
206
  connectPath: string;
206
207
  }
207
208
 
209
+ export interface GetGooglePickerSessionResult {
210
+ accessToken: string;
211
+ accountLabel: string | null;
212
+ apiKey: string;
213
+ appId: string;
214
+ }
215
+
208
216
  export type CreativeContextRecommendationProvider =
209
217
  | "google-slides"
210
218
  | "figma"
@@ -365,6 +373,16 @@ export function useCreativeContextRootRecommendations(
365
373
  );
366
374
  }
367
375
 
376
+ export function useCreativeContextGooglePickerSession(
377
+ connectionId: string | null,
378
+ ) {
379
+ return useActionQuery<GetGooglePickerSessionResult>(
380
+ CREATIVE_CONTEXT_ACTIONS.googlePickerSession,
381
+ connectionId ? { connectionId } : undefined,
382
+ { enabled: false },
383
+ );
384
+ }
385
+
368
386
  export function useCreativeContextBrandProfile() {
369
387
  return useActionQuery<GetBrandProfileResult>(
370
388
  CREATIVE_CONTEXT_ACTIONS.getBrandProfile,
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { googleSlidesPickerSelections } from "./google-slides-picker.js";
4
+
5
+ describe("googleSlidesPickerSelections", () => {
6
+ it("returns unique presentation ids with safe canonical URLs", () => {
7
+ expect(
8
+ googleSlidesPickerSelections({
9
+ docs: [
10
+ {
11
+ id: "deck_123456789",
12
+ name: "Launch",
13
+ url: "https://attacker.example/x",
14
+ },
15
+ { id: "deck_123456789", name: "Duplicate" },
16
+ { id: "deck_987654321", name: "Roadmap" },
17
+ { id: "bad/id", name: "Invalid id" },
18
+ { name: "Missing id" },
19
+ ],
20
+ }),
21
+ ).toEqual([
22
+ {
23
+ externalId: "deck_123456789",
24
+ title: "Launch",
25
+ canonicalUrl:
26
+ "https://docs.google.com/presentation/d/deck_123456789/edit",
27
+ },
28
+ {
29
+ externalId: "deck_987654321",
30
+ title: "Roadmap",
31
+ canonicalUrl:
32
+ "https://docs.google.com/presentation/d/deck_987654321/edit",
33
+ },
34
+ ]);
35
+ });
36
+ });
@@ -0,0 +1,146 @@
1
+ const GOOGLE_PICKER_SCRIPT = "https://apis.google.com/js/api.js";
2
+ const GOOGLE_SLIDES_MIME_TYPE = "application/vnd.google-apps.presentation";
3
+ const GOOGLE_PICKER_LOAD_TIMEOUT_MS = 15_000;
4
+ const GOOGLE_PICKER_SELECTION_TIMEOUT_MS = 5 * 60_000;
5
+ const GOOGLE_DRIVE_FILE_ID = /^[A-Za-z0-9_-]{8,256}$/;
6
+
7
+ declare global {
8
+ interface Window {
9
+ gapi?: any;
10
+ google?: any;
11
+ __creativeContextGooglePickerScript?: Promise<void>;
12
+ }
13
+ }
14
+
15
+ export interface GoogleSlidesPickerSelection {
16
+ externalId: string;
17
+ title: string;
18
+ canonicalUrl?: string;
19
+ }
20
+
21
+ export function googleSlidesPickerSelections(
22
+ value: unknown,
23
+ ): GoogleSlidesPickerSelection[] {
24
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
25
+ const docs = (value as { docs?: unknown }).docs;
26
+ if (!Array.isArray(docs)) return [];
27
+ const seen = new Set<string>();
28
+ return docs.flatMap((entry) => {
29
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
30
+ const record = entry as Record<string, unknown>;
31
+ const externalId = typeof record.id === "string" ? record.id.trim() : "";
32
+ if (!GOOGLE_DRIVE_FILE_ID.test(externalId) || seen.has(externalId)) {
33
+ return [];
34
+ }
35
+ seen.add(externalId);
36
+ const title =
37
+ typeof record.name === "string" && record.name.trim()
38
+ ? record.name.trim().slice(0, 300)
39
+ : "Google Slides presentation";
40
+ const canonicalUrl = `https://docs.google.com/presentation/d/${encodeURIComponent(externalId)}/edit`;
41
+ return [{ externalId, title, canonicalUrl }];
42
+ });
43
+ }
44
+
45
+ async function withGooglePickerTimeout<T>(
46
+ promise: Promise<T>,
47
+ message: string,
48
+ timeoutMs = GOOGLE_PICKER_LOAD_TIMEOUT_MS,
49
+ ): Promise<T> {
50
+ let timeout: ReturnType<typeof setTimeout> | undefined;
51
+ try {
52
+ return await Promise.race([
53
+ promise,
54
+ new Promise<never>((_, reject) => {
55
+ timeout = setTimeout(() => reject(new Error(message)), timeoutMs);
56
+ }),
57
+ ]);
58
+ } finally {
59
+ if (timeout) clearTimeout(timeout);
60
+ }
61
+ }
62
+
63
+ async function loadGooglePicker(): Promise<void> {
64
+ if (!window.gapi) {
65
+ window.__creativeContextGooglePickerScript ??= new Promise(
66
+ (resolve, reject) => {
67
+ const script = document.createElement("script");
68
+ script.src = GOOGLE_PICKER_SCRIPT;
69
+ script.async = true;
70
+ script.onload = () => resolve();
71
+ script.onerror = () =>
72
+ reject(new Error("Could not load Google Picker."));
73
+ document.head.appendChild(script);
74
+ },
75
+ );
76
+ await withGooglePickerTimeout(
77
+ window.__creativeContextGooglePickerScript,
78
+ "Google Picker script timed out.",
79
+ );
80
+ }
81
+ if (!window.gapi?.load) throw new Error("Google Picker did not initialize.");
82
+ await withGooglePickerTimeout(
83
+ new Promise<void>((resolve, reject) => {
84
+ window.gapi.load("picker", {
85
+ callback: resolve,
86
+ onerror: () => reject(new Error("Could not load Google Picker.")),
87
+ });
88
+ }),
89
+ "Google Picker initialization timed out.",
90
+ );
91
+ }
92
+
93
+ export async function chooseGoogleSlidesPresentations(input: {
94
+ accessToken: string;
95
+ apiKey: string;
96
+ appId: string;
97
+ }): Promise<GoogleSlidesPickerSelection[]> {
98
+ await loadGooglePicker();
99
+ const google = window.google;
100
+ if (!google?.picker) throw new Error("Google Picker is unavailable.");
101
+ return withGooglePickerTimeout(
102
+ new Promise((resolve, reject) => {
103
+ const view = new google.picker.DocsView(
104
+ google.picker.ViewId.PRESENTATIONS,
105
+ )
106
+ .setMimeTypes(GOOGLE_SLIDES_MIME_TYPE)
107
+ .setSelectFolderEnabled(false);
108
+ const picker = new google.picker.PickerBuilder()
109
+ .addView(view)
110
+ .enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
111
+ .setOAuthToken(input.accessToken)
112
+ .setDeveloperKey(input.apiKey)
113
+ .setAppId(input.appId)
114
+ .setTitle("Choose Google Slides presentations")
115
+ .setCallback((data: unknown) => {
116
+ if (
117
+ data &&
118
+ typeof data === "object" &&
119
+ (data as { action?: unknown }).action ===
120
+ google.picker.Action.CANCEL
121
+ ) {
122
+ resolve([]);
123
+ return;
124
+ }
125
+ if (
126
+ !data ||
127
+ typeof data !== "object" ||
128
+ (data as { action?: unknown }).action !==
129
+ google.picker.Action.PICKED
130
+ ) {
131
+ return;
132
+ }
133
+ const selections = googleSlidesPickerSelections(data);
134
+ if (!selections.length) {
135
+ reject(new Error("Google Picker returned no presentations."));
136
+ return;
137
+ }
138
+ resolve(selections);
139
+ })
140
+ .build();
141
+ picker.setVisible(true);
142
+ }),
143
+ "Google Picker selection timed out.",
144
+ GOOGLE_PICKER_SELECTION_TIMEOUT_MS,
145
+ );
146
+ }
@@ -1,6 +1,7 @@
1
1
  export {
2
2
  CREATIVE_CONTEXT_ACTIONS,
3
3
  useCreativeContextBrandProfile,
4
+ useCreativeContextGooglePickerSession,
4
5
  useCreativeContextImportStatus,
5
6
  useCreativeContextPack,
6
7
  useCreativeContextPacks,
@@ -15,6 +16,7 @@ export {
15
16
  type GetBrandProfileResult,
16
17
  type GetContextImportStatusResult,
17
18
  type GetContextPackResult,
19
+ type GetGooglePickerSessionResult,
18
20
  type ListContextPacksResult,
19
21
  type ListContextSourcesParams,
20
22
  type ListContextSourcesResult,
@@ -104,6 +104,8 @@ export type CreativeContextMessageKey =
104
104
  | "chooseConnection"
105
105
  | "setupConnection"
106
106
  | "connectProvider"
107
+ | "choosePresentations"
108
+ | "googlePickerDescription"
107
109
  | "suggestions"
108
110
  | "logo";
109
111
 
@@ -213,6 +215,9 @@ export const creativeContextMessagesByLocale: Record<
213
215
  chooseConnection: "Choose a workspace connection",
214
216
  setupConnection: "Connect this provider before importing a source.",
215
217
  connectProvider: "Connect provider",
218
+ choosePresentations: "Choose presentations",
219
+ googlePickerDescription:
220
+ "Choose the presentations Google should make available to this Library connection. No Drive-wide permission is requested.",
216
221
  suggestions: "Review suggestions",
217
222
  logo: "Canonical logo",
218
223
  },
@@ -309,6 +314,9 @@ export const creativeContextMessagesByLocale: Record<
309
314
  chooseConnection: "選擇工作區連線",
310
315
  setupConnection: "匯入來源前,請先連結此提供者。",
311
316
  connectProvider: "連結提供者",
317
+ choosePresentations: "選擇簡報",
318
+ googlePickerDescription:
319
+ "選擇要提供給此資料庫連線的簡報,不會要求整個雲端硬碟的存取權。",
312
320
  suggestions: "審核建議",
313
321
  logo: "標準標誌",
314
322
  },
@@ -406,6 +414,9 @@ export const creativeContextMessagesByLocale: Record<
406
414
  chooseConnection: "选择工作区连接",
407
415
  setupConnection: "导入来源前,请先连接此提供程序。",
408
416
  connectProvider: "连接提供程序",
417
+ choosePresentations: "选择演示文稿",
418
+ googlePickerDescription:
419
+ "选择要提供给此资料库连接的演示文稿,不会请求整个云端硬盘的访问权限。",
409
420
  suggestions: "审核建议",
410
421
  logo: "标准徽标",
411
422
  },
@@ -510,6 +521,9 @@ export const creativeContextMessagesByLocale: Record<
510
521
  chooseConnection: "Elegir una conexión del espacio de trabajo",
511
522
  setupConnection: "Conecta este proveedor antes de importar una fuente.",
512
523
  connectProvider: "Conectar proveedor",
524
+ choosePresentations: "Elegir presentaciones",
525
+ googlePickerDescription:
526
+ "Elige las presentaciones disponibles para esta conexión. No se solicita acceso a todo Drive.",
513
527
  suggestions: "Revisar sugerencias",
514
528
  logo: "Logotipo canónico",
515
529
  },
@@ -615,6 +629,9 @@ export const creativeContextMessagesByLocale: Record<
615
629
  chooseConnection: "Choisir une connexion d’espace de travail",
616
630
  setupConnection: "Connectez ce fournisseur avant d’importer une source.",
617
631
  connectProvider: "Connecter le fournisseur",
632
+ choosePresentations: "Choisir des présentations",
633
+ googlePickerDescription:
634
+ "Choisissez les présentations accessibles à cette connexion. Aucun accès à l’ensemble de Drive n’est demandé.",
618
635
  suggestions: "Examiner les suggestions",
619
636
  logo: "Logo canonique",
620
637
  },
@@ -719,6 +736,9 @@ export const creativeContextMessagesByLocale: Record<
719
736
  chooseConnection: "Arbeitsbereichsverbindung auswählen",
720
737
  setupConnection: "Verbinde diesen Anbieter vor dem Import einer Quelle.",
721
738
  connectProvider: "Anbieter verbinden",
739
+ choosePresentations: "Präsentationen auswählen",
740
+ googlePickerDescription:
741
+ "Wähle die Präsentationen für diese Bibliotheksverbindung aus. Es wird kein Zugriff auf das gesamte Drive angefordert.",
722
742
  suggestions: "Vorschläge prüfen",
723
743
  logo: "Kanonisches Logo",
724
744
  },
@@ -826,6 +846,9 @@ export const creativeContextMessagesByLocale: Record<
826
846
  setupConnection:
827
847
  "ソースをインポートする前にプロバイダーを接続してください。",
828
848
  connectProvider: "プロバイダーに接続",
849
+ choosePresentations: "プレゼンテーションを選択",
850
+ googlePickerDescription:
851
+ "このライブラリ接続で利用するプレゼンテーションを選択します。Drive 全体への権限は要求しません。",
829
852
  suggestions: "候補を確認",
830
853
  logo: "正式ロゴ",
831
854
  },
@@ -928,6 +951,9 @@ export const creativeContextMessagesByLocale: Record<
928
951
  chooseConnection: "워크스페이스 연결 선택",
929
952
  setupConnection: "소스를 가져오기 전에 이 제공자를 연결하세요.",
930
953
  connectProvider: "제공자 연결",
954
+ choosePresentations: "프레젠테이션 선택",
955
+ googlePickerDescription:
956
+ "이 라이브러리 연결에서 사용할 프레젠테이션을 선택하세요. Drive 전체 권한은 요청하지 않습니다.",
931
957
  suggestions: "추천 검토",
932
958
  logo: "대표 로고",
933
959
  },
@@ -1032,6 +1058,9 @@ export const creativeContextMessagesByLocale: Record<
1032
1058
  chooseConnection: "Escolher uma conexão do espaço de trabalho",
1033
1059
  setupConnection: "Conecte este provedor antes de importar uma fonte.",
1034
1060
  connectProvider: "Conectar provedor",
1061
+ choosePresentations: "Escolher apresentações",
1062
+ googlePickerDescription:
1063
+ "Escolha as apresentações disponíveis para esta conexão. Nenhum acesso a todo o Drive é solicitado.",
1035
1064
  suggestions: "Revisar sugestões",
1036
1065
  logo: "Logotipo canônico",
1037
1066
  },
@@ -1133,6 +1162,9 @@ export const creativeContextMessagesByLocale: Record<
1133
1162
  chooseConnection: "वर्कस्पेस कनेक्शन चुनें",
1134
1163
  setupConnection: "स्रोत आयात करने से पहले इस प्रदाता को कनेक्ट करें।",
1135
1164
  connectProvider: "प्रदाता कनेक्ट करें",
1165
+ choosePresentations: "प्रेज़ेंटेशन चुनें",
1166
+ googlePickerDescription:
1167
+ "इस लाइब्रेरी कनेक्शन के लिए प्रेज़ेंटेशन चुनें। पूरे Drive की अनुमति नहीं मांगी जाती।",
1136
1168
  suggestions: "सुझावों की समीक्षा करें",
1137
1169
  logo: "मानक लोगो",
1138
1170
  },
@@ -1234,6 +1266,9 @@ export const creativeContextMessagesByLocale: Record<
1234
1266
  chooseConnection: "اختر اتصال مساحة عمل",
1235
1267
  setupConnection: "اربط هذا المزود قبل استيراد المصدر.",
1236
1268
  connectProvider: "ربط موفر الخدمة",
1269
+ choosePresentations: "اختيار العروض التقديمية",
1270
+ googlePickerDescription:
1271
+ "اختر العروض المتاحة لهذا الاتصال بالمكتبة. لا يتم طلب إذن للوصول إلى Drive بالكامل.",
1237
1272
  suggestions: "مراجعة الاقتراحات",
1238
1273
  logo: "الشعار المعتمد",
1239
1274
  },