victor-dashboard/index.html
2025-06-02 21:43:39 +05:30

1217 lines
50 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Victor AI Agent Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Plus Jakarta Sans', sans-serif;
}
</style>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.animate-pulse-soft {
animation: pulse-soft 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse-soft {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.search-input:focus {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
@media (max-width: 640px) {
.mobile-stack {
flex-direction: column;
}
.mobile-full {
width: 100%;
}
}
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.loading-skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* === Custom Enhancements === */
.glassy-card {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(12px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
html {
font-size: 15px;
}
@media (min-width: 1024px) {
html {
font-size: 16px;
}
}
.text-shadow {
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-up {
animation: fadeUp 0.4s ease-out;
}
.tooltip:hover::after {
content: attr(data-tooltip);
position: absolute;
background: #333;
color: white;
padding: 4px 8px;
font-size: 12px;
border-radius: 4px;
top: -32px;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
z-index: 100;
}
.tooltip {
position: relative;
cursor: pointer;
}
/* Mobile-specific styles */
@media (max-width: 768px) {
.mobile-stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
}
}
/* Collapsible styles */
.collapsible-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.collapsible-content.open {
max-height: 200px;
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.slide-down {
animation: slideDown 0.3s ease-out;
}
</style>
<style>
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeLine {
from { opacity: 0; }
to { opacity: 1; }
}
.animate-slide-up {
animation: slideUp 0.4s ease-out;
}
.animate-fade {
animation: fadeLine 0.6s ease-in;
}
</style>
</head>
<body class="bg-gray-50">
<!-- Mobile Header -->
<div class="lg:hidden bg-white shadow-sm border-b sticky top-0 z-50">
<div class="px-4 py-3 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-blue-600 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
<i class="fas fa-robot text-white text-lg"></i>
</div>
<div>
<h1 class="text-lg font-bold text-gray-800">Victor AI</h1>
<p class="text-xs text-gray-500">Trade Intelligence</p>
</div>
</div>
<button id="mobileMenuBtn" class="p-2 rounded-lg hover:bg-gray-100">
<i class="fas fa-bars text-gray-600"></i>
</button>
</div>
</div>
<div class="min-h-screen flex">
<!-- Sidebar -->
<div id="sidebar" class="hidden lg:flex fixed lg:relative z-40 w-64 bg-white shadow-xl border-r border-gray-200 flex-col">
<div class="p-6 border-b border-gray-100">
<div class="flex items-center gap-3">
<div class="w-12 h-12 bg-gradient-to-br from-blue-600 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
<i class="fas fa-robot text-white text-xl"></i>
</div>
<div>
<h1 class="text-xl font-bold gradient-text">Victor AI</h1>
<p class="text-sm text-gray-500">Trade Intelligence Dashboard</p>
</div>
</div>
</div>
<nav class="flex-1 p-4 space-y-2">
<a href="#" class="flex items-center gap-3 px-4 py-3 bg-blue-50 text-blue-700 rounded-xl font-medium">
<i class="fas fa-search w-4"></i>
<span>Search Database</span>
</a>
<a href="#" class="flex items-center gap-3 px-4 py-3 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors">
<i class="fas fa-chart-bar w-4"></i>
<span>Analytics</span>
</a>
<a href="#" class="flex items-center gap-3 px-4 py-3 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors">
<i class="fas fa-bookmark w-4"></i>
<span>Saved Searches</span>
</a>
<a href="#" class="flex items-center gap-3 px-4 py-3 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors">
<i class="fas fa-cog w-4"></i>
<span>Settings</span>
</a>
</nav>
<div class="p-4 border-t border-gray-100">
<div class="bg-gradient-to-r from-blue-50 to-purple-50 p-4 rounded-xl">
<h3 class="font-semibold text-gray-800 mb-2">Need Help?</h3>
<p class="text-sm text-gray-600 mb-3">Get support with your searches</p>
<button class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
Contact Support
</button>
</div>
</div>
</div>
<!-- Mobile Sidebar Overlay -->
<div id="sidebarOverlay" class="hidden fixed inset-0 bg-black bg-opacity-50 z-30 lg:hidden"></div>
<!-- Main Content -->
<div class="flex-1 lg:ml-0">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 lg:py-8">
<!-- Desktop Header -->
<div class="hidden lg:block mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-800 mb-2">Trade Intelligence Search</h1>
<p class="text-white">Find exporters, importers, and trade data from around the world</p>
</div>
<div class="text-right">
<p class="text-sm text-white">Last updated</p>
<p class="font-semibold text-gray-800" id="lastUpdated">Just now</p>
</div>
</div>
</div>
<!-- Search Section -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-4 lg:p-6 mb-6 lg:mb-8">
<div class="flex flex-col sm:flex-row gap-3">
<div class="flex-1">
<input
id="query"
type="text"
placeholder="find exporter from india, find importer from uk, find gulfood data"
value=""
class="w-full px-4 py-3 rounded-xl border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition-all search-input text-sm lg:text-base"
/>
</div>
<button
onclick="window.callWorkflow()"
class="w-full sm:w-auto bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-xl font-semibold transition-all flex items-center justify-center gap-2 min-w-[120px]"
id="searchBtn"
>
<i class="fas fa-search"></i>
<span>Search</span>
</button>
</div>
<!-- Quick Search Suggestions -->
<div class="mt-4 flex flex-wrap gap-2">
<span class="text-sm text-gray-500 mr-2">Quick searches:</span>
<button onclick="window.setQuickSearch('find exporter from india')" class="text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-1 rounded-full transition-colors">
Exporters from India
</button>
<button onclick="window.setQuickSearch('find importer from uk')" class="text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-1 rounded-full transition-colors">
UK Importers
</button>
<button onclick="window.setQuickSearch('find gulfood data')" class="text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-1 rounded-full transition-colors">
Gulfood Exhibition
</button>
</div>
</div>
<!-- Mobile Icon Cards (visible only on mobile) -->
<div class="lg:hidden mb-6">
<div class="mobile-stats-grid">
<div class="bg-white rounded-xl p-3 shadow-sm border border-gray-200 text-center">
<div class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center mx-auto mb-2">
<i class="fas fa-search text-blue-600"></i>
</div>
<p class="text-xs text-gray-500 mb-1">Total Searches</p>
<p class="text-sm font-bold text-gray-800" id="mobileSearches">0</p>
</div>
<div class="bg-white rounded-xl p-3 shadow-sm border border-gray-200 text-center">
<div class="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center mx-auto mb-2">
<i class="fas fa-chart-line text-green-600"></i>
</div>
<p class="text-xs text-gray-500 mb-1">Results Found</p>
<p class="text-sm font-bold text-gray-800" id="mobileResults">0</p>
</div>
<div class="bg-white rounded-xl p-3 shadow-sm border border-gray-200 text-center">
<div class="w-8 h-8 bg-purple-100 rounded-lg flex items-center justify-center mx-auto mb-2">
<i class="fas fa-globe text-purple-600"></i>
</div>
<p class="text-xs text-gray-500 mb-1">Countries</p>
<p class="text-sm font-bold text-gray-800">150+</p>
</div>
<div class="bg-white rounded-xl p-3 shadow-sm border border-gray-200 text-center">
<div class="w-8 h-8 bg-orange-100 rounded-lg flex items-center justify-center mx-auto mb-2">
<i class="fas fa-bolt text-orange-600"></i>
</div>
<p class="text-xs text-gray-500 mb-1">Response Time</p>
<p class="text-sm font-bold text-gray-800" id="mobileResponseTime">0ms</p>
</div>
</div>
<!-- Mobile Collapsible Execute Section -->
<div class="mt-4 bg-white rounded-xl shadow-sm border border-gray-200">
<button
id="mobileExecuteToggle"
onclick="toggleMobileExecute()"
class="w-full p-4 flex items-center justify-between text-left"
>
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<i class="fas fa-terminal text-purple-600 text-lg"></i>
</div>
<div>
<h3 class="font-semibold text-gray-800">Create / Update / Delete</h3>
<p class="text-xs text-gray-500">Execute CRUD operations</p>
</div>
</div>
<i class="fas fa-chevron-down text-gray-400 transition-transform" id="executeChevron"></i>
</button>
<div id="mobileExecuteContent" class="collapsible-content">
<div class="px-4 pb-4 slide-down">
<div class="flex flex-col gap-3">
<input
id="mobileCrudCommand"
type="text"
placeholder="e.g., Create importer Indi Tech from India email info@inditech.in"
class="w-full px-4 py-3 rounded-xl border border-gray-300 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all search-input text-sm"
/>
<button
onclick="window.callMobileCrudOperation()"
class="w-full bg-purple-600 hover:bg-purple-700 text-white px-6 py-3 rounded-xl font-semibold transition-all flex items-center justify-center gap-2"
id="mobileCrudBtn"
>
<i class="fas fa-terminal"></i>
<span>Execute</span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Desktop CRUD Command Section (hidden on mobile) -->
<div class="hidden lg:block bg-white rounded-2xl shadow-sm border border-gray-200 p-4 lg:p-6 mb-6 fade-up">
<h2 class="text-lg font-bold text-gray-800 mb-3">🔧 Create / Update / Delete</h2>
<div class="flex flex-col sm:flex-row gap-3">
<input
id="crudCommand"
type="text"
placeholder="e.g., Create importer Indi Tech from India email info@inditech.in"
class="w-full px-4 py-3 rounded-xl border border-gray-300 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all search-input text-sm lg:text-base"
/>
<button
onclick="window.callCrudOperation()"
class="w-full sm:w-auto bg-purple-600 hover:bg-purple-700 text-white px-6 py-3 rounded-xl font-semibold transition-all flex items-center justify-center gap-2 min-w-[120px]"
id="crudBtn"
>
<i class="fas fa-terminal"></i>
<span>Execute</span>
</button>
</div>
</div>
<!-- CRUD Result Section -->
<div id="crudResultSection" class="hidden mb-6 fade-up">
<div class="bg-white rounded-xl border border-gray-200 p-4">
<h3 class="text-sm font-semibold text-gray-700 mb-3">📝 Operation Result</h3>
<div id="crudResultHtml" class="text-sm text-gray-800 leading-relaxed"></div>
</div>
</div>
<!-- Desktop Stats Cards (hidden on mobile) -->
<div class="hidden lg:grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6 mb-6 lg:mb-8">
<div class="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">Total Searches</p>
<p class="text-xl lg:text-2xl font-bold text-gray-800" id="totalSearches">0</p>
</div>
<div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<i class="fas fa-search text-blue-600"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">Results Found</p>
<p class="text-xl lg:text-2xl font-bold text-gray-800" id="totalResults">0</p>
</div>
<div class="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<i class="fas fa-chart-line text-green-600"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">Countries</p>
<p class="text-xl lg:text-2xl font-bold text-gray-800">150+</p>
</div>
<div class="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<i class="fas fa-globe text-purple-600"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">Response Time</p>
<p class="text-xl lg:text-2xl font-bold text-gray-800" id="responseTime">0ms</p>
</div>
<div class="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<i class="fas fa-bolt text-orange-600"></i>
</div>
</div>
</div>
</div>
<!-- Results Section -->
<div id="resultsSection" class="hidden">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-4 lg:mb-6">
<h2 class="text-xl lg:text-2xl font-bold text-gray-800 mb-2 sm:mb-0">Search Results</h2>
<div class="flex items-center gap-4">
<p id="resultsCount" class="text-sm text-white"></p>
<div class="flex items-center gap-2">
<button onclick="window.exportResults()" class="text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-2 rounded-lg transition-colors flex items-center gap-2">
<i class="fas fa-download text-xs"></i>
Export
</button>
<button onclick="window.shareResults()" class="text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 px-3 py-2 rounded-lg transition-colors flex items-center gap-2">
<i class="fas fa-share text-xs"></i>
Share
</button>
</div>
</div>
</div>
<div id="results" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 lg:gap-6"></div>
<!-- Load More Button -->
<div id="loadMoreSection" class="hidden mt-8 text-center">
<button onclick="window.loadMoreResults()" class="bg-gray-100 hover:bg-gray-200 text-gray-700 px-6 py-3 rounded-xl font-medium transition-colors">
Load More Results
</button>
</div>
</div>
<!-- Empty State -->
<div id="emptyState" class="text-center py-12 lg:py-16">
<div class="max-w-md mx-auto">
<div class="w-20 h-20 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-6">
<i class="fas fa-search text-blue-600 text-2xl"></i>
</div>
<h3 class="text-xl font-semibold text-gray-800 mb-3">Start Your Search</h3>
<p class="text-white mb-6">Enter your search query above to find exporters, importers, and trade data from our comprehensive database.</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div class="bg-white p-4 rounded-lg border border-gray-200">
<h4 class="font-medium text-gray-800 mb-2">Find Exporters</h4>
<p class="text-gray-600">Search by country, product, or company name</p>
</div>
<div class="bg-white p-4 rounded-lg border border-gray-200">
<h4 class="font-medium text-gray-800 mb-2">Find Importers</h4>
<p class="text-gray-600">Discover potential buyers worldwide</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Declare all variables in global scope first
let totalSearchCount = 0;
let totalResultCount = 0;
let currentSearchData = [];
// Mobile execute toggle function
function toggleMobileExecute() {
const content = document.getElementById('mobileExecuteContent');
const chevron = document.getElementById('executeChevron');
if (content.classList.contains('open')) {
content.classList.remove('open');
chevron.style.transform = 'rotate(0deg)';
} else {
content.classList.add('open');
chevron.style.transform = 'rotate(180deg)';
}
}
// Enhanced search function with better error handling
async function callWorkflow() {
const query = document.getElementById('query').value.trim();
if (!query) {
alert('Please enter a search query');
return;
}
const searchBtn = document.getElementById('searchBtn');
const results = document.getElementById('results');
const resultsSection = document.getElementById('resultsSection');
const resultsCount = document.getElementById('resultsCount');
const emptyState = document.getElementById('emptyState');
// Show loading state and clear previous results
searchBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Searching...';
searchBtn.disabled = true;
resultsSection.classList.add('hidden');
emptyState.classList.add('hidden');
// Show loading skeleton
results.innerHTML = Array(6).fill().map(() => `
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="animate-pulse">
<div class="flex items-center gap-3 mb-4">
<div class="w-8 h-6 bg-gray-200 rounded"></div>
<div class="h-6 bg-gray-200 rounded flex-1"></div>
</div>
<div class="space-y-3">
<div class="h-4 bg-gray-200 rounded w-3/4"></div>
<div class="h-4 bg-gray-200 rounded w-1/2"></div>
<div class="h-4 bg-gray-200 rounded w-2/3"></div>
</div>
</div>
</div>
`).join('');
resultsSection.classList.remove('hidden');
const startTime = Date.now();
try {
console.log('Making API request with query:', query);
const response = await fetch(`https://thecyberlearn.app.n8n.cloud/webhook/search-airtable?query=${encodeURIComponent(query)}`, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
console.log('Response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('Full API Response:', data);
const responseTime = Date.now() - startTime;
// Process the API response
let cards = '';
let resultCount = 0;
// Handle the API response format (your API returns an array)
if (data && Array.isArray(data) && data.length > 0) {
const apiData = data[0];
console.log('Processing API data:', apiData);
if (apiData.success && apiData.content) {
// Try originalMessage first, then fallback to message
const messageContent = apiData.content.originalMessage || apiData.content.message;
console.log('Message content:', messageContent);
if (messageContent) {
const companies = parseApiResponse(messageContent);
resultCount = companies.length;
currentSearchData = companies;
companies.forEach((company, index) => {
cards += generateCompanyCardFromData(company, index + 1);
});
} else {
console.log('No message content found in API response');
}
} else {
console.log('API response indicates failure or no content');
}
} else if (data && data.success && data.content) {
// Handle single object response (fallback)
const messageContent = data.content.originalMessage || data.content.message;
if (messageContent) {
const companies = parseApiResponse(messageContent);
resultCount = companies.length;
currentSearchData = companies;
companies.forEach((company, index) => {
cards += generateCompanyCardFromData(company, index + 1);
});
}
} else if (data && data.output) {
// Fallback for older API format
const companies = parseApiResponse(data.output);
resultCount = companies.length;
currentSearchData = companies;
companies.forEach((company, index) => {
cards += generateCompanyCardFromData(company, index + 1);
});
} else {
console.log('Unexpected API response format:', data);
}
// Update UI
updateStats(resultCount, responseTime);
resultsCount.textContent = `Found ${resultCount} ${data[0]?.tableType || 'company'}(s) matching "${query}"`;
if (cards) {
results.innerHTML = cards;
resultsSection.classList.remove('hidden');
// Show load more if there are many results
if (resultCount > 20) {
document.getElementById('loadMoreSection').classList.remove('hidden');
}
} else {
results.innerHTML = `
<div class="col-span-full bg-white rounded-xl p-8 text-center">
<div class="text-4xl mb-3">😔</div>
<p class="text-gray-600 font-semibold">No results found</p>
<p class="text-gray-500 text-sm mt-2">Try a different search term</p>
</div>`;
resultsSection.classList.remove('hidden');
}
} catch (error) {
console.error('Search error:', error);
let errorMessage = 'Search failed';
let errorDetail = 'Please try again or contact support';
if (error.message.includes('401')) {
errorMessage = 'Authentication Error';
errorDetail = 'The API key or endpoint URL needs to be updated. Please contact the administrator.';
} else if (error.message.includes('404')) {
errorMessage = 'Service Not Found';
errorDetail = 'The search service is currently unavailable.';
} else if (error.message.includes('Failed to fetch')) {
errorMessage = 'Connection Error';
errorDetail = 'Please check your internet connection and try again.';
}
results.innerHTML = `
<div class="col-span-full bg-red-50 border border-red-200 rounded-xl p-6 text-center">
<div class="text-red-600 text-2xl mb-3">⚠️</div>
<p class="text-red-700 font-semibold">${errorMessage}</p>
<p class="text-red-600 text-sm mt-2">${errorDetail}</p>
<div class="mt-4 p-3 bg-red-100 rounded-lg text-left">
<p class="text-xs text-red-700 font-mono">${error.message}</p>
</div>
<button onclick="location.reload()" class="mt-4 bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg text-sm transition-colors">
Refresh Page
</button>
</div>`;
resultsSection.classList.remove('hidden');
} finally {
searchBtn.innerHTML = '<i class="fas fa-search"></i> <span>Search</span>';
searchBtn.disabled = false;
}
}
// Enhanced parsing function - handles your exact API format
function parseApiResponse(responseText) {
let companies = [];
try {
// Clean up the text and remove markdown/HTML formatting
let cleanText = responseText
.replace(/<[^>]*>/g, '') // Remove HTML tags
.replace(/\*\*/g, '') // Remove bold markdown
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Convert markdown links to plain text
.replace(/`([^`]+)`/g, '$1') // Remove code formatting
.replace(/🔍.*?🚢/g, '') // Remove search header with ship emoji
.replace(/🔍.*?📦/g, '') // Remove search header with package emoji
.replace(/📊.*?────+/g, '') // Remove stats line
.replace(/💡.*$/gm, '') // Remove suggestions
.trim();
console.log('Cleaned text:', cleanText.substring(0, 500)); // Debug log
// Split by numbered company entries (1. 2. 3. etc.)
const splitPattern = /\b(\d+)\.\s+/;
const parts = cleanText.split(splitPattern);
// Extract company blocks (every 3rd element after split)
const companyBlocks = [];
for (let i = 2; i < parts.length; i += 2) {
if (parts[i] && parts[i].trim()) {
companyBlocks.push(parts[i].trim());
}
}
console.log('Company blocks found:', companyBlocks.length); // Debug log
companyBlocks.forEach((block, index) => {
if (!block || block.trim().length === 0) return;
console.log(`Processing block ${index + 1}:`, block.substring(0, 200)); // Debug log
const lines = block.split('\n').map(line => line.trim()).filter(line => line.length > 0);
// Extract company name (first line)
const companyName = lines[0] || '';
// Extract phone numbers (look for ☎️ emoji followed by numbers)
const phoneMatches = block.match(/☎️\s*([\d\s\-\(\)\.E\+]+)/g) || [];
const phone = phoneMatches[0] ? phoneMatches[0].replace(/☎️\s*/, '').trim() : '';
// Extract emails (look for 📧 emoji followed by email)
const emailMatches = block.match(/📧\s*([^\s\n]+@[^\s\n]+)/g) || [];
const email = emailMatches[0] ? emailMatches[0].replace(/📧\s*/, '').trim() : '';
// Extract websites (look for 🔗 emoji followed by URL)
const websiteMatches = block.match(/🔗\s*(https?:\/\/[^\s\n]+)/g) || [];
const website = websiteMatches[0] ? websiteMatches[0].replace(/🔗\s*/, '').replace(/\/$/, '') : '';
// Extract location (look for 📍 emoji followed by location)
const locationMatches = block.match(/📍\s*([^\n🔗📧☎📦🏷]+)/g) || [];
let location = 'Unknown';
if (locationMatches.length > 0) {
location = locationMatches[0].replace(/📍\s*/, '').trim();
}
// Fallback location detection
if (location === 'Unknown' || location.length < 2) {
if (block.includes('United Kingdom')) location = 'United Kingdom';
else if (block.includes('India')) location = 'India';
else if (block.includes('USA') || block.includes('United States')) location = 'USA';
else if (block.includes('UAE') || block.includes('Dubai')) location = 'UAE';
else if (block.includes('China')) location = 'China';
}
// Extract products (look for 📦 emoji)
const productMatches = block.match(/📦\s*([^\n🏷]+)/g) || [];
const products = [];
productMatches.forEach(match => {
const productText = match.replace(/📦\s*/, '').trim();
const productList = productText.split(/[,&]/).map(p => p.trim()).filter(p => p.length > 0);
products.push(...productList);
});
// Extract brands (look for 🏷️ emoji)
const brandMatches = block.match(/🏷️\s*([^\n]+)/g) || [];
const brands = [];
brandMatches.forEach(match => {
const brandText = match.replace(/🏷️\s*/, '').trim();
const brandList = brandText.split(/[,&]/).map(b => b.trim()).filter(b => b.length > 0);
brands.push(...brandList);
});
// Combine products and brands
const allProducts = [...products, ...brands].slice(0, 8); // Limit to 8 items
if (companyName && companyName.length > 0) {
companies.push({
name: companyName,
phone: phone,
email: email,
website: website,
address: location,
products: allProducts
});
}
});
console.log('Final parsed companies:', companies); // Debug log
} catch (error) {
console.error('Error parsing API response:', error);
console.log('Raw response text:', responseText);
}
return companies;
}
// Enhanced card generation
function generateCompanyCardFromData(company, index) {
const location = company.address?.toLowerCase() || '';
let flagHtml = '';
if (location.includes('india')) {
flagHtml = '<div class="w-8 h-6 bg-gradient-to-b from-orange-400 via-white to-green-500 rounded border border-gray-300 relative"><div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-4 h-4 border-2 border-blue-800 rounded-full"></div></div>';
} else if (location.includes('united kingdom') || location.includes('uk')) {
flagHtml = '<div class="w-8 h-6 bg-blue-800 rounded border border-gray-300 relative"><div class="absolute inset-0 bg-gradient-to-br from-red-600 via-white to-red-600" style="clip-path: polygon(0 0, 100% 0, 0 100%)"></div><div class="absolute inset-0 bg-gradient-to-bl from-red-600 via-white to-red-600" style="clip-path: polygon(100% 0, 100% 100%, 0 100%)"></div><div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-1 h-full bg-red-600"></div><div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-full h-1 bg-red-600"></div></div>';
} else if (location.includes('usa') || location.includes('united states')) {
flagHtml = '<div class="w-8 h-6 bg-red-600 rounded border border-gray-300 relative"><div class="absolute top-0 left-0 w-3 h-3 bg-blue-800"></div><div class="absolute top-1 left-0 w-full h-0.5 bg-white"></div><div class="absolute top-2 left-0 w-full h-0.5 bg-white"></div></div>';
} else if (location.includes('uae') || location.includes('dubai')) {
flagHtml = '<div class="w-8 h-6 bg-gradient-to-b from-green-600 via-white via-black to-red-600 rounded border border-gray-300"></div>';
} else if (location.includes('china')) {
flagHtml = '<div class="w-8 h-6 bg-red-600 rounded border border-gray-300 relative"><div class="absolute top-1 left-1 text-yellow-400 text-xs">⭐</div></div>';
} else {
flagHtml = '<div class="w-8 h-6 bg-gray-300 rounded border border-gray-300 flex items-center justify-center text-xs">🌍</div>';
}
return `
<div class="bg-white rounded-xl shadow-sm border border-gray-200 hover:shadow-lg transition-all duration-300 card-hover">
<div class="p-4 lg:p-6">
<div class="flex items-center gap-3 mb-4">
${flagHtml}
<h3 class="text-base lg:text-lg font-bold text-blue-700 hover:text-blue-800 cursor-pointer line-clamp-2">${company.name}</h3>
</div>
<div class="space-y-3 text-sm text-gray-700">
${company.phone ? `
<div class="flex items-center gap-2">
<i class="fas fa-phone text-green-600 w-4"></i>
<span class="break-all">${company.phone}</span>
</div>
` : ''}
${company.email ? `
<div class="flex items-center gap-2">
<i class="fas fa-envelope text-blue-600 w-4"></i>
<a href="mailto:${company.email}" class="text-blue-600 hover:text-blue-800 break-all">${company.email}</a>
</div>
` : ''}
${company.website ? `
<div class="flex items-center gap-2">
<i class="fas fa-globe text-purple-600 w-4"></i>
<a href="${company.website}" class="text-blue-600 hover:text-blue-800 underline break-all" target="_blank" rel="noopener">${company.website}</a>
</div>
` : ''}
${company.address ? `
<div class="flex items-start gap-2">
<i class="fas fa-map-marker-alt text-red-600 w-4 mt-0.5"></i>
<span class="text-gray-600">${company.address}</span>
</div>
` : ''}
</div>
${company.products && company.products.length > 0 ? `
<div class="mt-4 flex flex-wrap gap-2">
${company.products.slice(0, 4).map(product => `
<span class="bg-blue-50 text-blue-700 px-2 py-1 rounded-full text-xs font-medium">${product}</span>
`).join('')}
${company.products.length > 4 ? `
<span class="bg-gray-100 text-gray-600 px-2 py-1 rounded-full text-xs font-medium">+${company.products.length - 4} more</span>
` : ''}
</div>
` : ''}
<div class="mt-4 pt-4 border-t border-gray-100 flex gap-2">
</div>
</div>
</div>
`;
}
// Update stats for both desktop and mobile
function updateStats(resultCount, responseTime) {
totalSearchCount++;
totalResultCount += resultCount;
// Desktop stats
document.getElementById('totalSearches').textContent = totalSearchCount;
document.getElementById('totalResults').textContent = totalResultCount;
document.getElementById('responseTime').textContent = responseTime + 'ms';
// Mobile stats
document.getElementById('mobileSearches').textContent = totalSearchCount;
document.getElementById('mobileResults').textContent = totalResultCount;
document.getElementById('mobileResponseTime').textContent = responseTime + 'ms';
updateLastUpdated();
}
// Update last updated time
function updateLastUpdated() {
document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
}
// Quick search functionality
function setQuickSearch(query) {
document.getElementById('query').value = query;
window.callWorkflow();
}
// Export functionality
function exportResults() {
if (currentSearchData.length === 0) return;
const headers = ['Company Name', 'Phone', 'Email', 'Website', 'Address', 'Products'];
const csvContent = [
headers.join(','),
...currentSearchData.map(company => [
`"${company.name}"`,
`"${company.phone || ''}"`,
`"${company.email || ''}"`,
`"${company.website || ''}"`,
`"${company.address || ''}"`,
`"${(company.products || []).join('; ')}"`
].join(','))
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `search_results_${new Date().toISOString().split('T')[0]}.csv`;
a.click();
window.URL.revokeObjectURL(url);
}
// Share functionality
function shareResults() {
if (navigator.share && currentSearchData.length > 0) {
navigator.share({
title: 'Trade Search Results',
text: `Found ${currentSearchData.length} companies in search results`,
url: window.location.href
});
} else {
navigator.clipboard.writeText(window.location.href).then(() => {
alert('Link copied to clipboard!');
});
}
}
// Load more functionality
function loadMoreResults() {
document.getElementById('loadMoreSection').classList.add('hidden');
}
// Additional functionality
function viewCompanyDetails(companyName) {
alert(`Viewing details for: ${companyName}`);
}
function saveCompany(companyName) {
alert(`Saved: ${companyName}`);
}
function shareCompany(companyName) {
if (navigator.share) {
navigator.share({
title: companyName,
text: `Check out this company: ${companyName}`,
url: window.location.href
});
} else {
navigator.clipboard.writeText(`${companyName} - ${window.location.href}`).then(() => {
alert('Company info copied to clipboard!');
});
}
}
// Desktop CRUD operation
async function callCrudOperation() {
const queryInput = document.getElementById('crudCommand');
const query = queryInput.value.trim();
if (!query) {
alert('Please enter a CRUD command');
return;
}
const crudBtn = document.getElementById('crudBtn');
crudBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Executing...';
crudBtn.disabled = true;
try {
const response = await fetch(`https://thecyberlearn.app.n8n.cloud/webhook/search-airtable?query=${encodeURIComponent(query)}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
const raw = result?.content?.originalMessage || result?.content?.message || '✅ Operation completed.';
const clean = raw
.replace(/<[^>]*>/g, '')
.replace(/\*\*/g, '')
.replace(/Complete dataset returned.*included/gi, '')
.trim();
const operation = result?.operation || 'info';
const type = result?.content?.messageType || 'success';
const success = result?.success;
// Determine toast style
let bg = 'bg-emerald-100', border = 'border-emerald-300', text = 'text-emerald-900', icon = 'fa-check-circle', title = 'Success!';
if (/cannot create/i.test(clean)) {
bg = 'bg-yellow-50'; border = 'border-yellow-200'; text = 'text-yellow-800'; icon = 'fa-clone'; title = 'Duplicate Found';
} else if (!success && (operation === 'delete' || type === 'error')) {
bg = 'bg-red-100'; border = 'border-red-300'; text = 'text-red-800'; icon = 'fa-times-circle'; title = 'Error!';
} else if (operation === 'delete' && success) {
bg = 'bg-rose-100'; border = 'border-rose-300'; text = 'text-rose-800'; icon = 'fa-trash-alt'; title = 'Deleted!';
} else if (operation === 'update') {
bg = 'bg-amber-100'; border = 'border-amber-300'; text = 'text-amber-900'; icon = 'fa-pen'; title = 'Updated!';
} else if (operation === 'read') {
bg = 'bg-blue-100'; border = 'border-blue-300'; text = 'text-blue-900'; icon = 'fa-info-circle'; title = 'Fetched!';
}
// Build toast element
const toast = document.createElement('div');
toast.className = `fade-up fixed bottom-6 right-6 max-w-sm w-full z-50 shadow-xl rounded-xl border px-4 py-3 ${bg} ${border} ${text} animate-slide-up`;
toast.innerHTML = `
<div class="flex justify-between items-start gap-3">
<div class="flex items-start gap-2">
<i class="fas ${icon} mt-1 text-lg"></i>
<div>
<div class="font-semibold">${title}</div>
<div class="mt-1 space-y-1 text-sm leading-snug">${clean.split('\n').map(line => `<div class="animate-fade">${line}</div>`).join('')}</div>
</div>
</div>
<button onclick="this.parentElement.parentElement.remove()" class="text-xl font-bold hover:text-black/50 transition">×</button>
</div>
`;
document.body.appendChild(toast);
queryInput.value = '';
setTimeout(() => toast.remove(), 8000);
} catch (error) {
console.error('CRUD error:', error);
alert('❌ ' + error.message);
} finally {
crudBtn.innerHTML = '<i class="fas fa-terminal"></i> <span>Execute</span>';
crudBtn.disabled = false;
}
}
// Mobile CRUD operation
async function callMobileCrudOperation() {
const queryInput = document.getElementById('mobileCrudCommand');
const query = queryInput.value.trim();
if (!query) {
alert('Please enter a CRUD command');
return;
}
const crudBtn = document.getElementById('mobileCrudBtn');
crudBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Executing...';
crudBtn.disabled = true;
try {
const response = await fetch(`https://thecyberlearn.app.n8n.cloud/webhook/search-airtable?query=${encodeURIComponent(query)}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
const raw = result?.content?.originalMessage || result?.content?.message || '✅ Operation completed.';
const clean = raw
.replace(/<[^>]*>/g, '')
.replace(/\*\*/g, '')
.replace(/Complete dataset returned.*included/gi, '')
.trim();
const operation = result?.operation || 'info';
const type = result?.content?.messageType || 'success';
const success = result?.success;
// Determine toast style
let bg = 'bg-emerald-100', border = 'border-emerald-300', text = 'text-emerald-900', icon = 'fa-check-circle', title = 'Success!';
if (/cannot create/i.test(clean)) {
bg = 'bg-yellow-50'; border = 'border-yellow-200'; text = 'text-yellow-800'; icon = 'fa-clone'; title = 'Duplicate Found';
} else if (!success && (operation === 'delete' || type === 'error')) {
bg = 'bg-red-100'; border = 'border-red-300'; text = 'text-red-800'; icon = 'fa-times-circle'; title = 'Error!';
} else if (operation === 'delete' && success) {
bg = 'bg-rose-100'; border = 'border-rose-300'; text = 'text-rose-800'; icon = 'fa-trash-alt'; title = 'Deleted!';
} else if (operation === 'update') {
bg = 'bg-amber-100'; border = 'border-amber-300'; text = 'text-amber-900'; icon = 'fa-pen'; title = 'Updated!';
} else if (operation === 'read') {
bg = 'bg-blue-100'; border = 'border-blue-300'; text = 'text-blue-900'; icon = 'fa-info-circle'; title = 'Fetched!';
}
// Build toast element
const toast = document.createElement('div');
toast.className = `fade-up fixed bottom-6 left-4 right-4 max-w-sm mx-auto z-50 shadow-xl rounded-xl border px-4 py-3 ${bg} ${border} ${text} animate-slide-up`;
toast.innerHTML = `
<div class="flex justify-between items-start gap-3">
<div class="flex items-start gap-2">
<i class="fas ${icon} mt-1 text-lg"></i>
<div>
<div class="font-semibold">${title}</div>
<div class="mt-1 space-y-1 text-sm leading-snug">${clean.split('\n').map(line => `<div class="animate-fade">${line}</div>`).join('')}</div>
</div>
</div>
<button onclick="this.parentElement.parentElement.remove()" class="text-xl font-bold hover:text-black/50 transition">×</button>
</div>
`;
document.body.appendChild(toast);
queryInput.value = '';
// Auto-collapse the mobile execute section after successful operation
const content = document.getElementById('mobileExecuteContent');
const chevron = document.getElementById('executeChevron');
content.classList.remove('open');
chevron.style.transform = 'rotate(0deg)';
setTimeout(() => toast.remove(), 8000);
} catch (error) {
console.error('Mobile CRUD error:', error);
alert('❌ ' + error.message);
} finally {
crudBtn.innerHTML = '<i class="fas fa-terminal"></i> <span>Execute</span>';
crudBtn.disabled = false;
}
}
// Make functions available globally
window.callWorkflow = callWorkflow;
window.setQuickSearch = setQuickSearch;
window.exportResults = exportResults;
window.shareResults = shareResults;
window.loadMoreResults = loadMoreResults;
window.saveCompany = saveCompany;
window.callCrudOperation = callCrudOperation;
window.callMobileCrudOperation = callMobileCrudOperation;
window.shareCompany = shareCompany;
window.toggleMobileExecute = toggleMobileExecute;
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Mobile menu functionality
document.getElementById('mobileMenuBtn').addEventListener('click', function() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
sidebar.classList.toggle('hidden');
overlay.classList.toggle('hidden');
});
document.getElementById('sidebarOverlay').addEventListener('click', function() {
document.getElementById('sidebar').classList.add('hidden');
document.getElementById('sidebarOverlay').classList.add('hidden');
});
// Enter key support for search
document.getElementById('query').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
window.callWorkflow();
}
});
// Enter key support for mobile CRUD
document.getElementById('mobileCrudCommand')?.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
window.callMobileCrudOperation();
}
});
// Enter key support for desktop CRUD
document.getElementById('crudCommand')?.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
window.callCrudOperation();
}
});
// Initialize
updateLastUpdated();
});
</script>
</body>
</html>