Reubencf commited on
Commit
45b3fab
·
1 Parent(s): e1e2571

Readme updated

Browse files
Files changed (44) hide show
  1. README.md +461 -26
  2. app/api/auth/client-id/route.ts +7 -0
  3. app/api/image-proxy/route.ts +7 -0
  4. app/api/presentations/generate/route.ts +45 -16
  5. app/page.tsx +7 -0
  6. app/template/page.tsx +7 -0
  7. components/ThemeProvider.tsx +7 -0
  8. components/UnsplashImageSearch.tsx +7 -0
  9. components/editor/AIToolsDialog.tsx +7 -0
  10. components/editor/BottomToolbar.tsx +7 -0
  11. components/editor/GoogleSlidesEditor.tsx +103 -189
  12. components/home/HomePage.tsx +164 -94
  13. components/slides/AgendaSlideLayout.tsx +7 -0
  14. components/slides/ImageAndTextLayout.tsx +7 -0
  15. components/slides/ReferenceLayout.tsx +7 -0
  16. components/slides/SlideFactory.tsx +198 -71
  17. components/slides/ThankYouLayout.tsx +7 -0
  18. components/slides/ThreeColumnLayout.tsx +7 -0
  19. components/slides/TitleAndBodyLayout.tsx +7 -0
  20. components/slides/TitleSlideLayout.tsx +7 -0
  21. components/slides/galeryn/layouts.tsx +7 -0
  22. components/slides/neobrutalism/layouts.tsx +7 -0
  23. components/slides/noisy/layouts.tsx +7 -0
  24. components/slides/shared/PersistedDraggableSurface.tsx +7 -0
  25. components/ui/empty.tsx +7 -0
  26. components/ui/spinner.tsx +7 -0
  27. data/templates/galeryn.ts +7 -0
  28. data/templates/index.ts +7 -0
  29. data/templates/neo-brutalism.ts +7 -0
  30. data/templates/noisy.ts +7 -0
  31. hooks/useExport.ts +14 -1
  32. hooks/useKeyboardShortcuts.ts +7 -0
  33. hooks/useSlideHistory.ts +7 -0
  34. lib/ai-models.ts +7 -0
  35. lib/capture-element.ts +7 -0
  36. lib/editable-pptx-export.ts +7 -0
  37. lib/editor-themes.ts +7 -0
  38. lib/editor-types.ts +7 -0
  39. lib/generated-presentation.ts +7 -0
  40. lib/hf-client.ts +7 -0
  41. lib/layout-templates.ts +7 -0
  42. lib/template-options.ts +7 -0
  43. next.config.ts +7 -0
  44. types/index.ts +7 -0
README.md CHANGED
@@ -1,43 +1,478 @@
1
  ---
2
  title: Powerpoint AI
3
- emoji: 🎨
4
  colorFrom: green
5
  colorTo: yellow
6
  sdk: docker
7
  app_port: 7860
8
- pinned: false
9
  ---
10
 
11
- # PowerPoint AI Generator
12
 
13
- AI-powered presentation generator with a Google Slides-like editor. Create professional presentations using Llama-3.3-70B-Instruct via HuggingFace Inference, with automatic Unsplash image integration and Neo-Brutalism template support.
14
 
15
- ## Features
16
 
17
- - **AI Slide Generation** using Llama-3.3-70B-Instruct (HF Inference)
18
- - **Template System** with Neo-Brutalism theme (dot grids, thick borders, hard shadows, bright colors)
19
- - **Unsplash Integration** for automatic slide images
20
- - **Google Slides-like Editor** with drag-and-drop, inline text editing
21
- - **Multiple Slide Types**: Title, Bullets, Content-Image, Quote, Comparison, Chart, Stats, Section
22
- - **Export to PowerPoint** (.pptx)
23
- - **HuggingFace OAuth** login
24
 
25
- ## Tech Stack
26
 
27
- - Next.js 15 (App Router, Standalone output)
28
- - TypeScript, TailwindCSS v4
29
- - HuggingFace Inference Client
30
- - pptxgenjs for export
31
- - Radix UI primitives
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  ## Environment Variables
34
 
35
- Set these as **Secrets** in your HuggingFace Space settings:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- | Variable | Required | Description |
38
- |---|---|---|
39
- | `UNSPLASH_ACCESS_KEY` | Optional | Unsplash API key for slide images |
40
- | `HF_TOKEN` | Optional | HuggingFace API token (fallback if OAuth not used) |
41
- | `HUGGINGFACE_CLIENT_ID` | Optional | HF OAuth client ID |
42
- | `HUGGINGFACE_CLIENT_SECRET` | Optional | HF OAuth client secret |
43
- | `NEXT_PUBLIC_HUGGINGFACE_CLIENT_ID` | Optional | Public HF OAuth client ID |
 
1
  ---
2
  title: Powerpoint AI
3
+ emoji: "🎨"
4
  colorFrom: green
5
  colorTo: yellow
6
  sdk: docker
7
  app_port: 7860
8
+ pinned: true
9
  ---
10
 
11
+ # Powerpoint AI
12
 
13
+ Powerpoint AI is a Next.js presentation generator that turns a prompt into editable slides, lets the user refine them in a Google Slides-like editor, and exports the final deck as a `.pptx` file. The project is built around Hugging Face login and inference, a template-driven slide system, and a hybrid editor that supports both generated slide specs and draggable canvas elements.
14
 
15
+ This README is written as a learning guide. It explains:
16
 
17
+ 1. What the app does end-to-end.
18
+ 2. How to run it locally and deploy it to a Hugging Face Space.
19
+ 3. How the request flow moves from the homepage to the editor.
20
+ 4. What each important file does and how the files connect.
21
+ 5. How to rebuild the project manually if you want to learn by coding it yourself.
 
 
22
 
23
+ ## What The App Does
24
 
25
+ At a high level, the app works like this:
26
+
27
+ 1. The user opens `/` and enters a presentation prompt.
28
+ 2. The home page verifies that the user is logged in with Hugging Face.
29
+ 3. The prompt and selected template are stored in browser session/local storage.
30
+ 4. The app routes the user to `/editor`.
31
+ 5. The editor calls `/api/presentations/generate` with the prompt and model.
32
+ 6. The backend requests structured JSON slides from Hugging Face Inference.
33
+ 7. The response is normalized into the app's `SlideSpec` shape.
34
+ 8. The editor renders those slides through the template/theme system.
35
+ 9. The user edits text, layout, images, and formatting inside the editor.
36
+ 10. The user exports the deck as an editable PowerPoint file.
37
+
38
+ ## Stack
39
+
40
+ - Next.js App Router
41
+ - React + TypeScript
42
+ - Tailwind CSS
43
+ - Hugging Face OAuth + Inference
44
+ - Unsplash image search/download integration
45
+ - `pptxgenjs` for editable PPTX export
46
+
47
+ ## Prerequisites
48
+
49
+ You need:
50
+
51
+ - Node.js 20+
52
+ - npm
53
+ - A Hugging Face account
54
+ - A manual Hugging Face connected OAuth app
55
+ - Optional: an Unsplash access key if you want real image search results
56
 
57
  ## Environment Variables
58
 
59
+ Set these in `.env` for local development or in Hugging Face Space secrets/variables for deployment.
60
+
61
+ | Variable | Required | Purpose |
62
+ | --- | --- | --- |
63
+ | `HUGGINGFACE_CLIENT_ID` | Yes for login | Client ID from your manual Hugging Face connected app |
64
+ | `HF_TOKEN` | Optional | Fallback Hugging Face token if the user is not logged in |
65
+ | `HF_API_KEY` | Optional | Alternate fallback token name supported by the generation route |
66
+ | `UNSPLASH_ACCESS_KEY` | Optional | Enables live Unsplash image search/results |
67
+ | `NEXT_PUBLIC_UNSPLASH_ACCESS_KEY` | Optional | Alternate client-visible Unsplash key fallback used by some routes |
68
+
69
+ Notes:
70
+
71
+ - This project is currently set up for a **manual connected app**, not Hugging Face Spaces native OAuth.
72
+ - Do **not** add `hf_oauth: true` if you want to keep using your existing connected app.
73
+ - The allowed redirect URI in your Hugging Face connected app should be your actual app origin, for example:
74
+ - `http://localhost:3000/`
75
+ - `https://reubencf-powerpoint-ai.hf.space/`
76
+
77
+ ## Local Development
78
+
79
+ 1. Install dependencies:
80
+
81
+ ```bash
82
+ npm install
83
+ ```
84
+
85
+ 2. Create `.env` and add the variables you need:
86
+
87
+ ```env
88
+ HUGGINGFACE_CLIENT_ID=your_client_id
89
+ HF_TOKEN=your_optional_fallback_token
90
+ UNSPLASH_ACCESS_KEY=your_optional_unsplash_key
91
+ ```
92
+
93
+ 3. Start the dev server:
94
+
95
+ ```bash
96
+ npm run dev
97
+ ```
98
+
99
+ 4. Open:
100
+
101
+ ```text
102
+ http://localhost:3000
103
+ ```
104
+
105
+ 5. Log in with Hugging Face, enter a prompt, and verify that generation reaches `/editor`.
106
+
107
+ ## Hugging Face Space Deployment
108
+
109
+ This repo is configured as a Docker Space using the frontmatter at the top of this file.
110
+
111
+ To deploy:
112
+
113
+ 1. Create a Hugging Face Docker Space.
114
+ 2. Push this repo to the Space.
115
+ 3. Add the required Space secrets/variables:
116
+ - `HUGGINGFACE_CLIENT_ID`
117
+ - optionally `HF_TOKEN`
118
+ - optionally `UNSPLASH_ACCESS_KEY`
119
+ 4. In your manual connected Hugging Face app settings, add the Space origin as an allowed redirect URI.
120
+ 5. Redeploy the Space.
121
+
122
+ Important:
123
+
124
+ - The app currently uses `HUGGINGFACE_CLIENT_ID` through `/api/auth/client-id`.
125
+ - The login flow dynamically uses `window.location.origin` as the redirect target.
126
+ - On Hugging Face Spaces, the homepage detects iframe/Space hosting and escapes the container before login if needed.
127
+
128
+ ## End-To-End Architecture Flow
129
+
130
+ ### 1. Route entrypoints
131
+
132
+ - `/` loads the homepage prompt builder.
133
+ - `/editor` loads the editor only if a generation session was created from the homepage.
134
+ - `/template` exists as a template preview/browsing route.
135
+ - `/api/*` provides auth, generation, AI edit, image search, and image proxy services.
136
+
137
+ ### 2. Homepage to editor handoff
138
+
139
+ The homepage stores these keys before routing to the editor:
140
+
141
+ - `generationPrompt`
142
+ - `generationModel`
143
+ - `isGenerating`
144
+ - `editorAccess`
145
+ - `ppt_theme` in local storage
146
+
147
+ The editor reads those values on mount to decide whether to:
148
+
149
+ - redirect back to `/`
150
+ - restore the chosen theme
151
+ - call the generation API automatically
152
+
153
+ ### 3. Generation backend flow
154
+
155
+ `/api/presentations/generate` does the following:
156
+
157
+ 1. Validates the prompt.
158
+ 2. Resolves the Hugging Face token from headers, cookies, or env fallback.
159
+ 3. Builds the system prompt with `lib/slide-prompt.ts`.
160
+ 4. Calls the approved model through `lib/hf-client.ts`.
161
+ 5. Parses loose model JSON defensively.
162
+ 6. Normalizes the payload into the app's slide shape.
163
+ 7. Optionally enriches image slides with Unsplash data.
164
+ 8. Returns normalized slide data for the editor.
165
+
166
+ ### 4. Editor rendering flow
167
+
168
+ The editor supports two rendering modes:
169
+
170
+ - **Template mode**: uses `SlideSpec` and `SlideFactory` to render theme-specific layouts.
171
+ - **Canvas mode**: uses positioned `EditorElement` objects for draggable/editable items.
172
+
173
+ For generated presentations, the app first normalizes API data into `SlideSpec`, then builds canvas/editor elements where needed so the legacy editing features still work.
174
+
175
+ ### 5. Export flow
176
+
177
+ - The top bar export button calls `hooks/useExport.ts`.
178
+ - `useExport` delegates to `lib/editable-pptx-export.ts`.
179
+ - The exporter converts the current editor state into an editable `.pptx`.
180
+
181
+ ## How The Editor Works Internally
182
+
183
+ The editor is intentionally state-heavy because it coordinates:
184
+
185
+ - the list of slides
186
+ - the current slide index
187
+ - selected element state
188
+ - inline text editing state
189
+ - zoom state
190
+ - undo/redo history
191
+ - AI text editing
192
+ - image replacement/search
193
+ - template-rendered slides and older element-based slides
194
+
195
+ The two most important ideas are:
196
+
197
+ 1. `SlideSpec` is the declarative template-friendly shape.
198
+ 2. `SlideModel` / `EditorElement` is the interactive editor canvas shape.
199
+
200
+ That split is why some code looks duplicated: the app preserves a template rendering pipeline while also keeping direct canvas editing features.
201
+
202
+ ## How Templates And Themes Are Organized
203
+
204
+ There are three template families right now:
205
+
206
+ - `neobrutalism`
207
+ - `galeryn`
208
+ - `noisy`
209
+
210
+ Each template has:
211
+
212
+ - a declarative definition in `data/templates/*.ts`
213
+ - theme-specific React layouts in `components/slides/<theme>/layouts.tsx`
214
+ - fallback generic layout components in `components/slides/*.tsx`
215
+
216
+ The registry in `data/templates/index.ts` describes:
217
+
218
+ - template IDs
219
+ - layout IDs
220
+ - field definitions
221
+ - style tokens
222
+ - default slide data
223
+
224
+ The `SlideFactory` uses that registry plus the selected template ID to render the correct slide component.
225
+
226
+ ## How AI Text Editing, Image Search, And Export Connect
227
+
228
+ ### AI text editing
229
+
230
+ - The editor opens `components/editor/AIToolsDialog.tsx`.
231
+ - That dialog talks to `/api/ai-edit-text/route.ts`.
232
+ - The route uses the Hugging Face provider layer to rewrite selected text.
233
+
234
+ ### Image search
235
+
236
+ - The editor opens `components/UnsplashImageSearch.tsx`.
237
+ - That component calls `/api/search-images/route.ts`.
238
+ - `/api/unsplash-download/route.ts` is used for download attribution.
239
+ - `/api/image-proxy/route.ts` helps keep remote images safe for capture/export flows.
240
+
241
+ ### Export
242
+
243
+ - The editor export button calls `hooks/useExport.ts`.
244
+ - `useExport` calls `lib/editable-pptx-export.ts`.
245
+ - The export library reads current slides, theme data, and layout information to create a real PowerPoint file.
246
+
247
+ ## File Map
248
+
249
+ This section is intentionally explicit so a new contributor can jump from route to component to helper without guessing.
250
+
251
+ ### App routes and app shell
252
+
253
+ - `app/layout.tsx`
254
+ - Root HTML/layout wrapper.
255
+ - Adds global fonts, metadata, and the `ThemeProvider`.
256
+ - `app/globals.css`
257
+ - Global styles and utility-level CSS used across pages/components.
258
+ - `app/page.tsx`
259
+ - Root route entrypoint.
260
+ - Re-exports `components/home/HomePage.tsx`.
261
+ - `app/editor/page.tsx`
262
+ - Guards `/editor` with `sessionStorage.editorAccess`.
263
+ - Renders `GoogleSlidesEditor`.
264
+ - `app/template/page.tsx`
265
+ - Template preview/browsing route.
266
+
267
+ ### API routes
268
+
269
+ - `app/api/presentations/generate/route.ts`
270
+ - Main slide generation endpoint.
271
+ - Calls `HFClient`, `buildSlidePrompt`, and Unsplash helpers.
272
+ - `app/api/ai-edit-text/route.ts`
273
+ - AI text rewriting endpoint for the editor.
274
+ - `app/api/search-images/route.ts`
275
+ - Searches Unsplash images for the editor modal.
276
+ - `app/api/unsplash-download/route.ts`
277
+ - Handles Unsplash download tracking.
278
+ - `app/api/image-proxy/route.ts`
279
+ - Proxies remote images for safe browser capture/export use.
280
+ - `app/api/auth/client-id/route.ts`
281
+ - Returns `HUGGINGFACE_CLIENT_ID` to the client login flow.
282
+
283
+ ### Homepage and top-level UI
284
+
285
+ - `components/home/HomePage.tsx`
286
+ - Landing page.
287
+ - Restores auth, handles login, stores generation state, and routes to `/editor`.
288
+ - `components/ThemeProvider.tsx`
289
+ - Theme context wrapper built on `next-themes`.
290
+ - `components/UnsplashImageSearch.tsx`
291
+ - Search/select modal for remote images.
292
+
293
+ ### Editor components
294
+
295
+ - `components/editor/GoogleSlidesEditor.tsx`
296
+ - Main workspace.
297
+ - Owns generation bootstrap, editor state, slide switching, toolbar wiring, and export entry.
298
+ - `components/editor/BottomToolbar.tsx`
299
+ - Floating toolbar for layout, typography, AI tools, zoom, and slide actions.
300
+ - `components/editor/AIToolsDialog.tsx`
301
+ - Text-editing dialog for AI-powered text changes.
302
+
303
+ ### Slide rendering
304
+
305
+ - `components/slides/SlideFactory.tsx`
306
+ - Central dispatcher that chooses the correct themed or fallback slide layout.
307
+ - `components/slides/TitleSlideLayout.tsx`
308
+ - Generic fallback layout for title/subtitle slides.
309
+ - `components/slides/AgendaSlideLayout.tsx`
310
+ - Generic fallback layout for agenda slides.
311
+ - `components/slides/TitleAndBodyLayout.tsx`
312
+ - Generic fallback layout for title/body slides.
313
+ - `components/slides/ThreeColumnLayout.tsx`
314
+ - Generic fallback layout for three-column slides.
315
+ - `components/slides/ImageAndTextLayout.tsx`
316
+ - Generic fallback layout for image/text slides.
317
+ - `components/slides/ReferenceLayout.tsx`
318
+ - Generic fallback layout for reference/list slides.
319
+ - `components/slides/ThankYouLayout.tsx`
320
+ - Generic fallback layout for ending slides.
321
+ - `components/slides/neobrutalism/layouts.tsx`
322
+ - Theme-specific layouts for the Neo-Brutalism template.
323
+ - `components/slides/galeryn/layouts.tsx`
324
+ - Theme-specific layouts for the Galeryn template.
325
+ - `components/slides/noisy/layouts.tsx`
326
+ - Theme-specific layouts for the Noisy template.
327
+ - `components/slides/shared/PersistedDraggableSurface.tsx`
328
+ - Shared editable/draggable slide-surface logic used by themed layouts.
329
+
330
+ ### Template registry
331
+
332
+ - `data/templates/index.ts`
333
+ - Template registry and template-related type definitions.
334
+ - `data/templates/neo-brutalism.ts`
335
+ - Neo-Brutalism template declaration.
336
+ - `data/templates/galeryn.ts`
337
+ - Galeryn template declaration.
338
+ - `data/templates/noisy.ts`
339
+ - Noisy template declaration.
340
+
341
+ ### Hooks
342
+
343
+ - `hooks/useExport.ts`
344
+ - Small export wrapper used by the editor.
345
+ - `hooks/useKeyboardShortcuts.ts`
346
+ - Keyboard shortcut behavior for editor actions.
347
+ - `hooks/useSlideHistory.ts`
348
+ - Undo/redo history storage for slides.
349
+
350
+ ### Library helpers
351
+
352
+ - `lib/ai-models.ts`
353
+ - Approved model constants and model validation.
354
+ - `lib/hf-client.ts`
355
+ - Hugging Face inference wrapper and provider-specific error handling.
356
+ - `lib/slide-prompt.ts`
357
+ - System prompt builder and layout normalization helpers.
358
+ - `lib/generated-presentation.ts`
359
+ - Typed generated-slide response helpers and `SlideSpec` mapping.
360
+ - `lib/template-options.ts`
361
+ - Template picker labels and normalization of saved IDs.
362
+ - `lib/theme-system.ts`
363
+ - Theme-level types/helpers used by the rendering system.
364
+ - `lib/editor-types.ts`
365
+ - Shared editor model types.
366
+ - `lib/editor-themes.ts`
367
+ - Theme/color/font definitions used by the editor and export code.
368
+ - `lib/layout-templates.ts`
369
+ - Default canvas layout creation helpers.
370
+ - `lib/editable-pptx-export.ts`
371
+ - Editable PowerPoint export implementation.
372
+ - `lib/capture-element.ts`
373
+ - DOM capture helper used by image/export flows.
374
+ - `lib/utils.ts`
375
+ - Small shared utility helpers.
376
+
377
+ ### Shared types
378
+
379
+ - `types/index.ts`
380
+ - Project-wide shared types that do not belong to a single feature module.
381
+
382
+ ### Config
383
+
384
+ - `next.config.ts`
385
+ - Next.js configuration.
386
+ - `package.json`
387
+ - Scripts, dependencies, and project metadata.
388
+ - `Dockerfile`
389
+ - Runtime image used by the Hugging Face Docker Space.
390
+
391
+ ## How To Manually Build This Project Yourself
392
+
393
+ If you want to learn by recreating the app manually, build it in this order:
394
+
395
+ ### Step 1: Create the app shell
396
+
397
+ 1. Start a Next.js app with TypeScript and App Router.
398
+ 2. Add `app/layout.tsx` and `app/globals.css`.
399
+ 3. Add a `ThemeProvider` so dark/light mode is available globally.
400
+
401
+ ### Step 2: Build the homepage
402
+
403
+ 1. Create a landing page with:
404
+ - a prompt textarea
405
+ - a template picker
406
+ - a submit button
407
+ 2. Add client-side storage for:
408
+ - the prompt
409
+ - the selected template
410
+ - a session flag that allows `/editor`
411
+ 3. Add Hugging Face login/logout state restoration.
412
+
413
+ ### Step 3: Add Hugging Face login
414
+
415
+ 1. Create a manual connected app in Hugging Face settings.
416
+ 2. Add `HUGGINGFACE_CLIENT_ID` to env/secrets.
417
+ 3. Create `/api/auth/client-id/route.ts`.
418
+ 4. Use `oauthLoginUrl` and `oauthHandleRedirectIfPresent` on the homepage.
419
+ 5. Store the returned access token in local storage for later API calls.
420
+
421
+ ### Step 4: Create the generation API
422
+
423
+ 1. Create `/api/presentations/generate/route.ts`.
424
+ 2. Accept a prompt and optional model input.
425
+ 3. Resolve the Hugging Face token from headers/cookies/env.
426
+ 4. Build a strict system prompt that asks for presentation JSON.
427
+ 5. Call the model through a small Hugging Face client wrapper.
428
+ 6. Parse and normalize the model response into your own slide format.
429
+
430
+ ### Step 5: Define the slide data model
431
+
432
+ 1. Create a template-side shape like `SlideSpec`.
433
+ 2. Create an editor-side shape like `SlideModel` / `EditorElement`.
434
+ 3. Add normalization helpers that map generated API data into those shapes.
435
+
436
+ ### Step 6: Build the template system
437
+
438
+ 1. Define templates in `data/templates/*.ts`.
439
+ 2. Add a registry in `data/templates/index.ts`.
440
+ 3. Create a `SlideFactory` that renders a slide by template ID + layout ID.
441
+ 4. Add generic fallback layouts so rendering still works if a template-specific layout is missing.
442
+
443
+ ### Step 7: Build the editor
444
+
445
+ 1. Create an editor page that reads the generation session.
446
+ 2. Call the generation API on first load.
447
+ 3. Store slides, current selection, zoom, and undo/redo history.
448
+ 4. Render:
449
+ - a slide thumbnail rail
450
+ - a main canvas
451
+ - a toolbar
452
+ - dialogs/modals for AI editing and image search
453
+
454
+ ### Step 8: Add AI text editing and images
455
+
456
+ 1. Create `/api/ai-edit-text/route.ts`.
457
+ 2. Add a dialog to send selected text for rewrite.
458
+ 3. Create Unsplash search and download routes.
459
+ 4. Add an image picker modal in the editor.
460
+
461
+ ### Step 9: Add export
462
+
463
+ 1. Create a hook for export actions.
464
+ 2. Convert the current slide state into `pptxgenjs` calls.
465
+ 3. Expose a top-bar export button in the editor.
466
+
467
+ ### Step 10: Polish and verify
468
+
469
+ 1. Add keyboard shortcuts and history.
470
+ 2. Add route guards so `/editor` is only reached through the expected flow.
471
+ 3. Verify login, generation, editing, image selection, and export.
472
+
473
+ ## Contributor Notes
474
 
475
+ - Prefer the template registry and `SlideSpec` path when adding new slide styles.
476
+ - Keep behavior changes separate from cleanup/documentation changes.
477
+ - If you change storage keys or generation payload shapes, update both the homepage/editor handoff and this README.
478
+ - If you add a new route or editor subsystem, add it to the file map above so new contributors can follow the flow quickly.
 
 
 
app/api/auth/client-id/route.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import { NextResponse } from 'next/server';
2
 
