Spaces:
Running
Running
| // BlogWizard Pro - Main JavaScript | |
| class BlogGenerator { | |
| constructor() { | |
| this.currentBlog = ''; | |
| this.currentStep = 1; | |
| this.totalSteps = 4; | |
| this.viewMode = 'editor'; // 'editor' or 'preview' | |
| this.apiKeys = {}; | |
| this.outlineStructure = []; | |
| this.initializeEventListeners(); | |
| this.loadSettings(); | |
| this.initDarkMode(); | |
| this.initMultiStepForm(); | |
| this.loadApiKeys(); | |
| } | |
| initializeEventListeners() { | |
| // Form submission | |
| document.getElementById('blogForm').addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| this.generateBlog(); | |
| }); | |
| // Blog output changes | |
| document.getElementById('blogOutput').addEventListener('input', (e) => { | |
| this.updateWordCount(); | |
| this.currentBlog = e.target.value; | |
| this.updatePreview(); | |
| }); | |
| // Model change | |
| document.getElementById('model').addEventListener('change', (e) => { | |
| localStorage.setItem('selectedModel', e.target.value); | |
| }); | |
| } | |
| initMultiStepForm() { | |
| this.updateStepDisplay(); | |
| } | |
| initDarkMode() { | |
| const html = document.documentElement; | |
| // Check for saved preference | |
| const darkMode = localStorage.getItem('darkMode') === 'true'; | |
| if (darkMode) { | |
| html.classList.add('dark'); | |
| } | |
| } | |
| loadSettings() { | |
| const savedModel = localStorage.getItem('selectedModel'); | |
| const savedProvider = localStorage.getItem('aiProvider'); | |
| if (savedModel) { | |
| document.getElementById('model').value = savedModel; | |
| } | |
| // Update placeholder based on selected model | |
| this.updateApiKeyPlaceholder(document.getElementById('model').value); | |
| // Add model change listener to update placeholder | |
| document.getElementById('model').addEventListener('change', (e) => { | |
| this.updateApiKeyPlaceholder(e.target.value); | |
| this.autoSwitchApiKey(e.target.value); | |
| }); | |
| } | |
| loadApiKeys() { | |
| const savedKeys = localStorage.getItem('apiKeys'); | |
| if (savedKeys) { | |
| this.apiKeys = JSON.parse(savedKeys); | |
| this.displaySavedKeys(); | |
| } | |
| } | |
| displaySavedKeys() { | |
| const savedKeysDiv = document.getElementById('savedKeys'); | |
| if (Object.keys(this.apiKeys).length === 0) { | |
| savedKeysDiv.innerHTML = '<p class="text-sm text-gray-500 dark:text-gray-400">No saved API keys yet</p>'; | |
| return; | |
| } | |
| savedKeysDiv.innerHTML = Object.entries(this.apiKeys).map(([model, key]) => ` | |
| <div class="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700 rounded"> | |
| <span class="text-sm text-gray-700 dark:text-gray-300"> | |
| ${model.replace('-', ' ').toUpperCase()}: ${key.substring(0, 10)}... | |
| </span> | |
| <button onclick="useSavedKey('${model}')" class="text-primary hover:underline text-sm"> | |
| Use | |
| </button> | |
| </div> | |
| `).join(''); | |
| } | |
| saveApiKeyModel(model, key) { | |
| this.apiKeys[model] = key; | |
| localStorage.setItem('apiKeys', JSON.stringify(this.apiKeys)); | |
| this.displaySavedKeys(); | |
| this.showToast(`API key saved for ${model}`, 'success'); | |
| } | |
| autoSwitchApiKey(model) { | |
| if (this.apiKeys[model]) { | |
| document.getElementById('apiKey').value = this.apiKeys[model]; | |
| this.validateApiKey(this.apiKeys[model]); | |
| } | |
| } | |
| updateStepDisplay() { | |
| // Hide all steps | |
| for (let i = 1; i <= this.totalSteps; i++) { | |
| document.getElementById(`step${i}`).classList.add('hidden'); | |
| document.getElementById(`step${i}Indicator`).classList.remove('active'); | |
| } | |
| // Show current step | |
| document.getElementById(`step${this.currentStep}`).classList.remove('hidden'); | |
| document.getElementById(`step${this.currentStep}Indicator`).classList.add('active'); | |
| // Update navigation buttons | |
| const prevBtn = document.getElementById('prevBtn'); | |
| const nextBtn = document.getElementById('nextBtn'); | |
| const generateBtn = document.getElementById('generateBtn'); | |
| if (this.currentStep === 1) { | |
| prevBtn.classList.add('hidden'); | |
| nextBtn.classList.remove('hidden'); | |
| generateBtn.classList.add('hidden'); | |
| } else if (this.currentStep === this.totalSteps) { | |
| prevBtn.classList.remove('hidden'); | |
| nextBtn.classList.add('hidden'); | |
| generateBtn.classList.remove('hidden'); | |
| this.updateSummary(); | |
| } else { | |
| prevBtn.classList.remove('hidden'); | |
| nextBtn.classList.remove('hidden'); | |
| generateBtn.classList.add('hidden'); | |
| } | |
| } | |
| updateSummary() { | |
| const topic = document.getElementById('topic').value; | |
| const keywords = document.getElementById('keywords').value; | |
| const tone = document.getElementById('tone').value; | |
| const length = document.getElementById('length').value; | |
| const language = document.getElementById('language').value; | |
| const model = document.getElementById('model').value; | |
| const summary = document.getElementById('summary'); | |
| summary.innerHTML = ` | |
| <div class="flex justify-between"> | |
| <span class="font-medium">Topic:</span> | |
| <span>${topic || 'Not specified'}</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="font-medium">Keywords:</span> | |
| <span>${keywords || 'None'}</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="font-medium">Tone:</span> | |
| <span>${tone.charAt(0).toUpperCase() + tone.slice(1)}</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="font-medium">Length:</span> | |
| <span>${length.charAt(0).toUpperCase() + length.slice(1)}</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="font-medium">Language:</span> | |
| <span>${this.getLanguageName(language)}</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="font-medium">Model:</span> | |
| <span>${model.replace('-', ' ').toUpperCase()}</span> | |
| </div> | |
| `; | |
| } | |
| getLanguageName(code) { | |
| const languages = { | |
| 'en': 'English', | |
| 'es': 'Spanish', | |
| 'fr': 'French', | |
| 'de': 'German', | |
| 'zh': 'Chinese' | |
| }; | |
| return languages[code] || code; | |
| } | |
| updateApiKeyPlaceholder(model) { | |
| const apiKeyInput = document.getElementById('apiKey'); | |
| if (model.startsWith('gemini')) { | |
| apiKeyInput.placeholder = 'Enter Gemini API key...'; | |
| } else if (model.startsWith('claude')) { | |
| apiKeyInput.placeholder = 'sk-ant-...'; | |
| } else { | |
| apiKeyInput.placeholder = 'sk-...'; | |
| } | |
| } | |
| async generateBlog() { | |
| const topic = document.getElementById('topic').value; | |
| const keywords = document.getElementById('keywords').value; | |
| const tone = document.getElementById('tone').value; | |
| const length = document.getElementById('length').value; | |
| const language = document.getElementById('language').value; | |
| const apiKey = document.getElementById('apiKey').value; | |
| const model = document.getElementById('model').value; | |
| if (!apiKey) { | |
| this.showToast('Please enter your API key in settings', 'error'); | |
| return; | |
| } | |
| // Show loading state | |
| const outputSection = document.getElementById('outputSection'); | |
| const loadingSpinner = document.getElementById('loadingSpinner'); | |
| const blogOutput = document.getElementById('blogOutput'); | |
| outputSection.classList.remove('hidden'); | |
| outputSection.classList.add('slide-in'); | |
| loadingSpinner.classList.remove('hidden'); | |
| blogOutput.value = ''; | |
| // Create prompt | |
| const prompt = this.createPrompt(topic, keywords, tone, length, language); | |
| try { | |
| // Simulate API call (replace with actual API call) | |
| const generatedContent = await this.callAI(prompt, apiKey, model); | |
| setTimeout(() => { | |
| loadingSpinner.classList.add('hidden'); | |
| blogOutput.value = generatedContent; | |
| this.currentBlog = generatedContent; | |
| this.updateWordCount(); | |
| this.updateTimestamp(); | |
| this.showToast('Blog generated successfully!', 'success'); | |
| }, 2000); | |
| } catch (error) { | |
| loadingSpinner.classList.add('hidden'); | |
| this.showToast('Error generating blog: ' + error.message, 'error'); | |
| } | |
| } | |
| createPrompt(topic, keywords, tone, length, language) { | |
| const wordCounts = { | |
| short: '300-500', | |
| medium: '500-800', | |
| long: '800-1200' | |
| }; | |
| return `Write a blog post about "${topic}". | |
| Keywords to include: ${keywords}. | |
| Tone: ${tone}. | |
| Length: ${wordCounts[length]} words. | |
| Language: ${language}. | |
| Make it engaging and well-structured with an introduction, body paragraphs, and conclusion. | |
| Include a catchy title.`; | |
| } | |
| async callAI(prompt, apiKey, model) { | |
| // Check if it's a Gemini model | |
| if (model.startsWith('gemini')) { | |
| return await this.callGemini(prompt, apiKey, model); | |
| } else if (model.startsWith('gpt')) { | |
| return await this.callOpenAI(prompt, apiKey, model); | |
| } else if (model.startsWith('claude')) { | |
| return await this.callClaude(prompt, apiKey, model); | |
| } | |
| // Fallback to Gemini if model not recognized | |
| return await this.callGemini(prompt, apiKey, 'gemini-pro'); | |
| } | |
| async callOpenAI(prompt, apiKey, model) { | |
| try { | |
| const response = await fetch('https://api.openai.com/v1/chat/completions', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${apiKey}` | |
| }, | |
| body: JSON.stringify({ | |
| model: model, | |
| messages: [{ role: 'user', content: prompt }], | |
| max_tokens: 2000, | |
| temperature: 0.7 | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error?.message || `HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| return data.choices[0].message.content; | |
| } catch (error) { | |
| if (error.message.includes('API key')) { | |
| throw new Error('Invalid OpenAI API key. Please check your API key and try again.'); | |
| } else if (error.message.includes('quota')) { | |
| throw new Error('OpenAI API quota exceeded. Please check your usage and billing.'); | |
| } else { | |
| throw new Error(`OpenAI API error: ${error.message}`); | |
| } | |
| } | |
| } | |
| async callClaude(prompt, apiKey, model) { | |
| try { | |
| const response = await fetch('https://api.anthropic.com/v1/messages', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-api-key': apiKey, | |
| 'anthropic-version': '2023-06-01' | |
| }, | |
| body: JSON.stringify({ | |
| model: model, | |
| max_tokens: 2000, | |
| messages: [{ role: 'user', content: prompt }] | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error?.message || `HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| return data.content[0].text; | |
| } catch (error) { | |
| if (error.message.includes('API key')) { | |
| throw new Error('Invalid Claude API key. Please check your API key and try again.'); | |
| } else if (error.message.includes('quota')) { | |
| throw new Error('Claude API quota exceeded. Please check your usage and billing.'); | |
| } else { | |
| throw new Error(`Claude API error: ${error.message}`); | |
| } | |
| } | |
| } | |
| async callGemini(prompt, apiKey, model) { | |
| try { | |
| const response = await fetch(`https://generativelanguage.googleapis.com/v1/models/${model}:generateContent?key=${apiKey}`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| contents: [{ | |
| parts: [{ | |
| text: prompt | |
| }] | |
| }] | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error?.message || `HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| return data.candidates[0].content.parts[0].text; | |
| } catch (error) { | |
| if (error.message.includes('API key')) { | |
| throw new Error('Invalid Gemini API key. Please check your API key and try again.'); | |
| } else if (error.message.includes('quota')) { | |
| throw new Error('Gemini API quota exceeded. Please check your usage and billing.'); | |
| } else { | |
| throw new Error(`Gemini API error: ${error.message}`); | |
| } | |
| } | |
| } | |
| updateWordCount() { | |
| const blogOutput = document.getElementById('blogOutput'); | |
| const text = blogOutput.value; | |
| const wordCount = text.trim() ? text.trim().split(/\s+/).length : 0; | |
| const readingTime = Math.max(1, Math.ceil(wordCount / 200)); // Average reading speed: 200 words/min | |
| document.getElementById('wordCount').textContent = `${wordCount} words`; | |
| document.getElementById('readingTime').textContent = `${readingTime} min read`; | |
| // Calculate readability score (simplified Flesch Reading Ease) | |
| if (text) { | |
| const sentences = text.split(/[.!?]+/).length; | |
| const avgWordsPerSentence = wordCount / sentences; | |
| const avgSyllables = this.countSyllables(text) / wordCount; | |
| const score = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * avgSyllables); | |
| let readability = Math.round(score); | |
| if (readability > 100) readability = 100; | |
| if (readability < 0) readability = 0; | |
| let level = 'Very Difficult'; | |
| if (readability >= 90) level = 'Very Easy'; | |
| else if (readability >= 80) level = 'Easy'; | |
| else if (readability >= 70) level = 'Fairly Easy'; | |
| else if (readability >= 60) level = 'Standard'; | |
| else if (readability >= 50) level = 'Fairly Difficult'; | |
| else if (readability >= 30) level = 'Difficult'; | |
| document.getElementById('readabilityScore').textContent = `Score: ${readability} (${level})`; | |
| } else { | |
| document.getElementById('readabilityScore').textContent = 'Score: --'; | |
| } | |
| } | |
| countSyllables(text) { | |
| const words = text.toLowerCase().split(/\s+/); | |
| let syllableCount = 0; | |
| words.forEach(word => { | |
| word = word.replace(/[^a-z]/g, ''); | |
| if (word.length === 0) return; | |
| let count = 0; | |
| let prevCharWasVowel = false; | |
| const vowels = 'aeiouy'; | |
| for (let i = 0; i < word.length; i++) { | |
| const isVowel = vowels.includes(word[i]); | |
| if (isVowel && !prevCharWasVowel) { | |
| count++; | |
| } | |
| prevCharWasVowel = isVowel; | |
| } | |
| if (word.endsWith('e')) count--; | |
| if (count === 0) count = 1; | |
| syllableCount += count; | |
| }); | |
| return syllableCount; | |
| } | |
| updateTimestamp() { | |
| const now = new Date(); | |
| const timestamp = now.toLocaleString(); | |
| document.getElementById('timestamp').textContent = `Generated on ${timestamp}`; | |
| } | |
| regenerateBlog() { | |
| this.generateBlog(); | |
| } | |
| copyBlog() { | |
| const blogOutput = document.getElementById('blogOutput'); | |
| blogOutput.select(); | |
| document.execCommand('copy'); | |
| this.showToast('Blog copied to clipboard!', 'success'); | |
| } | |
| downloadBlog(format) { | |
| const blogOutput = document.getElementById('blogOutput'); | |
| const topic = document.getElementById('topic').value || 'blog'; | |
| const timestamp = new Date().toISOString().slice(0, 10); | |
| const filename = `${topic}-${timestamp}.${format}`; | |
| let content = blogOutput.value; | |
| if (format === 'md' && !content.startsWith('#')) { | |
| // Add markdown title if not present | |
| content = `# ${topic}\n\n${content}`; | |
| } | |
| const blob = new Blob([content], { type: format === 'md' ? 'text/markdown' : 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| this.showToast(`Blog downloaded as ${format.toUpperCase()}!`, 'success'); | |
| } | |
| formatText(formatType) { | |
| const textarea = document.getElementById('blogOutput'); | |
| const start = textarea.selectionStart; | |
| const end = textarea.selectionEnd; | |
| const selectedText = textarea.value.substring(start, end); | |
| let formattedText = ''; | |
| switch(formatType) { | |
| case 'bold': | |
| formattedText = `**${selectedText}**`; | |
| break; | |
| case 'italic': | |
| formattedText = `*${selectedText}*`; | |
| break; | |
| case 'heading': | |
| formattedText = selectedText ? `## ${selectedText}` : '## '; | |
| break; | |
| case 'list': | |
| formattedText = selectedText ? `- ${selectedText}` : '- '; | |
| break; | |
| case 'link': | |
| const url = prompt('Enter URL:', 'https://'); | |
| formattedText = url ? `[${selectedText || 'link text'}](${url})` : selectedText; | |
| break; | |
| case 'quote': | |
| formattedText = selectedText ? `> ${selectedText}` : '> '; | |
| break; | |
| case 'code': | |
| formattedText = selectedText ? `\`${selectedText}\`` : '``'; | |
| break; | |
| default: | |
| formattedText = selectedText; | |
| } | |
| textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end); | |
| textarea.selectionStart = textarea.selectionEnd = start + formattedText.length; | |
| textarea.focus(); | |
| this.currentBlog = textarea.value; | |
| this.updateWordCount(); | |
| this.updatePreview(); | |
| } | |
| toggleViewMode() { | |
| const editorView = document.getElementById('editorView'); | |
| const previewView = document.getElementById('previewView'); | |
| const viewModeText = document.getElementById('viewModeText'); | |
| const editorToolbar = document.getElementById('editorToolbar'); | |
| if (this.viewMode === 'editor') { | |
| this.viewMode = 'preview'; | |
| editorView.classList.add('hidden'); | |
| previewView.classList.remove('hidden'); | |
| editorToolbar.classList.add('hidden'); | |
| viewModeText.textContent = 'Edit'; | |
| this.updatePreview(); | |
| } else { | |
| this.viewMode = 'editor'; | |
| editorView.classList.remove('hidden'); | |
| previewView.classList.add('hidden'); | |
| editorToolbar.classList.remove('hidden'); | |
| viewModeText.textContent = 'Preview'; | |
| } | |
| } | |
| updatePreview() { | |
| const content = document.getElementById('blogOutput').value; | |
| const preview = document.getElementById('blogPreview'); | |
| // Simple markdown to HTML conversion | |
| let html = content | |
| .replace(/^## (.+)$/gm, '<h2>$1</h2>') | |
| .replace(/^# (.+)$/gm, '<h1>$1</h1>') | |
| .replace(/^\* (.+)$/gm, '<li>$1</li>') | |
| .replace(/^- (.+)$/gm, '<li>$1</li>') | |
| .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') | |
| .replace(/\*(.+?)\*/g, '<em>$1</em>') | |
| .replace(/`(.+?)`/g, '<code>$1</code>') | |
| .replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>') | |
| .replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>') | |
| .replace(/\n\n/g, '</p><p>') | |
| .replace(/\n/g, '<br>'); | |
| // Wrap in paragraphs if no HTML tags exist | |
| if (!html.includes('<')) { | |
| html = `<p>${html}</p>`; | |
| } | |
| preview.innerHTML = html; | |
| } | |
| showTooltip(field) { | |
| const tooltips = { | |
| 'topic': 'Enter the main topic or title for your blog post. Be specific for better results.', | |
| 'keywords': 'Add relevant keywords to include in your blog. Separate them with commas.', | |
| 'tone': 'Choose the writing style: Professional for formal content, Casual for friendly tone, Humorous for entertaining content, Inspirational for motivation, or Technical for detailed explanations.', | |
| 'length': 'Select the approximate word count: Short (300-500 words), Medium (500-800 words), or Long (800-1200 words).', | |
| 'language': 'Choose the language for your blog post.', | |
| 'model': 'Select the AI model to generate your content. Each has unique strengths and capabilities.', | |
| 'apiKey': 'Enter your API key for the selected AI model. Your key is stored locally and never shared.' | |
| }; | |
| const tooltip = document.getElementById('tooltip'); | |
| const tooltipContent = document.getElementById('tooltipContent'); | |
| const button = event.target.closest('button'); | |
| const rect = button.getBoundingClientRect(); | |
| tooltipContent.textContent = tooltips[field]; | |
| tooltip.style.top = `${rect.bottom + 10}px`; | |
| tooltip.style.left = `${rect.left + rect.width / 2}px`; | |
| tooltip.style.transform = 'translateX(-50%)'; | |
| tooltip.classList.remove('hidden'); | |
| // Hide tooltip when clicking outside | |
| setTimeout(() => { | |
| document.addEventListener('click', function hideTooltip(e) { | |
| if (!tooltip.contains(e.target) && !button.contains(e.target)) { | |
| tooltip.classList.add('hidden'); | |
| document.removeEventListener('click', hideTooltip); | |
| } | |
| }); | |
| }, 100); | |
| } | |
| validateCurrentStep() { | |
| switch(this.currentStep) { | |
| case 1: | |
| const topic = document.getElementById('topic').value.trim(); | |
| if (!topic) { | |
| this.showToast('Please enter a blog topic', 'error'); | |
| return false; | |
| } | |
| break; | |
| case 3: | |
| const apiKey = document.getElementById('apiKey').value.trim(); | |
| if (!apiKey) { | |
| this.showToast('Please enter your API key', 'error'); | |
| return false; | |
| } | |
| break; | |
| } | |
| return true; | |
| } | |
| showToast(message, type = 'info') { | |
| const toast = document.createElement('div'); | |
| toast.className = 'toast'; | |
| toast.innerHTML = ` | |
| <div class="flex items-center"> | |
| <i data-feather="${type === 'success' ? 'check-circle' : type === 'error' ? 'x-circle' : 'info'}" | |
| class="mr-2 ${type === 'success' ? 'text-green-500' : type === 'error' ? 'text-red-500' : 'text-blue-500'}"></i> | |
| <span>${message}</span> | |
| </div> | |
| `; | |
| document.body.appendChild(toast); | |
| feather.replace(); | |
| setTimeout(() => toast.classList.add('show'), 100); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| async suggestStructure() { | |
| const topic = document.getElementById('topic').value; | |
| const keywords = document.getElementById('keywords').value; | |
| const length = document.getElementById('length').value; | |
| if (!topic) { | |
| this.showToast('Please enter a topic first', 'error'); | |
| return; | |
| } | |
| const sections = { | |
| short: ['Introduction', 'Main Point', 'Conclusion'], | |
| medium: ['Introduction', 'Point 1', 'Point 2', 'Point 3', 'Conclusion'], | |
| long: ['Introduction', 'Background', 'Key Concepts', 'Detailed Analysis', 'Case Studies', 'Best Practices', 'Future Outlook', 'Conclusion'] | |
| }; | |
| this.outlineStructure = sections[length] || sections.medium; | |
| const outlineContent = document.getElementById('outlineContent'); | |
| outlineContent.innerHTML = this.outlineStructure.map((section, index) => ` | |
| <div class="flex items-center gap-2 p-2 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-600"> | |
| <span class="text-gray-400">${index + 1}.</span> | |
| <input type="text" value="${section}" | |
| class="flex-1 px-2 py-1 border-0 bg-transparent text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-1 focus:ring-primary" | |
| onchange="updateOutlineSection(${index}, this.value)"> | |
| <button onclick="removeOutlineSection(${index})" class="text-red-500 hover:text-red-700"> | |
| <i data-feather="trash-2" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| `).join(''); | |
| outlineContent.classList.remove('hidden'); | |
| feather.replace(); | |
| this.showToast('Structure suggested! You can edit sections above.', 'success'); | |
| } | |
| updateOutlineSection(index, value) { | |
| this.outlineStructure[index] = value; | |
| } | |
| removeOutlineSection(index) { | |
| this.outlineStructure.splice(index, 1); | |
| this.suggestStructure(); // Refresh the outline | |
| } | |
| } | |
| // Initialize the app | |
| let blogGenerator; | |
| document.addEventListener('DOMContentLoaded', () => { | |
| blogGenerator = new BlogGenerator(); | |
| }); | |
| // Global functions for onclick handlers | |
| function saveApiKey() { | |
| const apiKey = document.getElementById('apiKey').value; | |
| const model = document.getElementById('model').value; | |
| if (apiKey && model) { | |
| blogGenerator.saveApiKeyModel(model, apiKey); | |
| } else { | |
| blogGenerator.showToast('Please enter an API key', 'error'); | |
| } | |
| } | |
| function useSavedKey(model) { | |
| document.getElementById('model').value = model; | |
| document.getElementById('apiKey').value = blogGenerator.apiKeys[model]; | |
| blogGenerator.validateApiKey(blogGenerator.apiKeys[model]); | |
| blogGenerator.showToast('API key loaded', 'success'); | |
| } | |
| function validateApiKey(key) { | |
| const statusSpan = document.getElementById('keyStatus'); | |
| if (!key) { | |
| statusSpan.innerHTML = ''; | |
| return; | |
| } | |
| let isValid = false; | |
| let provider = ''; | |
| if (key.startsWith('sk-ant-')) { | |
| isValid = key.length > 20; | |
| provider = 'Claude'; | |
| } else if (key.startsWith('sk-')) { | |
| isValid = key.length > 20; | |
| provider = 'OpenAI'; | |
| } else if (key.length > 20) { | |
| isValid = true; | |
| provider = 'Gemini'; | |
| } | |
| if (isValid) { | |
| statusSpan.innerHTML = `<span class="text-green-500">✓ Valid ${provider} key</span>`; | |
| } else { | |
| statusSpan.innerHTML = `<span class="text-red-500">✗ Invalid format</span>`; | |
| } | |
| } | |
| function handleToneChange(value) { | |
| const customToneField = document.getElementById('customToneField'); | |
| if (value === 'custom') { | |
| customToneField.classList.remove('hidden'); | |
| } else { | |
| customToneField.classList.add('hidden'); | |
| } | |
| } | |
| function handleImageUpload(event) { | |
| const file = event.target.files[0]; | |
| if (file && file.type.startsWith('image/')) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const previewDiv = document.getElementById('uploadedImagePreview'); | |
| const previewImg = document.getElementById('previewImg'); | |
| previewImg.src = e.target.result; | |
| previewDiv.classList.remove('hidden'); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| } | |
| async function suggestStockImages() { | |
| const topic = document.getElementById('topic').value; | |
| const keywords = document.getElementById('keywords').value; | |
| if (!topic) { | |
| blogGenerator.showToast('Please enter a topic first', 'error'); | |
| return; | |
| } | |
| const suggestionsDiv = document.getElementById('stockImageSuggestions'); | |
| suggestionsDiv.innerHTML = '<div class="col-span-full text-center">Loading image suggestions...</div>'; | |
| suggestionsDiv.classList.remove('hidden'); | |
| // Generate 4 stock image suggestions using static.photos | |
| const imageUrls = [ | |
| `http://static.photos/technology/320x240/1`, | |
| `http://static.photos/office/320x240/2`, | |
| `http://static.photos/workspace/320x240/3`, | |
| `http://static.photos/abstract/320x240/4` | |
| ]; | |
| setTimeout(() => { | |
| suggestionsDiv.innerHTML = imageUrls.map((url, index) => ` | |
| <div class="relative group cursor-pointer" onclick="selectStockImage('${url}')"> | |
| <img src="${url}" alt="Suggestion ${index + 1}" class="w-full h-32 object-cover rounded-lg"> | |
| <div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-all duration-200 rounded-lg flex items-center justify-center"> | |
| <i data-feather="check-circle" class="w-8 h-8 text-white opacity-0 group-hover:opacity-100 transition-all duration-200"></i> | |
| </div> | |
| </div> | |
| `).join(''); | |
| feather.replace(); | |
| }, 1000); | |
| } | |
| function selectStockImage(url) { | |
| document.getElementById('imageUrl').value = url; | |
| blogGenerator.showToast('Image selected!', 'success'); | |
| } | |
| function showApiGuide() { | |
| document.getElementById('apiGuideModal').classList.remove('hidden'); | |
| feather.replace(); | |
| } | |
| function closeApiGuide() { | |
| document.getElementById('apiGuideModal').classList.add('hidden'); | |
| } | |
| function updateOutlineSection(index, value) { | |
| blogGenerator.updateOutlineSection(index, value); | |
| } | |
| function removeOutlineSection(index) { | |
| blogGenerator.removeOutlineSection(index); | |
| } | |
| function fillTemplate(type) { | |
| const templates = { | |
| marketing: { | |
| topic: 'The Future of AI in Marketing', | |
| keywords: 'AI, marketing, automation, future', | |
| tone: 'informative', | |
| length: 'medium', | |
| language: 'en' | |
| }, | |
| security: { | |
| topic: 'Web Security Best Practices for Developers', | |
| keywords: 'security, web development, best practices', | |
| tone: 'technical', | |
| length: 'long', | |
| language: 'en' | |
| }, | |
| productivity: { | |
| topic: 'Top Productivity Tools for Solopreneurs', | |
| keywords: 'productivity, tools, solopreneur, business', | |
| tone: 'casual', | |
| length: 'short', | |
| language: 'en' | |
| } | |
| }; | |
| const template = templates[type]; | |
| Object.keys(template).forEach(key => { | |
| const element = document.getElementById(key); | |
| if (element) { | |
| element.value = template[key]; | |
| } | |
| }); | |
| blogGenerator.showToast('Template applied!', 'success'); | |
| } | |
| function regenerateBlog() { | |
| blogGenerator.regenerateBlog(); | |
| } | |
| function copyBlog() { | |
| blogGenerator.copyBlog(); | |
| } | |
| function downloadBlog(format) { | |
| blogGenerator.downloadBlog(format); | |
| } | |
| function toggleDarkMode() { | |
| const html = document.documentElement; | |
| const darkMode = !html.classList.contains('dark'); | |
| if (darkMode) { | |
| html.classList.add('dark'); | |
| } else { | |
| html.classList.remove('dark'); | |
| } | |
| localStorage.setItem('darkMode', darkMode); | |
| } | |
| function changeStep(direction) { | |
| blogGenerator.currentStep += direction; | |
| // Validate current step before moving forward | |
| if (direction > 0) { | |
| if (!blogGenerator.validateCurrentStep()) { | |
| return; | |
| } | |
| } | |
| blogGenerator.currentStep = Math.max(1, Math.min(blogGenerator.totalSteps, blogGenerator.currentStep)); | |
| blogGenerator.updateStepDisplay(); | |
| } | |
| function openHelpModal() { | |
| document.getElementById('helpModal').classList.remove('hidden'); | |
| feather.replace(); | |
| } | |
| function closeHelpModal() { | |
| document.getElementById('helpModal').classList.add('hidden'); | |
| } | |
| function showTooltip(field) { | |
| blogGenerator.showTooltip(field); | |
| } | |
| function toggleViewMode() { | |
| blogGenerator.toggleViewMode(); | |
| } | |
| function formatText(formatType) { | |
| blogGenerator.formatText(formatType); | |
| } | |