You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

633 lines
26 KiB

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>AI Prompt Composer Engine</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Arial;background:#f5f5f5;padding:40px}
.container{max-width:960px;margin:auto}
h1{margin-bottom:20px}
.prompt-row{display:flex;gap:8px;width:100%}
.prompt-row input{flex:1;padding:10px;border:1px solid #ccc;border-radius:8px;font-size:14px}
.extra-row{margin-top:10px;display:flex;gap:8px}
.extra-row input{flex:1;padding:10px;border:1px solid #ccc;border-radius:8px;font-size:13px}
button{padding:10px 16px;border:none;border-radius:8px;background:black;color:white;cursor:pointer;font-size:13px}
button.secondary{background:#555}
button:disabled{background:#999;cursor:not-allowed}
.clear-field-btn{padding:4px 10px;border:1px solid #ddd;border-radius:6px;background:white;color:#999;cursor:pointer;font-size:12px;transition:all .15s;flex-shrink:0}
.clear-field-btn:hover{border-color:#ef4444;color:#ef4444;background:#fff5f5}
.selected-area{margin-top:16px;padding:12px 16px;background:white;border-radius:10px;border:1px solid #ddd;display:none}
/* ── Category Tabs ── */
.category-tabs{display:flex;gap:8px;margin-top:20px;flex-wrap:wrap}
.cat-tab{display:flex;align-items:center;gap:6px;padding:7px 14px;border-radius:20px;border:1px solid #ddd;background:white;cursor:pointer;font-size:13px;color:#555;transition:all .15s;user-select:none}
.cat-tab:hover{border-color:#333;color:#111}
.cat-tab.active{background:#111;color:white;border-color:#111}
/* ── Keyword box ── */
.keyword-box{margin-top:12px;padding:16px;background:white;border-radius:10px;border:1px solid #ddd;min-height:80px}
.box-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.box-title{font-size:14px;font-weight:bold}
.category-badge{font-size:11px;padding:3px 10px;border-radius:20px;background:#f0f0f0;color:#555}
.category-badge.images{background:#fef3c7;color:#d97706}
.category-badge.shopping{background:#d1fae5;color:#065f46}
.category-badge.videos{background:#fee2e2;color:#991b1b}
.category-badge.news{background:#dbeafe;color:#1e40af}
.category-badge.places{background:#ede9fe;color:#5b21b6}
.category-badge.general{background:#f0f0f0;color:#555}
/* ── Chips ── */
.chip{display:inline-block;padding:7px 14px;margin:4px;border-radius:20px;border:1px solid #ccc;cursor:pointer;background:#fafafa;font-size:13px;transition:all .15s}
.chip:hover{border-color:#000;background:#f0f0f0}
.chip.selected{background:#16a34a;color:white;border-color:#16a34a}
.chip.top{border-color:#f59e0b;background:#fffbeb;font-weight:600}
.chip.top.selected{background:#16a34a;border-color:#16a34a;color:white}
.kw-loading-spinner{display:flex;align-items:center;gap:8px;color:#999;font-size:13px;padding:8px 0}
.spinner{width:16px;height:16px;border:2px solid #ddd;border-top:2px solid #333;border-radius:50%;animation:spin .8s linear infinite;flex-shrink:0}
@keyframes spin{to{transform:rotate(360deg)}}
.kw-msg{color:#999;font-size:13px;padding:8px 0;display:block}
.load-more-btn{display:inline-block;padding:6px 14px;margin:6px 4px;border-radius:20px;border:1px dashed #ccc;background:white;color:#666;font-size:12px;cursor:pointer;transition:all .2s}
.load-more-btn:hover{border-color:#333;color:#333;border-style:solid}
.load-more-btn:disabled{color:#bbb;cursor:not-allowed}
/* ── Results box ── */
.results-box{margin-top:16px;display:none}
.results-box.visible{display:block}
.results-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.results-title{font-size:14px;font-weight:bold;color:#333}
.results-grid{display:grid;gap:12px;margin-top:8px}
/* Video card */
.video-card{background:white;border:1px solid #ddd;border-radius:10px;overflow:hidden;cursor:pointer;transition:transform .15s,box-shadow .15s;text-decoration:none;color:inherit;display:block}
.video-card:hover{transform:translateY(-2px);box-shadow:0 4px 16px rgba(0,0,0,.1)}
.video-card img{width:100%;height:150px;object-fit:cover}
.video-card .vc-info{padding:10px}
.video-card .vc-title{font-size:13px;font-weight:600;margin-bottom:4px;line-height:1.4}
.video-card .vc-channel{font-size:11px;color:#888}
.video-card .vc-date{font-size:10px;color:#aaa;margin-top:2px}
/* Shopping card */
.shop-card{background:white;border:1px solid #ddd;border-radius:10px;padding:14px;cursor:pointer;transition:transform .15s,box-shadow .15s;text-decoration:none;color:inherit;display:block}
.shop-card:hover{transform:translateY(-2px);box-shadow:0 4px 16px rgba(0,0,0,.1)}
.shop-card img{width:100%;height:120px;object-fit:contain;margin-bottom:8px;border-radius:6px;background:#f5f5f5}
.shop-card .sc-title{font-size:13px;font-weight:600;margin-bottom:4px;line-height:1.4}
.shop-card .sc-source{font-size:11px;color:#16a34a;font-weight:500}
.shop-card .sc-snippet{font-size:11px;color:#666;margin-top:4px;line-height:1.4}
/* Image card */
.image-card{background:white;border:1px solid #ddd;border-radius:10px;overflow:hidden;cursor:pointer;transition:transform .15s;text-decoration:none;display:block}
.image-card:hover{transform:scale(1.02)}
.image-card img{width:100%;height:160px;object-fit:cover}
.image-card .ic-title{font-size:11px;color:#666;padding:6px 10px}
/* News card */
.news-card{background:white;border:1px solid #ddd;border-radius:10px;padding:14px;display:flex;gap:12px;text-decoration:none;color:inherit;transition:box-shadow .15s}
.news-card:hover{box-shadow:0 4px 16px rgba(0,0,0,.1)}
.news-card img{width:80px;height:80px;object-fit:cover;border-radius:6px;flex-shrink:0}
.news-card .nc-info{flex:1}
.news-card .nc-title{font-size:13px;font-weight:600;line-height:1.4;margin-bottom:4px}
.news-card .nc-source{font-size:11px;color:#1e40af;font-weight:500}
.news-card .nc-snippet{font-size:11px;color:#666;margin-top:4px;line-height:1.4}
/* Places card */
.place-card{background:white;border:1px solid #ddd;border-radius:10px;padding:14px;display:flex;gap:12px;text-decoration:none;color:inherit;transition:box-shadow .15s}
.place-card:hover{box-shadow:0 4px 16px rgba(0,0,0,.1)}
.place-card img{width:70px;height:70px;object-fit:cover;border-radius:6px;flex-shrink:0;background:#f5f5f5}
.place-card .pc-info{flex:1}
.place-card .pc-title{font-size:13px;font-weight:600;margin-bottom:4px}
.place-card .pc-source{font-size:11px;color:#5b21b6;font-weight:500}
.place-card .pc-snippet{font-size:11px;color:#666;margin-top:4px;line-height:1.4}
/* ── Actions ── */
.actions{margin-top:16px;display:flex;gap:8px;align-items:center}
/* ── Chat ── */
.chat-block{margin-top:24px;background:white;padding:20px;border-radius:10px;border:1px solid #ddd}
.go-label{font-weight:bold;font-size:15px;margin-bottom:10px}
.prompt-line{margin:4px 0;font-size:14px;color:#333}
.kw-line{margin:4px 0;font-size:13px;color:#555}
.cat-line{margin:4px 0;font-size:12px;color:#888}
.answer{background:#f1f1f1;padding:14px;border-radius:8px;margin-top:12px;line-height:1.7;font-size:14px}
.answer strong{color:#111}
.answer h2{font-size:15px;margin:10px 0 6px}
/* ── Skeleton shimmer ── */
.chip.skeleton{
background:linear-gradient(90deg,#f0f0f0 25%,#e0e0e0 50%,#f0f0f0 75%);
background-size:200% 100%;
animation:shimmer 1.2s infinite;
border-color:transparent;
cursor:default;
min-width:60px;
height:34px;
}
@keyframes shimmer{
0%{background-position:200% 0}
100%{background-position:-200% 0}
}
</style>
</head>
<body>
<div class="container">
<h1>AI Prompt Composer Engine</h1>
<!-- Prompt -->
<div class="prompt-row">
<input type="text" id="prompt" placeholder="Type your prompt or topic..." />
<button type="button" class="clear-field-btn" onclick="clearPrompt()"></button>
<button type="button" id="goBtn" onclick="askAI()">GO</button>
</div>
<!-- Extra -->
<div class="extra-row">
<input type="text" id="extra" placeholder="Add extra instruction (optional)" />
<button type="button" class="clear-field-btn" onclick="clearExtra()"></button>
</div>
<!-- Selected keywords -->
<div class="selected-area" id="selectedArea">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<b>Selected Keywords</b>
<button type="button" class="clear-field-btn" onclick="clearSelected()">✕ Clear</button>
</div>
<div id="selectedKeywords"></div>
</div>
<!-- Category Tabs -->
<div class="category-tabs">
<div class="cat-tab active" data-cat="general" onclick="selectCategory('general')">🔍 General</div>
<div class="cat-tab" data-cat="shopping" onclick="selectCategory('shopping')">🛍️ Shopping</div>
<div class="cat-tab" data-cat="images" onclick="selectCategory('images')">🖼️ Images</div>
<div class="cat-tab" data-cat="videos" onclick="selectCategory('videos')">🎬 Videos</div>
<div class="cat-tab" data-cat="news" onclick="selectCategory('news')">📰 News</div>
<div class="cat-tab" data-cat="places" onclick="selectCategory('places')">📍 Places</div>
</div>
<div></div>
<!-- Suggestions -->
<div class="keyword-box">
<div class="box-header">
<span class="box-title">Suggestions</span>
<div style="display:flex;align-items:center;gap:8px">
<span class="category-badge general" id="categoryBadge">General</span>
<button type="button" class="clear-field-btn" onclick="clearSuggestions()"></button>
</div>
</div>
<div id="keywords"><span class="kw-msg">Start typing to see suggestions...</span></div>
</div>
<!-- Results box -->
<div class="results-box" id="resultsBox">
<div class="results-header">
<span class="results-title" id="resultsTitle"></span>
</div>
<div class="results-grid" id="resultsGrid"></div>
</div>
<!-- Actions -->
<div class="actions">
<button type="button" id="askBtn" onclick="askAI()">Ask AI</button>
<button type="button" class="secondary" onclick="clearAll()">Clear All</button>
</div>
<div id="chat"></div>
</div>
<script>
let selected = [];
let chatHistory = [];
let step = 0;
let debounceTimer;
let currentQuery = "";
let currentOffset = 0;
let lastFetchedQuery = "";
let currentCategory = "general";
const promptInput = document.getElementById("prompt");
const keywordsDiv = document.getElementById("keywords");
const selectedDiv = document.getElementById("selectedKeywords");
const selectedArea = document.getElementById("selectedArea");
const CATEGORIES = {
general: { label: "General", badge: "general", icon: "🔍" },
shopping: { label: "Shopping", badge: "shopping", icon: "🛍️" },
images: { label: "Images", badge: "images", icon: "🖼️" },
videos: { label: "Videos", badge: "videos", icon: "🎬" },
news: { label: "News", badge: "news", icon: "📰" },
places: { label: "Places", badge: "places", icon: "📍" }
};
// ── Gibberish check ──
function isGibberish(text) {
const words = text.toLowerCase().split(/\s+/);
let gc = 0;
for (const w of words) {
if (w.length <= 3) continue;
if (w.match(/[^aeiou]{6,}/)) { gc++; continue; }
const v = (w.match(/[aeiou]/g) || []).length;
if (w.length > 5 && v / w.length < 0.1) { gc++; continue; }
if (/[a-z]\d{3,}[a-z]|[0-9]{4,}[a-z]/.test(w)) { gc++; continue; }
}
return gc > words.length / 2;
}
// ── Select category ──
function selectCategory(cat) {
currentCategory = cat;
document.querySelectorAll(".cat-tab").forEach(t => t.classList.remove("active"));
document.querySelector(`[data-cat="${cat}"]`).classList.add("active");
const badge = document.getElementById("categoryBadge");
badge.textContent = CATEGORIES[cat].label;
badge.className = `category-badge ${cat}`;
lastFetchedQuery = "";
const q = promptInput.value.trim();
if (q.length >= 3 && !isGibberish(q)) {
keywordsDiv.innerHTML = `<div class="kw-loading-spinner"><div class="spinner"></div><span>Loading ${CATEGORIES[cat].label} suggestions...</span></div>`;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => fetchKeywords(q), 300);
if (cat !== "general") fetchCategoryResults(q, cat);
else document.getElementById("resultsBox").classList.remove("visible");
} else {
document.getElementById("resultsBox").classList.remove("visible");
}
}
// ── Typing listener ──
promptInput.addEventListener("input", () => {
clearTimeout(debounceTimer);
const q = promptInput.value.trim();
currentQuery = q;
if (q.length < 4) {
keywordsDiv.innerHTML = '<span class="kw-msg">Keep typing...</span>';
document.getElementById("resultsBox").classList.remove("visible");
return;
}
if (isGibberish(q)) {
keywordsDiv.innerHTML = '<span class="kw-msg">⚠️ Please enter a meaningful topic.</span>';
return;
}
if (!keywordsDiv.querySelector('.chip')) {
showSkeletonChips();
}
debounceTimer = setTimeout(() => {
fetchKeywords(q);
if (currentCategory !== "general") fetchCategoryResults(q, currentCategory);
}, 1000);
});
// ── Skeleton chips ──
function showSkeletonChips() {
keywordsDiv.innerHTML = "";
for (let i = 0; i < 8; i++) {
const sk = document.createElement("span");
sk.className = "chip skeleton";
sk.style.width = `${60 + Math.random() * 60}px`;
sk.innerHTML = "&nbsp;";
keywordsDiv.appendChild(sk);
}
}
// ── Fetch suggestions ──
async function fetchKeywords(query) {
const cacheKey = `${query}__${currentCategory}`;
if (cacheKey === lastFetchedQuery) return;
lastFetchedQuery = cacheKey;
currentOffset = 0;
showSkeletonChips();
try {
const url = `http://127.0.0.1:8000/suggest?query=${encodeURIComponent(query)}&offset=0&limit=15&category=${currentCategory}`;
const res = await fetch(url);
const data = await res.json();
if (data.cache === "gibberish") {
keywordsDiv.innerHTML = '<span class="kw-msg">⚠️ Please enter a meaningful topic.</span>';
return;
}
if (!data.suggestions || !data.suggestions.length) {
keywordsDiv.innerHTML = '<span class="kw-msg">No suggestions found. Try a more specific topic.</span>';
return;
}
renderChips(data.suggestions, false);
} catch (e) {
keywordsDiv.innerHTML = '<span class="kw-msg">Backend not reachable.</span>';
}
}
// ── Fetch category results ──
async function fetchCategoryResults(query, category) {
const resultsBox = document.getElementById("resultsBox");
const resultsGrid = document.getElementById("resultsGrid");
const resultsTitle = document.getElementById("resultsTitle");
if (category === "general") {
resultsBox.classList.remove("visible");
return;
}
resultsGrid.innerHTML = "";
const endpointMap = {
videos: "/search/videos",
shopping: "/search/shopping",
images: "/search/images",
news: "/search/news",
places: "/search/places"
};
const endpoint = endpointMap[category];
if (!endpoint) return;
try {
const res = await fetch(`http://127.0.0.1:8000${endpoint}?query=${encodeURIComponent(query)}`);
const data = await res.json();
if (data.error || !data.results || !data.results.length) {
resultsBox.classList.remove("visible");
return;
}
const icons = { shopping:"🛍️", images:"🖼️", videos:"🎬", news:"📰", places:"📍" };
resultsTitle.textContent = `${icons[category]} ${CATEGORIES[category].label} results for "${query}"`;
resultsBox.classList.add("visible");
if (category === "videos") {
resultsGrid.style.gridTemplateColumns = "repeat(auto-fill, minmax(260px, 1fr))";
resultsGrid.innerHTML = data.results.map(v => `
<a href="${v.url}" target="_blank" class="video-card">
<img src="${v.thumbnail}" alt="${escHtml(v.title)}" onerror="this.style.display='none'"/>
<div class="vc-info">
<div class="vc-title">${escHtml(v.title)}</div>
<div class="vc-channel">📺 ${escHtml(v.channel)}</div>
<div class="vc-date">${v.published}</div>
</div>
</a>`).join("");
} else if (category === "shopping") {
resultsGrid.style.gridTemplateColumns = "repeat(auto-fill, minmax(220px, 1fr))";
resultsGrid.innerHTML = data.results.map(s => `
<a href="${s.link}" target="_blank" class="shop-card">
${s.image ? `<img src="${s.image}" alt="" onerror="this.style.display='none'"/>` : ""}
<div class="sc-title">${escHtml(s.title)}</div>
<div class="sc-source">🔗 ${escHtml(s.source)}</div>
<div class="sc-snippet">${escHtml(s.snippet)}</div>
</a>`).join("");
} else if (category === "images") {
resultsGrid.style.gridTemplateColumns = "repeat(auto-fill, minmax(180px, 1fr))";
resultsGrid.innerHTML = data.results.map(img => `
<a href="${img.context_link}" target="_blank" class="image-card">
<img src="${img.thumbnail}" alt="${escHtml(img.title)}" onerror="this.background='#eee'"/>
<div class="ic-title">${escHtml(img.source)}</div>
</a>`).join("");
} else if (category === "news") {
resultsGrid.style.gridTemplateColumns = "1fr";
resultsGrid.innerHTML = data.results.map(n => `
<a href="${n.link}" target="_blank" class="news-card">
${n.image ? `<img src="${n.image}" alt="" onerror="this.style.display='none'"/>` : ""}
<div class="nc-info">
<div class="nc-title">${escHtml(n.title)}</div>
<div class="nc-source">📰 ${escHtml(n.source)}</div>
<div class="nc-snippet">${escHtml(n.snippet)}</div>
</div>
</a>`).join("");
} else if (category === "places") {
resultsGrid.style.gridTemplateColumns = "1fr";
resultsGrid.innerHTML = data.results.map(p => `
<a href="${p.link}" target="_blank" class="place-card">
${p.image ? `<img src="${p.image}" alt="" onerror="this.style.display='none'"/>` : ""}
<div class="pc-info">
<div class="pc-title">${escHtml(p.title)}</div>
<div class="pc-source">📍 ${escHtml(p.source)}</div>
<div class="pc-snippet">${escHtml(p.snippet)}</div>
</div>
</a>`).join("");
}
} catch (e) {
resultsBox.classList.remove("visible");
}
}
// ── Render chips ──
function renderChips(items, append) {
if (!append) { keywordsDiv.innerHTML = ""; currentOffset = 15; }
else { const old = document.getElementById("loadMoreBtn"); if (old) old.remove(); }
if (!items.length && !append) {
keywordsDiv.innerHTML = '<span class="kw-msg">No suggestions found.</span>';
return;
}
items.forEach((k, i) => {
const chip = document.createElement("span");
chip.className = "chip" + (selected.includes(k) ? " selected" : "");
if (!append && i < 5) chip.classList.add("top");
chip.innerText = k;
chip.dataset.kw = k;
chip.onclick = () => toggleChip(k, chip);
keywordsDiv.appendChild(chip);
});
const btn = document.createElement("button");
btn.id = "loadMoreBtn";
btn.className = "load-more-btn";
btn.innerText = "+ More";
btn.onclick = loadMore;
keywordsDiv.appendChild(btn);
}
// ── Toggle chip ──
function toggleChip(k, chipEl) {
if (selected.includes(k)) {
selected = selected.filter(x => x !== k);
chipEl.classList.remove("selected");
} else {
selected.push(k);
chipEl.classList.add("selected");
}
renderSelected();
}
// ── Render selected keywords ──
function renderSelected() {
selectedDiv.innerHTML = "";
if (!selected.length) { selectedArea.style.display = "none"; return; }
selectedArea.style.display = "block";
selected.forEach(k => {
const chip = document.createElement("span");
chip.className = "chip selected";
chip.innerText = k + " ✕";
chip.onclick = () => {
selected = selected.filter(x => x !== k);
const kw = keywordsDiv.querySelector(`[data-kw="${k}"]`);
if (kw) kw.classList.remove("selected");
renderSelected();
};
selectedDiv.appendChild(chip);
});
}
// ── Load more ──
async function loadMore() {
const btn = document.getElementById("loadMoreBtn");
btn.innerText = "Loading...";
btn.disabled = true;
try {
const existing = Array.from(keywordsDiv.querySelectorAll(".chip"))
.map(c => c.dataset.kw).filter(Boolean);
const res = await fetch(
`http://127.0.0.1:8000/suggest/more?query=${encodeURIComponent(currentQuery)}&category=${currentCategory}&existing=${encodeURIComponent(existing.join(","))}`
);
const data = await res.json();
if (!data.suggestions || !data.suggestions.length) {
btn.innerText = "No more suggestions";
return;
}
const newItems = data.suggestions.filter(
s => !existing.map(e => e.toLowerCase()).includes(s.toLowerCase())
);
if (!newItems.length) { btn.innerText = "No more suggestions"; return; }
currentOffset += 10;
renderChips(newItems, true);
} catch (e) {
btn.innerText = "+ More";
btn.disabled = false;
}
}
// ── Ask AI ──
async function askAI() {
const prompt = promptInput.value.trim();
const extra = document.getElementById("extra").value.trim();
const combined = extra ? `${prompt}. ${extra}` : prompt;
if (!combined) return;
document.getElementById("goBtn").disabled = true;
document.getElementById("askBtn").disabled = true;
step++;
const chat = document.getElementById("chat");
const block = document.createElement("div");
block.className = "chat-block";
const catInfo = CATEGORIES[currentCategory];
block.innerHTML = `
<div class="go-label">${step}. Go</div>
<div class="prompt-line"><b>Prompt:</b> ${escHtml(combined)}</div>
<div class="cat-line"><b>Category:</b> ${catInfo.icon} ${catInfo.label}</div>
<div class="kw-line"><b>Keywords:</b> ${selected.length ? selected.map(escHtml).join(", ") : "<i>none selected</i>"}</div>
<div class="answer"><i>⏳ AI is generating answer...</i></div>`;
chat.appendChild(block);
block.scrollIntoView({ behavior: "smooth" });
let answer = "";
try {
const res = await fetch("http://127.0.0.1:8000/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: combined,
selected_attributes: selected,
category: currentCategory,
chat_history: chatHistory
})
});
const data = await res.json();
answer = data.answer || "No answer returned.";
} catch {
answer = "Backend not running. Start the server on port 8000.";
}
if (selected.length > 0) {
fetch("http://127.0.0.1:8000/feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: combined,
selected_chips: selected,
domain: currentQuery
})
}).catch(() => {});
}
block.querySelector(".answer").innerHTML = `<b>AI Answer</b><br/><br/>${formatAnswer(answer)}`;
chatHistory.push({ query: combined, chips: [...selected], category: currentCategory, answer });
document.getElementById("extra").value = "";
document.getElementById("goBtn").disabled = false;
document.getElementById("askBtn").disabled = false;
promptInput.focus();
}
// ── Clear functions ──
function clearAll() {
promptInput.value = "";
document.getElementById("extra").value = "";
selected = []; chatHistory = []; step = 0; lastFetchedQuery = ""; currentCategory = "general";
document.querySelectorAll(".cat-tab").forEach(t => t.classList.remove("active"));
document.querySelector("[data-cat='general']").classList.add("active");
const badge = document.getElementById("categoryBadge");
badge.textContent = "General"; badge.className = "category-badge general";
renderSelected();
keywordsDiv.innerHTML = '<span class="kw-msg">Start typing to see suggestions...</span>';
document.getElementById("resultsBox").classList.remove("visible");
document.getElementById("chat").innerHTML = "";
}
function clearPrompt() {
promptInput.value = "";
keywordsDiv.innerHTML = '<span class="kw-msg">Start typing...</span>';
lastFetchedQuery = "";
document.getElementById("resultsBox").classList.remove("visible");
promptInput.focus();
}
function clearExtra() { document.getElementById("extra").value = ""; }
function clearSelected() {
selected = [];
document.querySelectorAll(".chip.selected").forEach(c => c.classList.remove("selected"));
renderSelected();
}
function clearSuggestions() {
keywordsDiv.innerHTML = '<span class="kw-msg">Start typing to see suggestions...</span>';
lastFetchedQuery = "";
}
// ── Format AI answer ──
function formatAnswer(text) {
return text
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/## (.*)/g,'<h2>$1</h2>')
.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>')
.replace(/\*(.*?)\*/g,'<em>$1</em>')
.replace(/---/g,'<hr style="border:none;border-top:1px solid #ddd;margin:10px 0"/>')
.replace(/\n\n+/g,'</p><p>')
.replace(/\n/g,'<br/>')
.replace(/^/,'<p>').replace(/$/,'</p>');
}
function escHtml(t) {
return String(t).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
</script>
</body>
</html>