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
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>
|
|
|
|
<!-- 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 = " ";
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
.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,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |