@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.
- package/agent-native.package.json +11 -0
- package/dist/actions/continue-context-import.d.ts +16 -1
- package/dist/actions/continue-context-import.js +4 -1
- package/dist/actions/continue-context-import.js.map +1 -1
- package/dist/actions/get-creative-context.d.ts +38 -0
- package/dist/actions/get-creative-context.d.ts.map +1 -1
- package/dist/actions/get-creative-context.js +59 -23
- package/dist/actions/get-creative-context.js.map +1 -1
- package/dist/actions/get-google-picker-session.d.ts +10 -0
- package/dist/actions/get-google-picker-session.d.ts.map +1 -0
- package/dist/actions/get-google-picker-session.js +38 -0
- package/dist/actions/get-google-picker-session.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +2 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/start-context-enrichment.d.ts +16 -1
- package/dist/actions/start-context-enrichment.js +6 -4
- package/dist/actions/start-context-enrichment.js.map +1 -1
- package/dist/actions/start-context-import.d.ts +16 -1
- package/dist/actions/start-context-import.js +2 -1
- package/dist/actions/start-context-import.js.map +1 -1
- package/dist/client/CreativeContextPanel.d.ts +1 -59
- package/dist/client/CreativeContextPanel.d.ts.map +1 -1
- package/dist/client/CreativeContextPanel.js +111 -36
- package/dist/client/CreativeContextPanel.js.map +1 -1
- package/dist/client/actions.d.ts +8 -0
- package/dist/client/actions.d.ts.map +1 -1
- package/dist/client/actions.js +4 -0
- package/dist/client/actions.js.map +1 -1
- package/dist/client/google-slides-picker.d.ts +19 -0
- package/dist/client/google-slides-picker.d.ts.map +1 -0
- package/dist/client/google-slides-picker.js +106 -0
- package/dist/client/google-slides-picker.js.map +1 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +1 -1
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +22 -0
- package/dist/client/messages.js.map +1 -1
- package/dist/connectors/google-slides.d.ts +1 -1
- package/dist/connectors/google-slides.d.ts.map +1 -1
- package/dist/connectors/google-slides.js +1 -2
- package/dist/connectors/google-slides.js.map +1 -1
- package/dist/connectors/normalize.d.ts +7 -0
- package/dist/connectors/normalize.d.ts.map +1 -1
- package/dist/connectors/normalize.js +122 -3
- package/dist/connectors/normalize.js.map +1 -1
- package/dist/schema/index.d.ts +30 -30
- package/dist/server/enrichment.d.ts +1 -0
- package/dist/server/enrichment.d.ts.map +1 -1
- package/dist/server/enrichment.js +26 -24
- package/dist/server/enrichment.js.map +1 -1
- package/dist/server/generation-context.d.ts +1 -1
- package/dist/server/generation-context.d.ts.map +1 -1
- package/dist/server/generation-context.js +1 -1
- package/dist/server/generation-context.js.map +1 -1
- package/dist/store/content.d.ts +13 -0
- package/dist/store/content.d.ts.map +1 -1
- package/dist/store/content.js +172 -1
- package/dist/store/content.js.map +1 -1
- package/dist/store/jobs.d.ts.map +1 -1
- package/dist/store/jobs.js +2 -2
- package/dist/store/jobs.js.map +1 -1
- package/dist/store/purge.d.ts.map +1 -1
- package/dist/store/purge.js +9 -8
- package/dist/store/purge.js.map +1 -1
- package/dist/store/suggestions.d.ts +1 -0
- package/dist/store/suggestions.d.ts.map +1 -1
- package/dist/store/suggestions.js +56 -38
- package/dist/store/suggestions.js.map +1 -1
- package/docs/skills/creative-context/SKILL.md +8 -0
- package/package.json +3 -3
- package/src/actions/continue-context-import.ts +4 -1
- package/src/actions/creative-context-a2a.spec.ts +98 -0
- package/src/actions/get-creative-context.spec.ts +121 -1
- package/src/actions/get-creative-context.ts +78 -29
- package/src/actions/get-google-picker-session.spec.ts +62 -0
- package/src/actions/get-google-picker-session.ts +45 -0
- package/src/actions/index.ts +2 -0
- package/src/actions/job-public-serialization.spec.ts +122 -0
- package/src/actions/start-context-enrichment.ts +6 -4
- package/src/actions/start-context-import.ts +2 -1
- package/src/client/CreativeContextPanel.tsx +163 -64
- package/src/client/actions.ts +18 -0
- package/src/client/google-slides-picker.spec.ts +36 -0
- package/src/client/google-slides-picker.ts +146 -0
- package/src/client/index.ts +2 -0
- package/src/client/messages.ts +35 -0
- package/src/client/source-config.spec.ts +24 -10
- package/src/connectors/google-slides.ts +1 -2
- package/src/connectors/normalize.spec.ts +144 -0
- package/src/connectors/normalize.ts +201 -3
- package/src/server/enrichment.spec.ts +94 -0
- package/src/server/enrichment.ts +27 -24
- package/src/server/generation-artifact-access.spec.ts +104 -0
- package/src/server/generation-context-a2a.spec.ts +20 -2
- package/src/server/generation-context.ts +6 -6
- package/src/server/isolated-a2a.spec.ts +21 -1
- package/src/store/access.integration.spec.ts +511 -26
- package/src/store/content.ts +213 -1
- package/src/store/jobs.ts +2 -0
- package/src/store/purge.ts +18 -9
- 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: "
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1083
|
-
|
|
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
|
-
{
|
|
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
|
-
) :
|
|
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
|
-
|
|
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
|
-
{
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
<span className="
|
|
1731
|
-
|
|
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
|
-
|
|
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
|
) : (
|
package/src/client/actions.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -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,
|
package/src/client/messages.ts
CHANGED
|
@@ -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
|
},
|