@agent-native/core 0.137.2 → 0.137.4
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/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/analytics/actions/gong-calls.ts +37 -17
- package/corpus/templates/analytics/app/lib/data-sources.ts +1 -1
- package/corpus/templates/analytics/server/lib/gong.ts +51 -2
- package/corpus/templates/assets/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/brain/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/brain/app/routes/settings.tsx +1 -1
- package/corpus/templates/brain/server/register-secrets.ts +1 -1
- package/corpus/templates/calendar/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/chat/AGENTS.md +11 -0
- package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/chat/app/root.tsx +1 -1
- package/corpus/templates/chat/app/routes/agent.tsx +29 -13
- package/corpus/templates/chat/vite.config.ts +42 -0
- package/corpus/templates/clips/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/clips/app/components/library/empty-state.tsx +1 -3
- package/corpus/templates/clips/app/components/library/library-grid.tsx +2 -6
- package/corpus/templates/clips/app/components/library/library-layout.tsx +2 -6
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +1 -3
- package/corpus/templates/clips/app/routes/bug-report.tsx +1 -8
- package/corpus/templates/clips/app/routes/import.tsx +1 -3
- package/corpus/templates/clips/changelog/2026-08-04-recording-buttons-are-now-text-only-for-a-cleaner-clips-inte.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-04-save-settings-now-matches-the-other-clips-settings-buttons.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/styles.css +1 -0
- package/corpus/templates/content/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/crm/app/routes/settings.tsx +2 -2
- package/corpus/templates/design/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/design/actions/import-figma-clipboard.ts +1 -1
- package/corpus/templates/dispatch/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/dispatch/changelog/2026-08-04-dispatch-keeps-connected-chats-out-of-local-history-by-defau.md +6 -0
- package/corpus/templates/factory/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/factory/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/factory/README.md +1 -1
- package/corpus/templates/forms/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/macros/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/mail/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -2
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +1 -1
- package/corpus/templates/plan/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/slides/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +53 -1
- package/corpus/templates/slides/actions/generate-image-api.ts +15 -4
- package/corpus/templates/slides/actions/import-google-slides-reference.ts +83 -0
- package/corpus/templates/slides/actions/import-pptx.ts +134 -117
- package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +387 -0
- package/corpus/templates/slides/app/components/editor/ImageGenPanel.tsx +78 -48
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +377 -187
- package/corpus/templates/slides/app/components/layout/Sidebar.tsx +0 -1
- package/corpus/templates/slides/app/hooks/use-deck-design-system.ts +20 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +15 -0
- package/corpus/templates/slides/app/lib/new-deck-reference-selection.ts +22 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +4 -1
- package/corpus/templates/slides/app/pages/Index.tsx +172 -85
- package/corpus/templates/slides/server/lib/assets-image-delegation.ts +4 -2
- package/corpus/templates/slides/shared/api.ts +14 -1
- package/corpus/templates/tasks/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +53 -1
- package/dist/chat-threads/schema.d.ts +57 -0
- package/dist/chat-threads/schema.js +3 -0
- package/dist/chat-threads/store.d.ts +26 -0
- package/dist/chat-threads/store.js +84 -6
- package/dist/client/AgentPanel.js +11 -5
- package/dist/client/ConnectBuilderCard.js +1 -1
- package/dist/client/agent-page/AgentEmptyState.d.ts +2 -1
- package/dist/client/agent-page/AgentEmptyState.js +6 -2
- package/dist/client/agent-page/AgentJobsTab.d.ts +3 -1
- package/dist/client/agent-page/AgentJobsTab.js +55 -44
- package/dist/client/agent-page/AgentTabFrame.d.ts +2 -1
- package/dist/client/agent-page/AgentTabFrame.js +4 -2
- package/dist/client/agent-page/AgentTabsPage.js +12 -8
- package/dist/client/agent-page/AgentWorkspaceContent.d.ts +10 -0
- package/dist/client/agent-page/AgentWorkspaceContent.js +110 -0
- package/dist/client/agent-page/AutomationDetailsDialog.js +1 -1
- package/dist/client/chat/run-recovery.js +3 -3
- package/dist/client/error-format.js +1 -1
- package/dist/client/integrations/IntegrationsPanel.d.ts +1 -0
- package/dist/client/integrations/IntegrationsPanel.js +149 -5
- package/dist/client/onboarding/FirstRunOnboarding.js +130 -15
- package/dist/client/onboarding/index.d.ts +1 -1
- package/dist/client/onboarding/index.js +1 -1
- package/dist/client/onboarding/use-onboarding.js +12 -2
- package/dist/client/onboarding/use-preview-mode.d.ts +4 -0
- package/dist/client/onboarding/use-preview-mode.js +9 -0
- package/dist/client/resources/BuiltinCapabilityDetail.js +2 -2
- package/dist/client/resources/ResourceTree.d.ts +4 -1
- package/dist/client/resources/ResourceTree.js +13 -8
- package/dist/client/resources/ResourcesPanel.js +7 -3
- package/dist/client/resources/mcp-integration-catalog.js +4 -4
- package/dist/client/resources/use-mcp-servers.js +6 -6
- package/dist/client/settings/AgentsSection.js +15 -16
- package/dist/client/settings/AutomationsSection.js +1 -1
- package/dist/client/settings/DemoModeSection.js +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +3 -1
- package/dist/client/settings/SettingsPanel.js +284 -88
- package/dist/client/settings/SettingsRow.js +1 -1
- package/dist/client/settings/SettingsSection.d.ts +6 -4
- package/dist/client/settings/SettingsSection.js +6 -6
- package/dist/client/settings/SettingsTabsPage.js +47 -25
- package/dist/client/settings/VoiceTranscriptionSection.d.ts +3 -1
- package/dist/client/settings/VoiceTranscriptionSection.js +20 -8
- package/dist/client/settings/agent-settings-search.d.ts +1 -1
- package/dist/client/settings/agent-settings-search.js +8 -8
- package/dist/client/transcription/BuilderTranscriptionCta.js +2 -2
- package/dist/client/use-chat-threads.d.ts +9 -0
- package/dist/client/use-chat-threads.js +22 -6
- package/dist/collab/routes.d.ts +1 -1
- package/dist/integrations/google-docs-poller.js +9 -1
- package/dist/integrations/webhook-handler.js +11 -1
- package/dist/jobs/scheduler.d.ts +4 -1
- package/dist/jobs/scheduler.js +34 -9
- package/dist/localization/default-messages.js +40 -40
- package/dist/mcp-client/oauth-routes.js +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
- package/dist/provider-api/actions/provider-api.d.ts +15 -15
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/onboarding.js +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/browser-team-tools.d.ts +1 -0
- package/dist/server/agent-chat/browser-team-tools.js +1 -0
- package/dist/server/agent-chat-plugin.js +14 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-teams.d.ts +2 -0
- package/dist/server/agent-teams.js +2 -0
- package/dist/server/workspace-provider-oauth.js +1 -1
- package/dist/setup-connections/onboarding.js +2 -2
- package/dist/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +53 -1
- package/dist/templates/chat/AGENTS.md +11 -0
- package/dist/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/dist/templates/chat/app/root.tsx +1 -1
- package/dist/templates/chat/app/routes/agent.tsx +29 -13
- package/dist/templates/chat/vite.config.ts +42 -0
- package/dist/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +53 -1
- package/dist/templates/default/AGENTS.md +11 -0
- package/dist/templates/default/vite.config.ts +42 -0
- package/dist/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +53 -1
- package/dist/templates/workspace-core/AGENTS.md +11 -0
- package/dist/templates/workspace-root/AGENTS.md +14 -0
- package/dist/vite/client.js +56 -0
- package/package.json +2 -2
- package/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +53 -1
- package/src/templates/chat/AGENTS.md +11 -0
- package/src/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/src/templates/chat/app/root.tsx +1 -1
- package/src/templates/chat/app/routes/agent.tsx +29 -13
- package/src/templates/chat/vite.config.ts +42 -0
- package/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/src/templates/default/.agents/skills/frontend-design/SKILL.md +53 -1
- package/src/templates/default/AGENTS.md +11 -0
- package/src/templates/default/vite.config.ts +42 -0
- package/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
- package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +53 -1
- package/src/templates/workspace-core/AGENTS.md +11 -0
- package/src/templates/workspace-root/AGENTS.md +14 -0
|
@@ -60,6 +60,139 @@ async function uploadFirstSlideImage(
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
export async function importPptxBufferToDeck(args: {
|
|
64
|
+
fileBuffer: Buffer;
|
|
65
|
+
title?: string;
|
|
66
|
+
deckId?: string;
|
|
67
|
+
source?: string;
|
|
68
|
+
}): Promise<{
|
|
69
|
+
id: string;
|
|
70
|
+
title: string;
|
|
71
|
+
slideCount: number;
|
|
72
|
+
theme: Awaited<ReturnType<typeof parsePptx>>["theme"];
|
|
73
|
+
imported: true;
|
|
74
|
+
url: string;
|
|
75
|
+
imagesSkipped?: number;
|
|
76
|
+
}> {
|
|
77
|
+
const { fileBuffer, title, deckId, source = "import-pptx" } = args;
|
|
78
|
+
const presentation = await parsePptx(fileBuffer);
|
|
79
|
+
const deckTitle = title || presentation.title || "Imported Presentation";
|
|
80
|
+
const ownerEmail = getRequestUserEmail();
|
|
81
|
+
if (!ownerEmail) throw new Error("no authenticated user");
|
|
82
|
+
const themeFont = presentation.theme?.fonts?.[0];
|
|
83
|
+
|
|
84
|
+
// Check edit access before uploading any embedded images — uploads are
|
|
85
|
+
// a side effect with real storage cost, so an unauthorized caller must
|
|
86
|
+
// be rejected before that side effect happens, not after.
|
|
87
|
+
if (deckId) {
|
|
88
|
+
await assertAccess("deck", deckId, "editor");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Convert each parsed slide to our HTML format, uploading the first
|
|
92
|
+
// embedded image (if any) so it renders as a real image instead of a
|
|
93
|
+
// text placeholder. Concurrency is capped so a large deck doesn't fire
|
|
94
|
+
// one outbound upload per slide at once. An image can end up unused
|
|
95
|
+
// (unsupported format, or upload storage not configured) without
|
|
96
|
+
// failing the whole import — the slide's text still imports fine — but
|
|
97
|
+
// that shouldn't be a silent, invisible degradation, so it's counted
|
|
98
|
+
// and returned to the caller.
|
|
99
|
+
const uploadLimit = pLimit(4);
|
|
100
|
+
const results = await Promise.all(
|
|
101
|
+
presentation.slides.map((parsedSlide, i) =>
|
|
102
|
+
uploadLimit(async () => {
|
|
103
|
+
const imageUrl = await uploadFirstSlideImage(
|
|
104
|
+
parsedSlide,
|
|
105
|
+
i,
|
|
106
|
+
ownerEmail,
|
|
107
|
+
);
|
|
108
|
+
const html = convertToSlideHtml(parsedSlide, imageUrl, themeFont);
|
|
109
|
+
return {
|
|
110
|
+
slide: {
|
|
111
|
+
id: `slide-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
112
|
+
content: html,
|
|
113
|
+
layout: parsedSlide.layoutHint ?? "content",
|
|
114
|
+
notes: parsedSlide.notes,
|
|
115
|
+
},
|
|
116
|
+
// Only the first image on a slide is ever uploaded, so every
|
|
117
|
+
// other image on that slide is unconditionally dropped too —
|
|
118
|
+
// not just the first one when it's unsupported.
|
|
119
|
+
imageSkippedCount: Math.max(
|
|
120
|
+
0,
|
|
121
|
+
parsedSlide.images.length - (imageUrl ? 1 : 0),
|
|
122
|
+
),
|
|
123
|
+
};
|
|
124
|
+
}),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
const slides = results.map((r) => r.slide);
|
|
128
|
+
const imagesSkipped = results.reduce(
|
|
129
|
+
(total, r) => total + r.imageSkippedCount,
|
|
130
|
+
0,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const db = getDb();
|
|
134
|
+
const now = new Date().toISOString();
|
|
135
|
+
|
|
136
|
+
if (deckId) {
|
|
137
|
+
const existing = await db
|
|
138
|
+
.select()
|
|
139
|
+
.from(schema.decks)
|
|
140
|
+
.where(eq(schema.decks.id, deckId));
|
|
141
|
+
|
|
142
|
+
if (!existing.length) {
|
|
143
|
+
throw new Error(`Deck ${deckId} not found`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const data = { title: deckTitle, slides, updatedAt: now };
|
|
147
|
+
await db
|
|
148
|
+
.update(schema.decks)
|
|
149
|
+
.set({ title: deckTitle, data: JSON.stringify(data), updatedAt: now })
|
|
150
|
+
.where(eq(schema.decks.id, deckId));
|
|
151
|
+
|
|
152
|
+
notifyClients(deckId);
|
|
153
|
+
await writeAppState("refresh-signal", {
|
|
154
|
+
ts: now,
|
|
155
|
+
source,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
id: deckId,
|
|
160
|
+
title: deckTitle,
|
|
161
|
+
slideCount: slides.length,
|
|
162
|
+
theme: presentation.theme,
|
|
163
|
+
imported: true,
|
|
164
|
+
url: getDeckUrl(deckId),
|
|
165
|
+
...(imagesSkipped > 0 ? { imagesSkipped } : {}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Create new deck
|
|
170
|
+
const id = `deck-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
171
|
+
const data = { title: deckTitle, slides, createdAt: now, updatedAt: now };
|
|
172
|
+
await db.insert(schema.decks).values({
|
|
173
|
+
id,
|
|
174
|
+
title: deckTitle,
|
|
175
|
+
data: JSON.stringify(data),
|
|
176
|
+
ownerEmail,
|
|
177
|
+
orgId: getRequestOrgId(),
|
|
178
|
+
createdAt: now,
|
|
179
|
+
updatedAt: now,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
notifyClients(id);
|
|
183
|
+
await writeAppState("refresh-signal", { ts: now, source });
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
id,
|
|
187
|
+
title: deckTitle,
|
|
188
|
+
slideCount: slides.length,
|
|
189
|
+
theme: presentation.theme,
|
|
190
|
+
imported: true,
|
|
191
|
+
url: getDeckUrl(id),
|
|
192
|
+
...(imagesSkipped > 0 ? { imagesSkipped } : {}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
63
196
|
export default defineAction({
|
|
64
197
|
description:
|
|
65
198
|
"Import a PPTX file and create a slide deck from it. " +
|
|
@@ -85,122 +218,6 @@ export default defineAction({
|
|
|
85
218
|
}),
|
|
86
219
|
run: async ({ filePath, deckId, title }) => {
|
|
87
220
|
const { data: fileBuffer } = await readUserUploadedFile(filePath);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const deckTitle = title || presentation.title || "Imported Presentation";
|
|
91
|
-
const ownerEmail = getRequestUserEmail();
|
|
92
|
-
if (!ownerEmail) throw new Error("no authenticated user");
|
|
93
|
-
const themeFont = presentation.theme?.fonts?.[0];
|
|
94
|
-
|
|
95
|
-
// Check edit access before uploading any embedded images — uploads are
|
|
96
|
-
// a side effect with real storage cost, so an unauthorized caller must
|
|
97
|
-
// be rejected before that side effect happens, not after.
|
|
98
|
-
if (deckId) {
|
|
99
|
-
await assertAccess("deck", deckId, "editor");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Convert each parsed slide to our HTML format, uploading the first
|
|
103
|
-
// embedded image (if any) so it renders as a real image instead of a
|
|
104
|
-
// text placeholder. Concurrency is capped so a large deck doesn't fire
|
|
105
|
-
// one outbound upload per slide at once. An image can end up unused
|
|
106
|
-
// (unsupported format, or upload storage not configured) without
|
|
107
|
-
// failing the whole import — the slide's text still imports fine — but
|
|
108
|
-
// that shouldn't be a silent, invisible degradation, so it's counted
|
|
109
|
-
// and returned to the caller.
|
|
110
|
-
const uploadLimit = pLimit(4);
|
|
111
|
-
const results = await Promise.all(
|
|
112
|
-
presentation.slides.map((parsedSlide, i) =>
|
|
113
|
-
uploadLimit(async () => {
|
|
114
|
-
const imageUrl = await uploadFirstSlideImage(
|
|
115
|
-
parsedSlide,
|
|
116
|
-
i,
|
|
117
|
-
ownerEmail,
|
|
118
|
-
);
|
|
119
|
-
const html = convertToSlideHtml(parsedSlide, imageUrl, themeFont);
|
|
120
|
-
return {
|
|
121
|
-
slide: {
|
|
122
|
-
id: `slide-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
123
|
-
content: html,
|
|
124
|
-
layout: parsedSlide.layoutHint ?? "content",
|
|
125
|
-
notes: parsedSlide.notes,
|
|
126
|
-
},
|
|
127
|
-
// Only the first image on a slide is ever uploaded, so every
|
|
128
|
-
// other image on that slide is unconditionally dropped too —
|
|
129
|
-
// not just the first one when it's unsupported.
|
|
130
|
-
imageSkippedCount: Math.max(
|
|
131
|
-
0,
|
|
132
|
-
parsedSlide.images.length - (imageUrl ? 1 : 0),
|
|
133
|
-
),
|
|
134
|
-
};
|
|
135
|
-
}),
|
|
136
|
-
),
|
|
137
|
-
);
|
|
138
|
-
const slides = results.map((r) => r.slide);
|
|
139
|
-
const imagesSkipped = results.reduce(
|
|
140
|
-
(total, r) => total + r.imageSkippedCount,
|
|
141
|
-
0,
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
const db = getDb();
|
|
145
|
-
const now = new Date().toISOString();
|
|
146
|
-
|
|
147
|
-
if (deckId) {
|
|
148
|
-
const existing = await db
|
|
149
|
-
.select()
|
|
150
|
-
.from(schema.decks)
|
|
151
|
-
.where(eq(schema.decks.id, deckId));
|
|
152
|
-
|
|
153
|
-
if (!existing.length) {
|
|
154
|
-
throw new Error(`Deck ${deckId} not found`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const data = { title: deckTitle, slides, updatedAt: now };
|
|
158
|
-
await db
|
|
159
|
-
.update(schema.decks)
|
|
160
|
-
.set({ title: deckTitle, data: JSON.stringify(data), updatedAt: now })
|
|
161
|
-
.where(eq(schema.decks.id, deckId));
|
|
162
|
-
|
|
163
|
-
notifyClients(deckId);
|
|
164
|
-
await writeAppState("refresh-signal", {
|
|
165
|
-
ts: now,
|
|
166
|
-
source: "import-pptx",
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
id: deckId,
|
|
171
|
-
title: deckTitle,
|
|
172
|
-
slideCount: slides.length,
|
|
173
|
-
theme: presentation.theme,
|
|
174
|
-
imported: true,
|
|
175
|
-
url: getDeckUrl(deckId),
|
|
176
|
-
...(imagesSkipped > 0 ? { imagesSkipped } : {}),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Create new deck
|
|
181
|
-
const id = `deck-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
182
|
-
const data = { title: deckTitle, slides, createdAt: now, updatedAt: now };
|
|
183
|
-
await db.insert(schema.decks).values({
|
|
184
|
-
id,
|
|
185
|
-
title: deckTitle,
|
|
186
|
-
data: JSON.stringify(data),
|
|
187
|
-
ownerEmail,
|
|
188
|
-
orgId: getRequestOrgId(),
|
|
189
|
-
createdAt: now,
|
|
190
|
-
updatedAt: now,
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
notifyClients(id);
|
|
194
|
-
await writeAppState("refresh-signal", { ts: now, source: "import-pptx" });
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
id,
|
|
198
|
-
title: deckTitle,
|
|
199
|
-
slideCount: slides.length,
|
|
200
|
-
theme: presentation.theme,
|
|
201
|
-
imported: true,
|
|
202
|
-
url: getDeckUrl(id),
|
|
203
|
-
...(imagesSkipped > 0 ? { imagesSkipped } : {}),
|
|
204
|
-
};
|
|
221
|
+
return importPptxBufferToDeck({ fileBuffer, deckId, title });
|
|
205
222
|
},
|
|
206
223
|
});
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { agentNativePath } from "@agent-native/core/client/api-path";
|
|
2
|
+
import { useActionMutation } from "@agent-native/core/client/hooks";
|
|
3
|
+
import { oauthRedirectUri } from "@agent-native/core/client/host";
|
|
4
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
5
|
+
import {
|
|
6
|
+
IconBrandGoogleDrive,
|
|
7
|
+
IconFolderOpen,
|
|
8
|
+
IconLoader2,
|
|
9
|
+
IconPlugConnected,
|
|
10
|
+
} from "@tabler/icons-react";
|
|
11
|
+
import { useCallback, useEffect, useState } from "react";
|
|
12
|
+
|
|
13
|
+
declare global {
|
|
14
|
+
interface Window {
|
|
15
|
+
gapi?: any;
|
|
16
|
+
google?: any;
|
|
17
|
+
__googlePickerScriptPromise?: Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface GoogleDocsStatus {
|
|
22
|
+
configured: boolean;
|
|
23
|
+
connected: boolean;
|
|
24
|
+
pickerConfigured: boolean;
|
|
25
|
+
accounts: Array<{ email: string; scope?: string }>;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface PickerToken {
|
|
30
|
+
accessToken: string;
|
|
31
|
+
accountEmail: string;
|
|
32
|
+
apiKey: string;
|
|
33
|
+
appId: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
message?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface GoogleSlidesReferenceImportProps {
|
|
39
|
+
onImported: (imported: { id: string; title: string }) => void | Promise<void>;
|
|
40
|
+
onBusyChange?: (busy: boolean) => void;
|
|
41
|
+
title: string;
|
|
42
|
+
chooseLabel: string;
|
|
43
|
+
connectLabel: string;
|
|
44
|
+
pickingLabel: string;
|
|
45
|
+
connectedLabel: string;
|
|
46
|
+
unavailableLabel: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function endpoint(path: string): string {
|
|
50
|
+
return new URL(agentNativePath(path), window.location.origin).toString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type JsonReadResult<T> = { ok: true; data: T } | { ok: false; error: Error };
|
|
54
|
+
|
|
55
|
+
async function readJson<T>(response: Response): Promise<JsonReadResult<T>> {
|
|
56
|
+
try {
|
|
57
|
+
return { ok: true, data: (await response.json()) as T };
|
|
58
|
+
} catch (caught) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: new Error("The server returned an unreadable response.", {
|
|
62
|
+
cause: caught,
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function errorFromResponse(
|
|
69
|
+
response: Response,
|
|
70
|
+
data: { error?: string; message?: string },
|
|
71
|
+
fallback: string,
|
|
72
|
+
): string {
|
|
73
|
+
return data.message || data.error || `${fallback} (${response.status})`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function loadGooglePickerScript(): Promise<void> {
|
|
77
|
+
if (window.gapi) return Promise.resolve();
|
|
78
|
+
if (!window.__googlePickerScriptPromise) {
|
|
79
|
+
window.__googlePickerScriptPromise = new Promise((resolve, reject) => {
|
|
80
|
+
const script = document.createElement("script");
|
|
81
|
+
script.src = "https://apis.google.com/js/api.js";
|
|
82
|
+
script.async = true;
|
|
83
|
+
script.onload = () => resolve();
|
|
84
|
+
script.onerror = () => reject(new Error("Could not load Google Picker."));
|
|
85
|
+
document.head.appendChild(script);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return window.__googlePickerScriptPromise;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function loadPickerApi(): Promise<void> {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const gapi = window.gapi;
|
|
94
|
+
if (!gapi) {
|
|
95
|
+
reject(new Error("Google Picker did not load."));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
gapi.load("picker", {
|
|
99
|
+
callback: () => resolve(),
|
|
100
|
+
onerror: () => reject(new Error("Could not load Google Picker.")),
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function GoogleSlidesReferenceImport({
|
|
106
|
+
onImported,
|
|
107
|
+
onBusyChange,
|
|
108
|
+
title,
|
|
109
|
+
chooseLabel,
|
|
110
|
+
connectLabel,
|
|
111
|
+
pickingLabel,
|
|
112
|
+
connectedLabel,
|
|
113
|
+
unavailableLabel,
|
|
114
|
+
}: GoogleSlidesReferenceImportProps) {
|
|
115
|
+
const t = useT();
|
|
116
|
+
const [status, setStatus] = useState<GoogleDocsStatus | null>(null);
|
|
117
|
+
const [error, setError] = useState<string | null>(null);
|
|
118
|
+
const [connecting, setConnecting] = useState(false);
|
|
119
|
+
const [choosing, setChoosing] = useState(false);
|
|
120
|
+
const importMutation = useActionMutation("import-google-slides-reference");
|
|
121
|
+
|
|
122
|
+
const refreshStatus =
|
|
123
|
+
useCallback(async (): Promise<GoogleDocsStatus | null> => {
|
|
124
|
+
const response = await fetch(
|
|
125
|
+
endpoint("/_agent-native/google-docs/status"),
|
|
126
|
+
{
|
|
127
|
+
credentials: "same-origin",
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
const result = await readJson<GoogleDocsStatus>(response);
|
|
131
|
+
if (!result.ok) {
|
|
132
|
+
setStatus(null);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (response.ok && result.data) {
|
|
136
|
+
setStatus(result.data);
|
|
137
|
+
return result.data;
|
|
138
|
+
}
|
|
139
|
+
setStatus(null);
|
|
140
|
+
return null;
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
void refreshStatus();
|
|
145
|
+
}, [refreshStatus]);
|
|
146
|
+
|
|
147
|
+
const reportBusy = useCallback(
|
|
148
|
+
(busy: boolean) => {
|
|
149
|
+
onBusyChange?.(busy);
|
|
150
|
+
},
|
|
151
|
+
[onBusyChange],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const connect = useCallback(async () => {
|
|
155
|
+
setConnecting(true);
|
|
156
|
+
reportBusy(true);
|
|
157
|
+
setError(null);
|
|
158
|
+
const popup = window.open(
|
|
159
|
+
"about:blank",
|
|
160
|
+
"google-docs-oauth",
|
|
161
|
+
"popup,width=520,height=720",
|
|
162
|
+
);
|
|
163
|
+
try {
|
|
164
|
+
const callbackUrl = oauthRedirectUri(
|
|
165
|
+
"/_agent-native/google-docs/callback",
|
|
166
|
+
);
|
|
167
|
+
const authUrl = new URL(endpoint("/_agent-native/google-docs/auth-url"));
|
|
168
|
+
authUrl.searchParams.set("redirect_uri", callbackUrl);
|
|
169
|
+
authUrl.searchParams.set(
|
|
170
|
+
"return",
|
|
171
|
+
window.location.pathname + window.location.search,
|
|
172
|
+
);
|
|
173
|
+
const response = await fetch(authUrl.toString(), {
|
|
174
|
+
credentials: "same-origin",
|
|
175
|
+
});
|
|
176
|
+
const result = await readJson<{
|
|
177
|
+
url?: string;
|
|
178
|
+
error?: string;
|
|
179
|
+
message?: string;
|
|
180
|
+
}>(response);
|
|
181
|
+
if (!result.ok) throw result.error;
|
|
182
|
+
const data = result.data;
|
|
183
|
+
if (!response.ok || !data?.url) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
errorFromResponse(
|
|
186
|
+
response,
|
|
187
|
+
data ?? {},
|
|
188
|
+
"Could not start Google OAuth",
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (!popup) {
|
|
193
|
+
window.location.href = data.url;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
popup.location.href = data.url;
|
|
197
|
+
|
|
198
|
+
const deadline = Date.now() + 90_000;
|
|
199
|
+
while (Date.now() < deadline && !popup.closed) {
|
|
200
|
+
await new Promise((resolve) => window.setTimeout(resolve, 1200));
|
|
201
|
+
const next = await refreshStatus();
|
|
202
|
+
if (next?.connected) {
|
|
203
|
+
popup.close();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
await refreshStatus();
|
|
208
|
+
} catch (caught) {
|
|
209
|
+
popup?.close();
|
|
210
|
+
setError(caught instanceof Error ? caught.message : String(caught));
|
|
211
|
+
} finally {
|
|
212
|
+
setConnecting(false);
|
|
213
|
+
reportBusy(false);
|
|
214
|
+
}
|
|
215
|
+
}, [refreshStatus, reportBusy]);
|
|
216
|
+
|
|
217
|
+
const chooseDocument = useCallback(async () => {
|
|
218
|
+
setChoosing(true);
|
|
219
|
+
reportBusy(true);
|
|
220
|
+
setError(null);
|
|
221
|
+
try {
|
|
222
|
+
const response = await fetch(
|
|
223
|
+
endpoint("/_agent-native/google-docs/picker-token"),
|
|
224
|
+
{
|
|
225
|
+
credentials: "same-origin",
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
const result = await readJson<PickerToken>(response);
|
|
229
|
+
if (!result.ok) throw result.error;
|
|
230
|
+
const token = result.data;
|
|
231
|
+
if (!response.ok || !token) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
errorFromResponse(
|
|
234
|
+
response,
|
|
235
|
+
token ?? {},
|
|
236
|
+
"Could not open Google Picker",
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await loadGooglePickerScript();
|
|
242
|
+
await loadPickerApi();
|
|
243
|
+
|
|
244
|
+
const google = window.google;
|
|
245
|
+
if (!google?.picker) {
|
|
246
|
+
throw new Error("Google Picker is unavailable.");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await new Promise<void>((resolve, reject) => {
|
|
250
|
+
const view = new google.picker.DocsView(
|
|
251
|
+
google.picker.ViewId.PRESENTATIONS,
|
|
252
|
+
)
|
|
253
|
+
.setMimeTypes("application/vnd.google-apps.presentation")
|
|
254
|
+
.setSelectFolderEnabled(false);
|
|
255
|
+
const picker = new google.picker.PickerBuilder()
|
|
256
|
+
.addView(view)
|
|
257
|
+
.setOAuthToken(token.accessToken)
|
|
258
|
+
.setDeveloperKey(token.apiKey)
|
|
259
|
+
.setAppId(token.appId)
|
|
260
|
+
.setTitle("Choose a Google Slides deck")
|
|
261
|
+
.setCallback((data: any) => {
|
|
262
|
+
if (data.action === google.picker.Action.CANCEL) {
|
|
263
|
+
resolve();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (data.action !== google.picker.Action.PICKED) return;
|
|
267
|
+
const deck = data.docs?.[0];
|
|
268
|
+
if (!deck?.id) {
|
|
269
|
+
reject(new Error("Google Picker returned no deck."));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
void importMutation
|
|
273
|
+
.mutateAsync({
|
|
274
|
+
fileId: deck.id,
|
|
275
|
+
...(typeof deck.name === "string" && deck.name
|
|
276
|
+
? { title: deck.name }
|
|
277
|
+
: {}),
|
|
278
|
+
})
|
|
279
|
+
.then(async (imported) => {
|
|
280
|
+
if (!imported || typeof imported.id !== "string") {
|
|
281
|
+
throw new Error(
|
|
282
|
+
"The Google Slides import did not create a deck.",
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
await onImported({
|
|
286
|
+
id: imported.id,
|
|
287
|
+
title:
|
|
288
|
+
typeof imported.title === "string" && imported.title
|
|
289
|
+
? imported.title
|
|
290
|
+
: deck.name || "Imported Google Slides",
|
|
291
|
+
});
|
|
292
|
+
resolve();
|
|
293
|
+
})
|
|
294
|
+
.catch(reject);
|
|
295
|
+
})
|
|
296
|
+
.build();
|
|
297
|
+
picker.setVisible(true);
|
|
298
|
+
});
|
|
299
|
+
} catch (caught) {
|
|
300
|
+
setError(caught instanceof Error ? caught.message : String(caught));
|
|
301
|
+
} finally {
|
|
302
|
+
setChoosing(false);
|
|
303
|
+
reportBusy(false);
|
|
304
|
+
}
|
|
305
|
+
}, [importMutation, onImported, reportBusy]);
|
|
306
|
+
|
|
307
|
+
const connectedAccount = status?.accounts?.[0]?.email;
|
|
308
|
+
const needsConnect = status && !status.connected;
|
|
309
|
+
const canPick = !!status?.connected && !!status.pickerConfigured;
|
|
310
|
+
const pickerMissing = !!status?.connected && !status.pickerConfigured;
|
|
311
|
+
const configured = status?.configured !== false;
|
|
312
|
+
const unavailable = !status || !configured || pickerMissing || needsConnect;
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<div className="rounded-lg border border-border/70 bg-muted/35 px-3 py-3 text-xs text-muted-foreground">
|
|
316
|
+
<div className="flex items-start gap-2">
|
|
317
|
+
<div className="mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-md border border-border/70 bg-background">
|
|
318
|
+
{connecting || choosing ? (
|
|
319
|
+
<IconLoader2 className="size-3.5 animate-spin text-muted-foreground" />
|
|
320
|
+
) : (
|
|
321
|
+
<IconBrandGoogleDrive className="size-3.5 text-primary" />
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
<div className="min-w-0 flex-1">
|
|
325
|
+
<div className="flex items-center gap-2">
|
|
326
|
+
<p className="truncate text-[12px] font-medium text-foreground">
|
|
327
|
+
{title}
|
|
328
|
+
</p>
|
|
329
|
+
{connectedAccount && (
|
|
330
|
+
<span className="hidden truncate text-[10px] text-muted-foreground/80 sm:inline">
|
|
331
|
+
{connectedAccount}
|
|
332
|
+
</span>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
<p className="mt-0.5 line-clamp-2">
|
|
336
|
+
{error ? error : unavailable ? unavailableLabel : connectedLabel}
|
|
337
|
+
</p>
|
|
338
|
+
{!configured && (
|
|
339
|
+
<p className="mt-1 text-[11px] text-amber-500">
|
|
340
|
+
{t("raw.googleOAuthNotConfigured")}
|
|
341
|
+
</p>
|
|
342
|
+
)}
|
|
343
|
+
{pickerMissing && (
|
|
344
|
+
<p className="mt-1 text-[11px] text-amber-500">
|
|
345
|
+
{t("raw.googlePickerNeedsKeys")}
|
|
346
|
+
</p>
|
|
347
|
+
)}
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
{(needsConnect || canPick) && (
|
|
352
|
+
<div className="mt-2 flex flex-wrap items-center gap-2 pl-8">
|
|
353
|
+
{needsConnect && (
|
|
354
|
+
<button
|
|
355
|
+
type="button"
|
|
356
|
+
onClick={() => void connect()}
|
|
357
|
+
disabled={connecting || !configured}
|
|
358
|
+
className="inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-md border border-border bg-background px-2 text-[11px] font-medium text-foreground hover:bg-accent disabled:cursor-default disabled:opacity-60"
|
|
359
|
+
>
|
|
360
|
+
{connecting ? (
|
|
361
|
+
<IconLoader2 className="size-3.5 animate-spin" />
|
|
362
|
+
) : (
|
|
363
|
+
<IconPlugConnected className="size-3.5" />
|
|
364
|
+
)}
|
|
365
|
+
{connecting ? pickingLabel : connectLabel}
|
|
366
|
+
</button>
|
|
367
|
+
)}
|
|
368
|
+
{canPick && (
|
|
369
|
+
<button
|
|
370
|
+
type="button"
|
|
371
|
+
onClick={() => void chooseDocument()}
|
|
372
|
+
disabled={choosing}
|
|
373
|
+
className="inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-md border border-border bg-background px-2 text-[11px] font-medium text-foreground hover:bg-accent disabled:cursor-default disabled:opacity-60"
|
|
374
|
+
>
|
|
375
|
+
{choosing ? (
|
|
376
|
+
<IconLoader2 className="size-3.5 animate-spin" />
|
|
377
|
+
) : (
|
|
378
|
+
<IconFolderOpen className="size-3.5" />
|
|
379
|
+
)}
|
|
380
|
+
{choosing ? pickingLabel : chooseLabel}
|
|
381
|
+
</button>
|
|
382
|
+
)}
|
|
383
|
+
</div>
|
|
384
|
+
)}
|
|
385
|
+
</div>
|
|
386
|
+
);
|
|
387
|
+
}
|