Spaces:
Running
Running
Made many changes
Browse files- .claude/settings.local.json +3 -1
- app/api/export-pptx/route.ts +37 -32
- app/api/generate-slides/route.ts +39 -20
- components/auth/HuggingFaceLogin.tsx +228 -0
- components/editor/AIToolsDialog.tsx +201 -0
- components/editor/GoogleSlidesEditor.tsx +169 -126
- components/editor/PowerPointRibbon.tsx +20 -40
- components/editor/ShapesPanel.tsx +50 -12
- components/shapes/ShapePicker.tsx +1 -1
- lib/gemini-client.ts +1 -1
- lib/orchestrator.ts +8 -6
.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 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 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.
|
| 99 |
-
fontSize:
|
| 100 |
bold: true,
|
| 101 |
color: colors.title,
|
| 102 |
align: 'left',
|
| 103 |
-
fontFace: '
|
|
|
|
| 104 |
});
|
| 105 |
|
| 106 |
-
// Add content points with theme colors
|
| 107 |
if (slideData.content && slideData.content.length > 0) {
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
});
|
|
|
|
| 117 |
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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: '
|
| 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.
|
| 82 |
-
2.
|
| 83 |
-
3.
|
| 84 |
-
4.
|
| 85 |
-
5.
|
|
|
|
|
|
|
| 86 |
|
| 87 |
OUTPUT FORMAT - Return ONLY a valid JSON array with this exact structure:
|
| 88 |
[
|
| 89 |
{
|
| 90 |
-
"title": "
|
| 91 |
-
"content": ["
|
| 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:
|
| 114 |
-
-
|
| 115 |
-
-
|
| 116 |
-
-
|
|
|
|
|
|
|
| 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
|
| 166 |
workshop: {
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 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 |
-
|
| 204 |
-
|
| 205 |
-
|
| 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:
|
| 228 |
-
solidBackground: '#
|
| 229 |
headingFont: 'Arial',
|
| 230 |
bodyFont: 'Arial'
|
| 231 |
},
|
| 232 |
|
| 233 |
-
//
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 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: '#
|
| 248 |
headingFont: 'Arial',
|
| 249 |
bodyFont: 'Arial'
|
| 250 |
},
|
| 251 |
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
#2c3e50 10px,
|
| 258 |
-
#34495e 10px,
|
| 259 |
-
#34495e 20px
|
| 260 |
-
)
|
| 261 |
-
`,
|
| 262 |
-
titleColor: '#ecf0f1',
|
| 263 |
-
textColor: '#bdc3c7',
|
| 264 |
gradient: false,
|
| 265 |
-
solidBackground: '#
|
| 266 |
headingFont: 'Arial',
|
| 267 |
bodyFont: 'Arial'
|
| 268 |
},
|
| 269 |
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
backgroundSize: '30px 30px, 100% 100%',
|
| 276 |
-
titleColor: '#2c3e50',
|
| 277 |
-
textColor: '#34495e',
|
| 278 |
gradient: false,
|
| 279 |
-
solidBackground: '#
|
| 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
|
| 890 |
-
const
|
| 891 |
-
if (
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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')
|
| 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 |
-
|
| 1399 |
-
setIsAIEditing(false);
|
| 1400 |
} catch (error) {
|
| 1401 |
console.error('AI editing error:', error);
|
| 1402 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2079 |
<div className="ml-auto flex items-center gap-2">
|
| 2080 |
-
|
|
|
|
|
|
|
| 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={
|
| 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 |
-
<
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
<option value="
|
| 339 |
-
<option value="
|
| 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 |
-
<
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
>
|
| 389 |
-
|
| 390 |
-
|
| 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-
|
| 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 |
-
|
| 71 |
-
<
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 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-
|
| 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-
|
| 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' }); //
|
| 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.
|
| 33 |
-
2.
|
| 34 |
-
3.
|
| 35 |
-
4.
|
|
|
|
|
|
|
| 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": "
|
| 45 |
-
"subtitle": "
|
| 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",
|