lumina-slides 8.9.4 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LUMINA_LLM_EXAMPLES.json +234 -0
  2. package/README.md +18 -18
  3. package/dist/lumina-slides.js +13207 -12659
  4. package/dist/lumina-slides.umd.cjs +215 -215
  5. package/dist/style.css +1 -1
  6. package/package.json +5 -4
  7. package/src/App.vue +16 -0
  8. package/src/animation/index.ts +11 -0
  9. package/src/animation/registry.ts +126 -0
  10. package/src/animation/stagger.ts +95 -0
  11. package/src/animation/types.ts +53 -0
  12. package/src/components/LandingPage.vue +229 -0
  13. package/src/components/LuminaDeck.vue +224 -0
  14. package/src/components/LuminaSpeakerNotes.vue +701 -0
  15. package/src/components/base/BaseSlide.vue +122 -0
  16. package/src/components/base/LuminaElement.vue +67 -0
  17. package/src/components/base/VideoPlayer.vue +204 -0
  18. package/src/components/layouts/LayoutAuto.vue +71 -0
  19. package/src/components/layouts/LayoutChart.vue +287 -0
  20. package/src/components/layouts/LayoutCustom.vue +92 -0
  21. package/src/components/layouts/LayoutDiagram.vue +253 -0
  22. package/src/components/layouts/LayoutFeatures.vue +121 -0
  23. package/src/components/layouts/LayoutFlex.vue +172 -0
  24. package/src/components/layouts/LayoutFree.vue +62 -0
  25. package/src/components/layouts/LayoutHalf.vue +127 -0
  26. package/src/components/layouts/LayoutStatement.vue +74 -0
  27. package/src/components/layouts/LayoutSteps.vue +106 -0
  28. package/src/components/layouts/LayoutTimeline.vue +104 -0
  29. package/src/components/layouts/LayoutVideo.vue +41 -0
  30. package/src/components/parts/FlexBullets.vue +45 -0
  31. package/src/components/parts/FlexButton.vue +132 -0
  32. package/src/components/parts/FlexImage.vue +54 -0
  33. package/src/components/parts/FlexOrdered.vue +44 -0
  34. package/src/components/parts/FlexSpacer.vue +13 -0
  35. package/src/components/parts/FlexStepper.vue +59 -0
  36. package/src/components/parts/FlexText.vue +29 -0
  37. package/src/components/parts/FlexTimeline.vue +67 -0
  38. package/src/components/parts/FlexTitle.vue +39 -0
  39. package/src/components/parts/LuminaBackground.vue +100 -0
  40. package/src/components/site/LivePreview.vue +101 -0
  41. package/src/components/site/SiteApi.vue +301 -0
  42. package/src/components/site/SiteDashboard.vue +604 -0
  43. package/src/components/site/SiteDocs.vue +3267 -0
  44. package/src/components/site/SiteExamples.vue +65 -0
  45. package/src/components/site/SiteFooter.vue +6 -0
  46. package/src/components/site/SiteHome.vue +362 -0
  47. package/src/components/site/SiteNavBar.vue +122 -0
  48. package/src/components/site/SitePlayground.vue +389 -0
  49. package/src/components/site/SitePromptBuilder.vue +266 -0
  50. package/src/components/site/SiteUserMenu.vue +90 -0
  51. package/src/components/studio/ActionEditor.vue +108 -0
  52. package/src/components/studio/ArrayEditor.vue +124 -0
  53. package/src/components/studio/CollapsibleSection.vue +33 -0
  54. package/src/components/studio/ColorField.vue +22 -0
  55. package/src/components/studio/EditorCanvas.vue +326 -0
  56. package/src/components/studio/EditorLayoutFeatures.vue +18 -0
  57. package/src/components/studio/EditorLayoutFixed.vue +46 -0
  58. package/src/components/studio/EditorLayoutFlex.vue +133 -0
  59. package/src/components/studio/EditorLayoutHalf.vue +18 -0
  60. package/src/components/studio/EditorLayoutStatement.vue +18 -0
  61. package/src/components/studio/EditorLayoutSteps.vue +18 -0
  62. package/src/components/studio/EditorLayoutTimeline.vue +18 -0
  63. package/src/components/studio/EditorNode.vue +89 -0
  64. package/src/components/studio/FieldEditor.vue +133 -0
  65. package/src/components/studio/IconPicker.vue +109 -0
  66. package/src/components/studio/LayerItem.vue +117 -0
  67. package/src/components/studio/LuminaStudio.vue +30 -0
  68. package/src/components/studio/SaveSuccessModal.vue +138 -0
  69. package/src/components/studio/SlideNavigator.vue +373 -0
  70. package/src/components/studio/SliderField.vue +44 -0
  71. package/src/components/studio/StudioInspector.vue +595 -0
  72. package/src/components/studio/StudioJsonEditor.vue +191 -0
  73. package/src/components/studio/StudioLayers.vue +145 -0
  74. package/src/components/studio/StudioSettings.vue +514 -0
  75. package/src/components/studio/StudioSidebar.vue +29 -0
  76. package/src/components/studio/StudioToolbar.vue +222 -0
  77. package/src/components/studio/fieldLabels.ts +224 -0
  78. package/src/components/studio/inspectors/DiagramEdgeEditor.vue +77 -0
  79. package/src/components/studio/inspectors/DiagramNodeEditor.vue +117 -0
  80. package/src/components/studio/nodes/StudioDiagramNode.vue +138 -0
  81. package/src/composables/useAuth.ts +87 -0
  82. package/src/composables/useEditor.ts +224 -0
  83. package/src/composables/useElementState.ts +81 -0
  84. package/src/composables/useFlexLayout.ts +122 -0
  85. package/src/composables/useKeyboard.ts +45 -0
  86. package/src/composables/useLumina.ts +32 -0
  87. package/src/composables/useStudio.ts +87 -0
  88. package/src/composables/useSwipeNav.ts +53 -0
  89. package/src/composables/useTransition.ts +373 -0
  90. package/src/core/Lumina.ts +819 -0
  91. package/src/core/animationConfig.ts +251 -0
  92. package/src/core/compression.ts +34 -0
  93. package/src/core/elementController.ts +170 -0
  94. package/src/core/elementId.ts +27 -0
  95. package/src/core/elementResolver.ts +207 -0
  96. package/src/core/events.ts +53 -0
  97. package/src/core/fonts.ts +100 -0
  98. package/src/core/presets.ts +231 -0
  99. package/src/core/prompts.ts +272 -0
  100. package/src/core/schema.ts +478 -0
  101. package/src/core/speaker-channel.ts +250 -0
  102. package/src/core/store.ts +461 -0
  103. package/src/core/theme.ts +666 -0
  104. package/src/core/types.ts +1611 -0
  105. package/src/directives/vStudio.ts +45 -0
  106. package/src/index.ts +175 -0
  107. package/src/main.ts +17 -0
  108. package/src/router/index.ts +92 -0
  109. package/src/style/main.css +462 -0
  110. package/src/utils/deep.ts +127 -0
  111. package/src/utils/firebase.ts +184 -0
  112. package/src/utils/streaming.ts +134 -0
  113. package/src/views/DashboardView.vue +32 -0
  114. package/src/views/DeckView.vue +289 -0
  115. package/src/views/HomeView.vue +17 -0
  116. package/src/views/SiteLayout.vue +21 -0
  117. package/src/views/StudioView.vue +61 -0
  118. package/src/vite-env.d.ts +6 -0
  119. package/IMPLEMENTATION.md +0 -418