3
  export async function GET() {
 
1
+ /*
2
+ * route.ts
3
+ * Purpose: Returns the configured Hugging Face OAuth client ID to the browser login flow.
4
+ * Used by: components/home/HomePage sign-in flow.
5
+ * Depends on: Space/app environment variables and NextResponse.
6
+ */
7
+
8
  import { NextResponse } from 'next/server';
9
 
10
  export async function GET() {
app/api/image-proxy/route.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import { NextRequest, NextResponse } from 'next/server';
2
 
3
  export async function GET(request: NextRequest) {
 
1
+ /*
2
+ * route.ts
3
+ * Purpose: Fetches remote images through a same-origin route so export/canvas code can safely reuse them.
4
+ * Used by: Image capture/export helpers and any UI path that needs proxied remote images.
5
+ * Depends on: Next.js route handlers and remote fetch.
6
+ */
7
+
8
  import { NextRequest, NextResponse } from 'next/server';
9
 
10
  export async function GET(request: NextRequest) {
app/api/presentations/generate/route.ts CHANGED
@@ -1,3 +1,11 @@
 
 
 
 
 
 
 
 
1
  import { NextRequest, NextResponse } from 'next/server';
2
  import { HFClient, HFGenerationError } from '@/lib/hf-client';
3
  import { LLAMA_PRESENTATION_MODEL, isAllowedPresentationModel } from '@/lib/ai-models';
@@ -27,6 +35,7 @@ class PresentationGenerationError extends Error {
27
  }
28
  }
29
 
 
30
  function toStringArray(value: unknown): string[] {
31
  if (Array.isArray(value)) {
32
  return value
@@ -144,6 +153,7 @@ function getProviderFailureMessage(error: unknown): string {
144
  return 'Slide generation failed.';
145
  }
146
 
 
147
  async function fetchModelResponse(hf: HFClient, prompt: string): Promise<string> {
148
  try {
149
  return await hf.generateSlideContent(prompt, LLAMA_PRESENTATION_MODEL);
@@ -174,6 +184,7 @@ async function fetchModelResponse(hf: HFClient, prompt: string): Promise<string>
174
  }
175
  }
176
 
 
177
  function parseGeneratedPayload(response: string): { presentationName?: string; slides: unknown[] } {
178
  const cleanedJson = response
179
  .trim()
@@ -248,6 +259,7 @@ function parseGeneratedPayload(response: string): { presentationName?: string; s
248
  };
249
  }
250
 
 
251
  function normalizeSlides(slides: unknown[]): Slide[] {
252
  const total = slides.length;
253
 
@@ -306,8 +318,38 @@ async function fetchImageForSlide(query: string): Promise<string | undefined> {
306
  }
307
  }
308
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  export async function POST(req: NextRequest) {
310
  try {
 
311
  const body = await req.json();
312
  const prompt = typeof body.prompt === 'string' ? body.prompt.trim() : '';
313
  const model = body.model;
@@ -323,13 +365,7 @@ export async function POST(req: NextRequest) {
323
  console.warn(`Ignoring unsupported presentation model "${model}" and forcing ${LLAMA_PRESENTATION_MODEL}.`);
324
  }
325
 
326
- let hfToken = req.headers.get('x-hf-token');
327
- if (!hfToken) hfToken = req.cookies.get('hf_api_key')?.value || null;
328
- if (!hfToken) {
329
- const authHeader = req.headers.get('authorization');
330
- if (authHeader?.startsWith('Bearer ')) hfToken = authHeader.slice(7);
331
- }
332
- if (!hfToken) hfToken = process.env.HF_TOKEN || process.env.HF_API_KEY || null;
333
 
334
  if (!hfToken) {
335
  return NextResponse.json(
@@ -342,7 +378,7 @@ export async function POST(req: NextRequest) {
342
  }
343
 
344
  const systemPrompt = buildSlidePrompt(prompt);
345
- const presentationName = prompt.length > 50 ? `${prompt.substring(0, 47)}...` : prompt;
346
  const hf = new HFClient({ apiKey: hfToken, model: LLAMA_PRESENTATION_MODEL });
347
 
348
  const responseText = await fetchModelResponse(hf, systemPrompt);
@@ -357,14 +393,7 @@ export async function POST(req: NextRequest) {
357
 
358
  const parsed = parseGeneratedPayload(responseText);
359
  const slides = normalizeSlides(parsed.slides);
360
-
361
- const slidesWithImages = await Promise.all(
362
- slides.map(async (slide) => {
363
- if (slide.layout !== 'image_and_text' || !slide.imageKeyword) return slide;
364
- const imageUrl = await fetchImageForSlide(slide.imageKeyword);
365
- return { ...slide, imageUrl };
366
- })
367
- );
368
 
369
  return NextResponse.json({
370
  presentationName: parsed.presentationName || presentationName,
 
1
+ /*
2
+ * route.ts
3
+ * Purpose: Turns a presentation prompt into normalized slide JSON and enriches
4
+ * image slides with Unsplash results before the editor receives the payload.
5
+ * Used by: GoogleSlidesEditor when a new prompt is submitted from the home page.
6
+ * Depends on: HFClient, prompt-building helpers, model allow-listing, and optional Unsplash access.
7
+ */
8
+
9
  import { NextRequest, NextResponse } from 'next/server';
10
  import { HFClient, HFGenerationError } from '@/lib/hf-client';
11
  import { LLAMA_PRESENTATION_MODEL, isAllowedPresentationModel } from '@/lib/ai-models';
 
35
  }
36
  }
37
 
38
+ // These helpers normalize loosely structured model output into predictable editor data.
39
  function toStringArray(value: unknown): string[] {
40
  if (Array.isArray(value)) {
41
  return value
 
153
  return 'Slide generation failed.';
154
  }
155
 
156
+ // Retry once for transient provider failures so short upstream hiccups do not surface immediately to users.
157
  async function fetchModelResponse(hf: HFClient, prompt: string): Promise<string> {
158
  try {
159
  return await hf.generateSlideContent(prompt, LLAMA_PRESENTATION_MODEL);
 
184
  }
185
  }
186
 
187
+ // The model sometimes wraps JSON in markdown or returns an array directly, so parsing is intentionally defensive.
188
  function parseGeneratedPayload(response: string): { presentationName?: string; slides: unknown[] } {
189
  const cleanedJson = response
190
  .trim()
 
259
  };
260
  }
261
 
262
+ // The editor expects a stable layout/content shape regardless of how the model phrased the response.
263
  function normalizeSlides(slides: unknown[]): Slide[] {
264
  const total = slides.length;
265
 
 
318
  }
319
  }
320
 
321
+ function resolveHuggingFaceToken(req: NextRequest) {
322
+ const headerToken = req.headers.get('x-hf-token');
323
+ if (headerToken) return headerToken;
324
+
325
+ const cookieToken = req.cookies.get('hf_api_key')?.value;
326
+ if (cookieToken) return cookieToken;
327
+
328
+ const authHeader = req.headers.get('authorization');
329
+ if (authHeader?.startsWith('Bearer ')) {
330
+ return authHeader.slice(7);
331
+ }
332
+
333
+ return process.env.HF_TOKEN || process.env.HF_API_KEY || null;
334
+ }
335
+
336
+ function buildPresentationName(prompt: string) {
337
+ return prompt.length > 50 ? `${prompt.substring(0, 47)}...` : prompt;
338
+ }
339
+
340
+ async function attachImagesToSlides(slides: Slide[]) {
341
+ return Promise.all(
342
+ slides.map(async (slide) => {
343
+ if (slide.layout !== 'image_and_text' || !slide.imageKeyword) return slide;
344
+ const imageUrl = await fetchImageForSlide(slide.imageKeyword);
345
+ return { ...slide, imageUrl };
346
+ })
347
+ );
348
+ }
349
+
350
  export async function POST(req: NextRequest) {
351
  try {
352
+ // The request shape is intentionally small because generation state is coordinated client-side.
353
  const body = await req.json();
354
  const prompt = typeof body.prompt === 'string' ? body.prompt.trim() : '';
355
  const model = body.model;
 
365
  console.warn(`Ignoring unsupported presentation model "${model}" and forcing ${LLAMA_PRESENTATION_MODEL}.`);
366
  }
367
 
368
+ const hfToken = resolveHuggingFaceToken(req);
 
 
 
 
 
 
369
 
370
  if (!hfToken) {
371
  return NextResponse.json(
 
378
  }
379
 
380
  const systemPrompt = buildSlidePrompt(prompt);
381
+ const presentationName = buildPresentationName(prompt);
382
  const hf = new HFClient({ apiKey: hfToken, model: LLAMA_PRESENTATION_MODEL });
383
 
384
  const responseText = await fetchModelResponse(hf, systemPrompt);
 
393
 
394
  const parsed = parseGeneratedPayload(responseText);
395
  const slides = normalizeSlides(parsed.slides);
396
+ const slidesWithImages = await attachImagesToSlides(slides);
 
 
 
 
 
 
 
397
 
398
  return NextResponse.json({
399
  presentationName: parsed.presentationName || presentationName,
app/page.tsx CHANGED
@@ -1 +1,8 @@
 
 
 
 
 
 
 
1
  export { default } from '@/components/home/HomePage';
 
1
+ /*
2
+ * page.tsx
3
+ * Purpose: Exposes the landing page component at the root route without adding extra route logic.
4
+ * Used by: Next.js router for /.
5
+ * Depends on: components/home/HomePage.
6
+ */
7
+
8
  export { default } from '@/components/home/HomePage';
app/template/page.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { useState } from 'react';
 
1
+ /*
2
+ * page.tsx
3
+ * Purpose: Renders the template-preview route used to inspect available slide themes/layouts.
4
+ * Used by: Next.js router for /template.
5
+ * Depends on: template registries and preview components.
6
+ */
7
+
8
  'use client';
9
 
10
  import React, { useState } from 'react';
components/ThemeProvider.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { createContext, useContext, useEffect, useState } from 'react';
 
1
+ /*
2
+ * ThemeProvider.tsx
3
+ * Purpose: Wraps next-themes so pages can read and toggle the global light/dark theme.
4
+ * Used by: app/layout.tsx and any component using useTheme.
5
+ * Depends on: next-themes React context.
6
+ */
7
+
8
  'use client';
9
 
10
  import React, { createContext, useContext, useEffect, useState } from 'react';
components/UnsplashImageSearch.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { useEffect, useState } from 'react';
 
1
+ /*
2
+ * UnsplashImageSearch.tsx
3
+ * Purpose: Provides the modal UI for searching and selecting remote images for slides.
4
+ * Used by: GoogleSlidesEditor and template image-selection flows.
5
+ * Depends on: search/download API routes and editor callbacks.
6
+ */
7
+
8
  'use client';
9
 
10
  import React, { useEffect, useState } from 'react';
components/editor/AIToolsDialog.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import React, { useState } from 'react';
2
  import { X, Sparkles, RefreshCw, FileText, Maximize2 } from 'lucide-react';
3
 
 
1
+ /*
2
+ * AIToolsDialog.tsx
3
+ * Purpose: Shows the focused text-editing dialog for refine/change/expand/regenerate AI actions.
4
+ * Used by: GoogleSlidesEditor when a text element is edited with AI.
5
+ * Depends on: AI edit route and editor apply callbacks.
6
+ */
7
+
8
  import React, { useState } from 'react';
9
  import { X, Sparkles, RefreshCw, FileText, Maximize2 } from 'lucide-react';
10
 
components/editor/BottomToolbar.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import React from "react";
2
  import {
3
  ChevronDown,
 
1
+ /*
2
+ * BottomToolbar.tsx
3
+ * Purpose: Renders the floating editor toolbar for layout, typography, AI tools, zoom, and slide actions.
4
+ * Used by: GoogleSlidesEditor.
5
+ * Depends on: Editor selection state, formatting callbacks, and toolbar menu state.
6
+ */
7
+
8
  import React from "react";
9
  import {
10
  ChevronDown,
components/editor/GoogleSlidesEditor.tsx CHANGED
@@ -1,3 +1,12 @@
 
 
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { useCallback, useEffect, useRef, useState } from 'react';
@@ -36,6 +45,97 @@ import {
36
  isPresentationGenerateSuccessResponse,
37
  mapPresentationSlidesToSlideSpecs,
38
  } from '@/lib/generated-presentation';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  export default function GoogleSlidesEditor() {
41
  // Presentation data
@@ -239,200 +339,14 @@ export default function GoogleSlidesEditor() {
239
  throw new Error('Generation returned no slides.');
240
  }
241
 
242
- // Transform API response to editor format
243
  const activeTheme = themes[resolvedTheme];
244
- const headingFont = activeTheme.headingFont || 'Arial';
245
- const bodyFont = activeTheme.bodyFont || 'Arial';
246
  const generatedSlides: SlideModel[] = data.slides.map((slide, index) => {
247
  const spec = specs[index];
248
- const layout = spec.layout;
249
- const contentText = spec.items?.map((item) => item.text).join('\n\n')
250
- || spec.body?.map((entry) => entry.text).join('\n\n')
251
- || spec.subtitle
252
- || '';
253
- let elements: EditorElement[] = [];
254
-
255
- if (layout === 'image_and_text' && slide.imageUrl) {
256
- elements = [
257
- {
258
- id: createId(),
259
- type: 'text',
260
- x: 60,
261
- y: 40,
262
- width: 680,
263
- height: 60,
264
- text: spec.title || `Slide ${index + 1}`,
265
- fontSize: 36,
266
- color: activeTheme.titleColor,
267
- fontWeight: 'bold',
268
- fontStyle: 'normal',
269
- textDecoration: 'none',
270
- align: 'left',
271
- fontFamily: headingFont,
272
- } as TextElement,
273
- {
274
- id: createId(),
275
- type: 'text',
276
- x: 60,
277
- y: 120,
278
- width: 340,
279
- height: 280,
280
- text: contentText,
281
- fontSize: 18,
282
- color: activeTheme.textColor,
283
- fontWeight: 'normal',
284
- fontStyle: 'normal',
285
- textDecoration: 'none',
286
- align: 'left',
287
- fontFamily: bodyFont,
288
- } as TextElement,
289
- {
290
- id: createId(),
291
- type: 'image',
292
- x: 440,
293
- y: 120,
294
- width: 300,
295
- height: 280,
296
- src: slide.imageUrl,
297
- maintainAspectRatio: true,
298
- } as ImageElement
299
- ];
300
- } else if (layout === 'three_columns' && Array.isArray(spec.columns) && spec.columns.length > 0) {
301
- elements = [
302
- {
303
- id: createId(),
304
- type: 'text',
305
- x: 50,
306
- y: 30,
307
- width: 700,
308
- height: 50,
309
- text: spec.title || `Slide ${index + 1}`,
310
- fontSize: 34,
311
- color: activeTheme.titleColor,
312
- fontWeight: 'bold',
313
- fontStyle: 'normal',
314
- textDecoration: 'none',
315
- align: 'left',
316
- fontFamily: headingFont,
317
- } as TextElement,
318
- ...spec.columns.slice(0, 3).flatMap((column, columnIndex: number) => {
319
- const x = 45 + (columnIndex * 245);
320
- return [
321
- {
322
- id: createId(),
323
- type: 'text',
324
- x,
325
- y: 115,
326
- width: 210,
327
- height: 45,
328
- text: column.heading || `Column ${columnIndex + 1}`,
329
- fontSize: 22,
330
- color: activeTheme.titleColor,
331
- fontWeight: 'bold',
332
- fontStyle: 'normal',
333
- textDecoration: 'none',
334
- align: 'left',
335
- fontFamily: headingFont,
336
- } as TextElement,
337
- {
338
- id: createId(),
339
- type: 'text',
340
- x,
341
- y: 175,
342
- width: 210,
343
- height: 190,
344
- text: column.text || '',
345
- fontSize: 16,
346
- color: activeTheme.textColor,
347
- fontWeight: 'normal',
348
- fontStyle: 'normal',
349
- textDecoration: 'none',
350
- align: 'left',
351
- fontFamily: bodyFont,
352
- } as TextElement,
353
- ];
354
- }),
355
- ];
356
- } else if (layout === 'title_subtitle' || layout === 'thank_you') {
357
- const subtitleElement = contentText
358
- ? [{
359
- id: createId(),
360
- type: 'text',
361
- x: 60,
362
- y: 120,
363
- width: 680,
364
- height: 280,
365
- text: contentText,
366
- fontSize: 20,
367
- color: activeTheme.textColor,
368
- fontWeight: 'normal',
369
- fontStyle: 'normal',
370
- textDecoration: 'none',
371
- align: 'left',
372
- fontFamily: bodyFont,
373
- } as TextElement]
374
- : [];
375
-
376
- elements = [
377
- {
378
- id: createId(),
379
- type: 'text',
380
- x: 60,
381
- y: 40,
382
- width: 680,
383
- height: 60,
384
- text: spec.title || `Slide ${index + 1}`,
385
- fontSize: 36,
386
- color: activeTheme.titleColor,
387
- fontWeight: 'bold',
388
- fontStyle: 'normal',
389
- textDecoration: 'none',
390
- align: 'left',
391
- fontFamily: headingFont,
392
- } as TextElement,
393
- ...subtitleElement,
394
- ];
395
- } else {
396
- elements = [
397
- {
398
- id: createId(),
399
- type: 'text',
400
- x: 60,
401
- y: 40,
402
- width: 680,
403
- height: 60,
404
- text: spec.title || `Slide ${index + 1}`,
405
- fontSize: 36,
406
- color: activeTheme.titleColor,
407
- fontWeight: 'bold',
408
- fontStyle: 'normal',
409
- textDecoration: 'none',
410
- align: 'left',
411
- fontFamily: headingFont,
412
- } as TextElement,
413
- {
414
- id: createId(),
415
- type: 'text',
416
- x: 60,
417
- y: 120,
418
- width: 680,
419
- height: 280,
420
- text: contentText,
421
- fontSize: 20,
422
- color: activeTheme.textColor,
423
- fontWeight: 'normal',
424
- fontStyle: 'normal',
425
- textDecoration: 'none',
426
- align: 'left',
427
- fontFamily: bodyFont,
428
- } as TextElement
429
- ];
430
- }
431
-
432
  return {
433
  id: createId(),
434
- layout,
435
- elements,
436
  };
437
  });
438
 
 
1
+ /*
2
+ * GoogleSlidesEditor.tsx
3
+ * Purpose: Hosts the main presentation workspace, including generation bootstrap,
4
+ * editor state, canvas rendering, slide management, and export entry points.
5
+ * Used by: app/editor/page.tsx
6
+ * Depends on: generation API routes, SlideFactory/template registries, editor hooks,
7
+ * theme definitions, and export helpers.
8
+ */
9
+
10
  'use client';
11
 
12
  import React, { useCallback, useEffect, useRef, useState } from 'react';
 
45
  isPresentationGenerateSuccessResponse,
46
  mapPresentationSlidesToSlideSpecs,
47
  } from '@/lib/generated-presentation';
48
+ import type { PresentationApiSlide } from '@/lib/generated-presentation';
49
+
50
+ type ActiveEditorTheme = (typeof themes)[keyof typeof themes];
51
+
52
+ function getGeneratedSlideText(spec: SlideSpec) {
53
+ return spec.items?.map((item) => item.text).join('\n\n')
54
+ || spec.body?.map((entry) => entry.text).join('\n\n')
55
+ || spec.subtitle
56
+ || '';
57
+ }
58
+
59
+ function createTextElement(
60
+ content: string,
61
+ activeTheme: ActiveEditorTheme,
62
+ fontFamily: string,
63
+ position: { x: number; y: number; width: number; height: number; fontSize: number },
64
+ fontWeight: 'normal' | 'bold' = 'normal'
65
+ ): TextElement {
66
+ return {
67
+ id: createId(),
68
+ type: 'text',
69
+ x: position.x,
70
+ y: position.y,
71
+ width: position.width,
72
+ height: position.height,
73
+ text: content,
74
+ fontSize: position.fontSize,
75
+ color: fontWeight === 'bold' ? activeTheme.titleColor : activeTheme.textColor,
76
+ fontWeight,
77
+ fontStyle: 'normal',
78
+ textDecoration: 'none',
79
+ align: 'left',
80
+ fontFamily,
81
+ };
82
+ }
83
+
84
+ function buildGeneratedSlideElements(
85
+ spec: SlideSpec,
86
+ sourceSlide: PresentationApiSlide,
87
+ slideIndex: number,
88
+ activeTheme: ActiveEditorTheme
89
+ ): EditorElement[] {
90
+ const headingFont = activeTheme.headingFont || 'Arial';
91
+ const bodyFont = activeTheme.bodyFont || 'Arial';
92
+ const title = spec.title || `Slide ${slideIndex + 1}`;
93
+ const contentText = getGeneratedSlideText(spec);
94
+
95
+ if (spec.layout === 'image_and_text' && sourceSlide.imageUrl) {
96
+ return [
97
+ createTextElement(title, activeTheme, headingFont, { x: 60, y: 40, width: 680, height: 60, fontSize: 36 }, 'bold'),
98
+ createTextElement(contentText, activeTheme, bodyFont, { x: 60, y: 120, width: 340, height: 280, fontSize: 18 }),
99
+ {
100
+ id: createId(),
101
+ type: 'image',
102
+ x: 440,
103
+ y: 120,
104
+ width: 300,
105
+ height: 280,
106
+ src: sourceSlide.imageUrl,
107
+ maintainAspectRatio: true,
108
+ } as ImageElement,
109
+ ];
110
+ }
111
+
112
+ if (spec.layout === 'three_columns' && Array.isArray(spec.columns) && spec.columns.length > 0) {
113
+ return [
114
+ createTextElement(title, activeTheme, headingFont, { x: 50, y: 30, width: 700, height: 50, fontSize: 34 }, 'bold'),
115
+ ...spec.columns.slice(0, 3).flatMap((column, columnIndex) => {
116
+ const x = 45 + (columnIndex * 245);
117
+ return [
118
+ createTextElement(column.heading || `Column ${columnIndex + 1}`, activeTheme, headingFont, { x, y: 115, width: 210, height: 45, fontSize: 22 }, 'bold'),
119
+ createTextElement(column.text || '', activeTheme, bodyFont, { x, y: 175, width: 210, height: 190, fontSize: 16 }),
120
+ ];
121
+ }),
122
+ ];
123
+ }
124
+
125
+ if (spec.layout === 'title_subtitle' || spec.layout === 'thank_you') {
126
+ return [
127
+ createTextElement(title, activeTheme, headingFont, { x: 60, y: 40, width: 680, height: 60, fontSize: 36 }, 'bold'),
128
+ ...(contentText
129
+ ? [createTextElement(contentText, activeTheme, bodyFont, { x: 60, y: 120, width: 680, height: 280, fontSize: 20 })]
130
+ : []),
131
+ ];
132
+ }
133
+
134
+ return [
135
+ createTextElement(title, activeTheme, headingFont, { x: 60, y: 40, width: 680, height: 60, fontSize: 36 }, 'bold'),
136
+ createTextElement(contentText, activeTheme, bodyFont, { x: 60, y: 120, width: 680, height: 280, fontSize: 20 }),
137
+ ];
138
+ }
139
 
140
  export default function GoogleSlidesEditor() {
141
  // Presentation data
 
339
  throw new Error('Generation returned no slides.');
340
  }
341
 
342
+ // Translate the generated slide specs into positioned editor elements for legacy canvas mode.
343
  const activeTheme = themes[resolvedTheme];
 
 
344
  const generatedSlides: SlideModel[] = data.slides.map((slide, index) => {
345
  const spec = specs[index];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  return {
347
  id: createId(),
348
+ layout: spec.layout,
349
+ elements: buildGeneratedSlideElements(spec, slide, index, activeTheme),
350
  };
351
  });
352
 
components/home/HomePage.tsx CHANGED
@@ -1,90 +1,158 @@
 
 
 
 
 
 
 
 
 
1
  'use client';
2
 
3
- import React, { useState, useRef, useEffect } from 'react';
4
  import { useRouter } from 'next/navigation';
5
  import { LogIn, Moon, Sun, ArrowUp, Check, ChevronDown, User, LogOut } from 'lucide-react';
6
- import { useTheme } from '@/components/ThemeProvider';
7
  import { oauthLoginUrl, oauthHandleRedirectIfPresent } from '@huggingface/hub';
 
8
  import { LLAMA_PRESENTATION_MODEL } from '@/lib/ai-models';
9
- import { TEMPLATE_OPTIONS, TemplateOptionId, getTemplateLabel, normalizeTemplateId } from '@/lib/template-options';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  export default function HomePage() {
 
 
 
12
  const [prompt, setPrompt] = useState('');
13
  const [template, setTemplate] = useState<TemplateOptionId>('neobrutalism');
14
  const [isGenerating, setIsGenerating] = useState(false);
15
  const [isSelectOpen, setIsSelectOpen] = useState(false);
16
- const [user, setUser] = useState<{ name: string; avatarUrl?: string } | null>(null);
17
  const [submitError, setSubmitError] = useState<string | null>(null);
18
  const [isAuthReady, setIsAuthReady] = useState(false);
19
  const [isHuggingFaceSpace, setIsHuggingFaceSpace] = useState(false);
 
 
20
  const textareaRef = useRef<HTMLTextAreaElement>(null);
21
  const selectRef = useRef<HTMLDivElement>(null);
22
- const { theme, setTheme } = useTheme();
23
- const [mounted, setMounted] = useState(false);
24
 
 
25
  useEffect(() => {
26
  setMounted(true);
27
- const hostname = window.location.hostname;
28
- const isEmbedded = window.self !== window.top;
29
- const isSpaceHost = hostname.endsWith('.hf.space') || hostname === 'huggingface.co';
30
- setIsHuggingFaceSpace(isSpaceHost || isEmbedded);
31
 
32
  const savedTemplate = normalizeTemplateId(localStorage.getItem('ppt_theme'));
33
  if (savedTemplate) {
34
  setTemplate(savedTemplate);
35
  }
36
-
37
  const initializeAuth = async () => {
38
  try {
39
- // Check stored auth
40
- const stored = localStorage.getItem('hf_oauth');
41
- if (stored) {
42
- try {
43
- const parsed = JSON.parse(stored);
44
- const expiresAt = parsed.accessTokenExpiresAt ? new Date(parsed.accessTokenExpiresAt) : null;
45
-
46
- if (expiresAt && expiresAt > new Date() && parsed.accessToken) {
47
- setUser({
48
- name: parsed.userInfo.name || parsed.userInfo.preferred_username,
49
- avatarUrl: parsed.userInfo.avatarUrl || parsed.userInfo.picture || '',
50
- });
51
- localStorage.setItem('hf_api_key', parsed.accessToken);
52
- setSubmitError(null);
53
- } else {
54
- localStorage.removeItem('hf_oauth');
55
- localStorage.removeItem('hf_api_key');
56
- sessionStorage.removeItem('editorAccess');
57
- }
58
- } catch {
59
- localStorage.removeItem('hf_oauth');
60
- localStorage.removeItem('hf_api_key');
61
- sessionStorage.removeItem('editorAccess');
62
- }
63
  }
64
 
65
- // Handle redirect
66
- const result = await oauthHandleRedirectIfPresent();
67
- if (result) {
68
- const ui = result.userInfo as any;
69
- const userData = {
70
- accessToken: result.accessToken,
71
- accessTokenExpiresAt: result.accessTokenExpiresAt,
72
- userInfo: {
73
- name: ui.preferred_username || ui.name,
74
- fullname: ui.name || '',
75
- avatarUrl: ui.picture || ui.avatarUrl || '',
76
- email: ui.email,
77
- }
78
- };
79
- localStorage.setItem('hf_oauth', JSON.stringify(userData));
80
- localStorage.setItem('hf_api_key', result.accessToken);
81
- setUser({ name: userData.userInfo.name, avatarUrl: userData.userInfo.avatarUrl });
82
  setSubmitError(null);
83
- // Clean URL
84
  window.history.replaceState({}, document.title, window.location.pathname);
85
  }
86
- } catch (err) {
87
- console.error('OAuth error:', err);
88
  } finally {
89
  setIsAuthReady(true);
90
  }
@@ -95,21 +163,25 @@ export default function HomePage() {
95
 
96
  const handleSignIn = async () => {
97
  try {
98
- const res = await fetch('/api/auth/client-id');
99
- const { clientId } = await res.json();
 
100
  if (!clientId) {
101
- console.error('HuggingFace Client ID not configured');
102
  return;
103
  }
 
104
  const loginUrl = await oauthLoginUrl({
105
  clientId,
106
  scopes: 'openid profile inference-api',
107
  redirectUrl: `${window.location.origin}/`,
108
  });
 
109
  const destination = `${loginUrl}&prompt=consent`;
 
 
110
  if (isHuggingFaceSpace) {
111
  const topNavigation = window.open(destination, '_top');
112
-
113
  if (!topNavigation) {
114
  window.open(destination, '_blank', 'noopener,noreferrer');
115
  }
@@ -117,24 +189,19 @@ export default function HomePage() {
117
  }
118
 
119
  window.location.assign(destination);
120
- } catch (err) {
121
- console.error('Sign in error:', err);
122
  }
123
  };
124
 
125
  const handleSignOut = () => {
126
- localStorage.removeItem('hf_oauth');
127
- localStorage.removeItem('hf_api_key');
128
- sessionStorage.removeItem('editorAccess');
129
  setUser(null);
130
  };
131
 
132
- const router = useRouter();
133
-
134
  const handleSubmit = () => {
135
  const trimmedPrompt = prompt.trim();
136
- if (!trimmedPrompt) return;
137
- if (!isAuthReady) return;
138
 
139
  if (!user) {
140
  setSubmitError('Please log in with Hugging Face before generating a presentation.');
@@ -144,6 +211,7 @@ export default function HomePage() {
144
  setSubmitError(null);
145
  setIsGenerating(true);
146
 
 
147
  sessionStorage.setItem('generationPrompt', trimmedPrompt);
148
  sessionStorage.setItem('generationModel', LLAMA_PRESENTATION_MODEL);
149
  sessionStorage.setItem('isGenerating', 'true');
@@ -153,28 +221,36 @@ export default function HomePage() {
153
  router.push('/editor');
154
  };
155
 
156
- const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
157
- if (e.key === 'Enter' && !e.shiftKey) {
158
- e.preventDefault();
 
 
 
 
 
 
 
159
  handleSubmit();
160
  }
161
  };
162
 
163
- // Auto-resize textarea
164
  useEffect(() => {
165
- if (textareaRef.current) {
166
- textareaRef.current.style.height = 'auto';
167
- textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 300)}px`;
168
- }
169
  }, [prompt]);
170
 
171
- // Close select on click outside
172
  useEffect(() => {
173
  const handleClickOutside = (event: MouseEvent) => {
174
  if (selectRef.current && !selectRef.current.contains(event.target as Node)) {
175
  setIsSelectOpen(false);
176
  }
177
  };
 
178
  document.addEventListener('mousedown', handleClickOutside);
179
  return () => document.removeEventListener('mousedown', handleClickOutside);
180
  }, []);
@@ -183,12 +259,12 @@ export default function HomePage() {
183
 
184
  return (
185
  <div className="min-h-screen flex flex-col bg-white dark:bg-[#09090b] selection:bg-zinc-200 dark:selection:bg-zinc-800 transition-colors duration-300 font-sans">
186
- {/* Navbar */}
187
  <nav className={`relative z-20 flex items-center justify-between px-6 ${isHuggingFaceSpace ? 'pb-6 pt-16' : 'py-6'}`}>
188
  <div className="flex items-center gap-2 text-zinc-950 dark:text-zinc-50">
189
  <span className="text-xl font-semibold tracking-tight">Powerpoint.ai</span>
190
  </div>
191
-
192
  <div className="flex items-center gap-6">
193
  <button
194
  onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
@@ -197,7 +273,7 @@ export default function HomePage() {
197
  {theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
198
  <span className="sr-only">Toggle theme</span>
199
  </button>
200
-
201
  {user ? (
202
  <div className="flex items-center gap-4 group">
203
  <div className="flex items-center gap-2">
@@ -214,7 +290,7 @@ export default function HomePage() {
214
  {user.name}
215
  </span>
216
  </div>
217
- <button
218
  onClick={handleSignOut}
219
  className="text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-50 transition-colors"
220
  title="Sign Out"
@@ -223,7 +299,7 @@ export default function HomePage() {
223
  </button>
224
  </div>
225
  ) : (
226
- <button
227
  onClick={handleSignIn}
228
  className="pointer-events-auto flex items-center gap-2 text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-50 transition-all text-sm font-medium"
229
  >
@@ -234,34 +310,26 @@ export default function HomePage() {
234
  </div>
235
  </nav>
236
 
237
- {/* Main Content */}
238
  <main className="flex-1 flex flex-col items-center justify-center p-6 md:p-12 w-full max-w-4xl mx-auto space-y-12 mb-20">
239
-
240
- {/* Hero Section */}
241
  <div className="text-center">
242
  <h1 className="text-3xl md:text-4xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
243
  What kind of presentation do you need today?
244
  </h1>
245
  </div>
246
 
247
- {/* Builder Input Section */}
248
  <div className="w-full max-w-3xl">
249
  <div className="bg-[#f4f4f5] dark:bg-[#18181b] rounded-[2rem] p-4 flex flex-col transition-all focus-within:ring-2 focus-within:ring-zinc-200 dark:focus-within:ring-zinc-800">
250
  <textarea
251
  ref={textareaRef}
252
  value={prompt}
253
- onChange={(e) => {
254
- setPrompt(e.target.value);
255
- if (submitError) {
256
- setSubmitError(null);
257
- }
258
- }}
259
  onKeyDown={handleKeyDown}
260
  placeholder="How can I help you today?"
261
  className="w-full min-h-[100px] bg-transparent text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-500 dark:placeholder:text-zinc-500 text-lg outline-none resize-none px-4 py-2"
262
  style={{ overflow: 'hidden' }}
263
  />
264
-
265
  <div className="flex items-center justify-end gap-2 mt-2 px-2 pb-2">
266
  <div className="relative" ref={selectRef}>
267
  <button
@@ -271,7 +339,7 @@ export default function HomePage() {
271
  <span className="truncate">{getTemplateLabel(template)}</span>
272
  <ChevronDown className={`h-3.5 w-3.5 opacity-50 transition-transform ${isSelectOpen ? 'rotate-180' : ''}`} />
273
  </button>
274
-
275
  {isSelectOpen && (
276
  <div className="absolute right-0 bottom-full mb-2 z-50 min-w-[12rem] overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-[#09090b] text-zinc-950 dark:text-zinc-50 shadow-xl animate-in fade-in slide-in-from-bottom-2">
277
  <div className="p-1">
@@ -294,6 +362,7 @@ export default function HomePage() {
294
  </div>
295
  )}
296
  </div>
 
297
  <button
298
  onClick={handleSubmit}
299
  disabled={!prompt.trim() || isGenerating || !isAuthReady}
@@ -303,6 +372,7 @@ export default function HomePage() {
303
  </button>
304
  </div>
305
  </div>
 
306
  {submitError && (
307
  <p className="mt-3 px-2 text-sm font-medium text-red-600 dark:text-red-400">
308
  {submitError}
 
1
+ /*
2
+ * HomePage.tsx
3
+ * Purpose: Renders the public landing page, restores Hugging Face auth state, and
4
+ * prepares the session data that hands a generation request off to the editor route.
5
+ * Used by: app/page.tsx
6
+ * Depends on: ThemeProvider, Hugging Face OAuth helpers, template option helpers,
7
+ * and the editor route's sessionStorage contract.
8
+ */
9
+
10
  'use client';
11
 
12
+ import React, { useEffect, useRef, useState } from 'react';
13
  import { useRouter } from 'next/navigation';
14
  import { LogIn, Moon, Sun, ArrowUp, Check, ChevronDown, User, LogOut } from 'lucide-react';
 
15
  import { oauthLoginUrl, oauthHandleRedirectIfPresent } from '@huggingface/hub';
16
+ import { useTheme } from '@/components/ThemeProvider';
17
  import { LLAMA_PRESENTATION_MODEL } from '@/lib/ai-models';
18
+ import {
19
+ TEMPLATE_OPTIONS,
20
+ TemplateOptionId,
21
+ getTemplateLabel,
22
+ normalizeTemplateId,
23
+ } from '@/lib/template-options';
24
+
25
+ type AuthenticatedUser = {
26
+ name: string;
27
+ avatarUrl?: string;
28
+ };
29
+
30
+ type StoredOAuthSession = {
31
+ accessToken?: string;
32
+ accessTokenExpiresAt?: string;
33
+ userInfo?: {
34
+ name?: string;
35
+ preferred_username?: string;
36
+ avatarUrl?: string;
37
+ picture?: string;
38
+ };
39
+ };
40
+
41
+ function clearStoredAuthSession() {
42
+ localStorage.removeItem('hf_oauth');
43
+ localStorage.removeItem('hf_api_key');
44
+ sessionStorage.removeItem('editorAccess');
45
+ }
46
+
47
+ function restoreStoredUser(): AuthenticatedUser | null {
48
+ const storedSession = localStorage.getItem('hf_oauth');
49
+ if (!storedSession) return null;
50
+
51
+ try {
52
+ const parsed = JSON.parse(storedSession) as StoredOAuthSession;
53
+ const expiresAt = parsed.accessTokenExpiresAt ? new Date(parsed.accessTokenExpiresAt) : null;
54
+ const isTokenValid = Boolean(parsed.accessToken && expiresAt && expiresAt > new Date());
55
+
56
+ if (!isTokenValid) {
57
+ clearStoredAuthSession();
58
+ return null;
59
+ }
60
+
61
+ localStorage.setItem('hf_api_key', parsed.accessToken as string);
62
+ return {
63
+ name: parsed.userInfo?.name || parsed.userInfo?.preferred_username || 'Hugging Face User',
64
+ avatarUrl: parsed.userInfo?.avatarUrl || parsed.userInfo?.picture || '',
65
+ };
66
+ } catch {
67
+ clearStoredAuthSession();
68
+ return null;
69
+ }
70
+ }
71
+
72
+ function persistOAuthRedirectSession(result: Awaited<ReturnType<typeof oauthHandleRedirectIfPresent>>) {
73
+ if (!result) return null;
74
+
75
+ const oauthUser = result.userInfo as {
76
+ preferred_username?: string;
77
+ name?: string;
78
+ picture?: string;
79
+ avatarUrl?: string;
80
+ email?: string;
81
+ };
82
+
83
+ const storedSession = {
84
+ accessToken: result.accessToken,
85
+ accessTokenExpiresAt: result.accessTokenExpiresAt,
86
+ userInfo: {
87
+ name: oauthUser.preferred_username || oauthUser.name,
88
+ fullname: oauthUser.name || '',
89
+ avatarUrl: oauthUser.picture || oauthUser.avatarUrl || '',
90
+ email: oauthUser.email,
91
+ },
92
+ };
93
+
94
+ localStorage.setItem('hf_oauth', JSON.stringify(storedSession));
95
+ localStorage.setItem('hf_api_key', result.accessToken);
96
+
97
+ return {
98
+ name: storedSession.userInfo.name || 'Hugging Face User',
99
+ avatarUrl: storedSession.userInfo.avatarUrl,
100
+ };
101
+ }
102
+
103
+ function isHuggingFaceSpacesRuntime() {
104
+ const hostname = window.location.hostname;
105
+ const isEmbedded = window.self !== window.top;
106
+ const isSpaceHost = hostname.endsWith('.hf.space') || hostname === 'huggingface.co';
107
+ return isSpaceHost || isEmbedded;
108
+ }
109
 
110
  export default function HomePage() {
111
+ const router = useRouter();
112
+ const { theme, setTheme } = useTheme();
113
+
114
  const [prompt, setPrompt] = useState('');
115
  const [template, setTemplate] = useState<TemplateOptionId>('neobrutalism');
116
  const [isGenerating, setIsGenerating] = useState(false);
117
  const [isSelectOpen, setIsSelectOpen] = useState(false);
118
+ const [user, setUser] = useState<AuthenticatedUser | null>(null);
119
  const [submitError, setSubmitError] = useState<string | null>(null);
120
  const [isAuthReady, setIsAuthReady] = useState(false);
121
  const [isHuggingFaceSpace, setIsHuggingFaceSpace] = useState(false);
122
+ const [mounted, setMounted] = useState(false);
123
+
124
  const textareaRef = useRef<HTMLTextAreaElement>(null);
125
  const selectRef = useRef<HTMLDivElement>(null);
 
 
126
 
127
+ // Restore the saved template choice and hydrate the browser-only auth/session state.
128
  useEffect(() => {
129
  setMounted(true);
130
+ setIsHuggingFaceSpace(isHuggingFaceSpacesRuntime());
 
 
 
131
 
132
  const savedTemplate = normalizeTemplateId(localStorage.getItem('ppt_theme'));
133
  if (savedTemplate) {
134
  setTemplate(savedTemplate);
135
  }
136
+
137
  const initializeAuth = async () => {
138
  try {
139
+ const restoredUser = restoreStoredUser();
140
+ if (restoredUser) {
141
+ setUser(restoredUser);
142
+ setSubmitError(null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  }
144
 
145
+ // Handle the post-login redirect on the home route and persist it like a refreshable session.
146
+ const redirectResult = await oauthHandleRedirectIfPresent();
147
+ const redirectedUser = persistOAuthRedirectSession(redirectResult);
148
+
149
+ if (redirectedUser) {
150
+ setUser(redirectedUser);
 
 
 
 
 
 
 
 
 
 
 
151
  setSubmitError(null);
 
152
  window.history.replaceState({}, document.title, window.location.pathname);
153
  }
154
+ } catch (error) {
155
+ console.error('OAuth error:', error);
156
  } finally {
157
  setIsAuthReady(true);
158
  }
 
163
 
164
  const handleSignIn = async () => {
165
  try {
166
+ const response = await fetch('/api/auth/client-id');
167
+ const { clientId } = await response.json();
168
+
169
  if (!clientId) {
170
+ console.error('Hugging Face client ID not configured');
171
  return;
172
  }
173
+
174
  const loginUrl = await oauthLoginUrl({
175
  clientId,
176
  scopes: 'openid profile inference-api',
177
  redirectUrl: `${window.location.origin}/`,
178
  });
179
+
180
  const destination = `${loginUrl}&prompt=consent`;
181
+
182
+ // Space embeds can block same-frame navigation, so prefer escaping the iframe shell.
183
  if (isHuggingFaceSpace) {
184
  const topNavigation = window.open(destination, '_top');
 
185
  if (!topNavigation) {
186
  window.open(destination, '_blank', 'noopener,noreferrer');
187
  }
 
189
  }
190
 
191
  window.location.assign(destination);
192
+ } catch (error) {
193
+ console.error('Sign in error:', error);
194
  }
195
  };
196
 
197
  const handleSignOut = () => {
198
+ clearStoredAuthSession();
 
 
199
  setUser(null);
200
  };
201
 
 
 
202
  const handleSubmit = () => {
203
  const trimmedPrompt = prompt.trim();
204
+ if (!trimmedPrompt || !isAuthReady) return;
 
205
 
206
  if (!user) {
207
  setSubmitError('Please log in with Hugging Face before generating a presentation.');
 
211
  setSubmitError(null);
212
  setIsGenerating(true);
213
 
214
+ // The editor route reads these keys on first load to start generation automatically.
215
  sessionStorage.setItem('generationPrompt', trimmedPrompt);
216
  sessionStorage.setItem('generationModel', LLAMA_PRESENTATION_MODEL);
217
  sessionStorage.setItem('isGenerating', 'true');
 
221
  router.push('/editor');
222
  };
223
 
224
+ const handlePromptChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
225
+ setPrompt(event.target.value);
226
+ if (submitError) {
227
+ setSubmitError(null);
228
+ }
229
+ };
230
+
231
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
232
+ if (event.key === 'Enter' && !event.shiftKey) {
233
+ event.preventDefault();
234
  handleSubmit();
235
  }
236
  };
237
 
238
+ // Keep the prompt box compact until the content genuinely needs more room.
239
  useEffect(() => {
240
+ if (!textareaRef.current) return;
241
+
242
+ textareaRef.current.style.height = 'auto';
243
+ textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 300)}px`;
244
  }, [prompt]);
245
 
246
+ // Close the template dropdown when the click lands outside the control cluster.
247
  useEffect(() => {
248
  const handleClickOutside = (event: MouseEvent) => {
249
  if (selectRef.current && !selectRef.current.contains(event.target as Node)) {
250
  setIsSelectOpen(false);
251
  }
252
  };
253
+
254
  document.addEventListener('mousedown', handleClickOutside);
255
  return () => document.removeEventListener('mousedown', handleClickOutside);
256
  }, []);
 
259
 
260
  return (
261
  <div className="min-h-screen flex flex-col bg-white dark:bg-[#09090b] selection:bg-zinc-200 dark:selection:bg-zinc-800 transition-colors duration-300 font-sans">
262
+ {/* Top navigation keeps theme/auth controls available before the user starts generating. */}
263
  <nav className={`relative z-20 flex items-center justify-between px-6 ${isHuggingFaceSpace ? 'pb-6 pt-16' : 'py-6'}`}>
264
  <div className="flex items-center gap-2 text-zinc-950 dark:text-zinc-50">
265
  <span className="text-xl font-semibold tracking-tight">Powerpoint.ai</span>
266
  </div>
267
+
268
  <div className="flex items-center gap-6">
269
  <button
270
  onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
 
273
  {theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
274
  <span className="sr-only">Toggle theme</span>
275
  </button>
276
+
277
  {user ? (
278
  <div className="flex items-center gap-4 group">
279
  <div className="flex items-center gap-2">
 
290
  {user.name}
291
  </span>
292
  </div>
293
+ <button
294
  onClick={handleSignOut}
295
  className="text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-50 transition-colors"
296
  title="Sign Out"
 
299
  </button>
300
  </div>
301
  ) : (
302
+ <button
303
  onClick={handleSignIn}
304
  className="pointer-events-auto flex items-center gap-2 text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-50 transition-all text-sm font-medium"
305
  >
 
310
  </div>
311
  </nav>
312
 
313
+ {/* The builder section keeps the landing page focused on a single action: start a deck. */}
314
  <main className="flex-1 flex flex-col items-center justify-center p-6 md:p-12 w-full max-w-4xl mx-auto space-y-12 mb-20">
 
 
315
  <div className="text-center">
316
  <h1 className="text-3xl md:text-4xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
317
  What kind of presentation do you need today?
318
  </h1>
319
  </div>
320
 
 
321
  <div className="w-full max-w-3xl">
322
  <div className="bg-[#f4f4f5] dark:bg-[#18181b] rounded-[2rem] p-4 flex flex-col transition-all focus-within:ring-2 focus-within:ring-zinc-200 dark:focus-within:ring-zinc-800">
323
  <textarea
324
  ref={textareaRef}
325
  value={prompt}
326
+ onChange={handlePromptChange}
 
 
 
 
 
327
  onKeyDown={handleKeyDown}
328
  placeholder="How can I help you today?"
329
  className="w-full min-h-[100px] bg-transparent text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-500 dark:placeholder:text-zinc-500 text-lg outline-none resize-none px-4 py-2"
330
  style={{ overflow: 'hidden' }}
331
  />
332
+
333
  <div className="flex items-center justify-end gap-2 mt-2 px-2 pb-2">
334
  <div className="relative" ref={selectRef}>
335
  <button
 
339
  <span className="truncate">{getTemplateLabel(template)}</span>
340
  <ChevronDown className={`h-3.5 w-3.5 opacity-50 transition-transform ${isSelectOpen ? 'rotate-180' : ''}`} />
341
  </button>
342
+
343
  {isSelectOpen && (
344
  <div className="absolute right-0 bottom-full mb-2 z-50 min-w-[12rem] overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-[#09090b] text-zinc-950 dark:text-zinc-50 shadow-xl animate-in fade-in slide-in-from-bottom-2">
345
  <div className="p-1">
 
362
  </div>
363
  )}
364
  </div>
365
+
366
  <button
367
  onClick={handleSubmit}
368
  disabled={!prompt.trim() || isGenerating || !isAuthReady}
 
372
  </button>
373
  </div>
374
  </div>
375
+
376
  {submitError && (
377
  <p className="mt-3 px-2 text-sm font-medium text-red-600 dark:text-red-400">
378
  {submitError}
components/slides/AgendaSlideLayout.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { useState } from 'react';
 
1
+ /*
2
+ * AgendaSlideLayout.tsx
3
+ * Purpose: Implements one of the generic fallback slide layouts used when a template-specific renderer is unavailable.
4
+ * Used by: components/slides/SlideFactory.
5
+ * Depends on: Template styling props and inline field-edit callbacks.
6
+ */
7
+
8
  'use client';
9
 
10
  import React, { useState } from 'react';
components/slides/ImageAndTextLayout.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { useState } from 'react';
 
1
+ /*
2
+ * ImageAndTextLayout.tsx
3
+ * Purpose: Implements one of the generic fallback slide layouts used when a template-specific renderer is unavailable.
4
+ * Used by: components/slides/SlideFactory.
5
+ * Depends on: Template styling props and inline field-edit callbacks.
6
+ */
7
+
8
  'use client';
9
 
10
  import React, { useState } from 'react';
components/slides/ReferenceLayout.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { useState } from 'react';
 
1
+ /*
2
+ * ReferenceLayout.tsx
3
+ * Purpose: Implements one of the generic fallback slide layouts used when a template-specific renderer is unavailable.
4
+ * Used by: components/slides/SlideFactory.
5
+ * Depends on: Template styling props and inline field-edit callbacks.
6
+ */
7
+
8
  'use client';
9
 
10
  import React, { useState } from 'react';
components/slides/SlideFactory.tsx CHANGED
@@ -1,21 +1,43 @@
 
 
 
 
 
 
 
 
 
1
  import React from 'react';
2
- import { SlideSpec, getTemplateById, TemplateStyles } from '@/data/templates';
3
 
4
- // Template-specific layout components
5
  import {
6
- NeoTitleSubtitle, NeoAgenda, NeoTitleAndText, NeoThreeColumns,
7
- NeoImageAndText, NeoReferences, NeoThankYou,
 
 
 
 
 
8
  } from './neobrutalism/layouts';
9
  import {
10
- NoisyTitleSubtitle, NoisyAgenda, NoisyThreeColumns, NoisyTitleAndText,
11
- NoisyImageAndText, NoisyReferences, NoisyThankYou,
 
 
 
 
 
12
  } from './noisy/layouts';
13
  import {
14
- GalerynTitleSubtitle, GalerynAgenda, GalerynThreeColumns, GalerynTitleAndText,
15
- GalerynImageAndText, GalerynReferences, GalerynThankYou,
 
 
 
 
 
16
  } from './galeryn/layouts';
17
 
18
- // Generic fallback layouts
19
  import TitleSlideLayout from './TitleSlideLayout';
20
  import AgendaSlideLayout from './AgendaSlideLayout';
21
  import TitleAndBodyLayout from './TitleAndBodyLayout';
@@ -24,7 +46,6 @@ import ImageAndTextLayout from './ImageAndTextLayout';
24
  import ReferenceLayout from './ReferenceLayout';
25
  import ThankYouLayout from './ThankYouLayout';
26
 
27
- // Re-export SlideSpec for backward compatibility
28
  export type { SlideSpec } from '@/data/templates';
29
 
30
  export interface RenderSlideOptions {
@@ -34,25 +55,40 @@ export interface RenderSlideOptions {
34
  onFormattingUpdate?: (slideId: string, key: string, patch: Record<string, unknown>) => void;
35
  }
36
 
37
- /**
38
- * Render the correct layout component for a given slide spec.
39
- * Dispatches to template-specific components when available,
40
- * falls back to generic layouts otherwise.
41
- */
42
- export function renderSlide(
43
- spec: SlideSpec,
44
- _theme?: string,
45
- options?: RenderSlideOptions
46
- ): React.ReactNode {
47
- const { isEditable = false, onFieldUpdate, onRequestImageSelect, onFormattingUpdate } = options || {};
48
 
49
- const template = getTemplateById(spec.templateId);
50
- if (!template) {
51
- return <div className="w-full h-full flex items-center justify-center bg-gray-100 text-gray-500">Unknown template: {spec.templateId}</div>;
52
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
- const styles: TemplateStyles = template.styles;
55
- const commonProps = {
56
  title: spec.title || '',
57
  subtitle: spec.subtitle,
58
  body: spec.body,
@@ -62,65 +98,156 @@ export function renderSlide(
62
  formatting: spec.formatting,
63
  styles,
64
  slideId: spec.id,
65
- isEditable,
66
- onFieldUpdate,
67
- onFormattingUpdate,
68
  };
 
69
 
70
- // Template-specific rendering
71
- if (spec.templateId === 'neobrutalism') {
72
- switch (spec.layout) {
73
- case 'title_subtitle': return <NeoTitleSubtitle {...commonProps} />;
74
- case 'agenda': return <NeoAgenda {...commonProps} />;
75
- case 'title_and_text': return <NeoTitleAndText {...commonProps} />;
76
- case 'three_columns': return <NeoThreeColumns {...commonProps} />;
77
- case 'image_and_text': return <NeoImageAndText {...commonProps} onRequestImageSelect={onRequestImageSelect} />;
78
- case 'references': return <NeoReferences {...commonProps} />;
79
- case 'thank_you': return <NeoThankYou {...commonProps} />;
80
- }
81
- }
82
 
83
- if (spec.templateId === 'noisy') {
84
- switch (spec.layout) {
85
- case 'title_subtitle': return <NoisyTitleSubtitle {...commonProps} />;
86
- case 'agenda': return <NoisyAgenda {...commonProps} />;
87
- case 'three_columns': return <NoisyThreeColumns {...commonProps} />;
88
- case 'title_and_text': return <NoisyTitleAndText {...commonProps} />;
89
- case 'image_and_text': return <NoisyImageAndText {...commonProps} onRequestImageSelect={onRequestImageSelect} />;
90
- case 'references': return <NoisyReferences {...commonProps} />;
91
- case 'thank_you': return <NoisyThankYou {...commonProps} />;
92
- }
93
  }
94
 
95
- if (spec.templateId === 'galeryn') {
96
- switch (spec.layout) {
97
- case 'title_subtitle': return <GalerynTitleSubtitle {...commonProps} />;
98
- case 'agenda': return <GalerynAgenda {...commonProps} />;
99
- case 'three_columns': return <GalerynThreeColumns {...commonProps} />;
100
- case 'title_and_text': return <GalerynTitleAndText {...commonProps} />;
101
- case 'image_and_text': return <GalerynImageAndText {...commonProps} onRequestImageSelect={onRequestImageSelect} />;
102
- case 'references': return <GalerynReferences {...commonProps} />;
103
- case 'thank_you': return <GalerynThankYou {...commonProps} />;
104
- }
105
  }
106
 
107
- // Generic fallback for unknown templates
 
 
 
 
 
 
 
108
  switch (spec.layout) {
109
  case 'title_subtitle':
110
- return <TitleSlideLayout title={spec.title || ''} subtitle={spec.subtitle} styles={styles} slideId={spec.id} isEditable={isEditable} onFieldUpdate={onFieldUpdate} />;
 
 
 
 
 
 
 
 
 
111
  case 'agenda':
112
- return <AgendaSlideLayout title={spec.title || ''} items={spec.items || []} styles={styles} slideId={spec.id} isEditable={isEditable} onFieldUpdate={onFieldUpdate} />;
 
 
 
 
 
 
 
 
 
113
  case 'title_and_text':
114
- return <TitleAndBodyLayout title={spec.title || ''} body={spec.body || []} styles={styles} slideId={spec.id} isEditable={isEditable} onFieldUpdate={onFieldUpdate} />;
 
 
 
 
 
 
 
 
 
115
  case 'three_columns':
116
- return <ThreeColumnLayout title={spec.title || ''} columns={spec.columns || []} styles={styles} slideId={spec.id} isEditable={isEditable} onFieldUpdate={onFieldUpdate} />;
 
 
 
 
 
 
 
 
 
117
  case 'image_and_text':
118
- return <ImageAndTextLayout title={spec.title || ''} body={spec.body?.[0]?.text || spec.subtitle || ''} imageUrl={spec.imageUrl} styles={styles} slideId={spec.id} isEditable={isEditable} onFieldUpdate={onFieldUpdate} />;
 
 
 
 
 
 
 
 
 
 
119
  case 'references':
120
- return <ReferenceLayout title={spec.title || ''} items={spec.items || []} styles={styles} slideId={spec.id} isEditable={isEditable} onFieldUpdate={onFieldUpdate} />;
 
 
 
 
 
 
 
 
 
121
  case 'thank_you':
122
- return <ThankYouLayout title={spec.title || ''} subtitle={spec.subtitle} styles={styles} slideId={spec.id} isEditable={isEditable} onFieldUpdate={onFieldUpdate} />;
 
 
 
 
 
 
 
 
 
123
  default:
124
- return <TitleSlideLayout title={spec.title || 'Slide'} subtitle={spec.subtitle} styles={styles} slideId={spec.id} isEditable={isEditable} onFieldUpdate={onFieldUpdate} />;
 
 
 
 
 
 
 
 
 
125
  }
126
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * SlideFactory.tsx
3
+ * Purpose: Centralizes slide rendering by mapping a SlideSpec to the correct
4
+ * theme-specific layout component, with generic fallback layouts as a safety net.
5
+ * Used by: GoogleSlidesEditor thumbnails/canvas and any code that needs to render
6
+ * template slides from declarative SlideSpec data.
7
+ * Depends on: data/templates for SlideSpec/template metadata and all slide layout components.
8
+ */
9
+
10
  import React from 'react';
11
+ import { SlideSpec, getTemplateById, TemplateStyles, LayoutType } from '@/data/templates';
12
 
 
13
  import {
14
+ NeoTitleSubtitle,
15
+ NeoAgenda,
16
+ NeoTitleAndText,
17
+ NeoThreeColumns,
18
+ NeoImageAndText,
19
+ NeoReferences,
20
+ NeoThankYou,
21
  } from './neobrutalism/layouts';
22
  import {
23
+ NoisyTitleSubtitle,
24
+ NoisyAgenda,
25
+ NoisyThreeColumns,
26
+ NoisyTitleAndText,
27
+ NoisyImageAndText,
28
+ NoisyReferences,
29
+ NoisyThankYou,
30
  } from './noisy/layouts';
31
  import {
32
+ GalerynTitleSubtitle,
33
+ GalerynAgenda,
34
+ GalerynThreeColumns,
35
+ GalerynTitleAndText,
36
+ GalerynImageAndText,
37
+ GalerynReferences,
38
+ GalerynThankYou,
39
  } from './galeryn/layouts';
40
 
 
41
  import TitleSlideLayout from './TitleSlideLayout';
42
  import AgendaSlideLayout from './AgendaSlideLayout';
43
  import TitleAndBodyLayout from './TitleAndBodyLayout';
 
46
  import ReferenceLayout from './ReferenceLayout';
47
  import ThankYouLayout from './ThankYouLayout';
48
 
 
49
  export type { SlideSpec } from '@/data/templates';
50
 
51
  export interface RenderSlideOptions {
 
55
  onFormattingUpdate?: (slideId: string, key: string, patch: Record<string, unknown>) => void;
56
  }
57
 
58
+ type LayoutComponent = React.ComponentType<any>;
 
 
 
 
 
 
 
 
 
 
59
 
60
+ const templateLayoutRegistry: Record<string, Partial<Record<LayoutType, LayoutComponent>>> = {
61
+ neobrutalism: {
62
+ title_subtitle: NeoTitleSubtitle as LayoutComponent,
63
+ agenda: NeoAgenda as LayoutComponent,
64
+ title_and_text: NeoTitleAndText as LayoutComponent,
65
+ three_columns: NeoThreeColumns as LayoutComponent,
66
+ image_and_text: NeoImageAndText as LayoutComponent,
67
+ references: NeoReferences as LayoutComponent,
68
+ thank_you: NeoThankYou as LayoutComponent,
69
+ },
70
+ noisy: {
71
+ title_subtitle: NoisyTitleSubtitle as LayoutComponent,
72
+ agenda: NoisyAgenda as LayoutComponent,
73
+ title_and_text: NoisyTitleAndText as LayoutComponent,
74
+ three_columns: NoisyThreeColumns as LayoutComponent,
75
+ image_and_text: NoisyImageAndText as LayoutComponent,
76
+ references: NoisyReferences as LayoutComponent,
77
+ thank_you: NoisyThankYou as LayoutComponent,
78
+ },
79
+ galeryn: {
80
+ title_subtitle: GalerynTitleSubtitle as LayoutComponent,
81
+ agenda: GalerynAgenda as LayoutComponent,
82
+ title_and_text: GalerynTitleAndText as LayoutComponent,
83
+ three_columns: GalerynThreeColumns as LayoutComponent,
84
+ image_and_text: GalerynImageAndText as LayoutComponent,
85
+ references: GalerynReferences as LayoutComponent,
86
+ thank_you: GalerynThankYou as LayoutComponent,
87
+ },
88
+ };
89
 
90
+ function buildCommonProps(spec: SlideSpec, styles: TemplateStyles, options?: RenderSlideOptions) {
91
+ return {
92
  title: spec.title || '',
93
  subtitle: spec.subtitle,
94
  body: spec.body,
 
98
  formatting: spec.formatting,
99
  styles,
100
  slideId: spec.id,
101
+ isEditable: options?.isEditable || false,
102
+ onFieldUpdate: options?.onFieldUpdate,
103
+ onFormattingUpdate: options?.onFormattingUpdate,
104
  };
105
+ }
106
 
107
+ function renderThemeSpecificLayout(
108
+ spec: SlideSpec,
109
+ commonProps: Record<string, unknown>,
110
+ onRequestImageSelect?: (slideId: string) => void
111
+ ) {
112
+ const templateLayouts = templateLayoutRegistry[spec.templateId];
113
+ const LayoutComponent = templateLayouts?.[spec.layout];
 
 
 
 
 
114
 
115
+ if (!LayoutComponent) {
116
+ return null;
 
 
 
 
 
 
 
 
117
  }
118
 
119
+ if (spec.layout === 'image_and_text') {
120
+ return <LayoutComponent {...commonProps} onRequestImageSelect={onRequestImageSelect} />;
 
 
 
 
 
 
 
 
121
  }
122
 
123
+ return <LayoutComponent {...commonProps} />;
124
+ }
125
+
126
+ function renderFallbackLayout(
127
+ spec: SlideSpec,
128
+ styles: TemplateStyles,
129
+ options?: RenderSlideOptions
130
+ ) {
131
  switch (spec.layout) {
132
  case 'title_subtitle':
133
+ return (
134
+ <TitleSlideLayout
135
+ title={spec.title || ''}
136
+ subtitle={spec.subtitle}
137
+ styles={styles}
138
+ slideId={spec.id}
139
+ isEditable={options?.isEditable}
140
+ onFieldUpdate={options?.onFieldUpdate}
141
+ />
142
+ );
143
  case 'agenda':
144
+ return (
145
+ <AgendaSlideLayout
146
+ title={spec.title || ''}
147
+ items={spec.items || []}
148
+ styles={styles}
149
+ slideId={spec.id}
150
+ isEditable={options?.isEditable}
151
+ onFieldUpdate={options?.onFieldUpdate}
152
+ />
153
+ );
154
  case 'title_and_text':
155
+ return (
156
+ <TitleAndBodyLayout
157
+ title={spec.title || ''}
158
+ body={spec.body || []}
159
+ styles={styles}
160
+ slideId={spec.id}
161
+ isEditable={options?.isEditable}
162
+ onFieldUpdate={options?.onFieldUpdate}
163
+ />
164
+ );
165
  case 'three_columns':
166
+ return (
167
+ <ThreeColumnLayout
168
+ title={spec.title || ''}
169
+ columns={spec.columns || []}
170
+ styles={styles}
171
+ slideId={spec.id}
172
+ isEditable={options?.isEditable}
173
+ onFieldUpdate={options?.onFieldUpdate}
174
+ />
175
+ );
176
  case 'image_and_text':
177
+ return (
178
+ <ImageAndTextLayout
179
+ title={spec.title || ''}
180
+ body={spec.body?.[0]?.text || spec.subtitle || ''}
181
+ imageUrl={spec.imageUrl}
182
+ styles={styles}
183
+ slideId={spec.id}
184
+ isEditable={options?.isEditable}
185
+ onFieldUpdate={options?.onFieldUpdate}
186
+ />
187
+ );
188
  case 'references':
189
+ return (
190
+ <ReferenceLayout
191
+ title={spec.title || ''}
192
+ items={spec.items || []}
193
+ styles={styles}
194
+ slideId={spec.id}
195
+ isEditable={options?.isEditable}
196
+ onFieldUpdate={options?.onFieldUpdate}
197
+ />
198
+ );
199
  case 'thank_you':
200
+ return (
201
+ <ThankYouLayout
202
+ title={spec.title || ''}
203
+ subtitle={spec.subtitle}
204
+ styles={styles}
205
+ slideId={spec.id}
206
+ isEditable={options?.isEditable}
207
+ onFieldUpdate={options?.onFieldUpdate}
208
+ />
209
+ );
210
  default:
211
+ return (
212
+ <TitleSlideLayout
213
+ title={spec.title || 'Slide'}
214
+ subtitle={spec.subtitle}
215
+ styles={styles}
216
+ slideId={spec.id}
217
+ isEditable={options?.isEditable}
218
+ onFieldUpdate={options?.onFieldUpdate}
219
+ />
220
+ );
221
  }
222
  }
223
+
224
+ /**
225
+ * Converts a declarative slide spec into a rendered React layout.
226
+ * Theme-specific layouts are preferred, while generic layouts ensure the editor
227
+ * can still render a slide if a template-specific component is missing.
228
+ */
229
+ export function renderSlide(
230
+ spec: SlideSpec,
231
+ _theme?: string,
232
+ options?: RenderSlideOptions
233
+ ): React.ReactNode {
234
+ const template = getTemplateById(spec.templateId);
235
+
236
+ if (!template) {
237
+ return (
238
+ <div className="w-full h-full flex items-center justify-center bg-gray-100 text-gray-500">
239
+ Unknown template: {spec.templateId}
240
+ </div>
241
+ );
242
+ }
243
+
244
+ const styles: TemplateStyles = template.styles;
245
+ const commonProps = buildCommonProps(spec, styles, options);
246
+ const themeSpecificLayout = renderThemeSpecificLayout(spec, commonProps, options?.onRequestImageSelect);
247
+
248
+ if (themeSpecificLayout) {
249
+ return themeSpecificLayout;
250
+ }
251
+
252
+ return renderFallbackLayout(spec, styles, options);
253
+ }
components/slides/ThankYouLayout.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { useState } from 'react';
 
1
+ /*
2
+ * ThankYouLayout.tsx
3
+ * Purpose: Implements one of the generic fallback slide layouts used when a template-specific renderer is unavailable.
4
+ * Used by: components/slides/SlideFactory.
5
+ * Depends on: Template styling props and inline field-edit callbacks.
6
+ */
7
+
8
  'use client';
9
 
10
  import React, { useState } from 'react';
components/slides/ThreeColumnLayout.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { useState } from 'react';
 
1
+ /*
2
+ * ThreeColumnLayout.tsx
3
+ * Purpose: Implements one of the generic fallback slide layouts used when a template-specific renderer is unavailable.
4
+ * Used by: components/slides/SlideFactory.
5
+ * Depends on: Template styling props and inline field-edit callbacks.
6
+ */
7
+
8
  'use client';
9
 
10
  import React, { useState } from 'react';
components/slides/TitleAndBodyLayout.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { useState } from 'react';
 
1
+ /*
2
+ * TitleAndBodyLayout.tsx
3
+ * Purpose: Implements one of the generic fallback slide layouts used when a template-specific renderer is unavailable.
4
+ * Used by: components/slides/SlideFactory.
5
+ * Depends on: Template styling props and inline field-edit callbacks.
6
+ */
7
+
8
  'use client';
9
 
10
  import React, { useState } from 'react';
components/slides/TitleSlideLayout.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { useState } from 'react';
 
1
+ /*
2
+ * TitleSlideLayout.tsx
3
+ * Purpose: Implements one of the generic fallback slide layouts used when a template-specific renderer is unavailable.
4
+ * Used by: components/slides/SlideFactory.
5
+ * Depends on: Template styling props and inline field-edit callbacks.
6
+ */
7
+
8
  'use client';
9
 
10
  import React, { useState } from 'react';
components/slides/galeryn/layouts.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { useEffect, useRef } from 'react';
 
1
+ /*
2
+ * layouts.tsx
3
+ * Purpose: Defines the Galeryn theme-specific slide layout components.
4
+ * Used by: components/slides/SlideFactory.
5
+ * Depends on: Galeryn template styles and shared editable-field helpers.
6
+ */
7
+
8
  'use client';
9
 
10
  import React, { useEffect, useRef } from 'react';
components/slides/neobrutalism/layouts.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { useRef, useState } from 'react';
 
1
+ /*
2
+ * layouts.tsx
3
+ * Purpose: Defines the Neo-Brutalism theme-specific slide layout components.
4
+ * Used by: components/slides/SlideFactory.
5
+ * Depends on: Neo-Brutalism template styles and shared editable-field helpers.
6
+ */
7
+
8
  'use client';
9
 
10
  import React, { useRef, useState } from 'react';
components/slides/noisy/layouts.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { useEffect, useId, useRef } from 'react';
 
1
+ /*
2
+ * layouts.tsx
3
+ * Purpose: Defines the Noisy/Distortion theme-specific slide layout components.
4
+ * Used by: components/slides/SlideFactory.
5
+ * Depends on: Noisy template styles and shared editable-field helpers.
6
+ */
7
+
8
  'use client';
9
 
10
  import React, { useEffect, useId, useRef } from 'react';
components/slides/shared/PersistedDraggableSurface.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  'use client';
2
 
3
  import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
 
1
+ /*
2
+ * PersistedDraggableSurface.tsx
3
+ * Purpose: Provides the reusable editable/draggable surface logic shared by slide layouts that persist field placement.
4
+ * Used by: Theme-specific slide layout components.
5
+ * Depends on: React drag/edit state and slide field update callbacks.
6
+ */
7
+
8
  'use client';
9
 
10
  import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
components/ui/empty.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import * as React from "react"
2
  import { cn } from "@/lib/utils"
3
 
 
1
+ /*
2
+ * empty.tsx
3
+ * Purpose: Contains reusable empty/loading state primitives used throughout the app.
4
+ * Used by: Editor loading/error states and any view that needs a structured empty state.
5
+ * Depends on: Shared UI styling helpers.
6
+ */
7
+
8
  import * as React from "react"
9
  import { cn } from "@/lib/utils"
10
 
components/ui/spinner.tsx CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import * as React from "react"
2
  import { cn } from "@/lib/utils"
3
 
 
1
+ /*
2
+ * spinner.tsx
3
+ * Purpose: Provides the small reusable loading spinner component.
4
+ * Used by: Editor loading states and any async UI state.
5
+ * Depends on: React props and shared styling.
6
+ */
7
+
8
  import * as React from "react"
9
  import { cn } from "@/lib/utils"
10
 
data/templates/galeryn.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import { Template } from './index';
2
 
3
  export const galerynTemplate: Template = {
 
1
+ /*
2
+ * galeryn.ts
3
+ * Purpose: Declares one presentation template, including its styles, layouts, and default field values.
4
+ * Used by: data/templates/index.ts and the template-driven editor/render pipeline.
5
+ * Depends on: Template type definitions from data/templates/index.ts.
6
+ */
7
+
8
  import { Template } from './index';
9
 
10
  export const galerynTemplate: Template = {
data/templates/index.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import { neoBrutalismTemplate } from './neo-brutalism';
2
  import { galerynTemplate } from './galeryn';
3
  import { noisyTemplate } from './noisy';
 
1
+ /*
2
+ * index.ts
3
+ * Purpose: Acts as the template registry and shared type definition source for template-driven slides.
4
+ * Used by: SlideFactory, GoogleSlidesEditor, and template-option helpers.
5
+ * Depends on: Individual template definition files.
6
+ */
7
+
8
  import { neoBrutalismTemplate } from './neo-brutalism';
9
  import { galerynTemplate } from './galeryn';
10
  import { noisyTemplate } from './noisy';
data/templates/neo-brutalism.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import { Template } from './index';
2
 
3
  export const neoBrutalismTemplate: Template = {
 
1
+ /*
2
+ * neo-brutalism.ts
3
+ * Purpose: Declares one presentation template, including its styles, layouts, and default field values.
4
+ * Used by: data/templates/index.ts and the template-driven editor/render pipeline.
5
+ * Depends on: Template type definitions from data/templates/index.ts.
6
+ */
7
+
8
  import { Template } from './index';
9
 
10
  export const neoBrutalismTemplate: Template = {
data/templates/noisy.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import { Template } from './index';
2
 
3
  export const noisyTemplate: Template = {
 
1
+ /*
2
+ * noisy.ts
3
+ * Purpose: Declares one presentation template, including its styles, layouts, and default field values.
4
+ * Used by: data/templates/index.ts and the template-driven editor/render pipeline.
5
+ * Depends on: Template type definitions from data/templates/index.ts.
6
+ */
7
+
8
  import { Template } from './index';
9
 
10
  export const noisyTemplate: Template = {
hooks/useExport.ts CHANGED
@@ -1,3 +1,11 @@
 
 
 
 
 
 
 
 
1
  import type { SlideModel } from '@/lib/editor-types';
2
  import { themes } from '@/lib/editor-themes';
3
  import type { SlideSpec } from '@/data/templates';
@@ -12,10 +20,15 @@ interface UseExportParams {
12
  }
13
 
14
  export function useExport({
15
- slideRef, slides, slideSpecs, currentTheme,
 
 
 
16
  presentationTitle,
17
  }: UseExportParams) {
18
  const exportToPPTX = async () => {
 
 
19
  if (!slideRef.current || slides.length === 0) return;
20
 
21
  try {
 
1
+ /*
2
+ * useExport.ts
3
+ * Purpose: Wraps the presentation export flow so the editor can trigger a PPTX
4
+ * download without owning the file-generation details.
5
+ * Used by: GoogleSlidesEditor
6
+ * Depends on: editable-pptx-export and the editor's current slide/theme state.
7
+ */
8
+
9
  import type { SlideModel } from '@/lib/editor-types';
10
  import { themes } from '@/lib/editor-themes';
11
  import type { SlideSpec } from '@/data/templates';
 
20
  }
21
 
22
  export function useExport({
23
+ slideRef,
24
+ slides,
25
+ slideSpecs,
26
+ currentTheme,
27
  presentationTitle,
28
  }: UseExportParams) {
29
  const exportToPPTX = async () => {
30
+ // The editor only exposes export once a canvas exists, but this guard keeps the
31
+ // hook resilient if the button is triggered during an intermediate render state.
32
  if (!slideRef.current || slides.length === 0) return;
33
 
34
  try {
hooks/useKeyboardShortcuts.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import { useEffect } from 'react';
2
  import type { TextElement, EditorElement } from '@/lib/editor-types';
3
 
 
1
+ /*
2
+ * useKeyboardShortcuts.ts
3
+ * Purpose: Encapsulates keyboard shortcut handling for editor selection, formatting, and undo/redo actions.
4
+ * Used by: GoogleSlidesEditor.
5
+ * Depends on: Window keyboard events and editor mutation callbacks.
6
+ */
7
+
8
  import { useEffect } from 'react';
9
  import type { TextElement, EditorElement } from '@/lib/editor-types';
10
 
hooks/useSlideHistory.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import { useState, useRef, useCallback, useEffect } from 'react';
2
  import type { SlideModel } from '@/lib/editor-types';
3
 
 
1
+ /*
2
+ * useSlideHistory.ts
3
+ * Purpose: Stores undo/redo history for the editor slide state.
4
+ * Used by: GoogleSlidesEditor.
5
+ * Depends on: React state snapshots of the current slide collection.
6
+ */
7
+
8
  import { useState, useRef, useCallback, useEffect } from 'react';
9
  import type { SlideModel } from '@/lib/editor-types';
10
 
lib/ai-models.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  export const LLAMA_PRESENTATION_MODEL = 'meta-llama/Llama-3.3-70B-Instruct:hf-inference';
2
  export const LLAMA_PRESENTATION_PROVIDER = 'hf' as const;
3
 
 
1
+ /*
2
+ * ai-models.ts
3
+ * Purpose: Defines the approved AI models and model validation helpers used by generation/edit routes.
4
+ * Used by: HomePage, generation routes, and HF client setup.
5
+ * Depends on: Shared model constants.
6
+ */
7
+
8
  export const LLAMA_PRESENTATION_MODEL = 'meta-llama/Llama-3.3-70B-Instruct:hf-inference';
9
  export const LLAMA_PRESENTATION_PROVIDER = 'hf' as const;
10
 
lib/capture-element.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  interface CaptureOptions {
2
  scale?: number;
3
  }
 
1
+ /*
2
+ * capture-element.ts
3
+ * Purpose: Captures DOM slide content as an image while handling cross-origin-safe image loading.
4
+ * Used by: Export and image-capture flows.
5
+ * Depends on: html-to-image style capture logic and the image proxy route.
6
+ */
7
+
8
  interface CaptureOptions {
9
  scale?: number;
10
  }
lib/editable-pptx-export.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import PptxGenJS from 'pptxgenjs';
2
  import { getTemplateById, type SlideSpec } from '@/data/templates';
3
  import type { SlideModel, TextElement, ImageElement, ShapeElement } from '@/lib/editor-types';
 
1
+ /*
2
+ * editable-pptx-export.ts
3
+ * Purpose: Builds an editable PowerPoint file from the current slide/editor state.
4
+ * Used by: hooks/useExport.
5
+ * Depends on: pptxgenjs, editor types, and template/theme helpers.
6
+ */
7
+
8
  import PptxGenJS from 'pptxgenjs';
9
  import { getTemplateById, type SlideSpec } from '@/data/templates';
10
  import type { SlideModel, TextElement, ImageElement, ShapeElement } from '@/lib/editor-types';
lib/editor-themes.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  export const TEMPLATE_THEMES = new Set<string>(['neobrutalism', 'galeryn', 'noisy']);
2
 
3
  // Theme definitions - Includes gradients, patterns, and image-based themes
 
1
+ /*
2
+ * editor-themes.ts
3
+ * Purpose: Defines the editor theme palette/font registry that powers non-template canvas rendering.
4
+ * Used by: GoogleSlidesEditor, export helpers, and layout background utilities.
5
+ * Depends on: Theme constants and PowerPoint-compatible font lists.
6
+ */
7
+
8
  export const TEMPLATE_THEMES = new Set<string>(['neobrutalism', 'galeryn', 'noisy']);
9
 
10
  // Theme definitions - Includes gradients, patterns, and image-based themes
lib/editor-types.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  export type ElementType = 'text' | 'image' | 'shape';
2
  export type ShapeType = 'rectangle' | 'circle' | 'triangle' | 'arrow' | 'star' | 'diamond' | 'hexagon' | 'line';
3
 
 
1
+ /*
2
+ * editor-types.ts
3
+ * Purpose: Contains the shared editor model types for text, images, shapes, slides, and guides.
4
+ * Used by: GoogleSlidesEditor, export helpers, hooks, and layout utilities.
5
+ * Depends on: TypeScript only.
6
+ */
7
+
8
  export type ElementType = 'text' | 'image' | 'shape';
9
  export type ShapeType = 'rectangle' | 'circle' | 'triangle' | 'arrow' | 'star' | 'diamond' | 'hexagon' | 'line';
10
 
lib/generated-presentation.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import type { SlideSpec } from '@/data/templates';
2
  import { normalizeLayout } from '@/lib/slide-prompt';
3
 
 
1
+ /*
2
+ * generated-presentation.ts
3
+ * Purpose: Defines the typed shape of generated presentation responses and helpers for normalizing them into SlideSpec data.
4
+ * Used by: GoogleSlidesEditor and generation API consumers.
5
+ * Depends on: SlideSpec types and slide-prompt layout normalization.
6
+ */
7
+
8
  import type { SlideSpec } from '@/data/templates';
9
  import { normalizeLayout } from '@/lib/slide-prompt';
10
 
lib/hf-client.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import { InferenceClient } from "@huggingface/inference";
2
  import { LLAMA_PRESENTATION_MODEL } from "./ai-models";
3
 
 
1
+ /*
2
+ * hf-client.ts
3
+ * Purpose: Wraps Hugging Face Inference calls behind a small client with provider/model error handling.
4
+ * Used by: Presentation generation and AI text editing routes.
5
+ * Depends on: Hugging Face SDK and model configuration helpers.
6
+ */
7
+
8
  import { InferenceClient } from "@huggingface/inference";
9
  import { LLAMA_PRESENTATION_MODEL } from "./ai-models";
10
 
lib/layout-templates.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import type { TextElement, ImageElement, EditorElement } from './editor-types';
2
  import { themes } from './editor-themes';
3
 
 
1
+ /*
2
+ * layout-templates.ts
3
+ * Purpose: Provides default editor canvas layouts, background helpers, and element factory utilities.
4
+ * Used by: GoogleSlidesEditor and legacy canvas slide creation paths.
5
+ * Depends on: Editor theme definitions and editor element types.
6
+ */
7
+
8
  import type { TextElement, ImageElement, EditorElement } from './editor-types';
9
  import { themes } from './editor-themes';
10
 
lib/template-options.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  export const TEMPLATE_OPTIONS = [
2
  { id: 'neobrutalism', label: 'Neo-Brutal' },
3
  { id: 'galeryn', label: 'Galeryn' },
 
1
+ /*
2
+ * template-options.ts
3
+ * Purpose: Maps user-facing template picker labels to normalized template IDs.
4
+ * Used by: HomePage and any code restoring template choices from storage.
5
+ * Depends on: Template ID conventions.
6
+ */
7
+
8
  export const TEMPLATE_OPTIONS = [
9
  { id: 'neobrutalism', label: 'Neo-Brutal' },
10
  { id: 'galeryn', label: 'Galeryn' },
next.config.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import type { NextConfig } from "next";
2
 
3
  const nextConfig: NextConfig = {
 
1
+ /*
2
+ * next.config.ts
3
+ * Purpose: Stores the Next.js build/runtime configuration for the app.
4
+ * Used by: Next.js build and dev server.
5
+ * Depends on: Next.js config schema and package transpilation needs.
6
+ */
7
+
8
  import type { NextConfig } from "next";
9
 
10
  const nextConfig: NextConfig = {
types/index.ts CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  // =============================================================================
2
  // PRESENTATION CONFIGURATION INTERFACES
3
  // =============================================================================
 
1
+ /*
2
+ * index.ts
3
+ * Purpose: Holds shared top-level project types that do not belong to a specific runtime module.
4
+ * Used by: Any code importing shared project-wide types.
5
+ * Depends on: TypeScript only.
6
+ */
7
+
8
  // =============================================================================
9
  // PRESENTATION CONFIGURATION INTERFACES
10
  // =============================================================================