Reubencf commited on
Commit
38c4ddd
Β·
1 Parent(s): 6d99465

Made many changes

Browse files
.claude/settings.local.json CHANGED
@@ -17,7 +17,9 @@
17
  "Bash(netstat:*)",
18
  "Bash(findstr:*)",
19
  "Bash(timeout:*)",
20
- "Bash(NUL)"
 
 
21
  ],
22
  "deny": [],
23
  "ask": []
 
17
  "Bash(netstat:*)",
18
  "Bash(findstr:*)",
19
  "Bash(timeout:*)",
20
+ "Bash(NUL)",
21
+ "Bash(npm ls:*)",
22
+ "Bash(npm view:*)"
23
  ],
24
  "deny": [],
25
  "ask": []
app/api/export-pptx/route.ts CHANGED
@@ -34,13 +34,10 @@ const themeColors: Record<string, { background: string; title: string; text: str
34
  workshop: { background: 'EFE8DF', title: '0E0E0E', text: '151515' },
35
  light: { background: 'FFFFFF', title: '202124', text: '5f6368' },
36
  dark: { background: '1a1a1a', title: 'FFFFFF', text: 'e0e0e0' },
37
- oceanBreeze: { background: '667eea', title: 'FFFFFF', text: 'f0f0f0' },
38
- sunsetGlow: { background: 'ff6b6b', title: 'FFFFFF', text: 'FFFFFF' },
39
- forestMist: { background: '134e5e', title: 'FFFFFF', text: 'e8f5e9' },
40
- midnightBlue: { background: '0f2027', title: '64b5f6', text: 'e1f5fe' },
41
- geometricDark: { background: '16213e', title: '00d2ff', text: 'e0e0e0' },
42
- stripedProfessional: { background: '2c3e50', title: 'ecf0f1', text: 'bdc3c7' },
43
- dotGrid: { background: 'ecf0f1', title: '2c3e50', text: '34495e' },
44
  corporateCity: { background: '1a1a1a', title: 'FFFFFF', text: 'e0e0e0' },
45
  techInnovation: { background: '0d1b2a', title: '00d9ff', text: 'e0e0e0' },
46
  natureSerene: { background: '4caf50', title: 'FFFFFF', text: 'FFFFFF' },
@@ -87,45 +84,53 @@ export async function POST(request: NextRequest) {
87
  slides.forEach((slideData, index) => {
88
  const slide = pres.addSlide();
89
 
90
- // Add theme background
91
  slide.background = { fill: colors.background };
92
 
93
- // Add title with theme colors
94
  slide.addText(slideData.title, {
95
  x: 0.5,
96
  y: 0.5,
97
  w: 9,
98
- h: 1.0,
99
- fontSize: 36,
100
  bold: true,
101
  color: colors.title,
102
  align: 'left',
103
- fontFace: 'Arial',
 
104
  });
105
 
106
- // Add content points with theme colors
107
  if (slideData.content && slideData.content.length > 0) {
108
- slideData.content.forEach((point, pointIndex) => {
109
- // Add bullet point
110
- slide.addShape('circle', {
111
- x: 0.7,
112
- y: 1.8 + (pointIndex * 0.5),
113
- w: 0.12,
114
- h: 0.12,
115
- fill: { color: colors.title }
 
 
 
 
 
 
116
  });
 
117
 
118
- // Add content text
119
- slide.addText(point, {
120
- x: 1.0,
121
- y: 1.7 + (pointIndex * 0.5),
122
- w: 8.5,
123
- h: 0.4,
124
- fontSize: 18,
125
- color: colors.text,
126
- fontFace: 'Arial',
127
- valign: 'middle',
128
- });
129
  });
130
  }
131
 
 
34
  workshop: { background: 'EFE8DF', title: '0E0E0E', text: '151515' },
35
  light: { background: 'FFFFFF', title: '202124', text: '5f6368' },
36
  dark: { background: '1a1a1a', title: 'FFFFFF', text: 'e0e0e0' },
37
+ darkModern: { background: '1a1a2e', title: '64b5f6', text: 'e1f5fe' },
38
+ professionalBlue: { background: '1e3a8a', title: 'FFFFFF', text: 'dbeafe' },
39
+ elegantGreen: { background: '065f46', title: 'FFFFFF', text: 'd1fae5' },
40
+ sophisticatedPurple: { background: '581c87', title: 'FFFFFF', text: 'e9d5ff' },
 
 
 
41
  corporateCity: { background: '1a1a1a', title: 'FFFFFF', text: 'e0e0e0' },
42
  techInnovation: { background: '0d1b2a', title: '00d9ff', text: 'e0e0e0' },
43
  natureSerene: { background: '4caf50', title: 'FFFFFF', text: 'FFFFFF' },
 
84
  slides.forEach((slideData, index) => {
85
  const slide = pres.addSlide();
86
 
87
+ // Add theme background with proper color format
88
  slide.background = { fill: colors.background };
89
 
90
+ // Add title with theme colors and better positioning
91
  slide.addText(slideData.title, {
92
  x: 0.5,
93
  y: 0.5,
94
  w: 9,
95
+ h: 1.2,
96
+ fontSize: 40,
97
  bold: true,
98
  color: colors.title,
99
  align: 'left',
100
+ fontFace: 'Calibri',
101
+ valign: 'top',
102
  });
103
 
104
+ // Add content points with theme colors and better spacing
105
  if (slideData.content && slideData.content.length > 0) {
106
+ const contentArray: any[] = [];
107
+
108
+ slideData.content.forEach((point) => {
109
+ contentArray.push({
110
+ text: point,
111
+ options: {
112
+ bullet: true,
113
+ fontSize: 20,
114
+ color: colors.text,
115
+ fontFace: 'Calibri',
116
+ paraSpaceBefore: 6,
117
+ paraSpaceAfter: 6,
118
+ indentLevel: 0,
119
+ }
120
  });
121
+ });
122
 
123
+ // Add all content as a single text block with bullets
124
+ slide.addText(contentArray, {
125
+ x: 0.5,
126
+ y: 1.8,
127
+ w: 9,
128
+ h: 3.2,
129
+ fontSize: 20,
130
+ color: colors.text,
131
+ fontFace: 'Calibri',
132
+ valign: 'top',
133
+ align: 'left',
134
  });
135
  }
136
 
app/api/generate-slides/route.ts CHANGED
@@ -50,23 +50,38 @@ export async function POST(request: NextRequest) {
50
  // Get API token based on auth method
51
  let apiToken: string | undefined;
52
 
53
- if (authMethod === 'api-key') {
 
 
 
 
 
54
  const authHeader = request.headers.get('authorization');
55
  if (authHeader?.startsWith('Bearer ')) {
56
  apiToken = authHeader.slice(7);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  } else {
58
  return NextResponse.json(
59
- { error: 'API token required for API key authentication' },
60
  { status: 401 }
61
  );
62
  }
63
- } else {
64
- // For OAuth, we'd normally get the token from session
65
- // For demo purposes, we'll require API key
66
- return NextResponse.json(
67
- { error: 'Please use API key authentication for this demo' },
68
- { status: 401 }
69
- );
70
  }
71
 
72
  // Initialize Hugging Face client (supports multiple providers)
@@ -78,17 +93,19 @@ export async function POST(request: NextRequest) {
78
  Your task: Create a professional, engaging presentation about: "${prompt}"
79
 
80
  CRITICAL INSTRUCTIONS:
81
- 1. Analyze the topic carefully and determine the optimal number of slides (typically 5-10 slides)
82
- 2. Structure the presentation logically with clear flow: Introduction β†’ Main Points β†’ Analysis/Examples β†’ Benefits/Impact β†’ Conclusion
83
- 3. Each content point must be specific, actionable, and relevant to the topic - NO generic placeholders
84
- 4. Use concrete examples, statistics, or real-world applications when appropriate
85
- 5. Make content compelling and audience-focused
 
 
86
 
87
  OUTPUT FORMAT - Return ONLY a valid JSON array with this exact structure:
88
  [
89
  {
90
- "title": "Presentation Title Based on Topic",
91
- "content": ["Subtitle or tagline", "Author information if relevant"],
92
  "layout": "titleContent",
93
  "imageKeywords": ""
94
  },
@@ -110,10 +127,12 @@ LAYOUT RULES:
110
  - "twoContent" β†’ Comparisons, before/after, pros/cons (use sparingly)
111
 
112
  IMAGE KEYWORDS RULES:
113
- - For titleContentImage layout: Provide 2-4 specific, relevant search terms
114
- - Examples: "artificial intelligence neural network", "sustainable energy solar panels", "team collaboration office"
115
- - Leave empty ("") for titleContent and twoContent layouts
116
- - Make keywords specific to the slide's actual content
 
 
117
 
118
  CONTENT QUALITY RULES:
119
  - Each bullet point should be 15-25 words with specific information
 
50
  // Get API token based on auth method
51
  let apiToken: string | undefined;
52
 
53
+ // First check for HF API key in cookies (from login)
54
+ const hfApiKeyCookie = request.cookies.get('hf_api_key')?.value;
55
+
56
+ if (hfApiKeyCookie) {
57
+ apiToken = hfApiKeyCookie;
58
+ } else if (authMethod === 'api-key') {
59
  const authHeader = request.headers.get('authorization');
60
  if (authHeader?.startsWith('Bearer ')) {
61
  apiToken = authHeader.slice(7);
62
+ } else {
63
+ // Check localStorage API key sent via header
64
+ const localStorageKey = request.headers.get('x-hf-api-key');
65
+ if (localStorageKey) {
66
+ apiToken = localStorageKey;
67
+ } else {
68
+ return NextResponse.json(
69
+ { error: 'API token required. Please connect your Hugging Face account.' },
70
+ { status: 401 }
71
+ );
72
+ }
73
+ }
74
+ } else {
75
+ // Check localStorage API key sent via header
76
+ const localStorageKey = request.headers.get('x-hf-api-key');
77
+ if (localStorageKey) {
78
+ apiToken = localStorageKey;
79
  } else {
80
  return NextResponse.json(
81
+ { error: 'Please connect your Hugging Face account to use AI generation.' },
82
  { status: 401 }
83
  );
84
  }
 
 
 
 
 
 
 
85
  }
86
 
87
  // Initialize Hugging Face client (supports multiple providers)
 
93
  Your task: Create a professional, engaging presentation about: "${prompt}"
94
 
95
  CRITICAL INSTRUCTIONS:
96
+ 1. The FIRST slide must have "Reuben AI" as the title
97
+ 2. Analyze the topic carefully and determine the optimal number of slides (typically 5-10 slides)
98
+ 3. Structure the presentation logically with clear flow: Introduction β†’ Main Points β†’ Analysis/Examples β†’ Benefits/Impact β†’ Conclusion
99
+ 4. Each content point must be specific, detailed, actionable, and relevant to the topic - NO generic placeholders
100
+ 5. Use concrete examples, statistics, or real-world applications when appropriate
101
+ 6. Make content compelling and audience-focused with rich, detailed explanations
102
+ 7. Generate specific, high-quality image keywords for Unsplash that match the slide content
103
 
104
  OUTPUT FORMAT - Return ONLY a valid JSON array with this exact structure:
105
  [
106
  {
107
+ "title": "Reuben AI",
108
+ "content": ["${prompt}", "Professional AI-Powered Presentation"],
109
  "layout": "titleContent",
110
  "imageKeywords": ""
111
  },
 
127
  - "twoContent" β†’ Comparisons, before/after, pros/cons (use sparingly)
128
 
129
  IMAGE KEYWORDS RULES:
130
+ - For titleContentImage layout: ALWAYS provide 2-4 specific, relevant search terms for high-quality Unsplash images
131
+ - Keywords should be concrete nouns and descriptive terms that will find professional stock photos
132
+ - Examples: "modern office workspace", "artificial intelligence technology", "sustainable green energy", "team collaboration meeting"
133
+ - Leave empty ("") ONLY for the first title slide and conclusion slides
134
+ - Make keywords specific and visual - think about what would make a good photograph
135
+ - At the end of the presentation, list all image keywords used for easy reference
136
 
137
  CONTENT QUALITY RULES:
138
  - Each bullet point should be 15-25 words with specific information
components/auth/HuggingFaceLogin.tsx ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { X, LogIn, LogOut, User, Key } from 'lucide-react';
3
+
4
+ interface HuggingFaceLoginProps {
5
+ onAuthChange?: (isAuthenticated: boolean) => void;
6
+ }
7
+
8
+ export default function HuggingFaceLogin({ onAuthChange }: HuggingFaceLoginProps) {
9
+ const [isOpen, setIsOpen] = useState(false);
10
+ const [apiKey, setApiKey] = useState('');
11
+ const [isLoading, setIsLoading] = useState(false);
12
+ const [error, setError] = useState('');
13
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
14
+
15
+ useEffect(() => {
16
+ // Check if user is already authenticated
17
+ checkAuthentication();
18
+ }, []);
19
+
20
+ const checkAuthentication = async () => {
21
+ try {
22
+ const response = await fetch('/api/auth/hf', {
23
+ method: 'GET',
24
+ });
25
+ const data = await response.json();
26
+ setIsAuthenticated(data.authenticated);
27
+ if (onAuthChange) {
28
+ onAuthChange(data.authenticated);
29
+ }
30
+ } catch (error) {
31
+ console.error('Failed to check authentication:', error);
32
+ }
33
+ };
34
+
35
+ const handleLogin = async () => {
36
+ if (!apiKey.trim()) {
37
+ setError('Please enter your Hugging Face API key');
38
+ return;
39
+ }
40
+
41
+ setIsLoading(true);
42
+ setError('');
43
+
44
+ try {
45
+ const response = await fetch('/api/auth/hf', {
46
+ method: 'POST',
47
+ headers: {
48
+ 'Content-Type': 'application/json',
49
+ },
50
+ body: JSON.stringify({ apiKey: apiKey.trim() }),
51
+ });
52
+
53
+ const data = await response.json();
54
+
55
+ if (!response.ok) {
56
+ throw new Error(data.error || 'Authentication failed');
57
+ }
58
+
59
+ // Save to localStorage as well for client-side usage
60
+ localStorage.setItem('hf_api_key', apiKey.trim());
61
+
62
+ setIsAuthenticated(true);
63
+ setIsOpen(false);
64
+ setApiKey('');
65
+
66
+ if (onAuthChange) {
67
+ onAuthChange(true);
68
+ }
69
+
70
+ // Show success message
71
+ alert('Successfully connected to Hugging Face! You can now use HF models for generation.');
72
+ } catch (error) {
73
+ setError((error as Error).message);
74
+ } finally {
75
+ setIsLoading(false);
76
+ }
77
+ };
78
+
79
+ const handleLogout = async () => {
80
+ try {
81
+ await fetch('/api/auth/hf', {
82
+ method: 'DELETE',
83
+ });
84
+
85
+ // Clear localStorage
86
+ localStorage.removeItem('hf_api_key');
87
+
88
+ setIsAuthenticated(false);
89
+
90
+ if (onAuthChange) {
91
+ onAuthChange(false);
92
+ }
93
+ } catch (error) {
94
+ console.error('Failed to logout:', error);
95
+ }
96
+ };
97
+
98
+ return (
99
+ <>
100
+ {/* Login Button */}
101
+ <button
102
+ onClick={() => isAuthenticated ? handleLogout() : setIsOpen(true)}
103
+ className="flex items-center gap-2 px-3 py-1.5 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
104
+ title={isAuthenticated ? 'Logout from Hugging Face' : 'Login with Hugging Face'}
105
+ >
106
+ {isAuthenticated ? (
107
+ <>
108
+ <User className="w-4 h-4 text-green-600" />
109
+ <span className="hidden sm:inline">HF Connected</span>
110
+ <LogOut className="w-3 h-3" />
111
+ </>
112
+ ) : (
113
+ <>
114
+ <svg
115
+ className="w-4 h-4"
116
+ viewBox="0 0 32 32"
117
+ fill="currentColor"
118
+ >
119
+ <path d="M16.878 1.377c-1.097-.01-2.033.84-2.177 1.94-.144 1.1-1.816 14.35-1.816 14.35s-.28-1.311-.576-2.602c-.297-1.29-.561-2.27-.768-2.653-.207-.384-1.234-1.503-2.115-1.405-.882.098-1.65.956-1.555 1.9.095.943 2.633 10.51 2.633 10.51s-.827-1.23-1.604-2.444c-.516-.806-1.001-1.525-1.192-1.745-.382-.44-1.064-.615-1.665-.41-.752.257-1.28 1.014-1.16 1.823.12.81 4.919 8.378 9.568 9.64 4.649 1.263 9.13-1.8 10.24-6.282 1.11-4.482 3.05-13.01 3.16-13.65.111-.64-.185-1.29-.735-1.62-.55-.329-5.188-3.301-7.238-3.302z"/>
120
+ </svg>
121
+ <span className="hidden sm:inline">Connect HF</span>
122
+ </>
123
+ )}
124
+ </button>
125
+
126
+ {/* Login Dialog */}
127
+ {isOpen && (
128
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[2000]">
129
+ <div className="bg-white rounded-lg shadow-xl w-[450px]">
130
+ {/* Header */}
131
+ <div className="flex items-center justify-between p-4 border-b">
132
+ <div className="flex items-center gap-2">
133
+ <svg
134
+ className="w-6 h-6"
135
+ viewBox="0 0 32 32"
136
+ fill="currentColor"
137
+ >
138
+ <path d="M16.878 1.377c-1.097-.01-2.033.84-2.177 1.94-.144 1.1-1.816 14.35-1.816 14.35s-.28-1.311-.576-2.602c-.297-1.29-.561-2.27-.768-2.653-.207-.384-1.234-1.503-2.115-1.405-.882.098-1.65.956-1.555 1.9.095.943 2.633 10.51 2.633 10.51s-.827-1.23-1.604-2.444c-.516-.806-1.001-1.525-1.192-1.745-.382-.44-1.064-.615-1.665-.41-.752.257-1.28 1.014-1.16 1.823.12.81 4.919 8.378 9.568 9.64 4.649 1.263 9.13-1.8 10.24-6.282 1.11-4.482 3.05-13.01 3.16-13.65.111-.64-.185-1.29-.735-1.62-.55-.329-5.188-3.301-7.238-3.302z"/>
139
+ </svg>
140
+ <h2 className="text-lg font-semibold">Connect to Hugging Face</h2>
141
+ </div>
142
+ <button
143
+ onClick={() => setIsOpen(false)}
144
+ className="p-1 hover:bg-gray-100 rounded transition-colors"
145
+ >
146
+ <X className="w-5 h-5" />
147
+ </button>
148
+ </div>
149
+
150
+ {/* Content */}
151
+ <div className="p-4 space-y-4">
152
+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
153
+ <div className="flex items-start gap-2">
154
+ <span className="text-blue-600 text-lg">ℹ️</span>
155
+ <div className="text-sm">
156
+ <p className="font-medium text-blue-800">Why connect to Hugging Face?</p>
157
+ <p className="text-blue-700 mt-1">
158
+ Access thousands of AI models for advanced text generation and editing.
159
+ Get your free API key at{' '}
160
+ <a
161
+ href="https://huggingface.co/settings/tokens"
162
+ target="_blank"
163
+ rel="noopener noreferrer"
164
+ className="underline font-medium"
165
+ >
166
+ huggingface.co/settings/tokens
167
+ </a>
168
+ </p>
169
+ </div>
170
+ </div>
171
+ </div>
172
+
173
+ {/* API Key Input */}
174
+ <div>
175
+ <label className="block text-sm font-medium text-gray-700 mb-2">
176
+ <Key className="inline w-4 h-4 mr-1" />
177
+ Hugging Face API Key
178
+ </label>
179
+ <input
180
+ type="password"
181
+ value={apiKey}
182
+ onChange={(e) => setApiKey(e.target.value)}
183
+ className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
184
+ placeholder="hf_..."
185
+ disabled={isLoading}
186
+ />
187
+ </div>
188
+
189
+ {/* Error Message */}
190
+ {error && (
191
+ <div className="bg-red-50 border border-red-200 rounded-lg p-3">
192
+ <p className="text-sm text-red-700">{error}</p>
193
+ </div>
194
+ )}
195
+ </div>
196
+
197
+ {/* Footer */}
198
+ <div className="flex items-center justify-end gap-3 p-4 border-t">
199
+ <button
200
+ onClick={() => setIsOpen(false)}
201
+ className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
202
+ >
203
+ Cancel
204
+ </button>
205
+ <button
206
+ onClick={handleLogin}
207
+ disabled={isLoading}
208
+ className="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
209
+ >
210
+ {isLoading ? (
211
+ <>
212
+ <div className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full" />
213
+ Connecting...
214
+ </>
215
+ ) : (
216
+ <>
217
+ <LogIn className="w-4 h-4" />
218
+ Connect
219
+ </>
220
+ )}
221
+ </button>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ )}
226
+ </>
227
+ );
228
+ }
components/editor/AIToolsDialog.tsx ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { X, Sparkles, RefreshCw, FileText, Maximize2 } from 'lucide-react';
3
+
4
+ interface AIToolsDialogProps {
5
+ isOpen: boolean;
6
+ onClose: () => void;
7
+ selectedText: string;
8
+ onApply: (newText: string) => void;
9
+ onAIEdit: (action: 'refine' | 'change' | 'expand') => Promise<string>;
10
+ }
11
+
12
+ export default function AIToolsDialog({
13
+ isOpen,
14
+ onClose,
15
+ selectedText,
16
+ onApply,
17
+ onAIEdit
18
+ }: AIToolsDialogProps) {
19
+ const [currentText, setCurrentText] = useState(selectedText);
20
+ const [isProcessing, setIsProcessing] = useState(false);
21
+ const [selectedAction, setSelectedAction] = useState<'refine' | 'change' | 'expand' | null>(null);
22
+
23
+ if (!isOpen) return null;
24
+
25
+ const handleAction = async (action: 'refine' | 'change' | 'expand') => {
26
+ setIsProcessing(true);
27
+ setSelectedAction(action);
28
+ try {
29
+ const newText = await onAIEdit(action);
30
+ setCurrentText(newText);
31
+ } catch (error) {
32
+ console.error('AI edit error:', error);
33
+ } finally {
34
+ setIsProcessing(false);
35
+ setSelectedAction(null);
36
+ }
37
+ };
38
+
39
+ const handleApply = () => {
40
+ onApply(currentText);
41
+ onClose();
42
+ };
43
+
44
+ const handleCancel = () => {
45
+ setCurrentText(selectedText);
46
+ onClose();
47
+ };
48
+
49
+ return (
50
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[1000]">
51
+ <div className="bg-white rounded-lg shadow-xl w-[600px] max-h-[80vh] flex flex-col">
52
+ {/* Header */}
53
+ <div className="flex items-center justify-between p-4 border-b">
54
+ <div className="flex items-center gap-2">
55
+ <Sparkles className="w-5 h-5 text-purple-600" />
56
+ <h2 className="text-lg font-semibold">AI Text Tools</h2>
57
+ </div>
58
+ <button
59
+ onClick={handleCancel}
60
+ className="p-1 hover:bg-gray-100 rounded transition-colors"
61
+ >
62
+ <X className="w-5 h-5" />
63
+ </button>
64
+ </div>
65
+
66
+ {/* Content */}
67
+ <div className="flex-1 overflow-auto p-4">
68
+ <div className="space-y-4">
69
+ {/* Action buttons */}
70
+ <div className="grid grid-cols-3 gap-3">
71
+ <button
72
+ onClick={() => handleAction('refine')}
73
+ disabled={isProcessing}
74
+ className={`
75
+ p-4 rounded-lg border-2 transition-all
76
+ ${isProcessing && selectedAction === 'refine'
77
+ ? 'border-purple-500 bg-purple-50'
78
+ : 'border-gray-200 hover:border-purple-400 hover:bg-purple-50'}
79
+ ${isProcessing ? 'cursor-not-allowed opacity-75' : 'cursor-pointer'}
80
+ `}
81
+ >
82
+ <div className="flex flex-col items-center gap-2">
83
+ {isProcessing && selectedAction === 'refine' ? (
84
+ <div className="animate-spin h-6 w-6 border-2 border-purple-600 border-t-transparent rounded-full" />
85
+ ) : (
86
+ <Sparkles className="w-6 h-6 text-purple-600" />
87
+ )}
88
+ <div className="text-sm font-medium">Refine Text</div>
89
+ <div className="text-xs text-gray-500 text-center">Make it more professional</div>
90
+ </div>
91
+ </button>
92
+
93
+ <button
94
+ onClick={() => handleAction('change')}
95
+ disabled={isProcessing}
96
+ className={`
97
+ p-4 rounded-lg border-2 transition-all
98
+ ${isProcessing && selectedAction === 'change'
99
+ ? 'border-purple-500 bg-purple-50'
100
+ : 'border-gray-200 hover:border-purple-400 hover:bg-purple-50'}
101
+ ${isProcessing ? 'cursor-not-allowed opacity-75' : 'cursor-pointer'}
102
+ `}
103
+ >
104
+ <div className="flex flex-col items-center gap-2">
105
+ {isProcessing && selectedAction === 'change' ? (
106
+ <div className="animate-spin h-6 w-6 border-2 border-purple-600 border-t-transparent rounded-full" />
107
+ ) : (
108
+ <RefreshCw className="w-6 h-6 text-purple-600" />
109
+ )}
110
+ <div className="text-sm font-medium">Rewrite</div>
111
+ <div className="text-xs text-gray-500 text-center">Different wording, same meaning</div>
112
+ </div>
113
+ </button>
114
+
115
+ <button
116
+ onClick={() => handleAction('expand')}
117
+ disabled={isProcessing}
118
+ className={`
119
+ p-4 rounded-lg border-2 transition-all
120
+ ${isProcessing && selectedAction === 'expand'
121
+ ? 'border-purple-500 bg-purple-50'
122
+ : 'border-gray-200 hover:border-purple-400 hover:bg-purple-50'}
123
+ ${isProcessing ? 'cursor-not-allowed opacity-75' : 'cursor-pointer'}
124
+ `}
125
+ >
126
+ <div className="flex flex-col items-center gap-2">
127
+ {isProcessing && selectedAction === 'expand' ? (
128
+ <div className="animate-spin h-6 w-6 border-2 border-purple-600 border-t-transparent rounded-full" />
129
+ ) : (
130
+ <Maximize2 className="w-6 h-6 text-purple-600" />
131
+ )}
132
+ <div className="text-sm font-medium">Expand</div>
133
+ <div className="text-xs text-gray-500 text-center">Add more detail</div>
134
+ </div>
135
+ </button>
136
+ </div>
137
+
138
+ {/* Text preview */}
139
+ <div>
140
+ <label className="block text-sm font-medium text-gray-700 mb-2">
141
+ Preview
142
+ </label>
143
+ <div className="relative">
144
+ <textarea
145
+ value={currentText}
146
+ onChange={(e) => setCurrentText(e.target.value)}
147
+ className="w-full h-40 p-3 border rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-purple-500"
148
+ placeholder="Your text will appear here..."
149
+ />
150
+ {isProcessing && (
151
+ <div className="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center rounded-lg">
152
+ <div className="flex items-center gap-2">
153
+ <div className="animate-spin h-5 w-5 border-2 border-purple-600 border-t-transparent rounded-full" />
154
+ <span className="text-sm text-gray-600">AI is working...</span>
155
+ </div>
156
+ </div>
157
+ )}
158
+ </div>
159
+ <div className="mt-2 flex items-center justify-between text-xs text-gray-500">
160
+ <span>{currentText.split(' ').length} words</span>
161
+ <span>{currentText.length} characters</span>
162
+ </div>
163
+ </div>
164
+
165
+ {/* Comparison */}
166
+ {currentText !== selectedText && (
167
+ <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
168
+ <div className="flex items-start gap-2">
169
+ <FileText className="w-4 h-4 text-yellow-600 mt-0.5" />
170
+ <div className="flex-1">
171
+ <div className="text-sm font-medium text-yellow-800">Changes made</div>
172
+ <div className="text-xs text-yellow-700 mt-1">
173
+ The text has been modified. Click "Apply Changes" to save or "Cancel" to revert.
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ )}
179
+ </div>
180
+ </div>
181
+
182
+ {/* Footer */}
183
+ <div className="flex items-center justify-end gap-3 p-4 border-t">
184
+ <button
185
+ onClick={handleCancel}
186
+ className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
187
+ >
188
+ Cancel
189
+ </button>
190
+ <button
191
+ onClick={handleApply}
192
+ disabled={isProcessing}
193
+ className="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
194
+ >
195
+ Apply Changes
196
+ </button>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ );
201
+ }
components/editor/GoogleSlidesEditor.tsx CHANGED
@@ -89,6 +89,8 @@ import { Shape, createShape } from '@/lib/shapes-system';
89
  import ShapesPanel from '@/components/editor/ShapesPanel';
90
  import PowerPointRibbon from '@/components/editor/PowerPointRibbon';
91
  import ShapeRenderer from '@/components/shapes/ShapeRenderer';
 
 
92
 
93
  // ============================================================================
94
  // TYPE DEFINITIONS - Define the structure of all elements in the editor
@@ -162,121 +164,57 @@ const themes = {
162
  bodyFont: 'Arial'
163
  },
164
 
165
- // Workshop Theme - Paper & Grid Design
166
  workshop: {
167
- // Grid background pattern (24px grid on paper) - matches pinboard aesthetic
168
- backgroundImage: `
169
- repeating-linear-gradient(0deg, #D6CEC2 0, #D6CEC2 1px, transparent 1px, transparent 24px),
170
- repeating-linear-gradient(90deg, #D6CEC2 0, #D6CEC2 1px, transparent 1px, transparent 24px)
171
- `,
172
- backgroundSize: '24px 24px',
173
- backgroundPosition: '0 0',
174
- backgroundColor: '#EFE8DF', // Paper color
175
- titleColor: '#0E0E0E', // Black
176
- textColor: '#151515', // Off-black
177
  gradient: false,
178
  solidBackground: '#EFE8DF',
179
  headingFont: 'Anton',
180
- bodyFont: 'Manrope',
181
- // Theme accent colors (for sticky notes, labels, decorations)
182
- accentCoral: '#E76C63',
183
- accentGreen: '#6C8E5F',
184
- accentTeal: '#3AA6A6',
185
- accentSand: '#E9DCCF',
186
- accentPurple: '#6C58A6',
187
- paperDark: '#E6DED2',
188
- gridLine: '#D6CEC2',
189
- shadowColor: 'rgba(0, 0, 0, 0.28)'
190
- },
191
-
192
- // Gradient Themes
193
- oceanBreeze: {
194
- backgroundImage: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)',
195
- titleColor: '#ffffff',
196
- textColor: '#f0f0f0',
197
- gradient: true,
198
- solidBackground: '#667eea',
199
- headingFont: 'Arial',
200
- bodyFont: 'Arial'
201
  },
202
 
203
- sunsetGlow: {
204
- backgroundImage: 'linear-gradient(135deg, #ff6b6b 0%, #feca57 50%, #ee5a6f 100%)',
205
- titleColor: '#ffffff',
206
- textColor: '#ffffff',
207
- gradient: true,
208
- solidBackground: '#ff6b6b',
209
- headingFont: 'Arial',
210
- bodyFont: 'Arial'
211
- },
212
-
213
- forestMist: {
214
- backgroundImage: 'linear-gradient(135deg, #134e5e 0%, #71b280 100%)',
215
- titleColor: '#ffffff',
216
- textColor: '#e8f5e9',
217
- gradient: true,
218
- solidBackground: '#134e5e',
219
- headingFont: 'Arial',
220
- bodyFont: 'Arial'
221
- },
222
-
223
- midnightBlue: {
224
- backgroundImage: 'linear-gradient(135deg, #0f2027 0%, #203a43 50%, #2c5364 100%)',
225
  titleColor: '#64b5f6',
226
  textColor: '#e1f5fe',
227
- gradient: true,
228
- solidBackground: '#0f2027',
229
  headingFont: 'Arial',
230
  bodyFont: 'Arial'
231
  },
232
 
233
- // Pattern Themes
234
- geometricDark: {
235
- backgroundImage: `
236
- linear-gradient(45deg, #1a1a2e 25%, transparent 25%),
237
- linear-gradient(-45deg, #1a1a2e 25%, transparent 25%),
238
- linear-gradient(45deg, transparent 75%, #1a1a2e 75%),
239
- linear-gradient(-45deg, transparent 75%, #1a1a2e 75%),
240
- linear-gradient(#16213e, #16213e)
241
- `,
242
- backgroundSize: '20px 20px, 20px 20px, 20px 20px, 20px 20px, 100% 100%',
243
- backgroundPosition: '0 0, 0 10px, 10px -10px, -10px 0px, 0 0',
244
- titleColor: '#00d2ff',
245
- textColor: '#e0e0e0',
246
  gradient: false,
247
- solidBackground: '#16213e',
248
  headingFont: 'Arial',
249
  bodyFont: 'Arial'
250
  },
251
 
252
- stripedProfessional: {
253
- backgroundImage: `
254
- repeating-linear-gradient(
255
- 45deg,
256
- #2c3e50,
257
- #2c3e50 10px,
258
- #34495e 10px,
259
- #34495e 20px
260
- )
261
- `,
262
- titleColor: '#ecf0f1',
263
- textColor: '#bdc3c7',
264
  gradient: false,
265
- solidBackground: '#2c3e50',
266
  headingFont: 'Arial',
267
  bodyFont: 'Arial'
268
  },
269
 
270
- dotGrid: {
271
- backgroundImage: `
272
- radial-gradient(circle, #3498db 1px, transparent 1px),
273
- linear-gradient(#ecf0f1, #ecf0f1)
274
- `,
275
- backgroundSize: '30px 30px, 100% 100%',
276
- titleColor: '#2c3e50',
277
- textColor: '#34495e',
278
  gradient: false,
279
- solidBackground: '#ecf0f1',
280
  headingFont: 'Arial',
281
  bodyFont: 'Arial'
282
  },
@@ -768,6 +706,7 @@ export default function GoogleSlidesEditor() {
768
  const [currentTheme, setCurrentTheme] = useState<keyof typeof themes>('white'); // Active theme
769
  const [isGenerating, setIsGenerating] = useState(false); // AI generation in progress
770
  const [generationError, setGenerationError] = useState<string | null>(null); // AI errors
 
771
 
772
  // EDITOR UI STATE
773
  const [currentSlideIndex, setCurrentSlideIndex] = useState(0); // Which slide is active
@@ -780,6 +719,7 @@ export default function GoogleSlidesEditor() {
780
  const [showExportMenu, setShowExportMenu] = useState(false); // Export dropdown visible
781
  const [showAIMenu, setShowAIMenu] = useState(false); // AI menu visible
782
  const [isAIEditing, setIsAIEditing] = useState(false); // AI is processing text
 
783
 
784
  // DOM REFERENCES - For direct DOM manipulation
785
  const fileInputRef = useRef<HTMLInputElement>(null); // Hidden file input for images
@@ -886,14 +826,20 @@ export default function GoogleSlidesEditor() {
886
  };
887
 
888
  if (!generationModel.includes('gemini')) {
889
- // For HF models, try to get the OAuth token
890
- const hfOAuth = localStorage.getItem('hf_oauth');
891
- if (hfOAuth) {
892
- try {
893
- const oauthData = JSON.parse(hfOAuth);
894
- headers['x-hf-token'] = oauthData.accessToken;
895
- } catch (e) {
896
- console.error('Failed to parse HF OAuth data:', e);
 
 
 
 
 
 
897
  }
898
  }
899
  }
@@ -1196,6 +1142,48 @@ export default function GoogleSlidesEditor() {
1196
  return () => window.removeEventListener('keydown', onKey);
1197
  }, [selectedId, currentSlideIndex, handleUndo, handleRedo, isEditingTextId, currentSlide.elements, updateElement]); // saveToHistory is stable
1198
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1199
  // PDF Export function
1200
  const exportToPDF = async () => {
1201
  if (!slideRef.current || slides.length === 0) return;
@@ -1232,11 +1220,32 @@ export default function GoogleSlidesEditor() {
1232
 
1233
  // Capture the slide with html2canvas
1234
  const canvas = await html2canvas(slideRef.current, {
1235
- backgroundColor: '#ffffff',
1236
  scale: 2,
1237
  logging: false,
1238
  useCORS: true,
1239
  allowTaint: true,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1240
  });
1241
 
1242
  const imgData = canvas.toDataURL('image/png');
@@ -1248,7 +1257,9 @@ export default function GoogleSlidesEditor() {
1248
  pdf.addImage(imgData, 'PNG', 0, 0, 800, 450);
1249
  }
1250
 
1251
- pdf.save('presentation.pdf');
 
 
1252
 
1253
  // Restore original state
1254
  setCurrentSlideIndex(originalSlideIndex);
@@ -1302,7 +1313,9 @@ export default function GoogleSlidesEditor() {
1302
  const url = window.URL.createObjectURL(blob);
1303
  const a = document.createElement('a');
1304
  a.href = url;
1305
- a.download = 'presentation.pptx';
 
 
1306
  document.body.appendChild(a);
1307
  a.click();
1308
  window.URL.revokeObjectURL(url);
@@ -1352,25 +1365,22 @@ export default function GoogleSlidesEditor() {
1352
  );
1353
  };
1354
 
1355
- // AI Text Editing
1356
- const handleAIEdit = async (action: 'refine' | 'change' | 'expand') => {
1357
- if (!selectedId) return;
1358
-
1359
  const selectedElement = currentSlide.elements.find(e => e.id === selectedId);
1360
- if (!selectedElement || selectedElement.type !== 'text') return;
1361
-
 
 
1362
  const textEl = selectedElement as TextElement;
1363
  const originalText = textEl.text;
1364
-
1365
- setIsAIEditing(true);
1366
- setShowAIMenu(false);
1367
-
1368
  try {
1369
  // Get HF token if available
1370
  const headers: Record<string, string> = {
1371
  'Content-Type': 'application/json',
1372
  };
1373
-
1374
  const hfOAuth = localStorage.getItem('hf_oauth');
1375
  if (hfOAuth) {
1376
  try {
@@ -1380,7 +1390,7 @@ export default function GoogleSlidesEditor() {
1380
  console.error('Failed to parse HF OAuth data:', e);
1381
  }
1382
  }
1383
-
1384
  const response = await fetch('/api/ai-edit-text', {
1385
  method: 'POST',
1386
  headers,
@@ -1389,21 +1399,25 @@ export default function GoogleSlidesEditor() {
1389
  action,
1390
  }),
1391
  });
1392
-
1393
  if (!response.ok) {
1394
  throw new Error('Failed to edit text');
1395
  }
1396
-
1397
  const data = await response.json();
1398
- updateElement(selectedId, { text: data.text });
1399
- setIsAIEditing(false);
1400
  } catch (error) {
1401
  console.error('AI editing error:', error);
1402
- alert('Failed to edit text with AI. Please try again.');
1403
- setIsAIEditing(false);
1404
  }
1405
  };
1406
 
 
 
 
 
 
 
1407
  const addText = () => {
1408
  const newId = createId();
1409
  setSlides(prev => {
@@ -2073,17 +2087,25 @@ export default function GoogleSlidesEditor() {
2073
  return (
2074
  <div className="h-dvh w-full flex flex-col bg-gray-100 text-gray-800 min-w-[768px]">
2075
  {/* Topbar mimicking Google Slides */}
2076
- <div className="border-b bg-white text-black">
2077
  <div className="flex items-center gap-2 md:gap-3 px-2 md:px-3 py-2">
2078
- <input defaultValue="Untitled presentation" className="text-sm font-medium px-2 py-1 rounded hover:bg-gray-100 focus:bg-gray-100 outline-none text-black max-w-xs flex-shrink min-w-0" style={{ fontFamily: 'Inter, sans-serif' }} />
 
 
 
 
 
 
2079
  <div className="ml-auto flex items-center gap-2">
2080
- <button className="px-2 md:px-3 py-1.5 rounded border text-sm hover:bg-gray-50 flex items-center gap-1 md:gap-2 text-black" style={{ fontFamily: 'Inter, sans-serif' }}><FiPlay /> <span className="hidden sm:inline">Slideshow</span></button>
 
 
2081
  <div className="w-8 h-8 rounded-full bg-gray-200 flex-shrink-0" />
2082
  </div>
2083
  </div>
2084
 
2085
  {/* Toolbar */}
2086
- <div className="px-2 md:px-3 pb-2">
2087
  <PowerPointRibbon
2088
  handleUndo={handleUndo}
2089
  handleRedo={handleRedo}
@@ -2108,7 +2130,7 @@ export default function GoogleSlidesEditor() {
2108
  showAIMenu={showAIMenu}
2109
  setShowAIMenu={setShowAIMenu}
2110
  isAIEditing={isAIEditing}
2111
- handleAIEdit={handleAIEdit}
2112
  applyLayout={applyLayout}
2113
  currentTheme={currentTheme}
2114
  applyTheme={applyTheme}
@@ -2642,6 +2664,27 @@ export default function GoogleSlidesEditor() {
2642
  </div>
2643
  </main>
2644
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2645
  </div>
2646
  );
2647
  }
 
89
  import ShapesPanel from '@/components/editor/ShapesPanel';
90
  import PowerPointRibbon from '@/components/editor/PowerPointRibbon';
91
  import ShapeRenderer from '@/components/shapes/ShapeRenderer';
92
+ import AIToolsDialog from '@/components/editor/AIToolsDialog';
93
+ import HuggingFaceLogin from '@/components/auth/HuggingFaceLogin';
94
 
95
  // ============================================================================
96
  // TYPE DEFINITIONS - Define the structure of all elements in the editor
 
164
  bodyFont: 'Arial'
165
  },
166
 
167
+ // Professional Workshop Theme
168
  workshop: {
169
+ background: '#EFE8DF',
170
+ titleColor: '#0E0E0E',
171
+ textColor: '#151515',
 
 
 
 
 
 
 
172
  gradient: false,
173
  solidBackground: '#EFE8DF',
174
  headingFont: 'Anton',
175
+ bodyFont: 'Manrope'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  },
177
 
178
+ // Modern Dark Theme
179
+ darkModern: {
180
+ background: '#1a1a2e',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  titleColor: '#64b5f6',
182
  textColor: '#e1f5fe',
183
+ gradient: false,
184
+ solidBackground: '#1a1a2e',
185
  headingFont: 'Arial',
186
  bodyFont: 'Arial'
187
  },
188
 
189
+ // Professional Blue
190
+ professionalBlue: {
191
+ background: '#1e3a8a',
192
+ titleColor: '#ffffff',
193
+ textColor: '#dbeafe',
 
 
 
 
 
 
 
 
194
  gradient: false,
195
+ solidBackground: '#1e3a8a',
196
  headingFont: 'Arial',
197
  bodyFont: 'Arial'
198
  },
199
 
200
+ // Elegant Green
201
+ elegantGreen: {
202
+ background: '#065f46',
203
+ titleColor: '#ffffff',
204
+ textColor: '#d1fae5',
 
 
 
 
 
 
 
205
  gradient: false,
206
+ solidBackground: '#065f46',
207
  headingFont: 'Arial',
208
  bodyFont: 'Arial'
209
  },
210
 
211
+ // Sophisticated Purple
212
+ sophisticatedPurple: {
213
+ background: '#581c87',
214
+ titleColor: '#ffffff',
215
+ textColor: '#e9d5ff',
 
 
 
216
  gradient: false,
217
+ solidBackground: '#581c87',
218
  headingFont: 'Arial',
219
  bodyFont: 'Arial'
220
  },
 
706
  const [currentTheme, setCurrentTheme] = useState<keyof typeof themes>('white'); // Active theme
707
  const [isGenerating, setIsGenerating] = useState(false); // AI generation in progress
708
  const [generationError, setGenerationError] = useState<string | null>(null); // AI errors
709
+ const [presentationTitle, setPresentationTitle] = useState('Untitled Presentation'); // Editable title
710
 
711
  // EDITOR UI STATE
712
  const [currentSlideIndex, setCurrentSlideIndex] = useState(0); // Which slide is active
 
719
  const [showExportMenu, setShowExportMenu] = useState(false); // Export dropdown visible
720
  const [showAIMenu, setShowAIMenu] = useState(false); // AI menu visible
721
  const [isAIEditing, setIsAIEditing] = useState(false); // AI is processing text
722
+ const [showAIDialog, setShowAIDialog] = useState(false); // AI dialog visible
723
 
724
  // DOM REFERENCES - For direct DOM manipulation
725
  const fileInputRef = useRef<HTMLInputElement>(null); // Hidden file input for images
 
826
  };
827
 
828
  if (!generationModel.includes('gemini')) {
829
+ // For HF models, try to get the API key from localStorage
830
+ const hfApiKey = localStorage.getItem('hf_api_key');
831
+ if (hfApiKey) {
832
+ headers['x-hf-api-key'] = hfApiKey;
833
+ } else {
834
+ // Also check for OAuth token (legacy support)
835
+ const hfOAuth = localStorage.getItem('hf_oauth');
836
+ if (hfOAuth) {
837
+ try {
838
+ const oauthData = JSON.parse(hfOAuth);
839
+ headers['x-hf-token'] = oauthData.accessToken;
840
+ } catch (e) {
841
+ console.error('Failed to parse HF OAuth data:', e);
842
+ }
843
  }
844
  }
845
  }
 
1142
  return () => window.removeEventListener('keydown', onKey);
1143
  }, [selectedId, currentSlideIndex, handleUndo, handleRedo, isEditingTextId, currentSlide.elements, updateElement]); // saveToHistory is stable
1144
 
1145
+ // Helper function to sanitize CSS colors for export
1146
+ const sanitizeColorsForExport = (clonedDoc: Document) => {
1147
+ // Create a style element to override problematic colors
1148
+ const style = clonedDoc.createElement('style');
1149
+ style.textContent = `
1150
+ /* Override any potentially problematic colors with safe fallbacks */
1151
+ * {
1152
+ /* Ensure no lab(), lch(), oklch() or other modern color functions are used */
1153
+ transition: none !important;
1154
+ animation: none !important;
1155
+ }
1156
+ /* Replace any computed styles that might use modern color functions */
1157
+ .dark * {
1158
+ background-color: #242424 !important;
1159
+ color: #fafafa !important;
1160
+ }
1161
+ body, html {
1162
+ background: #ffffff !important;
1163
+ }
1164
+ `;
1165
+ clonedDoc.head.appendChild(style);
1166
+
1167
+ // Check and replace any inline styles with modern color functions
1168
+ const allElements = clonedDoc.querySelectorAll('*');
1169
+ allElements.forEach((el: HTMLElement) => {
1170
+ const style = el.getAttribute('style');
1171
+ if (style) {
1172
+ // Replace various modern color functions with safe fallbacks
1173
+ const sanitized = style
1174
+ .replace(/lab\([^)]+\)/g, '#000000')
1175
+ .replace(/lch\([^)]+\)/g, '#000000')
1176
+ .replace(/oklch\([^)]+\)/g, '#000000')
1177
+ .replace(/oklab\([^)]+\)/g, '#000000')
1178
+ .replace(/color\([^)]+\)/g, '#000000');
1179
+
1180
+ if (sanitized !== style) {
1181
+ el.setAttribute('style', sanitized);
1182
+ }
1183
+ }
1184
+ });
1185
+ };
1186
+
1187
  // PDF Export function
1188
  const exportToPDF = async () => {
1189
  if (!slideRef.current || slides.length === 0) return;
 
1220
 
1221
  // Capture the slide with html2canvas
1222
  const canvas = await html2canvas(slideRef.current, {
1223
+ backgroundColor: null, // Use transparent to preserve the theme background
1224
  scale: 2,
1225
  logging: false,
1226
  useCORS: true,
1227
  allowTaint: true,
1228
+ // Add onclone to fix color issues and preserve theme
1229
+ onclone: (clonedDoc) => {
1230
+ // Apply theme styles to cloned document
1231
+ const slideElement = clonedDoc.querySelector('[style*="width: 800px"]');
1232
+ if (slideElement && slideElement instanceof HTMLElement) {
1233
+ const theme = themes[currentTheme] as any;
1234
+ if (theme.backgroundImage) {
1235
+ slideElement.style.backgroundImage = theme.backgroundImage;
1236
+ slideElement.style.backgroundSize = theme.backgroundSize || 'cover';
1237
+ slideElement.style.backgroundPosition = theme.backgroundPosition || 'center';
1238
+ if (theme.backgroundColor) {
1239
+ slideElement.style.backgroundColor = theme.backgroundColor;
1240
+ }
1241
+ } else if (theme.background) {
1242
+ slideElement.style.backgroundColor = theme.background;
1243
+ } else if (theme.solidBackground) {
1244
+ slideElement.style.backgroundColor = theme.solidBackground;
1245
+ }
1246
+ }
1247
+ sanitizeColorsForExport(clonedDoc);
1248
+ },
1249
  });
1250
 
1251
  const imgData = canvas.toDataURL('image/png');
 
1257
  pdf.addImage(imgData, 'PNG', 0, 0, 800, 450);
1258
  }
1259
 
1260
+ // Use presentation title for filename, sanitize it
1261
+ const sanitizedTitle = presentationTitle.replace(/[^a-zA-Z0-9 -]/g, '').trim() || 'presentation';
1262
+ pdf.save(`${sanitizedTitle}.pdf`);
1263
 
1264
  // Restore original state
1265
  setCurrentSlideIndex(originalSlideIndex);
 
1313
  const url = window.URL.createObjectURL(blob);
1314
  const a = document.createElement('a');
1315
  a.href = url;
1316
+ // Use presentation title for filename, sanitize it
1317
+ const sanitizedTitle = presentationTitle.replace(/[^a-zA-Z0-9 -]/g, '').trim() || 'presentation';
1318
+ a.download = `${sanitizedTitle}.pptx`;
1319
  document.body.appendChild(a);
1320
  a.click();
1321
  window.URL.revokeObjectURL(url);
 
1365
  );
1366
  };
1367
 
1368
+ // AI Text Editing - returns the edited text for the dialog
1369
+ const handleAIEdit = async (action: 'refine' | 'change' | 'expand'): Promise<string> => {
 
 
1370
  const selectedElement = currentSlide.elements.find(e => e.id === selectedId);
1371
+ if (!selectedElement || selectedElement.type !== 'text') {
1372
+ throw new Error('No text element selected');
1373
+ }
1374
+
1375
  const textEl = selectedElement as TextElement;
1376
  const originalText = textEl.text;
1377
+
 
 
 
1378
  try {
1379
  // Get HF token if available
1380
  const headers: Record<string, string> = {
1381
  'Content-Type': 'application/json',
1382
  };
1383
+
1384
  const hfOAuth = localStorage.getItem('hf_oauth');
1385
  if (hfOAuth) {
1386
  try {
 
1390
  console.error('Failed to parse HF OAuth data:', e);
1391
  }
1392
  }
1393
+
1394
  const response = await fetch('/api/ai-edit-text', {
1395
  method: 'POST',
1396
  headers,
 
1399
  action,
1400
  }),
1401
  });
1402
+
1403
  if (!response.ok) {
1404
  throw new Error('Failed to edit text');
1405
  }
1406
+
1407
  const data = await response.json();
1408
+ return data.text;
 
1409
  } catch (error) {
1410
  console.error('AI editing error:', error);
1411
+ throw error;
 
1412
  }
1413
  };
1414
 
1415
+ // Handle opening AI dialog from ribbon
1416
+ const handleOpenAIDialog = () => {
1417
+ setShowAIMenu(false);
1418
+ setShowAIDialog(true);
1419
+ };
1420
+
1421
  const addText = () => {
1422
  const newId = createId();
1423
  setSlides(prev => {
 
2087
  return (
2088
  <div className="h-dvh w-full flex flex-col bg-gray-100 text-gray-800 min-w-[768px]">
2089
  {/* Topbar mimicking Google Slides */}
2090
+ <div className="border-b bg-white text-black overflow-visible">
2091
  <div className="flex items-center gap-2 md:gap-3 px-2 md:px-3 py-2">
2092
+ <input
2093
+ value={presentationTitle}
2094
+ onChange={(e) => setPresentationTitle(e.target.value)}
2095
+ className="text-sm font-medium px-2 py-1 rounded hover:bg-gray-100 focus:bg-gray-100 outline-none text-black max-w-xs flex-shrink min-w-0"
2096
+ style={{ fontFamily: 'Inter, sans-serif' }}
2097
+ placeholder="Enter presentation title"
2098
+ />
2099
  <div className="ml-auto flex items-center gap-2">
2100
+ <HuggingFaceLogin onAuthChange={(authenticated) => {
2101
+ console.log('HF Auth changed:', authenticated);
2102
+ }} />
2103
  <div className="w-8 h-8 rounded-full bg-gray-200 flex-shrink-0" />
2104
  </div>
2105
  </div>
2106
 
2107
  {/* Toolbar */}
2108
+ <div className="px-2 md:px-3 pb-2 overflow-visible">
2109
  <PowerPointRibbon
2110
  handleUndo={handleUndo}
2111
  handleRedo={handleRedo}
 
2130
  showAIMenu={showAIMenu}
2131
  setShowAIMenu={setShowAIMenu}
2132
  isAIEditing={isAIEditing}
2133
+ handleAIEdit={handleOpenAIDialog}
2134
  applyLayout={applyLayout}
2135
  currentTheme={currentTheme}
2136
  applyTheme={applyTheme}
 
2664
  </div>
2665
  </main>
2666
  </div>
2667
+
2668
+ {/* AI Tools Dialog */}
2669
+ {showAIDialog && selectedId && (() => {
2670
+ const selectedElement = currentSlide.elements.find(e => e.id === selectedId);
2671
+ if (selectedElement?.type === 'text') {
2672
+ const textEl = selectedElement as TextElement;
2673
+ return (
2674
+ <AIToolsDialog
2675
+ isOpen={showAIDialog}
2676
+ onClose={() => setShowAIDialog(false)}
2677
+ selectedText={textEl.text}
2678
+ onApply={(newText) => {
2679
+ updateElement(selectedId, { text: newText });
2680
+ setShowAIDialog(false);
2681
+ }}
2682
+ onAIEdit={handleAIEdit}
2683
+ />
2684
+ );
2685
+ }
2686
+ return null;
2687
+ })()}
2688
  </div>
2689
  );
2690
  }
components/editor/PowerPointRibbon.tsx CHANGED
@@ -55,7 +55,7 @@ interface PowerPointRibbonProps {
55
  showAIMenu: boolean;
56
  setShowAIMenu: (show: boolean) => void;
57
  isAIEditing: boolean;
58
- handleAIEdit: (action: string) => void;
59
 
60
  // Layout & Theme
61
  applyLayout: (layout: string) => void;
@@ -163,7 +163,7 @@ export default function PowerPointRibbon(props: PowerPointRibbonProps) {
163
  const isTextSelected = !!textEl;
164
 
165
  return (
166
- <div className="w-full bg-white border-b border-gray-200 overflow-x-auto">
167
  <div className="flex items-stretch divide-x divide-gray-200 px-2 pt-2 min-w-max">
168
  {/* Edit */}
169
  <Group label="Edit" withDivider>
@@ -328,16 +328,15 @@ export default function PowerPointRibbon(props: PowerPointRibbonProps) {
328
  >
329
  <option value="white">βšͺ White</option>
330
  <option value="workshop">πŸ“ Workshop</option>
331
- <optgroup label="Gradients">
332
- <option value="oceanBreeze">🌊 Ocean Breeze</option>
333
- <option value="sunsetGlow">πŸŒ… Sunset Glow</option>
334
- <option value="forestMist">🌲 Forest Mist</option>
335
- <option value="midnightBlue">πŸŒ™ Midnight Blue</option>
336
- </optgroup>
337
- <optgroup label="Patterns">
338
- <option value="geometricDark">β—† Geometric Dark</option>
339
- <option value="stripedProfessional">β–€ Striped Professional</option>
340
- <option value="dotGrid">β‹― Dot Grid</option>
341
  </optgroup>
342
  </select>
343
  <select
@@ -380,33 +379,14 @@ export default function PowerPointRibbon(props: PowerPointRibbonProps) {
380
  <div className="flex flex-col gap-1">
381
  {textEl && (
382
  <>
383
- <div className="relative">
384
- <MiniButton
385
- title="AI Edit"
386
- onClick={() => setShowAIMenu(!showAIMenu)}
387
- wide
388
- >
389
- {isAIEditing ? (
390
- <div className="animate-spin h-3 w-3 border-2 border-purple-600 border-t-transparent rounded-full" />
391
- ) : (
392
- <Sparkles className="w-4 h-4" />
393
- )}
394
- <span className="ml-1 text-[12px]">AI</span>
395
- </MiniButton>
396
- {showAIMenu && (
397
- <div className="absolute left-0 mt-1 bg-white border rounded shadow-lg z-10 min-w-[140px]">
398
- <button onClick={() => handleAIEdit('refine')} className="w-full px-3 py-2 text-left text-[12px] hover:bg-gray-100">
399
- ✨ Refine text
400
- </button>
401
- <button onClick={() => handleAIEdit('change')} className="w-full px-3 py-2 text-left text-[12px] hover:bg-gray-100">
402
- πŸ”„ Change content
403
- </button>
404
- <button onClick={() => handleAIEdit('expand')} className="w-full px-3 py-2 text-left text-[12px] hover:bg-gray-100">
405
- πŸ“ Make longer
406
- </button>
407
- </div>
408
- )}
409
- </div>
410
  <button
411
  title="Delete"
412
  onClick={() => {
@@ -431,7 +411,7 @@ export default function PowerPointRibbon(props: PowerPointRibbonProps) {
431
  <span className="ml-1 text-[12px]">Export</span>
432
  </MiniButton>
433
  {showExportMenu && (
434
- <div className="absolute left-0 mt-1 bg-white border rounded shadow-lg z-10 min-w-[100px]">
435
  <button onClick={() => { exportToPDF(); setShowExportMenu(false); }} className="w-full px-3 py-2 text-left text-[12px] hover:bg-gray-100 flex items-center gap-2">
436
  <Download className="w-3 h-3" /> PDF
437
  </button>
 
55
  showAIMenu: boolean;
56
  setShowAIMenu: (show: boolean) => void;
57
  isAIEditing: boolean;
58
+ handleAIEdit: (action: string | 'dialog') => void;
59
 
60
  // Layout & Theme
61
  applyLayout: (layout: string) => void;
 
163
  const isTextSelected = !!textEl;
164
 
165
  return (
166
+ <div className="w-full bg-white border-b border-gray-200 overflow-x-auto overflow-y-visible scrollbar-hide">
167
  <div className="flex items-stretch divide-x divide-gray-200 px-2 pt-2 min-w-max">
168
  {/* Edit */}
169
  <Group label="Edit" withDivider>
 
328
  >
329
  <option value="white">βšͺ White</option>
330
  <option value="workshop">πŸ“ Workshop</option>
331
+ <option value="darkModern">πŸŒ™ Dark Modern</option>
332
+ <option value="professionalBlue">πŸ’Ό Professional Blue</option>
333
+ <option value="elegantGreen">🌿 Elegant Green</option>
334
+ <option value="sophisticatedPurple">πŸ’œ Sophisticated Purple</option>
335
+ <optgroup label="Photo Themes">
336
+ <option value="corporateCity">🏒 Corporate City</option>
337
+ <option value="techInnovation">πŸ’» Tech Innovation</option>
338
+ <option value="natureSerene">🌳 Nature Serene</option>
339
+ <option value="minimalistConcrete">⬜ Minimalist</option>
 
340
  </optgroup>
341
  </select>
342
  <select
 
379
  <div className="flex flex-col gap-1">
380
  {textEl && (
381
  <>
382
+ <MiniButton
383
+ title="AI Edit"
384
+ onClick={() => handleAIEdit('dialog')}
385
+ wide
386
+ >
387
+ <Sparkles className="w-4 h-4" />
388
+ <span className="ml-1 text-[12px]">AI</span>
389
+ </MiniButton>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  <button
391
  title="Delete"
392
  onClick={() => {
 
411
  <span className="ml-1 text-[12px]">Export</span>
412
  </MiniButton>
413
  {showExportMenu && (
414
+ <div className="absolute left-0 mt-1 bg-white border rounded shadow-lg z-[100] min-w-[100px]">
415
  <button onClick={() => { exportToPDF(); setShowExportMenu(false); }} className="w-full px-3 py-2 text-left text-[12px] hover:bg-gray-100 flex items-center gap-2">
416
  <Download className="w-3 h-3" /> PDF
417
  </button>
components/editor/ShapesPanel.tsx CHANGED
@@ -1,8 +1,9 @@
1
- import React, { useState } from 'react';
2
  import { Shape } from '@/lib/shapes-system';
3
  import ShapePicker from '@/components/shapes/ShapePicker';
4
  import ShapeEditor from '@/components/shapes/ShapeEditor';
5
  import { Shapes, Plus, Edit2, Trash2, X } from 'lucide-react';
 
6
 
7
  interface ShapesPanelProps {
8
  shapes: Shape[];
@@ -23,6 +24,9 @@ export default function ShapesPanel({
23
  }: ShapesPanelProps) {
24
  const [showPicker, setShowPicker] = useState(false);
25
  const [showEditor, setShowEditor] = useState(false);
 
 
 
26
 
27
  const selectedShape = shapes.find(s => s.id === selectedShapeId);
28
 
@@ -32,6 +36,33 @@ export default function ShapesPanel({
32
  onShapeSelect(shape.id);
33
  };
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  const handleDeleteShape = () => {
36
  if (selectedShapeId) {
37
  onShapeDelete(selectedShapeId);
@@ -48,6 +79,7 @@ export default function ShapesPanel({
48
  <div className="relative">
49
  {/* Shapes Button */}
50
  <button
 
51
  onClick={() => setShowPicker(!showPicker)}
52
  className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
53
  showPicker
@@ -65,15 +97,21 @@ export default function ShapesPanel({
65
  )}
66
  </button>
67
 
68
- {/* Shape Picker Dropdown */}
69
- {showPicker && (
70
- <div className="absolute top-full mt-2 right-0 z-50">
71
- <ShapePicker
72
- onShapeSelect={handleAddShape}
73
- onClose={() => setShowPicker(false)}
74
- />
75
- </div>
76
- )}
 
 
 
 
 
 
77
 
78
  {/* Edit Shape Button (when shape is selected) */}
79
  {selectedShapeId && !showEditor && (
@@ -88,7 +126,7 @@ export default function ShapesPanel({
88
 
89
  {/* Shape Editor Dropdown */}
90
  {showEditor && selectedShape && (
91
- <div className="absolute top-full mt-2 right-0 z-50">
92
  <ShapeEditor
93
  shape={selectedShape}
94
  onChange={onShapeUpdate}
@@ -100,7 +138,7 @@ export default function ShapesPanel({
100
 
101
  {/* Shapes List (when shapes exist) */}
102
  {shapes.length > 0 && !showPicker && (
103
- <div className="absolute top-full mt-2 right-0 bg-white rounded-lg shadow-xl border border-gray-200 p-3 w-64 max-h-80 overflow-y-auto z-40">
104
  <div className="flex items-center justify-between mb-3">
105
  <h4 className="text-sm font-semibold text-gray-900">Shapes ({shapes.length})</h4>
106
  <button
 
1
+ import React, { useEffect, useRef, useState } from 'react';
2
  import { Shape } from '@/lib/shapes-system';
3
  import ShapePicker from '@/components/shapes/ShapePicker';
4
  import ShapeEditor from '@/components/shapes/ShapeEditor';
5
  import { Shapes, Plus, Edit2, Trash2, X } from 'lucide-react';
6
+ import { createPortal } from 'react-dom';
7
 
8
  interface ShapesPanelProps {
9
  shapes: Shape[];
 
24
  }: ShapesPanelProps) {
25
  const [showPicker, setShowPicker] = useState(false);
26
  const [showEditor, setShowEditor] = useState(false);
27
+ const [isPortalReady, setIsPortalReady] = useState(false);
28
+ const [pickerPosition, setPickerPosition] = useState<{ top: number; left: number } | null>(null);
29
+ const buttonRef = useRef<HTMLButtonElement>(null);
30
 
31
  const selectedShape = shapes.find(s => s.id === selectedShapeId);
32
 
 
36
  onShapeSelect(shape.id);
37
  };
38
 
39
+ const updatePickerPosition = () => {
40
+ if (!buttonRef.current) return;
41
+ const rect = buttonRef.current.getBoundingClientRect();
42
+ const panelWidth = 320; // ShapePicker w-80
43
+ const spacing = 8;
44
+ let left = rect.right - panelWidth;
45
+ left = Math.max(spacing, Math.min(left, window.innerWidth - panelWidth - spacing));
46
+ const top = rect.bottom + spacing;
47
+ setPickerPosition({ top, left });
48
+ };
49
+
50
+ useEffect(() => {
51
+ setIsPortalReady(true);
52
+ }, []);
53
+
54
+ useEffect(() => {
55
+ if (!showPicker) return;
56
+ updatePickerPosition();
57
+ const onScrollOrResize = () => updatePickerPosition();
58
+ window.addEventListener('scroll', onScrollOrResize, true);
59
+ window.addEventListener('resize', onScrollOrResize);
60
+ return () => {
61
+ window.removeEventListener('scroll', onScrollOrResize, true);
62
+ window.removeEventListener('resize', onScrollOrResize);
63
+ };
64
+ }, [showPicker]);
65
+
66
  const handleDeleteShape = () => {
67
  if (selectedShapeId) {
68
  onShapeDelete(selectedShapeId);
 
79
  <div className="relative">
80
  {/* Shapes Button */}
81
  <button
82
+ ref={buttonRef}
83
  onClick={() => setShowPicker(!showPicker)}
84
  className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
85
  showPicker
 
97
  )}
98
  </button>
99
 
100
+ {/* Shape Picker Dropdown (Portal) */}
101
+ {isPortalReady && showPicker && pickerPosition &&
102
+ createPortal(
103
+ <div
104
+ className="fixed z-[1000]"
105
+ style={{ top: pickerPosition.top, left: pickerPosition.left }}
106
+ >
107
+ <ShapePicker
108
+ onShapeSelect={handleAddShape}
109
+ onClose={() => setShowPicker(false)}
110
+ />
111
+ </div>,
112
+ document.body
113
+ )
114
+ }
115
 
116
  {/* Edit Shape Button (when shape is selected) */}
117
  {selectedShapeId && !showEditor && (
 
126
 
127
  {/* Shape Editor Dropdown */}
128
  {showEditor && selectedShape && (
129
+ <div className="absolute top-full mt-2 right-0 z-[100]">
130
  <ShapeEditor
131
  shape={selectedShape}
132
  onChange={onShapeUpdate}
 
138
 
139
  {/* Shapes List (when shapes exist) */}
140
  {shapes.length > 0 && !showPicker && (
141
+ <div className="absolute top-full mt-2 right-0 bg-white rounded-lg shadow-xl border border-gray-200 p-3 w-64 max-h-80 overflow-y-auto z-[100]">
142
  <div className="flex items-center justify-between mb-3">
143
  <h4 className="text-sm font-semibold text-gray-900">Shapes ({shapes.length})</h4>
144
  <button
components/shapes/ShapePicker.tsx CHANGED
@@ -40,7 +40,7 @@ export default function ShapePicker({ onShapeSelect, onClose }: ShapePickerProps
40
  </div>
41
 
42
  {/* Category Tabs */}
43
- <div className="flex gap-2 mb-4 overflow-x-auto">
44
  {Object.keys(categories).map((category) => (
45
  <button
46
  key={category}
 
40
  </div>
41
 
42
  {/* Category Tabs */}
43
+ <div className="flex gap-2 mb-4 overflow-x-auto scrollbar-hide">
44
  {Object.keys(categories).map((category) => (
45
  <button
46
  key={category}
lib/gemini-client.ts CHANGED
@@ -12,7 +12,7 @@ export class GeminiClient {
12
  // Use provided API key or environment variable, fallback to demo key
13
  const apiKey = config.apiKey || process.env.NEXT_PUBLIC_GEMINI_API_KEY || 'AIzaSyCfv-5BPXsihzyp-dn4oe5SBBvF1MDd-sE';
14
  this.genAI = new GoogleGenerativeAI(apiKey);
15
- this.model = this.genAI.getGenerativeModel({ model: 'gemini-2.0-flash-exp' }); // Use available model
16
  }
17
 
18
  async generateSlideContent(prompt: string): Promise<string> {
 
12
  // Use provided API key or environment variable, fallback to demo key
13
  const apiKey = config.apiKey || process.env.NEXT_PUBLIC_GEMINI_API_KEY || 'AIzaSyCfv-5BPXsihzyp-dn4oe5SBBvF1MDd-sE';
14
  this.genAI = new GoogleGenerativeAI(apiKey);
15
+ this.model = this.genAI.getGenerativeModel({ model: 'gemini-2.0-flash-exp' }); // Using Gemini 2.0 Flash
16
  }
17
 
18
  async generateSlideContent(prompt: string): Promise<string> {
lib/orchestrator.ts CHANGED
@@ -29,10 +29,12 @@ export interface PresentationJSON {
29
  const systemFewShot = `You are an expert presentation strategist and designer. Create a professional, engaging 5-10 slide presentation with detailed, topic-specific content.
30
 
31
  CRITICAL REQUIREMENTS:
32
- 1. All content must be specific to the user's topic - NO generic placeholders
33
- 2. Each bullet point should be 15-25 words with concrete information
34
- 3. Use relevant, actionable content with examples or data when appropriate
35
- 4. Structure presentation logically: Title β†’ Context β†’ Main Points β†’ Analysis β†’ Benefits β†’ Conclusion
 
 
36
 
37
  Return STRICT JSON matching this schema:
38
  {
@@ -41,8 +43,8 @@ Return STRICT JSON matching this schema:
41
  {
42
  "id": "slide-1",
43
  "layout": "title",
44
- "title": "Topic-Specific Title",
45
- "subtitle": "Engaging subtitle or tagline"
46
  },
47
  {
48
  "id": "slide-2",
 
29
  const systemFewShot = `You are an expert presentation strategist and designer. Create a professional, engaging 5-10 slide presentation with detailed, topic-specific content.
30
 
31
  CRITICAL REQUIREMENTS:
32
+ 1. The FIRST slide must have "Reuben AI" as the main title
33
+ 2. All content must be specific to the user's topic - NO generic placeholders
34
+ 3. Each bullet point should be 15-25 words with concrete, detailed information
35
+ 4. Use relevant, actionable content with examples or data when appropriate
36
+ 5. Structure presentation logically: Title β†’ Context β†’ Main Points β†’ Analysis β†’ Benefits β†’ Conclusion
37
+ 6. Generate specific, relevant image keywords for visual search (2-4 words) that will fetch high-quality images from Unsplash
38
 
39
  Return STRICT JSON matching this schema:
40
  {
 
43
  {
44
  "id": "slide-1",
45
  "layout": "title",
46
+ "title": "Reuben AI",
47
+ "subtitle": "Topic-specific subtitle based on user's input"
48
  },
49
  {
50
  "id": "slide-2",