@@ -0,0 +1,389 @@
1
+ <template>
2
+ <div class="min-h-screen pt-24 px-4 lg:px-8 max-w-full">
3
+ <div class="flex flex-col lg:flex-row gap-6 h-[calc(100vh-8rem)]">
4
+
5
+ <!-- JSON Editor Panel -->
6
+ <div class="w-full lg:w-1/3 flex flex-col">
7
+ <div class="flex items-center justify-between mb-4">
8
+ <h2 class="text-xl font-bold text-white">JSON Editor</h2>
9
+ <div class="flex gap-2">
10
+ <button @click="loadTemplate('flex')"
11
+ class="px-3 py-1.5 text-xs font-bold uppercase bg-blue-500/20 text-blue-400 rounded-lg hover:bg-blue-500/30 transition">
12
+ Flex
13
+ </button>
14
+ <button @click="loadTemplate('statement')"
15
+ class="px-3 py-1.5 text-xs font-bold uppercase bg-purple-500/20 text-purple-400 rounded-lg hover:bg-purple-500/30 transition">
16
+ Statement
17
+ </button>
18
+ <button @click="loadTemplate('custom')"
19
+ class="px-3 py-1.5 text-xs font-bold uppercase bg-green-500/20 text-green-400 rounded-lg hover:bg-green-500/30 transition">
20
+ Custom
21
+ </button>
22
+ <button @click="formatJson"
23
+ class="px-3 py-1.5 text-xs font-bold uppercase bg-white/10 text-white/60 rounded-lg hover:bg-white/20 transition">
24
+ Format
25
+ </button>
26
+ <button @click="exportHtml"
27
+ class="px-3 py-1.5 text-xs font-bold uppercase bg-pink-500/20 text-pink-400 rounded-lg hover:bg-pink-500/30 transition flex items-center gap-1.5"
28
+ title="Export as standalone HTML">
29
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none"
30
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
31
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
32
+ <polyline points="7 10 12 15 17 10"></polyline>
33
+ <line x1="12" y1="15" x2="12" y2="3"></line>
34
+ </svg>
35
+ Export
36
+ </button>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="flex-1 relative rounded-xl overflow-hidden border border-white/10 bg-black/50">
41
+ <textarea ref="editorRef" v-model="jsonInput" @input="onInput" spellcheck="false"
42
+ class="w-full h-full p-6 bg-transparent text-white font-mono text-sm leading-relaxed resize-none focus:outline-none"
43
+ placeholder='{"type": "flex", "elements": [...]}'></textarea>
44
+
45
+ <!-- Error Indicator -->
46
+ <div v-if="parseError"
47
+ class="absolute bottom-0 left-0 right-0 px-4 py-2 bg-red-500/20 border-t border-red-500/30 text-red-400 text-xs font-mono">
48
+ {{ parseError }}
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <!-- Preview Panel -->
54
+ <div class="w-full lg:w-2/3 flex flex-col">
55
+ <div class="flex items-center justify-between mb-4">
56
+ <h2 class="text-xl font-bold text-white">Live Preview</h2>
57
+ <div class="flex items-center gap-4">
58
+ <span v-if="slideIndex !== null" class="text-sm text-white/40">
59
+ Slide {{ slideIndex + 1 }} / {{ totalSlides }}
60
+ </span>
61
+ <div class="flex gap-1">
62
+ <button @click="prevSlide"
63
+ class="w-8 h-8 rounded-lg bg-white/10 text-white/60 hover:bg-white/20 transition flex items-center justify-center"
64
+ title="Previous slide (←)">
65
+
66
+ </button>
67
+ <button @click="nextSlide"
68
+ class="w-8 h-8 rounded-lg bg-white/10 text-white/60 hover:bg-white/20 transition flex items-center justify-center"
69
+ title="Next slide (→)">
70
+
71
+ </button>
72
+ <button @click="openSpeakerNotes"
73
+ class="ml-2 px-3 py-1.5 text-xs font-bold uppercase rounded-lg transition flex items-center gap-1.5"
74
+ :class="speakerNotesOpen ? 'bg-green-500/20 text-green-400' : 'bg-amber-500/20 text-amber-400 hover:bg-amber-500/30'"
75
+ title="Open Speaker Notes (S)">
76
+ <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor"
77
+ stroke-width="2">
78
+ <path d="M12 2L2 7l10 5 10-5-10-5z" />
79
+ <path d="M2 17l10 5 10-5" />
80
+ <path d="M2 12l10 5 10-5" />
81
+ </svg>
82
+ {{ speakerNotesOpen ? 'Notes Open' : 'Notes' }}
83
+ </button>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="flex-1 rounded-xl overflow-hidden border border-white/10 bg-black relative">
89
+ <div id="playground-container" class="w-full h-full"></div>
90
+
91
+ <!-- Empty State -->
92
+ <div v-if="!hasValidJson" class="absolute inset-0 flex items-center justify-center text-white/30">
93
+ <div class="text-center">
94
+ <p class="text-4xl mb-4">🎨</p>
95
+ <p class="text-lg font-medium">Enter valid JSON to see preview</p>
96
+ <p class="text-sm mt-2">Try clicking a template button above</p>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </template>
104
+
105
+ <script setup lang="ts">
106
+ import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue';
107
+ import { Lumina } from '../../core/Lumina';
108
+
109
+ const jsonInput = ref('');
110
+ const parseError = ref<string | null>(null);
111
+ const hasValidJson = ref(false);
112
+ const editorRef = ref<HTMLTextAreaElement | null>(null);
113
+ const slideIndex = ref<number | null>(null);
114
+ const totalSlides = ref(0);
115
+ const speakerNotesOpen = ref(false);
116
+
117
+ let engine: Lumina | null = null;
118
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
119
+
120
+ const TEMPLATES = {
121
+ flex: `{
122
+ "meta": { "title": "Playground" },
123
+ "slides": [
124
+ {
125
+ "type": "flex",
126
+ "direction": "horizontal",
127
+ "notes": "**Demo slide** - Show how the flex layout works. Point out the image on the left. Explain content stacking. Mention the button actions.",
128
+ "elements": [
129
+ {
130
+ "type": "image",
131
+ "src": "./brains.png",
132
+ "size": "half",
133
+ "fill": true
134
+ },
135
+ {
136
+ "type": "content",
137
+ "size": "half",
138
+ "valign": "center",
139
+ "padding": "xl",
140
+ "gap": "lg",
141
+ "elements": [
142
+ { "type": "title", "text": "Hello World", "size": "2xl" },
143
+ { "type": "text", "text": "Edit this JSON to see changes in real-time.", "muted": true },
144
+ { "type": "bullets", "items": ["Flow-based layout", "No coordinates", "LLM friendly"] },
145
+ { "type": "button", "label": "Get Started", "variant": "primary" }
146
+ ]
147
+ }
148
+ ]
149
+ },
150
+ {
151
+ "type": "statement",
152
+ "tag": "Next Steps",
153
+ "title": "Try the Speaker Notes!",
154
+ "subtitle": "Click the Notes button above to open the presenter view.",
155
+ "notes": "This is the second slide. Encourage them to try the bidirectional sync. Show the timer functionality. Press T to toggle timer."
156
+ }
157
+ ]
158
+ }`,
159
+ statement: `{
160
+ "meta": { "title": "Playground" },
161
+ "slides": [
162
+ {
163
+ "type": "statement",
164
+ "tag": "Welcome",
165
+ "title": "Hello World",
166
+ "subtitle": "Edit this JSON to see changes instantly.",
167
+ "notes": "Remember to greet the audience! Introduce yourself. Set expectations for the presentation."
168
+ }
169
+ ]
170
+ }`,
171
+ custom: `{
172
+ "meta": { "title": "Custom Slide" },
173
+ "slides": [
174
+ {
175
+ "type": "custom",
176
+ "html": "<div class='custom-container'><h1 class='custom-title'>Custom HTML Slide</h1><p class='custom-text'>This slide uses raw HTML content!</p><div class='custom-grid'><div class='custom-card'>Card 1</div><div class='custom-card'>Card 2</div><div class='custom-card'>Card 3</div></div></div>",
177
+ "css": ".custom-container { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; padding: 2rem; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); } .custom-title { font-size: 4rem; font-weight: bold; background: linear-gradient(90deg, #00d4ff, #7c3aed); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 1rem; } .custom-text { font-size: 1.5rem; color: rgba(255,255,255,0.7); margin-bottom: 3rem; } .custom-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; } .custom-card { padding: 2rem; background: rgba(255,255,255,0.05); border-radius: 1rem; border: 1px solid rgba(255,255,255,0.1); color: white; font-weight: 600; }",
178
+ "notes": "This is a custom HTML slide with inline CSS. You have full control over the layout!"
179
+ }
180
+ ]
181
+ }`
182
+ };
183
+
184
+ const loadTemplate = (name: keyof typeof TEMPLATES) => {
185
+ jsonInput.value = TEMPLATES[name];
186
+ updatePreview();
187
+ };
188
+
189
+ const formatJson = () => {
190
+ try {
191
+ const parsed = JSON.parse(jsonInput.value);
192
+ jsonInput.value = JSON.stringify(parsed, null, 2);
193
+ } catch {
194
+ // Ignore if invalid
195
+ }
196
+ };
197
+
198
+ const exportHtml = () => {
199
+ if (!jsonInput.value.trim()) return;
200
+
201
+ try {
202
+ let parsed = JSON.parse(jsonInput.value);
203
+
204
+ // Auto-wrap logic (same as preview)
205
+ if (parsed.type && !parsed.slides) {
206
+ parsed = {
207
+ meta: { title: 'Exported Deck' },
208
+ slides: [{ ...parsed, sizing: 'container' }]
209
+ };
210
+ }
211
+
212
+ // Apply container sizing
213
+ if (parsed.slides) {
214
+ parsed.slides.forEach((s: any) => s.sizing = 'container');
215
+ }
216
+
217
+ const html = `<!DOCTYPE html>
218
+ <html lang="en">
219
+ <head>
220
+ <meta charset="UTF-8">
221
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
222
+ <title>${parsed.meta?.title || 'Lumina Presentation'}</title>
223
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lumina-slides@8.2.5/dist/style.css">
224
+ <style>
225
+ body { margin: 0; padding: 0; overflow: hidden; background: #000; font-family: sans-serif; }
226
+ #app { width: 100vw; height: 100vh; }
227
+ </style>
228
+ </head>
229
+ <body>
230
+ <div id="app"></div>
231
+
232
+ <!-- Lumina Engine (Universal Build) -->
233
+ <script src="https://cdn.jsdelivr.net/npm/lumina-slides@8.2.5/dist/lumina-slides.umd.cjs"><\/script>
234
+
235
+ <script>
236
+ // Initialize when ready
237
+ document.addEventListener('DOMContentLoaded', () => {
238
+ const { Lumina } = LuminaSlides;
239
+
240
+ // The JSON from your playground
241
+ const deckData = ${JSON.stringify(parsed, null, 4)};
242
+
243
+ // Start the engine
244
+ const engine = new Lumina('#app', {
245
+ loop: true,
246
+ navigation: true,
247
+ ui: {
248
+ visible: true,
249
+ showSlideCount: true,
250
+ showControls: true,
251
+ showProgressBar: true
252
+ },
253
+ keys: {
254
+ next: ['ArrowRight', ' ', 'Enter'],
255
+ prev: ['ArrowLeft', 'Backspace']
256
+ },
257
+ animation: { enabled: true, durationIn: 0.5 }
258
+ });
259
+
260
+ engine.load(deckData);
261
+ });
262
+ <\/script>
263
+ </body>
264
+ </html>`;
265
+
266
+ const blob = new Blob([html], { type: 'text/html' });
267
+ const url = URL.createObjectURL(blob);
268
+ const a = document.createElement('a');
269
+ a.href = url;
270
+ a.download = 'lumina-deck.html';
271
+ document.body.appendChild(a);
272
+ a.click();
273
+ document.body.removeChild(a);
274
+ URL.revokeObjectURL(url);
275
+
276
+ } catch (e: any) {
277
+ alert('Invalid JSON: ' + e.message);
278
+ }
279
+ };
280
+
281
+ const onInput = () => {
282
+ if (debounceTimer) clearTimeout(debounceTimer);
283
+ debounceTimer = setTimeout(updatePreview, 300);
284
+ };
285
+
286
+ const updatePreview = async () => {
287
+ parseError.value = null;
288
+
289
+ if (!jsonInput.value.trim()) {
290
+ hasValidJson.value = false;
291
+ return;
292
+ }
293
+
294
+ try {
295
+ let parsed = JSON.parse(jsonInput.value);
296
+
297
+ // Auto-wrap single slide in deck structure
298
+ if (parsed.type && !parsed.slides) {
299
+ parsed = {
300
+ meta: { title: 'Playground' },
301
+ slides: [{ ...parsed, sizing: 'container' }]
302
+ };
303
+ }
304
+
305
+ // Apply container sizing to all slides
306
+ if (parsed.slides) {
307
+ parsed.slides.forEach((s: any) => s.sizing = 'container');
308
+ }
309
+
310
+ hasValidJson.value = true;
311
+
312
+ // Initialize or update engine
313
+ if (!engine) {
314
+ await nextTick();
315
+ engine = new Lumina('#playground-container', {
316
+ loop: true,
317
+ navigation: true,
318
+ ui: {
319
+ visible: true,
320
+ showSlideCount: true,
321
+ showControls: true,
322
+ showProgressBar: true
323
+ },
324
+ keys: {
325
+ next: ['ArrowRight', ' ', 'Enter'],
326
+ prev: ['ArrowLeft', 'Backspace']
327
+ },
328
+ animation: { enabled: true, durationIn: 0.5 }
329
+ });
330
+
331
+ engine.on('slideChange', (payload) => {
332
+ slideIndex.value = payload.index;
333
+ });
334
+ }
335
+
336
+ engine.load(parsed);
337
+ totalSlides.value = parsed.slides?.length || 0;
338
+ slideIndex.value = 0;
339
+
340
+ } catch (e: any) {
341
+ parseError.value = e.message || 'Invalid JSON';
342
+ hasValidJson.value = false;
343
+ }
344
+ };
345
+
346
+ const prevSlide = () => {
347
+ if (engine) engine.prev();
348
+ };
349
+
350
+ const nextSlide = () => {
351
+ if (engine) engine.next();
352
+ };
353
+
354
+ const openSpeakerNotes = () => {
355
+ if (!engine) return;
356
+ const win = engine.openSpeakerNotes();
357
+ if (win) {
358
+ speakerNotesOpen.value = true;
359
+ // Monitor for close
360
+ const checkClosed = setInterval(() => {
361
+ if (win.closed) {
362
+ speakerNotesOpen.value = false;
363
+ clearInterval(checkClosed);
364
+ }
365
+ }, 500);
366
+ } else {
367
+ alert('Popup blocked! Please allow popups for speaker notes.');
368
+ }
369
+ };
370
+
371
+ onMounted(() => {
372
+ // Start with flex template
373
+ loadTemplate('flex');
374
+ });
375
+
376
+ onUnmounted(() => {
377
+ if (engine) {
378
+ engine.destroy();
379
+ engine = null;
380
+ }
381
+ if (debounceTimer) clearTimeout(debounceTimer);
382
+ });
383
+ </script>
384
+
385
+ <style scoped>
386
+ textarea::placeholder {
387
+ color: rgba(255, 255, 255, 0.2);
388
+ }
389
+ </style>
@@ -0,0 +1,266 @@
1
+ <template>
2
+ <div class="min-h-screen pt-32 px-4 lg:px-8 max-w-5xl mx-auto pb-32">
3
+ <!-- Unified Header -->
4
+ <div class="text-center mb-16 max-w-2xl mx-auto">
5
+ <h1 class="text-4xl md:text-5xl font-black mb-4 tracking-tight text-white">Prompt Builder</h1>
6
+ <p class="text-lg text-white/50 leading-relaxed">
7
+ Create a customized prompt for any LLM to generate Lumina presentations.
8
+ </p>
9
+ </div>
10
+
11
+ <!-- Progress Steps -->
12
+ <div class="flex justify-center mb-12">
13
+ <div class="flex items-center gap-2">
14
+ <template v-for="(step, i) in steps" :key="i">
15
+ <button @click="currentStep = i" :class="[
16
+ 'w-10 h-10 rounded-full font-bold text-sm transition-all',
17
+ currentStep === i
18
+ ? 'bg-blue-500 text-white scale-110'
19
+ : currentStep > i
20
+ ? 'bg-green-500/20 text-green-400 border border-green-500/50'
21
+ : 'bg-white/10 text-white/40'
22
+ ]">
23
+ {{ currentStep > i ? '✓' : i + 1 }}
24
+ </button>
25
+ <div v-if="i < steps.length - 1" class="w-8 h-0.5 bg-white/10"></div>
26
+ </template>
27
+ </div>
28
+ </div>
29
+
30
+ <!-- Step Content -->
31
+ <div class="max-w-2xl mx-auto">
32
+ <!-- Step 1: Language -->
33
+ <div v-if="currentStep === 0" class="space-y-6">
34
+ <h2 class="text-2xl font-bold text-center mb-8">{{ steps[0] }}</h2>
35
+ <div class="grid grid-cols-2 md:grid-cols-3 gap-6">
36
+ <button v-for="lang in languages" :key="lang.code" @click="form.language = lang.code; nextStep()"
37
+ :class="[
38
+ 'p-8 rounded-3xl border transition-all text-center hover:scale-[1.02] active:scale-95 duration-500',
39
+ form.language === lang.code
40
+ ? 'bg-blue-600/20 border-blue-500/50 text-white shadow-[0_20px_40px_-10px_rgba(59,130,246,0.2)]'
41
+ : 'bg-white/[0.03] border-white/5 hover:border-white/20'
42
+ ]">
43
+ <span class="text-4xl block mb-4 transform group-hover:scale-110 transition-transform">{{
44
+ lang.flag
45
+ }}</span>
46
+ <span class="font-bold tracking-tight">{{ lang.name }}</span>
47
+ </button>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- Step 2: Style -->
52
+ <div v-if="currentStep === 1" class="space-y-6">
53
+ <h2 class="text-2xl font-bold text-center mb-8">{{ steps[1] }}</h2>
54
+ <div class="grid grid-cols-2 gap-6">
55
+ <button v-for="style in styles" :key="style.id" @click="form.style = style.id; nextStep()" :class="[
56
+ 'p-8 rounded-3xl border transition-all text-left hover:scale-[1.02] active:scale-95 duration-500 flex flex-col',
57
+ form.style === style.id
58
+ ? 'bg-blue-600/20 border-blue-500/50 shadow-[0_20px_40px_-10px_rgba(59,130,246,0.2)]'
59
+ : 'bg-white/[0.03] border-white/5 hover:border-white/20'
60
+ ]">
61
+ <span class="text-3xl mb-4 block">{{ style.icon }}</span>
62
+ <span class="text-xl font-bold block mb-1 tracking-tight">{{ style.name }}</span>
63
+ <span class="text-sm text-white/30 font-medium leading-relaxed">{{ style.desc }}</span>
64
+ </button>
65
+ </div>
66
+ </div>
67
+
68
+ <!-- Step 3: Topic -->
69
+ <div v-if="currentStep === 2" class="space-y-6">
70
+ <h2 class="text-2xl font-bold text-center mb-8">{{ steps[2] }}</h2>
71
+ <textarea v-model="form.topic" rows="4"
72
+ class="w-full p-4 rounded-xl bg-white/5 border border-white/10 text-white placeholder-white/30 focus:outline-none focus:border-blue-500"
73
+ placeholder="Describe your presentation topic in a few sentences..."></textarea>
74
+ <button @click="nextStep()" :disabled="!form.topic.trim()"
75
+ class="w-full py-4 rounded-xl bg-blue-500 text-white font-bold hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition">
76
+ Continue →
77
+ </button>
78
+ </div>
79
+
80
+ <!-- Step 4: Audience -->
81
+ <div v-if="currentStep === 3" class="space-y-6">
82
+ <h2 class="text-2xl font-bold text-center mb-8">{{ steps[3] }}</h2>
83
+ <div class="grid grid-cols-2 lg:grid-cols-3 gap-6">
84
+ <button v-for="aud in audiences" :key="aud.id" @click="form.audience = aud.id; nextStep()" :class="[
85
+ 'p-8 rounded-3xl border transition-all text-left hover:scale-[1.02] active:scale-95 duration-500 flex flex-col',
86
+ form.audience === aud.id
87
+ ? 'bg-blue-600/20 border-blue-500/50 shadow-[0_20px_40px_-10px_rgba(59,130,246,0.2)]'
88
+ : 'bg-white/[0.03] border-white/5 hover:border-white/20'
89
+ ]">
90
+ <span class="text-3xl mb-4 block">{{ aud.icon }}</span>
91
+ <span class="font-bold tracking-tight">{{ aud.name }}</span>
92
+ </button>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- Step 5: Slide Count -->
97
+ <div v-if="currentStep === 4" class="space-y-6">
98
+ <h2 class="text-2xl font-bold text-center mb-8">{{ steps[4] }}</h2>
99
+ <div class="flex items-center justify-center gap-4">
100
+ <button @click="form.slideCount = Math.max(3, form.slideCount - 1)"
101
+ class="w-12 h-12 rounded-full bg-white/10 text-xl font-bold hover:bg-white/20">−</button>
102
+ <span class="text-5xl font-bold w-20 text-center">{{ form.slideCount }}</span>
103
+ <button @click="form.slideCount = Math.min(20, form.slideCount + 1)"
104
+ class="w-12 h-12 rounded-full bg-white/10 text-xl font-bold hover:bg-white/20">+</button>
105
+ </div>
106
+ <p class="text-center text-white/40 text-sm">Recommended: 5-12 slides</p>
107
+
108
+ <!-- Speaker Notes Toggle -->
109
+ <div class="flex items-center justify-center gap-3 pt-4">
110
+ <button @click="form.includeNotes = !form.includeNotes" :class="[
111
+ 'relative w-12 h-6 rounded-full transition-colors duration-200 ease-in-out',
112
+ form.includeNotes ? 'bg-blue-500' : 'bg-white/10'
113
+ ]">
114
+ <span :class="[
115
+ 'absolute top-1 left-1 bg-white w-4 h-4 rounded-full transition-transform duration-200',
116
+ form.includeNotes ? 'translate-x-6' : 'translate-x-0'
117
+ ]"></span>
118
+ </button>
119
+ <span class="text-white font-medium cursor-pointer" @click="form.includeNotes = !form.includeNotes">
120
+ Include Speaker Notes
121
+ </span>
122
+ </div>
123
+ <button @click="nextStep()"
124
+ class="w-full py-4 rounded-xl bg-blue-500 text-white font-bold hover:bg-blue-600 transition">
125
+ Generate Prompt →
126
+ </button>
127
+ </div>
128
+
129
+ <!-- Step 6: Result -->
130
+ <div v-if="currentStep === 5" class="space-y-6">
131
+ <h2 class="text-2xl font-bold text-center mb-4">Your Prompt is Ready!</h2>
132
+ <p class="text-center text-white/60 mb-8">Copy and paste this into ChatGPT, Claude, or any LLM</p>
133
+
134
+ <div class="relative">
135
+ <div class="p-6 rounded-xl bg-black border border-white/10 max-h-96 overflow-y-auto">
136
+ <pre class="text-sm text-white/80 whitespace-pre-wrap font-mono">{{ generatedPrompt }}</pre>
137
+ </div>
138
+ <button @click="copyPrompt" :class="[
139
+ 'absolute top-4 right-4 px-4 py-2 rounded-lg font-bold text-sm transition',
140
+ copied ? 'bg-green-500 text-white' : 'bg-white/10 hover:bg-white/20'
141
+ ]">
142
+ {{ copied ? '✓ Copied!' : 'Copy' }}
143
+ </button>
144
+ </div>
145
+
146
+ <div class="flex gap-4">
147
+ <button @click="currentStep = 0"
148
+ class="flex-1 py-4 rounded-xl bg-white/10 text-white font-bold hover:bg-white/20 transition">
149
+ ← Start Over
150
+ </button>
151
+ <a href="https://chat.openai.com" target="_blank"
152
+ class="flex-1 py-4 rounded-xl bg-blue-500 text-white font-bold hover:bg-blue-600 transition text-center">
153
+ Open ChatGPT →
154
+ </a>
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <!-- Navigation -->
160
+ <div v-if="currentStep > 0 && currentStep < 5" class="max-w-2xl mx-auto mt-8">
161
+ <button @click="currentStep--" class="text-white/50 hover:text-white transition">
162
+ ← Back
163
+ </button>
164
+ </div>
165
+ </div>
166
+ </template>
167
+
168
+ <script setup lang="ts">
169
+ import { ref, computed, onMounted } from 'vue';
170
+
171
+ const currentStep = ref(0);
172
+ const copied = ref(false);
173
+ const basePrompt = ref('');
174
+
175
+ const steps = ['Language', 'Style', 'Topic', 'Audience', 'Slides', 'Done'];
176
+
177
+ const languages = [
178
+ { code: 'en', name: 'English', flag: '🇺🇸' },
179
+ { code: 'es', name: 'Español', flag: '🇪🇸' },
180
+ { code: 'pt', name: 'Português', flag: '🇧🇷' },
181
+ { code: 'fr', name: 'Français', flag: '🇫🇷' },
182
+ { code: 'de', name: 'Deutsch', flag: '🇩🇪' },
183
+ { code: 'it', name: 'Italiano', flag: '🇮🇹' },
184
+ ];
185
+
186
+ const styles = [
187
+ { id: 'minimalist', name: 'Minimalist', desc: 'Clean and simple', icon: '◻️' },
188
+ { id: 'corporate', name: 'Corporate', desc: 'Professional and formal', icon: '🏢' },
189
+ { id: 'creative', name: 'Creative', desc: 'Bold and expressive', icon: '🎨' },
190
+ { id: 'technical', name: 'Technical', desc: 'Data and detail focused', icon: '⚙️' },
191
+ ];
192
+
193
+ const audiences = [
194
+ { id: 'investors', name: 'Investors', icon: '💼' },
195
+ { id: 'executives', name: 'Executives', icon: '👔' },
196
+ { id: 'team', name: 'Team/Internal', icon: '👥' },
197
+ { id: 'customers', name: 'Customers', icon: '🎯' },
198
+ { id: 'students', name: 'Students', icon: '📚' },
199
+ { id: 'general', name: 'General Public', icon: '🌍' },
200
+ ];
201
+
202
+ const form = ref({
203
+ language: 'en',
204
+ style: 'minimalist',
205
+ topic: '',
206
+ audience: 'general',
207
+ slideCount: 8,
208
+ includeNotes: true
209
+ });
210
+
211
+ const nextStep = () => {
212
+ if (currentStep.value < steps.length - 1) {
213
+ currentStep.value++;
214
+ }
215
+ };
216
+
217
+ const generatedPrompt = computed(() => {
218
+ const langName = languages.find(l => l.code === form.value.language)?.name || 'English';
219
+ const styleName = styles.find(s => s.id === form.value.style)?.name || 'Minimalist';
220
+ const audName = audiences.find(a => a.id === form.value.audience)?.name || 'General';
221
+
222
+ return `${basePrompt.value}
223
+
224
+ ---
225
+
226
+ ## USER REQUEST
227
+
228
+ **Language**: ${langName}
229
+ **Style**: ${styleName}
230
+ **Audience**: ${audName}
231
+ **Slide Count**: ${form.value.slideCount}
232
+ **Topic**: ${form.value.topic}
233
+
234
+ **Requirements**:
235
+ ${form.value.includeNotes ? `- **JSON Schema**: Every slide object MUST include the "notes" property.
236
+ - Include key talking points and timing in the notes.` : ''}
237
+ - Follow all quality constraints defined in the system prompt.
238
+
239
+ Please generate the presentation now.`;
240
+ });
241
+
242
+ const copyPrompt = async () => {
243
+ try {
244
+ await navigator.clipboard.writeText(generatedPrompt.value);
245
+ copied.value = true;
246
+ setTimeout(() => copied.value = false, 2000);
247
+ } catch (e) {
248
+ console.error('Failed to copy:', e);
249
+ }
250
+ };
251
+
252
+ onMounted(async () => {
253
+ try {
254
+ // Use relative path which is safer for GitHub Pages subdirectories
255
+ // assuming index.html is at the project root which it is.
256
+ const url = './lumina-llm-prompt.txt';
257
+
258
+ const res = await fetch(url);
259
+ if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
260
+ basePrompt.value = await res.text();
261
+ } catch (e) {
262
+ console.error('Failed to load base prompt:', e);
263
+ basePrompt.value = "Error loading system prompt. Please ensure 'lumina-llm-prompt.txt' is in the public folder.";
264
+ }
265
+ });
266
+ </script>