Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Content Engine - Social Media SEO</title> | |
| <style> | |
| *{margin:0;padding:0;box-sizing:border-box} | |
| body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f0f2f5;color:#1a1a2e;min-height:100vh} | |
| .container{max-width:960px;margin:0 auto;padding:20px} | |
| header{text-align:center;padding:30px 0 8px} | |
| header h1{font-size:26px;font-weight:800;background:linear-gradient(135deg,#4361ee,#7209b7);-webkit-background-clip:text;-webkit-text-fill-color:transparent} | |
| header p{font-size:14px;color:#666;margin-top:4px} | |
| .input-card{background:#fff;border-radius:14px;padding:20px;box-shadow:0 1px 4px rgba(0,0,0,0.07);margin-bottom:20px} | |
| .input-row{display:flex;gap:10px;flex-wrap:wrap} | |
| .input-row input{flex:1;min-width:200px;padding:12px 16px;border:1px solid #ddd;border-radius:8px;font-size:15px;outline:none;transition:border .2s} | |
| .input-row input:focus{border-color:#4361ee} | |
| .btn-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px;align-items:center} | |
| .platform-checks{display:flex;gap:8px;flex-wrap:wrap;flex:1} | |
| .platform-check{display:flex;align-items:center;gap:5px;padding:7px 12px;border:1px solid #ddd;border-radius:8px;cursor:pointer;font-size:13px;transition:all .2s;user-select:none;background:#fff} | |
| .platform-check:hover{border-color:#4361ee} | |
| .platform-check.checked{background:#4361ee;color:#fff;border-color:#4361ee} | |
| .platform-check input{display:none} | |
| .action-btns{display:flex;gap:8px;flex-wrap:wrap} | |
| .analyze-btn{padding:10px 20px;background:#4361ee;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;transition:background .2s;white-space:nowrap} | |
| .analyze-btn:hover{background:#3651d4} | |
| .analyze-btn:disabled{opacity:0.5;cursor:not-allowed} | |
| .analyze-btn.green{background:#06d6a0} | |
| .analyze-btn.green:hover{background:#05bf8e} | |
| .results{margin-top:10px} | |
| .platform-tabs{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:16px} | |
| .platform-tab{padding:8px 16px;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;background:#fff;border:1px solid #ddd;transition:all .2s} | |
| .platform-tab:hover{background:#f0f2ff} | |
| .platform-tab.active{background:#4361ee;color:#fff;border-color:#4361ee} | |
| .platform-tab .badge{display:inline-block;margin-left:6px;background:rgba(255,255,255,0.3);padding:0 6px;border-radius:4px;font-size:11px} | |
| .platform-tab.active .badge{background:rgba(255,255,255,0.25)} | |
| .platform-block{display:none} | |
| .platform-block.active{display:block} | |
| .platform-heading{font-size:18px;font-weight:700;margin-bottom:12px;padding-bottom:8px;border-bottom:2px solid #4361ee} | |
| .section{margin-bottom:14px} | |
| .section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px} | |
| .section-title{font-size:14px;font-weight:600;color:#1a1a2e} | |
| .copy-all-btn{padding:4px 12px;border:1px solid #4361ee;border-radius:6px;background:transparent;color:#4361ee;cursor:pointer;font-size:11px;font-weight:500;transition:all .2s} | |
| .copy-all-btn:hover{background:#4361ee;color:#fff} | |
| .section-card{background:#fff;border-radius:10px;padding:14px;box-shadow:0 1px 3px rgba(0,0,0,0.05)} | |
| .item-row{display:flex;justify-content:space-between;align-items:center;padding:7px 0;border-bottom:1px solid #f0f0f0;gap:10px} | |
| .item-row:last-child{border-bottom:none} | |
| .item-text{flex:1;font-size:14px;line-height:1.4;word-break:break-word} | |
| .copy-btn{padding:4px 10px;border:1px solid #ddd;border-radius:5px;background:#fff;cursor:pointer;font-size:11px;color:#666;transition:all .2s;white-space:nowrap;flex-shrink:0} | |
| .copy-btn:hover{border-color:#4361ee;color:#4361ee} | |
| .copy-btn.copied,.copy-all-btn.copied{background:#06d6a0;border-color:#06d6a0;color:#fff} | |
| .single-line{font-size:14px;line-height:1.6;padding:4px 0} | |
| .error-msg{background:#fff0f0;color:#d32f2f;padding:14px 18px;border-radius:8px;font-size:14px;margin-bottom:10px} | |
| .loading{text-align:center;padding:40px 0;color:#888} | |
| .spinner{width:32px;height:32px;border:3px solid #e0e0e0;border-top-color:#4361ee;border-radius:50%;animation:spin .6s linear infinite;margin:0 auto 10px} | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| .hidden{display:none} | |
| footer{text-align:center;padding:24px;font-size:12px;color:#aaa} | |
| @media(max-width:640px){.btn-row{flex-direction:column}.action-btns{width:100%}.action-btns .analyze-btn{flex:1}} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>Content Engine</h1> | |
| <p>One topic. Every platform. Instantly.</p> | |
| </header> | |
| <div class="input-card"> | |
| <div class="input-row"> | |
| <input type="text" id="queryInput" placeholder="Enter your topic, keyword, or content..." onkeydown="if(event.key==='Enter')generateAll()"> | |
| </div> | |
| <div class="btn-row"> | |
| <div class="platform-checks" id="platformChecks"></div> | |
| <div class="action-btns"> | |
| <button class="analyze-btn" id="analyzeBtn" onclick="analyzeSingle()">Analyze</button> | |
| <button class="analyze-btn green" id="batchBtn" onclick="generateAll()">Generate All Selected</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="results" class="results hidden"></div> | |
| <footer>Content Engine — Powered by AI</footer> | |
| </div> | |
| <script> | |
| const PLATFORMS = [ | |
| {id:'youtube',label:'YouTube',icon:'🎬'}, | |
| {id:'instagram',label:'Instagram',icon:'📸'}, | |
| {id:'linkedin',label:'LinkedIn',icon:'💼'}, | |
| {id:'facebook',label:'Facebook',icon:'👍'}, | |
| {id:'twitter',label:'X / Twitter',icon:'🐦'}, | |
| {id:'tiktok',label:'TikTok',icon:'🎵'}, | |
| {id:'pinterest',label:'Pinterest',icon:'📌'}, | |
| {id:'grammar',label:'Grammar',icon:'✏️'}, | |
| ]; | |
| let checked = new Set(['youtube']); | |
| let batchData = {}; | |
| let currentTab = null; | |
| function init() { | |
| const container = document.getElementById('platformChecks'); | |
| PLATFORMS.forEach(p => { | |
| const el = document.createElement('label'); | |
| el.className = 'platform-check' + (checked.has(p.id) ? ' checked' : ''); | |
| el.innerHTML = `<input type="checkbox" ${checked.has(p.id)?'checked':''}>${p.icon} ${p.label}`; | |
| el.addEventListener('click', (e) => { | |
| if (e.target.tagName !== 'INPUT') { | |
| const cb = el.querySelector('input'); | |
| cb.checked = !cb.checked; | |
| } | |
| togglePlatform(p.id, el); | |
| }); | |
| el.querySelector('input').addEventListener('change', () => togglePlatform(p.id, el)); | |
| container.appendChild(el); | |
| }); | |
| document.getElementById('queryInput').focus(); | |
| } | |
| function togglePlatform(id, el) { | |
| const cb = el.querySelector('input'); | |
| if (cb.checked) checked.add(id); else checked.delete(id); | |
| el.classList.toggle('checked', cb.checked); | |
| } | |
| function getSelected() { return Array.from(checked); } | |
| async function analyzeSingle() { | |
| const platforms = getSelected(); | |
| if (platforms.length === 0) return; | |
| await doBatch(platforms); | |
| } | |
| async function generateAll() { | |
| const platforms = getSelected(); | |
| if (platforms.length === 0) return; | |
| await doBatch(platforms); | |
| } | |
| async function doBatch(platforms) { | |
| const query = document.getElementById('queryInput').value.trim(); | |
| if (!query) return; | |
| const btn = document.getElementById('batchBtn'); | |
| const sbtn = document.getElementById('analyzeBtn'); | |
| btn.disabled = true; btn.textContent = 'Generating...'; | |
| sbtn.disabled = true; | |
| const results = document.getElementById('results'); | |
| results.classList.remove('hidden'); | |
| results.innerHTML = `<div class="loading"><div class="spinner"></div><div>Generating content for ${platforms.length} platform${platforms.length>1?'s':''}...</div></div>`; | |
| try { | |
| const res = await fetch('/analyze/batch', { | |
| method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({content: query, platforms}) | |
| }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.detail || 'Server error'); | |
| batchData = data.results || {}; | |
| if (data.error) { results.innerHTML = `<div class="error-msg">${data.error}</div>`; return; } | |
| renderBatch(); | |
| } catch (err) { | |
| results.innerHTML = `<div class="error-msg">Error: ${err.message}</div>`; | |
| } finally { | |
| btn.disabled = false; btn.textContent = 'Generate All Selected'; | |
| sbtn.disabled = false; | |
| } | |
| } | |
| function renderBatch() { | |
| const ids = Object.keys(batchData).filter(k => batchData[k] && !batchData[k].error); | |
| if (ids.length === 0) { | |
| document.getElementById('results').innerHTML = `<div class="error-msg">No results returned. Try again.</div>`; | |
| return; | |
| } | |
| currentTab = ids[0]; | |
| let tabs = '', blocks = ''; | |
| ids.forEach(id => { | |
| const p = PLATFORMS.find(x => x.id === id); | |
| const label = p ? `${p.icon} ${p.label}` : id; | |
| const active = id === currentTab ? ' active' : ''; | |
| tabs += `<div class="platform-tab${active}" onclick="switchTab('${id}')">${label} <span class="badge">${getSectionCount(batchData[id])}</span></div>`; | |
| blocks += `<div class="platform-block${active}" id="block-${id}">${renderPlatformContent(id, batchData[id])}</div>`; | |
| }); | |
| document.getElementById('results').innerHTML = `<div class="platform-tabs">${tabs}</div><div>${blocks}</div>`; | |
| } | |
| function switchTab(id) { | |
| currentTab = id; | |
| document.querySelectorAll('.platform-tab').forEach(t => t.classList.remove('active')); | |
| document.querySelectorAll('.platform-block').forEach(b => b.classList.remove('active')); | |
| document.querySelector(`.platform-tab:nth-child(${Object.keys(batchData).indexOf(id)+1})`).classList.add('active'); | |
| document.getElementById('block-'+id).classList.add('active'); | |
| } | |
| function getSectionCount(data) { | |
| let c = 0; | |
| for (const k in data) if (Array.isArray(data[k]) && data[k].length > 0) c++; | |
| return c; | |
| } | |
| function renderPlatformContent(platform, data) { | |
| if (!data || data.error) return `<div class="error-msg">${data.error || 'No data'}</div>`; | |
| let h = ''; | |
| if (platform === 'youtube') { | |
| h += renderSection('Titles', data.video_titles, (t) => `${t.title} (CTR: ${t.expected_ctr})`); | |
| h += renderListSection('Tags', data.tags); | |
| h += renderKV('Description Template', data.description_template); | |
| h += renderListSection('Thumbnail Ideas', data.thumbnail_ideas); | |
| h += renderKV('Best Posting Time', data.best_posting_time); | |
| h += renderListSection('Engagement Tips', data.engagement_strategies); | |
| h += renderKV('Competition Gap', data.competition_gap_analysis); | |
| } else if (platform === 'instagram') { | |
| h += renderSection('Captions', data.captions, (c) => `${c.caption} (Tone: ${c.tone})`); | |
| if (data.hashtag_sets) { | |
| h += renderListSection('Hashtags (Small)', data.hashtag_sets.small); | |
| h += renderListSection('Hashtags (Medium)', data.hashtag_sets.medium); | |
| h += renderListSection('Hashtags (Large)', data.hashtag_sets.large); | |
| } | |
| h += renderKV('Best Posting Time', data.best_posting_time); | |
| if (data.content_ideas) { | |
| h += renderListSection('Reel Ideas', data.content_ideas.reels); | |
| h += renderListSection('Carousel Ideas', data.content_ideas.carousels); | |
| h += renderListSection('Story Ideas', data.content_ideas.stories); | |
| } | |
| h += renderListSection('Growth Strategies', data.growth_strategies); | |
| } else if (platform === 'linkedin') { | |
| h += renderSection('Post Drafts', data.post_drafts, (d) => `${d.headline} — Hook: ${d.hook} | Body: ${d.body}`); | |
| h += renderListSection('Hashtags', data.hashtags); | |
| h += renderListSection('Article Topics', data.article_topics); | |
| h += renderListSection('Leadership Angles', data.thought_leadership_angles); | |
| h += renderKV('Best Posting Time', data.best_posting_time); | |
| h += renderKV('Industry Insights', data.industry_insights); | |
| } else if (platform === 'facebook') { | |
| h += renderSection('Post Ideas', data.post_ideas, (p) => `${p.type}: ${p.content}`); | |
| h += renderListSection('Engagement Hooks', data.engagement_hooks); | |
| h += renderListSection('Hashtags', data.hashtags); | |
| h += renderListSection('Ad Copy Suggestions', data.ad_copy_suggestions); | |
| h += renderKV('Best Posting Time', data.best_posting_time); | |
| h += renderListSection('Page Growth Tips', data.page_growth_tips); | |
| } else if (platform === 'twitter') { | |
| h += renderSection('Tweet Threads', data.tweet_threads, (t) => `${t.theme}: ${t.tweets.join(' | ')}`); | |
| h += renderListSection('Viral Hooks', data.viral_hooks); | |
| h += renderListSection('Hashtags', data.hashtags); | |
| h += renderKV('Best Posting Time', data.best_posting_time); | |
| h += renderListSection('Engagement Tactics', data.engagement_tactics); | |
| } else if (platform === 'tiktok') { | |
| h += renderSection('Video Concepts', data.video_concepts, (v) => `Hook: ${v.hook} | Script: ${v.script_snippet} | Sound: ${v.sound_suggestion}`); | |
| h += renderListSection('Trending Angles', data.trending_angles); | |
| h += renderListSection('Hashtags', data.hashtags); | |
| h += renderKV('Best Posting Time', data.best_posting_time); | |
| h += renderListSection('Viral Strategies', data.viral_strategies); | |
| } else if (platform === 'pinterest') { | |
| h += renderSection('Pin Ideas', data.pin_ideas, (p) => `${p.title} — ${p.description} | Keywords: ${p.keyword_focus}`); | |
| h += renderListSection('Board Organization', data.board_organization); | |
| h += renderListSection('SEO Keywords', data.seo_keywords); | |
| h += renderKV('Best Posting Time', data.best_posting_time); | |
| h += renderListSection('Traffic Strategies', data.traffic_strategies); | |
| } else if (platform === 'grammar') { | |
| h += `<div class="section"><div class="section-header"><span class="section-title">Grammar Score: ${data.grammar_score}/100</span></div><div class="section-card">`; | |
| h += `<div class="item-row"><div class="item-text"><strong>Corrected Text</strong></div><button class="copy-btn" onclick="copyText(this,'${esc(data.corrected_text)}')">Copy</button></div></div></div>`; | |
| h += renderSection('Corrections', data.corrections, (c) => `${c.original} → ${c.corrected} (${c.error_type}: ${c.explanation})`); | |
| h += renderSection('Issues', data.issues, (iss) => `${iss.issue_type} at "${iss.location}": ${iss.suggestion}`); | |
| h += renderKV('Readability', data.readability_score); | |
| h += renderKV('Word Count', data.word_count); | |
| h += renderKV('Sentence Count', data.sentence_count); | |
| h += renderKV('Tone', data.tone); | |
| h += renderListSection('Style Suggestions', data.style_suggestions); | |
| } | |
| return h; | |
| } | |
| function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); } | |
| function copyText(btn, text) { | |
| navigator.clipboard.writeText(text).then(() => { | |
| btn.textContent = 'Copied!'; | |
| btn.classList.add('copied'); | |
| setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1500); | |
| }); | |
| } | |
| function copyAll(btn, items, sep) { | |
| const text = items.join(sep); | |
| navigator.clipboard.writeText(text).then(() => { | |
| btn.textContent = 'Copied!'; | |
| btn.classList.add('copied'); | |
| setTimeout(() => { btn.textContent = 'Copy All'; btn.classList.remove('copied'); }, 1500); | |
| }); | |
| } | |
| function renderSection(title, items, format) { | |
| const arr = items || []; | |
| const texts = arr.map((item,i) => format(item,i)); | |
| let h = `<div class="section"><div class="section-header"><span class="section-title">${title}</span>`; | |
| if (texts.length > 0) h += `<button class="copy-all-btn" onclick="copyAll(this,${JSON.stringify(texts)},'\\n\\n')">Copy All</button>`; | |
| h += `</div><div class="section-card">`; | |
| if (texts.length === 0) { h += `<div class="single-line" style="color:#999">No data</div>`; } | |
| else { | |
| texts.forEach(t => { | |
| h += `<div class="item-row"><span class="item-text">${t}</span><button class="copy-btn" onclick="copyText(this,'${esc(t)}')">Copy</button></div>`; | |
| }); | |
| } | |
| h += `</div></div>`; | |
| return h; | |
| } | |
| function renderListSection(title, items) { | |
| return renderSection(title, items, (item) => item); | |
| } | |
| function renderKV(title, value) { | |
| if (value == null || value === '') return ''; | |
| return `<div class="section"><div class="section-header"><span class="section-title">${title}</span><button class="copy-all-btn" onclick="copyText(this,'${esc(value)}')">Copy</button></div><div class="section-card"><div class="single-line">${value}</div></div></div>`; | |
| } | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |