USAMA BHATTI
Feat: Added Visual Search, API Key Auth, and Docker Optimization
ba2fc46
(function() {
// ----------------------------------------------------
// 1. CONFIGURATION: Security & Metadata
// ----------------------------------------------------
const scriptTag = document.currentScript;
// Auth & Config
const API_KEY = scriptTag.getAttribute("data-api-key");
const API_URL = scriptTag.getAttribute("data-api-url");
const THEME_COLOR = scriptTag.getAttribute("data-theme-color") || "#0084FF";
if (!API_KEY || !API_URL) {
console.error("OmniAgent Security Error: data-api-key or data-api-url is missing!");
return;
}
const CHAT_SESSION_ID = "omni_session_" + Math.random().toString(36).slice(2, 11);
// ----------------------------------------------------
// 2. STYLES: UI & Responsive Design (Tabs + Camera)
// ----------------------------------------------------
const style = document.createElement('style');
style.innerHTML = `
#omni-widget-container {
position: fixed; bottom: 20px; right: 20px; z-index: 999999;
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
#omni-chat-btn {
background: ${THEME_COLOR}; color: white; border: none; padding: 15px; border-radius: 50%;
cursor: pointer; box-shadow: 0 4px 15px rgba(0,0,0,0.3); width: 60px; height: 60px; font-size: 28px;
display: flex; align-items: center; justify-content: center; transition: all 0.3s;
}
#omni-chat-btn:hover { transform: scale(1.1); }
#omni-window {
display: none; width: 380px; height: 600px; background: white; border-radius: 16px;
box-shadow: 0 12px 40px rgba(0,0,0,0.25); flex-direction: column; overflow: hidden;
margin-bottom: 20px; animation: omniSlideUp 0.3s ease; border: 1px solid #e0e0e0;
}
@keyframes omniSlideUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
/* --- Header & Tabs --- */
#omni-header { background: ${THEME_COLOR}; padding: 15px; color: white; }
.omni-title { font-weight: bold; font-size: 16px; display: flex; align-items: center; gap: 8px; }
.omni-close { cursor: pointer; float: right; font-size: 24px; line-height: 16px; }
.omni-tabs { display: flex; background: #f1f1f1; border-bottom: 1px solid #ddd; }
.omni-tab { flex: 1; padding: 12px; text-align: center; cursor: pointer; font-size: 14px; font-weight: 600; color: #555; transition: 0.2s; }
.omni-tab.active { background: white; color: ${THEME_COLOR}; border-bottom: 3px solid ${THEME_COLOR}; }
/* --- Content Areas --- */
.omni-view { display: none; flex: 1; flex-direction: column; overflow: hidden; }
.omni-view.active { display: flex; }
/* --- Chat View --- */
#omni-messages { flex: 1; padding: 15px; overflow-y: auto; background: #f9f9f9; display: flex; flex-direction: column; gap: 10px; }
.omni-msg { padding: 10px 14px; border-radius: 12px; max-width: 80%; font-size: 14px; line-height: 1.4; }
.omni-msg.user { background: ${THEME_COLOR}; color: white; align-self: flex-end; border-bottom-right-radius: 2px; }
.omni-msg.bot { background: #e9eff5; color: #333; align-self: flex-start; border-bottom-left-radius: 2px; }
#omni-input-area { display: flex; border-top: 1px solid #eee; padding: 10px; background: white; }
#omni-input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 20px; outline: none; }
#omni-send { background: none; border: none; color: ${THEME_COLOR}; font-size: 20px; cursor: pointer; padding-left: 10px; }
/* --- Visual Search View --- */
#omni-visual-area { flex: 1; padding: 20px; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; overflow-y: auto; text-align: center; }
#omni-upload-box {
border: 2px dashed #ccc; border-radius: 12px; padding: 30px; margin-bottom: 20px;
width: 80%; cursor: pointer; transition: 0.2s; background: #fafafa;
}
#omni-upload-box:hover { border-color: ${THEME_COLOR}; background: #f0f7ff; }
#omni-file-input { display: none; }
.omni-result-card {
display: flex; align-items: center; gap: 10px; background: white; border: 1px solid #eee;
padding: 10px; border-radius: 8px; width: 100%; margin-bottom: 10px; text-align: left;
transition: 0.2s; text-decoration: none; color: inherit;
}
.omni-result-card:hover { transform: translateY(-2px); box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
.omni-result-img { width: 60px; height: 60px; object-fit: cover; border-radius: 6px; }
.omni-loader { border: 3px solid #f3f3f3; border-top: 3px solid ${THEME_COLOR}; border-radius: 50%; width: 24px; height: 24px; animation: spin 1s linear infinite; margin: 20px auto; display: none; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
`;
document.head.appendChild(style);
// ----------------------------------------------------
// 3. HTML STRUCTURE
// ----------------------------------------------------
const container = document.createElement('div');
container.id = 'omni-widget-container';
container.innerHTML = `
<div id="omni-window">
<div id="omni-header">
<span class="omni-close" onclick="window.toggleOmni()">×</span>
<div class="omni-title">
<span>🤖 AI Assistant</span>
</div>
</div>
<div class="omni-tabs">
<div class="omni-tab active" onclick="window.switchTab('chat')">💬 Chat</div>
<div class="omni-tab" onclick="window.switchTab('visual')">📷 Visual Search</div>
</div>
<!-- CHAT VIEW -->
<div id="omni-chat-view" class="omni-view active">
<div id="omni-messages"></div>
<div id="omni-input-area">
<input type="text" id="omni-input" placeholder="Ask anything..." autocomplete="off" />
<button id="omni-send">➤</button>
</div>
</div>
<!-- VISUAL VIEW -->
<div id="omni-visual-view" class="omni-view">
<div id="omni-visual-area">
<div id="omni-upload-box" onclick="document.getElementById('omni-file-input').click()">
<div style="font-size:40px; margin-bottom:10px;">📤</div>
<p style="margin:0; color:#666;">Click to Upload Image</p>
<p style="margin:0; font-size:12px; color:#999;">Find similar products</p>
</div>
<input type="file" id="omni-file-input" accept="image/*">
<div id="omni-visual-loader" class="omni-loader"></div>
<div id="omni-visual-results" style="width:100%;"></div>
</div>
</div>
</div>
<button id="omni-chat-btn" onclick="window.toggleOmni()">💬</button>
`;
document.body.appendChild(container);
// ----------------------------------------------------
// 4. UI LOGIC (Toggle & Tabs)
// ----------------------------------------------------
window.toggleOmni = function() {
const win = document.getElementById('omni-window');
const isVisible = win.style.display === 'flex';
win.style.display = isVisible ? 'none' : 'flex';
};
window.switchTab = function(tab) {
document.querySelectorAll('.omni-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.omni-view').forEach(v => v.classList.remove('active'));
if (tab === 'chat') {
document.querySelector('.omni-tabs .omni-tab:nth-child(1)').classList.add('active');
document.getElementById('omni-chat-view').classList.add('active');
} else {
document.querySelector('.omni-tabs .omni-tab:nth-child(2)').classList.add('active');
document.getElementById('omni-visual-view').classList.add('active');
}
};
// ----------------------------------------------------
// 5. CHAT ENGINE
// ----------------------------------------------------
const chatInput = document.getElementById('omni-input');
const sendBtn = document.getElementById('omni-send');
const msgContainer = document.getElementById('omni-messages');
async function sendMessage() {
const text = chatInput.value.trim();
if (!text) return;
addMsg(text, 'user');
chatInput.value = '';
const loadingDiv = document.createElement('div');
loadingDiv.className = 'omni-msg bot';
loadingDiv.innerHTML = '...';
msgContainer.appendChild(loadingDiv);
msgContainer.scrollTop = msgContainer.scrollHeight;
try {
const response = await fetch(`${API_URL}/api/v1/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: text,
session_id: CHAT_SESSION_ID,
api_key: API_KEY // Auth
})
});
msgContainer.removeChild(loadingDiv);
const data = await response.json();
if (response.ok) {
addMsg(data.response, 'bot');
} else {
addMsg("⚠️ Error: " + (data.detail || "Server error"), 'bot');
}
} catch (e) {
if(loadingDiv.parentNode) msgContainer.removeChild(loadingDiv);
addMsg("📡 Connection Error", 'bot');
}
}
function addMsg(text, sender) {
const div = document.createElement('div');
div.className = `omni-msg ${sender}`;
div.innerText = text; // Secure text insertion
msgContainer.appendChild(div);
msgContainer.scrollTop = msgContainer.scrollHeight;
}
sendBtn.onclick = sendMessage;
chatInput.onkeypress = (e) => { if(e.key === 'Enter') sendMessage(); };
// ----------------------------------------------------
// 6. VISUAL SEARCH ENGINE
// ----------------------------------------------------
const fileInput = document.getElementById('omni-file-input');
const visualLoader = document.getElementById('omni-visual-loader');
const visualResults = document.getElementById('omni-visual-results');
fileInput.onchange = async function() {
const file = fileInput.files[0];
if (!file) return;
visualLoader.style.display = 'block';
visualResults.innerHTML = '';
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch(`${API_URL}/api/v1/visual/search`, {
method: 'POST',
headers: {
'x-api-key': API_KEY // Header Auth ✅
},
body: formData
});
const data = await response.json();
visualLoader.style.display = 'none';
if (!response.ok) {
visualResults.innerHTML = `<p style="color:red; margin-top:20px;">Error: ${data.detail}</p>`;
return;
}
if (!data.results || data.results.length === 0) {
visualResults.innerHTML = '<p style="color:#777; margin-top:20px;">No similar products found.</p>';
return;
}
// Render Results
data.results.forEach(item => {
const score = (item.similarity * 100).toFixed(0);
const el = document.createElement('a');
el.className = 'omni-result-card';
el.href = `/product/${item.slug || '#'}`; // Dynamic link
el.target = '_blank';
el.innerHTML = `
<img src="${item.image_path}" class="omni-result-img" onerror="this.src='https://via.placeholder.com/60'">
<div>
<div style="font-weight:bold; font-size:14px;">Product Match</div>
<div style="font-size:12px; color:#27ae60;">${score}% Similarity</div>
</div>
`;
visualResults.appendChild(el);
});
} catch (e) {
visualLoader.style.display = 'none';
visualResults.innerHTML = '<p style="color:red;">Connection Failed</p>';
}
};
// Initial Hello
setTimeout(() => addMsg("Hi! I can help you find products by chatting or uploading an image.", "bot"), 1000);
})();