@agent-native/core 0.137.3 → 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.
Files changed (178) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  3. package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +53 -1
  4. package/corpus/templates/analytics/actions/gong-calls.ts +37 -17
  5. package/corpus/templates/analytics/app/lib/data-sources.ts +1 -1
  6. package/corpus/templates/analytics/server/lib/gong.ts +51 -2
  7. package/corpus/templates/assets/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  8. package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +53 -1
  9. package/corpus/templates/brain/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  10. package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +53 -1
  11. package/corpus/templates/brain/app/routes/settings.tsx +1 -1
  12. package/corpus/templates/brain/server/register-secrets.ts +1 -1
  13. package/corpus/templates/calendar/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  14. package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +53 -1
  15. package/corpus/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  16. package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +53 -1
  17. package/corpus/templates/chat/AGENTS.md +11 -0
  18. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
  19. package/corpus/templates/chat/app/root.tsx +1 -1
  20. package/corpus/templates/chat/app/routes/agent.tsx +29 -13
  21. package/corpus/templates/chat/vite.config.ts +42 -0
  22. package/corpus/templates/clips/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  23. package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +53 -1
  24. package/corpus/templates/clips/app/components/library/empty-state.tsx +1 -3
  25. package/corpus/templates/clips/app/components/library/library-grid.tsx +2 -6
  26. package/corpus/templates/clips/app/components/library/library-layout.tsx +2 -6
  27. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +1 -3
  28. package/corpus/templates/clips/app/routes/bug-report.tsx +1 -8
  29. package/corpus/templates/clips/app/routes/import.tsx +1 -3
  30. package/corpus/templates/clips/changelog/2026-08-04-recording-buttons-are-now-text-only-for-a-cleaner-clips-inte.md +6 -0
  31. package/corpus/templates/clips/changelog/2026-08-04-save-settings-now-matches-the-other-clips-settings-buttons.md +6 -0
  32. package/corpus/templates/clips/chrome-extension/src/styles.css +1 -0
  33. package/corpus/templates/content/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  34. package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +53 -1
  35. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  36. package/corpus/templates/crm/app/routes/settings.tsx +2 -2
  37. package/corpus/templates/design/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  38. package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +53 -1
  39. package/corpus/templates/design/actions/import-figma-clipboard.ts +1 -1
  40. package/corpus/templates/dispatch/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  41. package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +53 -1
  42. package/corpus/templates/dispatch/changelog/2026-08-04-dispatch-keeps-connected-chats-out-of-local-history-by-defau.md +6 -0
  43. package/corpus/templates/factory/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  44. package/corpus/templates/factory/.agents/skills/frontend-design/SKILL.md +53 -1
  45. package/corpus/templates/factory/README.md +1 -1
  46. package/corpus/templates/forms/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  47. package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +53 -1
  48. package/corpus/templates/macros/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  49. package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +53 -1
  50. package/corpus/templates/mail/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  51. package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +53 -1
  52. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -2
  53. package/corpus/templates/mail/app/pages/SettingsPage.tsx +1 -1
  54. package/corpus/templates/plan/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  55. package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +53 -1
  56. package/corpus/templates/slides/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  57. package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +53 -1
  58. package/corpus/templates/slides/actions/generate-image-api.ts +15 -4
  59. package/corpus/templates/slides/actions/import-google-slides-reference.ts +83 -0
  60. package/corpus/templates/slides/actions/import-pptx.ts +134 -117
  61. package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +387 -0
  62. package/corpus/templates/slides/app/components/editor/ImageGenPanel.tsx +78 -48
  63. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +377 -187
  64. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +0 -1
  65. package/corpus/templates/slides/app/hooks/use-deck-design-system.ts +20 -1
  66. package/corpus/templates/slides/app/i18n/en-US.ts +15 -0
  67. package/corpus/templates/slides/app/lib/new-deck-reference-selection.ts +22 -0
  68. package/corpus/templates/slides/app/pages/DeckEditor.tsx +4 -1
  69. package/corpus/templates/slides/app/pages/Index.tsx +172 -85
  70. package/corpus/templates/slides/server/lib/assets-image-delegation.ts +4 -2
  71. package/corpus/templates/slides/shared/api.ts +14 -1
  72. package/corpus/templates/tasks/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  73. package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +53 -1
  74. package/dist/chat-threads/schema.d.ts +57 -0
  75. package/dist/chat-threads/schema.js +3 -0
  76. package/dist/chat-threads/store.d.ts +26 -0
  77. package/dist/chat-threads/store.js +84 -6
  78. package/dist/client/AgentPanel.js +11 -5
  79. package/dist/client/ConnectBuilderCard.js +1 -1
  80. package/dist/client/agent-page/AgentEmptyState.d.ts +2 -1
  81. package/dist/client/agent-page/AgentEmptyState.js +6 -2
  82. package/dist/client/agent-page/AgentJobsTab.d.ts +3 -1
  83. package/dist/client/agent-page/AgentJobsTab.js +55 -44
  84. package/dist/client/agent-page/AgentTabFrame.d.ts +2 -1
  85. package/dist/client/agent-page/AgentTabFrame.js +4 -2
  86. package/dist/client/agent-page/AgentTabsPage.js +12 -8
  87. package/dist/client/agent-page/AgentWorkspaceContent.d.ts +10 -0
  88. package/dist/client/agent-page/AgentWorkspaceContent.js +110 -0
  89. package/dist/client/agent-page/AutomationDetailsDialog.js +1 -1
  90. package/dist/client/chat/run-recovery.js +3 -3
  91. package/dist/client/error-format.js +1 -1
  92. package/dist/client/integrations/IntegrationsPanel.d.ts +1 -0
  93. package/dist/client/integrations/IntegrationsPanel.js +149 -5
  94. package/dist/client/onboarding/FirstRunOnboarding.js +130 -15
  95. package/dist/client/onboarding/index.d.ts +1 -1
  96. package/dist/client/onboarding/index.js +1 -1
  97. package/dist/client/onboarding/use-onboarding.js +12 -2
  98. package/dist/client/onboarding/use-preview-mode.d.ts +4 -0
  99. package/dist/client/onboarding/use-preview-mode.js +9 -0
  100. package/dist/client/resources/BuiltinCapabilityDetail.js +2 -2
  101. package/dist/client/resources/ResourceTree.d.ts +4 -1
  102. package/dist/client/resources/ResourceTree.js +13 -8
  103. package/dist/client/resources/ResourcesPanel.js +7 -3
  104. package/dist/client/resources/mcp-integration-catalog.js +4 -4
  105. package/dist/client/resources/use-mcp-servers.js +6 -6
  106. package/dist/client/settings/AgentsSection.js +15 -16
  107. package/dist/client/settings/AutomationsSection.js +1 -1
  108. package/dist/client/settings/DemoModeSection.js +1 -1
  109. package/dist/client/settings/SettingsPanel.d.ts +3 -1
  110. package/dist/client/settings/SettingsPanel.js +284 -88
  111. package/dist/client/settings/SettingsRow.js +1 -1
  112. package/dist/client/settings/SettingsSection.d.ts +6 -4
  113. package/dist/client/settings/SettingsSection.js +6 -6
  114. package/dist/client/settings/SettingsTabsPage.js +47 -25
  115. package/dist/client/settings/VoiceTranscriptionSection.d.ts +3 -1
  116. package/dist/client/settings/VoiceTranscriptionSection.js +20 -8
  117. package/dist/client/settings/agent-settings-search.d.ts +1 -1
  118. package/dist/client/settings/agent-settings-search.js +8 -8
  119. package/dist/client/transcription/BuilderTranscriptionCta.js +2 -2
  120. package/dist/client/use-chat-threads.d.ts +9 -0
  121. package/dist/client/use-chat-threads.js +22 -6
  122. package/dist/collab/awareness.d.ts +2 -2
  123. package/dist/collab/routes.d.ts +1 -1
  124. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  125. package/dist/integrations/google-docs-poller.js +9 -1
  126. package/dist/integrations/webhook-handler.js +11 -1
  127. package/dist/jobs/scheduler.d.ts +4 -1
  128. package/dist/jobs/scheduler.js +34 -9
  129. package/dist/localization/default-messages.js +40 -40
  130. package/dist/mcp-client/oauth-routes.js +1 -1
  131. package/dist/notifications/routes.d.ts +1 -1
  132. package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
  133. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  134. package/dist/secrets/onboarding.js +1 -1
  135. package/dist/secrets/routes.d.ts +6 -6
  136. package/dist/server/agent-chat/browser-team-tools.d.ts +1 -0
  137. package/dist/server/agent-chat/browser-team-tools.js +1 -0
  138. package/dist/server/agent-chat-plugin.js +14 -1
  139. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  140. package/dist/server/agent-teams.d.ts +2 -0
  141. package/dist/server/agent-teams.js +2 -0
  142. package/dist/server/realtime-token.d.ts +1 -1
  143. package/dist/server/workspace-provider-oauth.js +1 -1
  144. package/dist/setup-connections/onboarding.js +2 -2
  145. package/dist/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  146. package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +53 -1
  147. package/dist/templates/chat/AGENTS.md +11 -0
  148. package/dist/templates/chat/app/components/layout/Layout.tsx +1 -1
  149. package/dist/templates/chat/app/root.tsx +1 -1
  150. package/dist/templates/chat/app/routes/agent.tsx +29 -13
  151. package/dist/templates/chat/vite.config.ts +42 -0
  152. package/dist/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  153. package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +53 -1
  154. package/dist/templates/default/AGENTS.md +11 -0
  155. package/dist/templates/default/vite.config.ts +42 -0
  156. package/dist/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  157. package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  158. package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +53 -1
  159. package/dist/templates/workspace-core/AGENTS.md +11 -0
  160. package/dist/templates/workspace-root/AGENTS.md +14 -0
  161. package/dist/vite/client.js +56 -0
  162. package/package.json +2 -2
  163. package/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  164. package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +53 -1
  165. package/src/templates/chat/AGENTS.md +11 -0
  166. package/src/templates/chat/app/components/layout/Layout.tsx +1 -1
  167. package/src/templates/chat/app/root.tsx +1 -1
  168. package/src/templates/chat/app/routes/agent.tsx +29 -13
  169. package/src/templates/chat/vite.config.ts +42 -0
  170. package/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  171. package/src/templates/default/.agents/skills/frontend-design/SKILL.md +53 -1
  172. package/src/templates/default/AGENTS.md +11 -0
  173. package/src/templates/default/vite.config.ts +42 -0
  174. package/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  175. package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +33 -0
  176. package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +53 -1
  177. package/src/templates/workspace-core/AGENTS.md +11 -0
  178. 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
- const presentation = await parsePptx(fileBuffer);
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
+ }