Site Builder
Editing:
tools-keywords.php
writable 0666
<?php /***************************************************************** * Promptinator — Keyword Generator (toolbar edition) v1.1 * ------------------------------------------------------------- * Uses the global key‑bar helper (openai/init.php). *****************************************************************/ require_once $_SERVER['DOCUMENT_ROOT'] . '/openai/init.php'; // ← path to helper ai_handle_key_post(); // save / delete API key /* ---------- 1. AJAX end‑point -------------------------------------- */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_GET['ajax'] ?? '') === '1') { header('Content-Type: application/json; charset=utf-8'); /* guard rails --------------------------------------------------- */ if (!ai_has_key()) { echo json_encode(['error' => 'No API key saved']); exit; } $topic = trim($_POST['topic'] ?? ''); $qty = max(1, min(30, intval($_POST['qty'] ?? 10))); $types = $_POST['types'] ?? []; if ($topic === '' || empty($types)) { echo json_encode(['error' => 'Topic & type required']); exit; } /* prompt -------------------------------------------------------- */ $prompt = "Generate keyword lists for the topic: {$topic}\n" . "Return ONLY pipe‑separated values for each requested TYPE in this format:\n" . "TYPE: kw1|kw2|kw3...\n" . "Make {$qty} unique keywords per TYPE, avoiding duplicates across lists.\n" . "Requested TYPES: " . implode(', ', $types); $txt = ai_chat($prompt); if ($txt === '' || str_starts_with($txt, '➡')) { echo json_encode(['error' => 'Empty response']); exit; } /* parse assistant output --------------------------------------- */ $out = []; foreach (explode("\n", $txt) as $line) { if (!preg_match('/^\s*([A-Za-z ].*?):\s*(.+)$/', $line, $m)) continue; $out[$m[1]] = array_map('trim', explode('|', $m[2])); } echo json_encode(['data' => $out]); exit; } ?><!doctype html> <html lang="en"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Keyword Generator • Promptinator</title> <style> :root{--bg:#f1f4fb;--card:#fff;--brand:#004cff;--brand-d:#0841c4; --green:#5af287;--red:#e24d4b;--radius:26px;--shadow:0 6px 30px rgba(0,0,0,.08); font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;font-size:16px} body{margin:0;min-height:100vh;background:var(--bg);display:flex;flex-direction:column;color:#111} .breadcrumb{background:#ececec;font-weight:600;padding:.7rem 1.2rem}.breadcrumb a{color:var(--brand);text-decoration:none} main{width:100%;max-width:960px;margin:2.4rem auto;padding:0 1.2rem;flex:1} .tool-card{background:var(--card);border-radius:var(--radius);box-shadow:var(--shadow);padding:2rem} label{font-weight:600;font-size:.95rem;display:flex;flex-direction:column;gap:.4rem} input,select,textarea{width:100%;padding:.7rem 1rem;border:1px solid #ccd4e8;border-radius:10px;font-size:1rem} textarea{resize:vertical;min-height:90px;font-family:ui-monospace,monospace} input:focus,select:focus,textarea:focus{outline:2px solid var(--brand)} .form-grid{display:grid;gap:1.4rem;margin-top:1.8rem} @media(min-width:700px){.form-grid{grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}} .btn{background:var(--brand);color:#fff;border:none;border-radius:10px;padding:.75rem 1.55rem; font-size:1rem;font-weight:600;cursor:pointer} .btn:hover{background:var(--brand-d)}.copyBtn{margin-top:.7rem} .tokenBox{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.4rem} .token{background:#e8efff;color:#003c9e;padding:.35rem .9rem;border-radius:18px;font-weight:600;position:relative} .token .x{position:absolute;top:-6px;right:-6px;border-radius:50%;background:#e24d4b;color:#fff; width:16px;height:16px;font-size:12px;line-height:16px;text-align:center;cursor:pointer} #spin{display:none;gap:.6ch;align-items:center;margin-top:.8rem}@keyframes rot{to{transform:rotate(360deg)}} #spin svg{width:20px;height:20px;animation:rot 1s linear infinite}#spin.show{display:flex} .tableWrap{overflow-x:auto;margin-top:1.4rem} table{border-collapse:collapse;width:100%}th,td{border:1px solid #ccd4e8;padding:.6rem .8rem;text-align:left;font-size:.95rem} .micBtn{position:absolute;right:.7rem;top:50%;transform:translateY(-50%);background:none;border:none;font-size:1.2rem;cursor:pointer} .ai-bar select{ width:auto !important; /* defeat width:100% */ padding:.55rem .85rem; /* match gear‑button height */ } </style> </head><body> <!-- Breadcrumb --> <nav class="breadcrumb"> <a href="/members/dashboard.php">Dashboard</a> » <a href="/ai-tools/tools.php">AI Toolbox</a> » Keyword Generator <a href="/<?= htmlspecialchars($_SESSION['slug'] ?? '') ?>/" style="float:right">View Site</a> </nav> <?php ai_render_key_bar(); ?><!-- shared key bar --> <?php require_once $_SERVER['DOCUMENT_ROOT'].'/ai-tools/share.php'; ?> <main> <div class="tool-card"> <h2 style="margin-top:0">Generate SEO keyword sets</h2> <?php if (!ai_has_key()): ?> <p>Please save your OpenAI key in the black bar above to unlock the generator.</p> <?php else: ?> <form id="gen" class="form-grid" autocomplete="off"> <label class="full" style="grid-column:1/-1">Topic / seed idea <div style="position:relative"> <textarea id="topic" name="topic" placeholder="e.g. Lightweight electric bicycles"></textarea> <button type="button" id="mic" class="micBtn">🎤</button> </div> </label> <label>How many per type? <select id="qty" name="qty"> <?php for ($i = 3; $i <= 30; $i++) echo "<option>$i</option>"; ?> </select> </label> <label>Add keyword type <select id="typeSel"> <option disabled selected value>— select type —</option> <option>Short‑tail</option><option>Long‑tail</option> <option>Question</option><option>Synonym / LSI</option> <option>Competitor brand</option><option>Trending</option> <option>Local / geo</option> </select> <div id="typeBox" class="tokenBox"></div> </label> <button class="btn" id="go" style="grid-column:1/-1" disabled>Generate keywords</button> <div id="spin"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/></svg><span id="s">0</span> s</div> </form> <section id="out" style="display:none"> <div class="tableWrap"><table id="tbl"></table></div> <button class="btn copyBtn" id="copyAll" style="display:none">Copy ALL as CSV</button> </section> <?php endif; ?> </div> </main> <script> (()=>{ const $=id=>document.getElementById(id); /* --- voice input -------------------------------------------------- */ if ($('mic') && (window.SpeechRecognition || window.webkitSpeechRecognition)) { const R=new (window.SpeechRecognition||window.webkitSpeechRecognition)(); R.lang='en-US'; $('mic').onclick=()=>{try{R.start();}catch{}}; R.onresult=e=>{ $('topic').value=e.results[0][0].transcript; toggleGo(); }; } else if ($('mic')) { $('mic').style.display='none'; } /* --- enable / disable GO button ----------------------------------- */ function toggleGo() { $('go').disabled = !$('topic').value.trim() || !$('typeBox').children.length; } $('topic').addEventListener('input', toggleGo); /* --- keyword types token box -------------------------------------- */ const typeSel=$('typeSel'), tBox=$('typeBox'); if (typeSel) { typeSel.onchange=()=>{ const txt=typeSel.value; if (!txt) return; if ([...tBox.children].some(t=>t.dataset.val===txt)) { typeSel.selectedIndex=0; return; } const n=document.createElement('span'); n.className='token'; n.dataset.val=txt; n.textContent=txt; n.insertAdjacentHTML('beforeend','<span class="x">×</span>'); n.querySelector('.x').onclick=()=>{ n.remove(); toggleGo(); }; tBox.appendChild(n); typeSel.selectedIndex=0; toggleGo(); }; } /* --- AJAX magic --------------------------------------------------- */ if ($('gen')) { $('gen').onsubmit=e=>{ e.preventDefault(); $('go').disabled=true; let sec=0; $('s').textContent=0; $('spin').classList.add('show'); const tick=setInterval(()=>$('s').textContent=++sec,1000); const fd=new FormData(); fd.append('topic', $('topic').value); fd.append('qty', $('qty').value); [...tBox.children].forEach(t=>fd.append('types[]', t.dataset.val)); fetch('?ajax=1', {method:'POST', body:fd}) .then(r=>r.json()).then(j=>{ clearInterval(tick); $('spin').classList.remove('show'); $('go').disabled=false; if (j.error) { alert(j.error); return; } renderTable(j.data); }).catch(err=>{ clearInterval(tick); $('spin').classList.remove('show'); $('go').disabled=false; alert(err); }); }; } /* --- render helpers ---------------------------------------------- */ function renderTable(data){ const tbl=$('tbl'); tbl.innerHTML=''; const rows=Object.entries(data); if (!rows.length) { alert('No keywords returned'); return; } tbl.innerHTML='<tr><th>Type</th><th>Keywords</th><th>Copy CSV</th></tr>'; rows.forEach(([type,list])=>{ const tr=document.createElement('tr'); const csv=list.join(', '); tr.innerHTML=`<td>${type}</td><td>${list.join('<br>')}</td> <td><button class="copyBtn" data-csv="${csv}">Copy</button></td>`; tbl.appendChild(tr); }); tbl.querySelectorAll('.copyBtn').forEach(btn=>{ btn.onclick=()=>navigator.clipboard.writeText(btn.dataset.csv).then(()=>flash(btn)); }); $('copyAll').style.display='block'; $('copyAll').onclick=()=>navigator.clipboard .writeText(rows.map(r=>r[1].join(', ')).join(', ')) .then(()=>flash($('copyAll'))); $('out').style.display=''; $('tbl').scrollIntoView({behavior:'smooth'}); } function flash(btn){const t=btn.textContent; btn.textContent='✔ Copied'; setTimeout(()=>btn.textContent=t,1200);} })(); </script> </body></html>
Save changes
Create folder
writable 0777
Create
Cancel