You need to add full openrouter integration, the llm will take the markdown and design an html slideshow based on that. You should also use the internet to find images relevant to the slideshow. You should use searchx.euan.live to do this
Browse files- components/ai-slideshow.js +244 -0
- index.html +1 -0
components/ai-slideshow.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class AISlideshow extends HTMLElement {
|
| 2 |
+
constructor() {
|
| 3 |
+
super();
|
| 4 |
+
this.apiKey = localStorage.getItem('openrouter-key') || '';
|
| 5 |
+
this.slides = [];
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
connectedCallback() {
|
| 9 |
+
this.attachShadow({ mode: 'open' });
|
| 10 |
+
this.render();
|
| 11 |
+
this.addEventListeners();
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
render() {
|
| 15 |
+
this.shadowRoot.innerHTML = `
|
| 16 |
+
<style>
|
| 17 |
+
:host {
|
| 18 |
+
display: block;
|
| 19 |
+
margin-top: 2rem;
|
| 20 |
+
}
|
| 21 |
+
.ai-section {
|
| 22 |
+
background: white;
|
| 23 |
+
border-radius: 0.75rem;
|
| 24 |
+
padding: 1.5rem;
|
| 25 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 26 |
+
}
|
| 27 |
+
.api-input {
|
| 28 |
+
display: flex;
|
| 29 |
+
gap: 0.5rem;
|
| 30 |
+
margin-bottom: 1rem;
|
| 31 |
+
}
|
| 32 |
+
.api-input input {
|
| 33 |
+
flex: 1;
|
| 34 |
+
padding: 0.5rem;
|
| 35 |
+
border: 1px solid #d1d5db;
|
| 36 |
+
border-radius: 0.375rem;
|
| 37 |
+
font-size: 0.875rem;
|
| 38 |
+
}
|
| 39 |
+
.api-input button {
|
| 40 |
+
padding: 0.5rem 1rem;
|
| 41 |
+
background: #4f46e5;
|
| 42 |
+
color: white;
|
| 43 |
+
border: none;
|
| 44 |
+
border-radius: 0.375rem;
|
| 45 |
+
cursor: pointer;
|
| 46 |
+
font-size: 0.875rem;
|
| 47 |
+
}
|
| 48 |
+
.api-input button:hover {
|
| 49 |
+
background: #4338ca;
|
| 50 |
+
}
|
| 51 |
+
.generate-btn {
|
| 52 |
+
width: 100%;
|
| 53 |
+
padding: 0.75rem;
|
| 54 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 55 |
+
color: white;
|
| 56 |
+
border: none;
|
| 57 |
+
border-radius: 0.5rem;
|
| 58 |
+
font-weight: 600;
|
| 59 |
+
cursor: pointer;
|
| 60 |
+
display: flex;
|
| 61 |
+
align-items: center;
|
| 62 |
+
justify-content: center;
|
| 63 |
+
gap: 0.5rem;
|
| 64 |
+
}
|
| 65 |
+
.generate-btn:hover {
|
| 66 |
+
opacity: 0.9;
|
| 67 |
+
}
|
| 68 |
+
.generate-btn:disabled {
|
| 69 |
+
opacity: 0.5;
|
| 70 |
+
cursor: not-allowed;
|
| 71 |
+
}
|
| 72 |
+
.loading {
|
| 73 |
+
display: inline-block;
|
| 74 |
+
width: 1rem;
|
| 75 |
+
height: 1rem;
|
| 76 |
+
border: 2px solid rgba(255,255,255,.3);
|
| 77 |
+
border-radius: 50%;
|
| 78 |
+
border-top-color: white;
|
| 79 |
+
animation: spin 1s ease-in-out infinite;
|
| 80 |
+
}
|
| 81 |
+
@keyframes spin {
|
| 82 |
+
to { transform: rotate(360deg); }
|
| 83 |
+
}
|
| 84 |
+
.error {
|
| 85 |
+
color: #ef4444;
|
| 86 |
+
font-size: 0.875rem;
|
| 87 |
+
margin-top: 0.5rem;
|
| 88 |
+
}
|
| 89 |
+
</style>
|
| 90 |
+
<div class="ai-section">
|
| 91 |
+
<h3 class="text-lg font-semibold mb-3">✨ AI Enhanced Slideshow</h3>
|
| 92 |
+
<div class="api-input">
|
| 93 |
+
<input type="password" id="api-key-input" placeholder="Enter OpenRouter API key" value="${this.apiKey}">
|
| 94 |
+
<button id="save-key">Save</button>
|
| 95 |
+
</div>
|
| 96 |
+
<button id="generate-ai" class="generate-btn">
|
| 97 |
+
<i data-feather="sparkles"></i>
|
| 98 |
+
Generate AI Slideshow
|
| 99 |
+
</button>
|
| 100 |
+
<div id="error" class="error"></div>
|
| 101 |
+
</div>
|
| 102 |
+
`;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
addEventListeners() {
|
| 106 |
+
const saveBtn = this.shadowRoot.getElementById('save-key');
|
| 107 |
+
const generateBtn = this.shadowRoot.getElementById('generate-ai');
|
| 108 |
+
const apiInput = this.shadowRoot.getElementById('api-key-input');
|
| 109 |
+
const errorDiv = this.shadowRoot.getElementById('error');
|
| 110 |
+
|
| 111 |
+
saveBtn.addEventListener('click', () => {
|
| 112 |
+
this.apiKey = apiInput.value.trim();
|
| 113 |
+
localStorage.setItem('openrouter-key', this.apiKey);
|
| 114 |
+
saveBtn.textContent = 'Saved!';
|
| 115 |
+
setTimeout(() => saveBtn.textContent = 'Save', 1000);
|
| 116 |
+
});
|
| 117 |
+
|
| 118 |
+
generateBtn.addEventListener('click', async () => {
|
| 119 |
+
if (!this.apiKey) {
|
| 120 |
+
errorDiv.textContent = 'Please enter your OpenRouter API key first';
|
| 121 |
+
return;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
const markdown = document.getElementById('markdown-input').value.trim();
|
| 125 |
+
if (!markdown) {
|
| 126 |
+
errorDiv.textContent = 'Please enter some markdown content';
|
| 127 |
+
return;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
generateBtn.disabled = true;
|
| 131 |
+
generateBtn.innerHTML = '<div class="loading"></div> Generating...';
|
| 132 |
+
errorDiv.textContent = '';
|
| 133 |
+
|
| 134 |
+
try {
|
| 135 |
+
const html = await this.generateAISlideshow(markdown);
|
| 136 |
+
document.getElementById('slides-preview').innerHTML = html;
|
| 137 |
+
document.getElementById('export-btn').disabled = false;
|
| 138 |
+
} catch (error) {
|
| 139 |
+
errorDiv.textContent = error.message;
|
| 140 |
+
} finally {
|
| 141 |
+
generateBtn.disabled = false;
|
| 142 |
+
generateBtn.innerHTML = '<i data-feather="sparkles"></i> Generate AI Slideshow';
|
| 143 |
+
}
|
| 144 |
+
});
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
async generateAISlideshow(markdown) {
|
| 148 |
+
const slides = markdown.split(/\n---\n/);
|
| 149 |
+
let html = '';
|
| 150 |
+
|
| 151 |
+
for (let i = 0; i < slides.length; i++) {
|
| 152 |
+
const slide = slides[i];
|
| 153 |
+
|
| 154 |
+
// Search for relevant images
|
| 155 |
+
const searchQuery = this.extractKeywords(slide);
|
| 156 |
+
const images = await this.searchImages(searchQuery);
|
| 157 |
+
|
| 158 |
+
// Generate AI-enhanced slide
|
| 159 |
+
const prompt = `
|
| 160 |
+
Create a beautiful, modern slide HTML based on this markdown content:
|
| 161 |
+
${slide}
|
| 162 |
+
|
| 163 |
+
Use Tailwind CSS classes for styling. Include:
|
| 164 |
+
1. A compelling title
|
| 165 |
+
2. Clean, readable typography
|
| 166 |
+
3. One or more relevant images from this list: ${JSON.stringify(images.slice(0, 3))}
|
| 167 |
+
4. Beautiful color scheme and layout
|
| 168 |
+
5. Subtle animations and transitions
|
| 169 |
+
|
| 170 |
+
Return only the HTML, no explanations.
|
| 171 |
+
`;
|
| 172 |
+
|
| 173 |
+
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
| 174 |
+
method: 'POST',
|
| 175 |
+
headers: {
|
| 176 |
+
'Authorization': `Bearer ${this.apiKey}`,
|
| 177 |
+
'Content-Type': 'application/json',
|
| 178 |
+
'HTTP-Referer': window.location.href,
|
| 179 |
+
'X-Title': 'Markdown Slides'
|
| 180 |
+
},
|
| 181 |
+
body: JSON.stringify({
|
| 182 |
+
model: 'anthropic/claude-3-haiku-20240307',
|
| 183 |
+
messages: [
|
| 184 |
+
{
|
| 185 |
+
role: 'user',
|
| 186 |
+
content: prompt
|
| 187 |
+
}
|
| 188 |
+
],
|
| 189 |
+
max_tokens: 2000,
|
| 190 |
+
temperature: 0.7
|
| 191 |
+
})
|
| 192 |
+
});
|
| 193 |
+
|
| 194 |
+
if (!response.ok) {
|
| 195 |
+
throw new Error('Failed to generate slide content');
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
const data = await response.json();
|
| 199 |
+
const slideHtml = data.choices[0].message.content;
|
| 200 |
+
|
| 201 |
+
html += `
|
| 202 |
+
<div class="ai-slide mb-6">
|
| 203 |
+
${slideHtml}
|
| 204 |
+
</div>
|
| 205 |
+
`;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
return html;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
extractKeywords(text) {
|
| 212 |
+
// Simple keyword extraction
|
| 213 |
+
const words = text.toLowerCase()
|
| 214 |
+
.replace(/[^\w\s]/g, '')
|
| 215 |
+
.split(/\s+/)
|
| 216 |
+
.filter(word => word.length > 3 && !['this', 'that', 'with', 'from', 'have', 'will', 'your', 'more'].includes(word));
|
| 217 |
+
|
| 218 |
+
const count = {};
|
| 219 |
+
words.forEach(word => count[word] = (count[word] || 0) + 1);
|
| 220 |
+
|
| 221 |
+
return Object.entries(count)
|
| 222 |
+
.sort(([,a], [,b]) => b - a)
|
| 223 |
+
.slice(0, 3)
|
| 224 |
+
.map(([word]) => word)
|
| 225 |
+
.join(' ');
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
async searchImages(query) {
|
| 229 |
+
try {
|
| 230 |
+
const response = await fetch(`https://searchx.euan.live/search?q=${encodeURIComponent(query)}&safesearch=true&format=json`);
|
| 231 |
+
const data = await response.json();
|
| 232 |
+
|
| 233 |
+
return data.results
|
| 234 |
+
.filter(result => result.url && result.url.match(/\.(jpg|jpeg|png|gif|webp)$/i))
|
| 235 |
+
.slice(0, 5)
|
| 236 |
+
.map(result => result.url);
|
| 237 |
+
} catch (error) {
|
| 238 |
+
console.error('Image search failed:', error);
|
| 239 |
+
return [];
|
| 240 |
+
}
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
customElements.define('ai-slideshow', AISlideshow);
|
index.html
CHANGED
|
@@ -10,6 +10,7 @@
|
|
| 10 |
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 11 |
<script src="https://cdn.jsdelivr.net/npm/pptxgenjs@3.7.0/dist/pptxgen.bundle.js"></script>
|
| 12 |
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
|
|
| 13 |
</head>
|
| 14 |
<body class="bg-gray-50 min-h-screen">
|
| 15 |
<custom-navbar></custom-navbar>
|
|
|
|
| 10 |
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 11 |
<script src="https://cdn.jsdelivr.net/npm/pptxgenjs@3.7.0/dist/pptxgen.bundle.js"></script>
|
| 12 |
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 13 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 14 |
</head>
|
| 15 |
<body class="bg-gray-50 min-h-screen">
|
| 16 |
<custom-navbar></custom-navbar>
|