quote-for / index.html
Ultronprime's picture
Add 3 files
6bb0874 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QuoteSense - Local Quotation Management</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.sidebar {
transition: all 0.3s ease;
}
.sidebar.collapsed {
width: 70px;
}
.sidebar.collapsed .nav-text {
display: none;
}
.sidebar.collapsed .logo-text {
display: none;
}
.sidebar.collapsed .expand-icon {
transform: rotate(180deg);
}
.drag-active {
border-color: #4f46e5 !important;
background-color: #eef2ff !important;
}
.progress-bar {
transition: width 0.3s ease;
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.rotate {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.document-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.document-card {
transition: all 0.2s ease;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s ease-in;
}
</style>
</head>
<body class="bg-gray-50 font-sans">
<div class="flex h-screen overflow-hidden">
<!-- Sidebar -->
<div class="sidebar bg-indigo-700 text-white w-64 flex flex-col">
<div class="p-4 flex items-center justify-between border-b border-indigo-600">
<div class="flex items-center">
<i class="fas fa-file-invoice-dollar text-2xl mr-3"></i>
<span class="logo-text text-xl font-bold">QuoteSense</span>
</div>
<button id="toggle-sidebar" class="expand-icon text-white p-1 rounded-full hover:bg-indigo-600 transition">
<i class="fas fa-chevron-left"></i>
</button>
</div>
<nav class="flex-1 overflow-y-auto py-4">
<ul>
<li>
<a href="#" class="nav-item flex items-center px-4 py-3 hover:bg-indigo-600 transition" data-tab="dashboard">
<i class="fas fa-tachometer-alt mr-3"></i>
<span class="nav-text">Dashboard</span>
</a>
</li>
<li>
<a href="#" class="nav-item flex items-center px-4 py-3 hover:bg-indigo-600 transition" data-tab="upload">
<i class="fas fa-cloud-upload-alt mr-3"></i>
<span class="nav-text">Upload Documents</span>
</a>
</li>
<li>
<a href="#" class="nav-item flex items-center px-4 py-3 hover:bg-indigo-600 transition" data-tab="documents">
<i class="fas fa-file-alt mr-3"></i>
<span class="nav-text">My Documents</span>
</a>
</li>
<li>
<a href="#" class="nav-item flex items-center px-4 py-3 hover:bg-indigo-600 transition" data-tab="items">
<i class="fas fa-list-ul mr-3"></i>
<span class="nav-text">Item Database</span>
</a>
</li>
<li>
<a href="#" class="nav-item flex items-center px-4 py-3 hover:bg-indigo-600 transition" data-tab="assistant">
<i class="fas fa-robot mr-3"></i>
<span class="nav-text">RAG Assistant</span>
</a>
</li>
</ul>
</nav>
<div class="p-4 border-t border-indigo-600">
<div class="flex items-center">
<div class="w-8 h-8 rounded-full bg-indigo-500 flex items-center justify-center mr-2">
<i class="fas fa-user text-sm"></i>
</div>
<div class="nav-text">
<div class="text-sm font-medium">Local User</div>
<div class="text-xs text-indigo-200">Offline Mode</div>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 overflow-auto">
<header class="bg-white shadow-sm p-4">
<div class="flex justify-between items-center">
<h1 class="text-2xl font-bold text-gray-800" id="page-title">Dashboard</h1>
<div class="flex items-center space-x-4">
<button id="rebuild-index" class="bg-indigo-100 text-indigo-700 px-3 py-1 rounded-md text-sm font-medium hover:bg-indigo-200 transition hidden">
<i class="fas fa-sync-alt mr-1"></i> Rebuild Index
</button>
<div class="relative">
<button class="bg-gray-100 p-2 rounded-full hover:bg-gray-200 transition">
<i class="fas fa-bell text-gray-600"></i>
</button>
<span class="absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full"></span>
</div>
</div>
</div>
</header>
<main class="p-4">
<!-- Dashboard Tab -->
<div id="dashboard" class="tab-content active">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-lg shadow p-4">
<div class="flex items-center">
<div class="p-3 rounded-full bg-indigo-100 text-indigo-600 mr-4">
<i class="fas fa-file-alt text-xl"></i>
</div>
<div>
<p class="text-gray-500 text-sm">Documents</p>
<h3 class="text-2xl font-bold" id="doc-count">0</h3>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-4">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100 text-green-600 mr-4">
<i class="fas fa-list-ul text-xl"></i>
</div>
<div>
<p class="text-gray-500 text-sm">Extracted Items</p>
<h3 class="text-2xl font-bold" id="item-count">0</h3>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-4">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100 text-blue-600 mr-4">
<i class="fas fa-percentage text-xl"></i>
</div>
<div>
<p class="text-gray-500 text-sm">Avg Confidence</p>
<h3 class="text-2xl font-bold" id="avg-confidence">0%</h3>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-4">
<div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100 text-purple-600 mr-4">
<i class="fas fa-database text-xl"></i>
</div>
<div>
<p class="text-gray-500 text-sm">Storage Used</p>
<h3 class="text-2xl font-bold" id="storage-used">0 MB</h3>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white rounded-lg shadow p-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold">Recent Uploads</h2>
<a href="#" class="text-sm text-indigo-600 hover:underline" data-tab="documents">View All</a>
</div>
<div class="space-y-3" id="recent-uploads">
<div class="text-center py-8 text-gray-400">
<i class="fas fa-file-upload text-4xl mb-2"></i>
<p>No recent uploads</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold">Quick Actions</h2>
</div>
<div class="grid grid-cols-2 gap-4">
<a href="#" class="quick-action bg-indigo-50 text-indigo-700 rounded-lg p-4 flex flex-col items-center justify-center hover:bg-indigo-100 transition" data-tab="upload">
<i class="fas fa-cloud-upload-alt text-2xl mb-2"></i>
<span>Upload Documents</span>
</a>
<a href="#" class="quick-action bg-green-50 text-green-700 rounded-lg p-4 flex flex-col items-center justify-center hover:bg-green-100 transition" data-tab="items">
<i class="fas fa-search text-2xl mb-2"></i>
<span>Search Items</span>
</a>
<a href="#" class="quick-action bg-blue-50 text-blue-700 rounded-lg p-4 flex flex-col items-center justify-center hover:bg-blue-100 transition" data-tab="assistant">
<i class="fas fa-robot text-2xl mb-2"></i>
<span>Ask Assistant</span>
</a>
<button id="clear-data" class="quick-action bg-red-50 text-red-700 rounded-lg p-4 flex flex-col items-center justify-center hover:bg-red-100 transition">
<i class="fas fa-trash-alt text-2xl mb-2"></i>
<span>Clear All Data</span>
</button>
</div>
</div>
</div>
</div>
<!-- Upload Documents Tab -->
<div id="upload" class="tab-content">
<div class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-xl font-semibold mb-4">Upload Quotation Documents</h2>
<div class="mb-6">
<div id="drop-zone" class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer hover:border-indigo-300 transition">
<i class="fas fa-cloud-upload-alt text-4xl text-indigo-500 mb-3"></i>
<h3 class="text-lg font-medium text-gray-700 mb-1">Drag & drop files here</h3>
<p class="text-gray-500 mb-4">or click to browse files</p>
<input type="file" id="file-input" class="hidden" multiple accept=".pdf,.jpg,.jpeg,.png,.webp">
<button id="browse-btn" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 transition">
Select Files
</button>
</div>
</div>
<div class="mb-4">
<h3 class="font-medium mb-2">Processing Options</h3>
<div class="flex flex-wrap gap-4">
<label class="flex items-center">
<input type="checkbox" class="form-checkbox text-indigo-600" checked>
<span class="ml-2">Extract line items</span>
</label>
<label class="flex items-center">
<input type="checkbox" class="form-checkbox text-indigo-600" checked>
<span class="ml-2">Identify suppliers</span>
</label>
<label class="flex items-center">
<input type="checkbox" class="form-checkbox text-indigo-600" checked>
<span class="ml-2">Extract prices</span>
</label>
</div>
</div>
<div id="upload-queue" class="mt-6 space-y-3">
<h3 class="font-medium">Upload Queue</h3>
<div id="queue-empty" class="text-center py-4 text-gray-400">
<i class="fas fa-inbox text-3xl mb-2"></i>
<p>No files in queue</p>
</div>
</div>
</div>
</div>
<!-- My Documents Tab -->
<div id="documents" class="tab-content">
<div class="bg-white rounded-lg shadow p-6 mb-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold">My Documents</h2>
<div class="relative">
<input type="text" placeholder="Search documents..." class="pl-8 pr-4 py-2 border rounded-md focus:outline-none focus:ring-1 focus:ring-indigo-500">
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Document</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Supplier</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Items</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200" id="documents-list">
<tr>
<td colspan="6" class="px-6 py-4 text-center text-gray-400">
<i class="fas fa-file-alt text-3xl mb-2"></i>
<p>No documents uploaded yet</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Item Database Tab -->
<div id="items" class="tab-content">
<div class="bg-white rounded-lg shadow p-6 mb-6">
<div class="flex flex-col md:flex-row md:justify-between md:items-center mb-6 gap-4">
<h2 class="text-xl font-semibold">Item Database</h2>
<div class="flex flex-col md:flex-row gap-3">
<div class="relative flex-1">
<input type="text" id="item-search" placeholder="Search items..." class="pl-8 pr-4 py-2 border rounded-md w-full focus:outline-none focus:ring-1 focus:ring-indigo-500">
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
<button id="export-csv" class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition flex items-center justify-center">
<i class="fas fa-file-export mr-2"></i> Export CSV
</button>
</div>
</div>
<div class="mb-4">
<div class="flex flex-wrap gap-4">
<div class="flex-1 min-w-[200px]">
<label class="block text-sm font-medium text-gray-700 mb-1">Supplier</label>
<select class="filter-select w-full border rounded-md p-2">
<option value="">All Suppliers</option>
</select>
</div>
<div class="flex-1 min-w-[200px]">
<label class="block text-sm font-medium text-gray-700 mb-1">Price Range</label>
<select class="filter-select w-full border rounded-md p-2">
<option value="">Any Price</option>
<option value="0-100">$0 - $100</option>
<option value="100-500">$100 - $500</option>
<option value="500-1000">$500 - $1,000</option>
<option value="1000+">$1,000+</option>
</select>
</div>
<div class="flex-1 min-w-[200px]">
<label class="block text-sm font-medium text-gray-700 mb-1">Sort By</label>
<select class="filter-select w-full border rounded-md p-2">
<option value="date-desc">Date (Newest)</option>
<option value="date-asc">Date (Oldest)</option>
<option value="price-desc">Price (High to Low)</option>
<option value="price-asc">Price (Low to High)</option>
</select>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Item</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Supplier</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Price</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Document</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Confidence</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200" id="items-list">
<tr>
<td colspan="6" class="px-6 py-4 text-center text-gray-400">
<i class="fas fa-list-ul text-3xl mb-2"></i>
<p>No items extracted yet</p>
</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-4 flex justify-between items-center">
<div class="text-sm text-gray-500">
Showing <span id="items-start">0</span> to <span id="items-end">0</span> of <span id="items-total">0</span> items
</div>
<div class="flex space-x-2">
<button class="pagination-btn bg-gray-100 text-gray-700 px-3 py-1 rounded-md hover:bg-gray-200 disabled:opacity-50" disabled>
<i class="fas fa-chevron-left"></i>
</button>
<button class="pagination-btn bg-gray-100 text-gray-700 px-3 py-1 rounded-md hover:bg-gray-200 disabled:opacity-50" disabled>
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
<!-- RAG Assistant Tab -->
<div id="assistant" class="tab-content">
<div class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-xl font-semibold mb-4">RAG Assistant</h2>
<p class="text-gray-600 mb-6">Ask natural language questions about your quotation data. All processing happens locally in your browser.</p>
<div class="mb-6">
<div class="relative">
<textarea id="assistant-query" rows="3" class="w-full p-4 border rounded-lg focus:outline-none focus:ring-1 focus:ring-indigo-500" placeholder="Example: What are the cheapest office chairs from IKEA?"></textarea>
<button id="ask-assistant" class="absolute right-3 bottom-3 bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 transition flex items-center">
<i class="fas fa-paper-plane mr-2"></i> Ask
</button>
</div>
</div>
<div id="assistant-response" class="hidden">
<div class="bg-indigo-50 rounded-lg p-4 mb-4">
<div class="flex items-start">
<div class="flex-shrink-0 mr-3">
<div class="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center">
<i class="fas fa-robot text-indigo-600"></i>
</div>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-medium mb-2">Answer</h3>
<div id="assistant-answer" class="prose max-w-none">
<!-- Answer will be inserted here -->
</div>
</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="font-medium mb-3">Sources</h3>
<div id="assistant-sources" class="space-y-3">
<!-- Sources will be inserted here -->
</div>
</div>
</div>
<div id="assistant-empty" class="text-center py-8 text-gray-400">
<i class="fas fa-comment-alt text-4xl mb-2"></i>
<p>Ask a question about your quotation data</p>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-semibold mb-4">Example Questions</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<button class="example-question bg-gray-50 hover:bg-gray-100 p-3 rounded-lg text-left transition">
<div class="font-medium">What are the average prices for Dell laptops?</div>
<div class="text-sm text-gray-500 mt-1">Get price statistics for a specific product</div>
</button>
<button class="example-question bg-gray-50 hover:bg-gray-100 p-3 rounded-lg text-left transition">
<div class="font-medium">Find the cheapest office chairs</div>
<div class="text-sm text-gray-500 mt-1">Find budget-friendly options</div>
</button>
<button class="example-question bg-gray-50 hover:bg-gray-100 p-3 rounded-lg text-left transition">
<div class="font-medium">Show me all items from IKEA</div>
<div class="text-sm text-gray-500 mt-1">Filter by supplier</div>
</button>
<button class="example-question bg-gray-50 hover:bg-gray-100 p-3 rounded-lg text-left transition">
<div class="font-medium">What furniture items are priced over $500?</div>
<div class="text-sm text-gray-500 mt-1">Find premium items</div>
</button>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- Modal -->
<div id="modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
<div class="p-4 border-b">
<h3 class="text-lg font-semibold" id="modal-title">Modal Title</h3>
<button id="close-modal" class="absolute top-4 right-4 text-gray-400 hover:text-gray-500">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-4" id="modal-content">
<!-- Modal content will be inserted here -->
</div>
<div class="p-4 border-t flex justify-end space-x-3">
<button id="cancel-modal" class="px-4 py-2 border rounded-md hover:bg-gray-50">Cancel</button>
<button id="confirm-modal" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Confirm</button>
</div>
</div>
</div>
<script>
// DOM Elements
const sidebar = document.querySelector('.sidebar');
const toggleSidebar = document.getElementById('toggle-sidebar');
const navItems = document.querySelectorAll('.nav-item');
const tabContents = document.querySelectorAll('.tab-content');
const pageTitle = document.getElementById('page-title');
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
const browseBtn = document.getElementById('browse-btn');
const uploadQueue = document.getElementById('upload-queue');
const queueEmpty = document.getElementById('queue-empty');
const clearDataBtn = document.getElementById('clear-data');
const rebuildIndexBtn = document.getElementById('rebuild-index');
const assistantQuery = document.getElementById('assistant-query');
const askAssistantBtn = document.getElementById('ask-assistant');
const assistantResponse = document.getElementById('assistant-response');
const assistantEmpty = document.getElementById('assistant-empty');
const exampleQuestions = document.querySelectorAll('.example-question');
const modal = document.getElementById('modal');
const closeModal = document.getElementById('close-modal');
const cancelModal = document.getElementById('cancel-modal');
const confirmModal = document.getElementById('confirm-modal');
const modalTitle = document.getElementById('modal-title');
const modalContent = document.getElementById('modal-content');
// Mock database (in a real app, this would be IndexedDB)
let mockDB = {
documents: [],
items: [],
settings: {
vectorIndexBuilt: false
}
};
// Initialize the app
function init() {
// Load mock data
loadMockData();
// Update dashboard stats
updateDashboardStats();
// Set up event listeners
setupEventListeners();
}
// Load some mock data for demonstration
function loadMockData() {
// Sample documents
mockDB.documents = [
{
id: 'doc1',
name: 'IKEA_Office_Furniture_Quote.pdf',
supplier: 'IKEA',
date: '2023-05-15',
status: 'processed',
items: 12,
confidence: 92
},
{
id: 'doc2',
name: 'Dell_Laptops_Quote.pdf',
supplier: 'Dell',
date: '2023-06-02',
status: 'processed',
items: 8,
confidence: 88
},
{
id: 'doc3',
name: 'Staples_Supplies_Quote.jpg',
supplier: 'Staples',
date: '2023-06-10',
status: 'processing',
items: 0,
confidence: 0
}
];
// Sample items
mockDB.items = [
{
id: 'item1',
documentId: 'doc1',
name: 'MARKUS Office Chair',
description: 'Ergonomic office chair with adjustable height',
supplier: 'IKEA',
price: 199.99,
date: '2023-05-15',
confidence: 95
},
{
id: 'item2',
documentId: 'doc1',
name: 'BEKANT Desk',
description: 'Large office desk with adjustable legs',
supplier: 'IKEA',
price: 249.99,
date: '2023-05-15',
confidence: 92
},
{
id: 'item3',
documentId: 'doc1',
name: 'ALEX Drawer Unit',
description: '5-drawer storage unit on casters',
supplier: 'IKEA',
price: 129.99,
date: '2023-05-15',
confidence: 90
},
{
id: 'item4',
documentId: 'doc2',
name: 'XPS 13 Laptop',
description: '13.4" FHD+ InfinityEdge Touch Laptop',
supplier: 'Dell',
price: 1299.99,
date: '2023-06-02',
confidence: 88
},
{
id: 'item5',
documentId: 'doc2',
name: 'Latitude 5420',
description: '14" Business Laptop, Core i7',
supplier: 'Dell',
price: 1599.99,
date: '2023-06-02',
confidence: 85
},
{
id: 'item6',
documentId: 'doc2',
name: 'UltraSharp 27 Monitor',
description: '27" 4K UHD Monitor with USB-C',
supplier: 'Dell',
price: 699.99,
date: '2023-06-02',
confidence: 90
}
];
}
// Update dashboard statistics
function updateDashboardStats() {
document.getElementById('doc-count').textContent = mockDB.documents.length;
document.getElementById('item-count').textContent = mockDB.items.length;
// Calculate average confidence
let totalConfidence = 0;
let count = 0;
mockDB.documents.forEach(doc => {
if (doc.status === 'processed') {
totalConfidence += doc.confidence;
count++;
}
});
const avgConfidence = count > 0 ? Math.round(totalConfidence / count) : 0;
document.getElementById('avg-confidence').textContent = `${avgConfidence}%`;
// Mock storage used (in a real app, calculate actual IndexedDB usage)
const storageMB = (mockDB.documents.length * 0.5 + mockDB.items.length * 0.01).toFixed(2);
document.getElementById('storage-used').textContent = `${storageMB} MB`;
// Update recent uploads
updateRecentUploads();
// Update documents list
updateDocumentsList();
// Update items list
updateItemsList();
// Show rebuild index button if we have items but no index
if (mockDB.items.length > 0 && !mockDB.settings.vectorIndexBuilt) {
rebuildIndexBtn.classList.remove('hidden');
} else {
rebuildIndexBtn.classList.add('hidden');
}
}
// Update recent uploads section
function updateRecentUploads() {
const recentUploadsContainer = document.getElementById('recent-uploads');
if (mockDB.documents.length === 0) {
recentUploadsContainer.innerHTML = `
<div class="text-center py-8 text-gray-400">
<i class="fas fa-file-upload text-4xl mb-2"></i>
<p>No recent uploads</p>
</div>
`;
return;
}
// Sort by date (newest first) and take up to 3
const sortedDocs = [...mockDB.documents].sort((a, b) => new Date(b.date) - new Date(a.date)).slice(0, 3);
let html = '';
sortedDocs.forEach(doc => {
const statusColor = doc.status === 'processed' ? 'bg-green-100 text-green-800' :
doc.status === 'processing' ? 'bg-yellow-100 text-yellow-800' : 'bg-red-100 text-red-800';
html += `
<div class="document-card bg-white border rounded-lg p-3 flex items-center justify-between">
<div class="flex items-center">
<div class="w-10 h-10 rounded-md bg-indigo-50 flex items-center justify-center mr-3">
<i class="fas fa-file-pdf text-indigo-500"></i>
</div>
<div>
<div class="font-medium truncate max-w-[180px]">${doc.name}</div>
<div class="text-xs text-gray-500">${formatDate(doc.date)}</div>
</div>
</div>
<div class="flex items-center">
<span class="text-xs px-2 py-1 rounded-full ${statusColor}">${doc.status}</span>
<button class="ml-2 text-gray-400 hover:text-indigo-600" data-doc-id="${doc.id}">
<i class="fas fa-ellipsis-v"></i>
</button>
</div>
</div>
`;
});
recentUploadsContainer.innerHTML = html;
}
// Update documents list
function updateDocumentsList() {
const documentsList = document.getElementById('documents-list');
if (mockDB.documents.length === 0) {
documentsList.innerHTML = `
<tr>
<td colspan="6" class="px-6 py-4 text-center text-gray-400">
<i class="fas fa-file-alt text-3xl mb-2"></i>
<p>No documents uploaded yet</p>
</td>
</tr>
`;
return;
}
let html = '';
mockDB.documents.forEach(doc => {
const statusColor = doc.status === 'processed' ? 'text-green-600' :
doc.status === 'processing' ? 'text-yellow-600' : 'text-red-600';
html += `
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<div class="h-10 w-10 rounded-md bg-indigo-50 flex items-center justify-center">
<i class="fas fa-file-pdf text-indigo-500"></i>
</div>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">${doc.name}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">${doc.supplier || 'Unknown'}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">${doc.items}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${statusColor}">
${doc.status}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
${formatDate(doc.date)}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button class="text-indigo-600 hover:text-indigo-900 mr-3" data-doc-id="${doc.id}">
<i class="fas fa-eye"></i>
</button>
<button class="text-red-600 hover:text-red-900" data-doc-id="${doc.id}">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
`;
});
documentsList.innerHTML = html;
}
// Update items list
function updateItemsList() {
const itemsList = document.getElementById('items-list');
if (mockDB.items.length === 0) {
itemsList.innerHTML = `
<tr>
<td colspan="6" class="px-6 py-4 text-center text-gray-400">
<i class="fas fa-list-ul text-3xl mb-2"></i>
<p>No items extracted yet</p>
</td>
</tr>
`;
return;
}
let html = '';
mockDB.items.forEach(item => {
const doc = mockDB.documents.find(d => d.id === item.documentId) || {};
html += `
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">${item.name}</div>
</td>
<td class="px-6 py-4">
<div class="text-sm text-gray-500 max-w-xs truncate">${item.description}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">${item.supplier}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">$${item.price.toFixed(2)}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-500">${doc.name || 'Unknown'}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="w-16 bg-gray-200 rounded-full h-2 mr-2">
<div class="bg-green-500 h-2 rounded-full" style="width: ${item.confidence}%"></div>
</div>
<span class="text-xs text-gray-500">${item.confidence}%</span>
</div>
</td>
</tr>
`;
});
itemsList.innerHTML = html;
// Update pagination info
document.getElementById('items-start').textContent = 1;
document.getElementById('items-end').textContent = mockDB.items.length;
document.getElementById('items-total').textContent = mockDB.items.length;
}
// Set up event listeners
function setupEventListeners() {
// Toggle sidebar
toggleSidebar.addEventListener('click', () => {
sidebar.classList.toggle('collapsed');
});
// Tab navigation
navItems.forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const tabId = item.getAttribute('data-tab');
// Update active tab
navItems.forEach(navItem => navItem.classList.remove('bg-indigo-600'));
item.classList.add('bg-indigo-600');
// Show corresponding content
tabContents.forEach(content => content.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
// Update page title
pageTitle.textContent = item.querySelector('.nav-text').textContent;
// Special handling for certain tabs
if (tabId === 'assistant') {
assistantResponse.classList.add('hidden');
assistantEmpty.classList.remove('hidden');
}
});
});
// File upload handling
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drag-active');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-active');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-active');
if (e.dataTransfer.files.length > 0) {
handleFiles(e.dataTransfer.files);
}
});
browseBtn.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length > 0) {
handleFiles(fileInput.files);
}
});
// Clear data button
clearDataBtn.addEventListener('click', () => {
showModal(
'Clear All Data',
'Are you sure you want to delete all uploaded documents and extracted data? This action cannot be undone.',
() => {
mockDB.documents = [];
mockDB.items = [];
mockDB.settings.vectorIndexBuilt = false;
updateDashboardStats();
showToast('All data has been cleared', 'success');
}
);
});
// Rebuild index button
rebuildIndexBtn.addEventListener('click', () => {
showModal(
'Rebuild Vector Index',
'This will rebuild the local search index to improve query performance. It may take a few moments.',
() => {
// Simulate processing
rebuildIndexBtn.innerHTML = '<i class="fas fa-spinner rotate mr-1"></i> Rebuilding...';
rebuildIndexBtn.disabled = true;
setTimeout(() => {
mockDB.settings.vectorIndexBuilt = true;
rebuildIndexBtn.innerHTML = '<i class="fas fa-check mr-1"></i> Index Rebuilt';
setTimeout(() => {
rebuildIndexBtn.innerHTML = '<i class="fas fa-sync-alt mr-1"></i> Rebuild Index';
rebuildIndexBtn.disabled = false;
rebuildIndexBtn.classList.add('hidden');
showToast('Vector index rebuilt successfully', 'success');
}, 2000);
}, 1500);
}
);
});
// RAG Assistant
askAssistantBtn.addEventListener('click', askAssistant);
assistantQuery.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
askAssistant();
}
});
exampleQuestions.forEach(question => {
question.addEventListener('click', () => {
assistantQuery.value = question.querySelector('.font-medium').textContent;
askAssistant();
});
});
// Modal
closeModal.addEventListener('click', () => modal.classList.add('hidden'));
cancelModal.addEventListener('click', () => modal.classList.add('hidden'));
// Quick actions
document.querySelectorAll('.quick-action').forEach(action => {
action.addEventListener('click', (e) => {
if (action.id !== 'clear-data') {
e.preventDefault();
const tabId = action.getAttribute('data-tab');
// Update active tab
navItems.forEach(navItem => navItem.classList.remove('bg-indigo-600'));
document.querySelector(`.nav-item[data-tab="${tabId}"]`).classList.add('bg-indigo-600');
// Show corresponding content
tabContents.forEach(content => content.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
// Update page title
pageTitle.textContent = document.querySelector(`.nav-item[data-tab="${tabId}"] .nav-text`).textContent;
}
});
});
}
// Handle uploaded files
function handleFiles(files) {
queueEmpty.classList.add('hidden');
for (let i = 0; i < files.length; i++) {
const file = files[i];
// Check if file type is supported
if (!file.type.match(/(pdf|image)/) && !file.name.match(/\.(pdf|jpg|jpeg|png|webp)$/i)) {
showToast(`File type not supported: ${file.name}`, 'error');
continue;
}
// Add to upload queue
const fileId = 'file-' + Date.now() + '-' + Math.floor(Math.random() * 1000);
const fileCard = createFileCard(file, fileId);
uploadQueue.appendChild(fileCard);
// Simulate processing
setTimeout(() => {
const progressBar = fileCard.querySelector('.progress-bar');
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 10;
if (progress >= 100) {
progress = 100;
clearInterval(interval);
// Update status to processing
const status = fileCard.querySelector('.file-status');
status.textContent = 'Processing';
status.className = 'file-status text-yellow-600';
// Simulate OCR and extraction
setTimeout(() => {
// Add to mock database
const docId = 'doc-' + Date.now();
const newDoc = {
id: docId,
name: file.name,
supplier: guessSupplier(file.name),
date: new Date().toISOString().split('T')[0],
status: 'processed',
items: Math.floor(Math.random() * 10) + 1,
confidence: Math.floor(Math.random() * 10) + 85
};
mockDB.documents.push(newDoc);
// Add some mock items
const supplier = newDoc.supplier;
const itemsCount = newDoc.items;
const items = generateMockItems(docId, supplier, itemsCount);
mockDB.items.push(...items);
// Update UI
updateDashboardStats();
// Update file card status
status.textContent = 'Completed';
status.className = 'file-status text-green-600';
fileCard.querySelector('.file-actions').innerHTML = `
<button class="text-green-600 hover:text-green-800" data-doc-id="${docId}">
<i class="fas fa-check-circle"></i>
</button>
`;
showToast(`${file.name} processed successfully`, 'success');
}, 1500);
}
progressBar.style.width = `${progress}%`;
fileCard.querySelector('.progress-text').textContent = `${Math.round(progress)}%`;
}, 200);
}, 500);
}
}
// Create file card for upload queue
function createFileCard(file, fileId) {
const card = document.createElement('div');
card.className = 'file-card bg-gray-50 rounded-lg p-3 flex items-center justify-between';
card.dataset.fileId = fileId;
card.innerHTML = `
<div class="flex items-center">
<div class="w-10 h-10 rounded-md bg-indigo-50 flex items-center justify-center mr-3">
<i class="fas ${getFileIcon(file)} text-indigo-500"></i>
</div>
<div>
<div class="font-medium truncate max-w-[180px]">${file.name}</div>
<div class="text-xs text-gray-500">${formatFileSize(file.size)}</div>
</div>
</div>
<div class="flex items-center">
<div class="mr-4 text-xs">
<span class="file-status text-blue-600">Uploading</span>
<div class="w-20 bg-gray-200 rounded-full h-1 mt-1">
<div class="progress-bar bg-blue-500 h-1 rounded-full" style="width: 0%"></div>
</div>
<span class="progress-text text-gray-500">0%</span>
</div>
<div class="file-actions">
<button class="text-red-600 hover:text-red-800">
<i class="fas fa-times"></i>
</button>
</div>
</div>
`;
return card;
}
// Ask the assistant a question
function askAssistant() {
const query = assistantQuery.value.trim();
if (!query) {
showToast('Please enter a question', 'warning');
return;
}
if (mockDB.items.length === 0) {
showToast('No items available to query. Please upload documents first.', 'warning');
return;
}
// Show loading state
askAssistantBtn.innerHTML = '<i class="fas fa-spinner rotate mr-1"></i> Processing';
askAssistantBtn.disabled = true;
// Simulate processing delay
setTimeout(() => {
// Generate mock response based on query
const response = generateMockResponse(query);
// Update UI
assistantEmpty.classList.add('hidden');
assistantResponse.classList.remove('hidden');
document.getElementById('assistant-answer').innerHTML = response.answer;
const sourcesContainer = document.getElementById('assistant-sources');
sourcesContainer.innerHTML = '';
response.sources.forEach(source => {
const sourceElement = document.createElement('div');
sourceElement.className = 'bg-white border rounded p-3 flex items-start';
sourceElement.innerHTML = `
<div class="flex-shrink-0 mr-3">
<div class="w-6 h-6 rounded-full bg-indigo-100 flex items-center justify-center">
<i class="fas fa-file-alt text-indigo-600 text-xs"></i>
</div>
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium">${source.name}</div>
<div class="text-xs text-gray-500">${source.supplier} • $${source.price.toFixed(2)}</div>
<div class="text-xs mt-1">${source.description}</div>
</div>
`;
sourcesContainer.appendChild(sourceElement);
});
// Reset button state
askAssistantBtn.innerHTML = '<i class="fas fa-paper-plane mr-2"></i> Ask';
askAssistantBtn.disabled = false;
// Rebuild index button if not built
if (!mockDB.settings.vectorIndexBuilt) {
rebuildIndexBtn.classList.remove('hidden');
}
}, 1500);
}
// Show modal dialog
function showModal(title, content, confirmCallback) {
modalTitle.textContent = title;
modalContent.textContent = content;
modal.classList.remove('hidden');
// Set up confirm button
confirmModal.onclick = () => {
modal.classList.add('hidden');
if (confirmCallback) confirmCallback();
};
}
// Show toast notification
function showToast(message, type = 'info') {
const colors = {
info: 'bg-blue-500',
success: 'bg-green-500',
warning: 'bg-yellow-500',
error: 'bg-red-500'
};
const toast = document.createElement('div');
toast.className = `fixed bottom-4 right-4 text-white px-4 py-2 rounded-md shadow-lg flex items-center ${colors[type]} fade-in`;
toast.innerHTML = `
<i class="fas ${getToastIcon(type)} mr-2"></i>
<span>${message}</span>
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('opacity-0');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Helper functions
function formatDate(dateString) {
const options = { year: 'numeric', month: 'short', day: 'numeric' };
return new Date(dateString).toLocaleDateString(undefined, options);
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function getFileIcon(file) {
if (file.type.match(/pdf/)) return 'fa-file-pdf';
if (file.type.match(/image/)) return 'fa-file-image';
return 'fa-file-alt';
}
function getToastIcon(type) {
switch (type) {
case 'success': return 'fa-check-circle';
case 'warning': return 'fa-exclamation-triangle';
case 'error': return 'fa-times-circle';
default: return 'fa-info-circle';
}
}
function guessSupplier(filename) {
const lowerName = filename.toLowerCase();
if (lowerName.includes('ikea')) return 'IKEA';
if (lowerName.includes('dell')) return 'Dell';
if (lowerName.includes('hp')) return 'HP';
if (lowerName.includes('lenovo')) return 'Lenovo';
if (lowerName.includes('staples')) return 'Staples';
if (lowerName.includes('amazon')) return 'Amazon';
return 'Unknown Supplier';
}
function generateMockItems(docId, supplier, count) {
const items = [];
const supplierItems = {
'IKEA': [
{ name: 'MARKUS Office Chair', desc: 'Ergonomic office chair with adjustable height', price: 199.99 },
{ name: 'BEKANT Desk', desc: 'Large office desk with adjustable legs', price: 249.99 },
{ name: 'ALEX Drawer Unit', desc: '5-drawer storage unit on casters', price: 129.99 },
{ name: 'MILLBERGET Chair', desc: 'Comfortable office chair', price: 79.99 },
{ name: 'LACK Side Table', desc: 'Lightweight side table', price: 29.99 }
],
'Dell': [
{ name: 'XPS 13 Laptop', desc: '13.4" FHD+ InfinityEdge Touch Laptop', price: 1299.99 },
{ name: 'Latitude 5420', desc: '14" Business Laptop, Core i7', price: 1599.99 },
{ name: 'UltraSharp 27 Monitor', desc: '27" 4K UHD Monitor with USB-C', price: 699.99 },
{ name: 'OptiPlex 7080', desc: 'Compact desktop computer', price: 899.99 },
{ name: 'Wireless Keyboard and Mouse', desc: 'Premium keyboard and mouse combo', price: 89.99 }
],
'Staples': [
{ name: 'Copy Paper', desc: '8.5 x 11 inch copy paper, 500 sheets', price: 5.99 },
{ name: 'Stapler', desc: 'Heavy-duty stapler', price: 12.99 },
{ name: 'Ballpoint Pens', desc: '12-pack of black ballpoint pens', price: 8.99 },
{ name: 'Sticky Notes', desc: '3x3 inch sticky notes, 12 pads', price: 9.99 },
{ name: 'File Folders', desc: 'Letter-size file folders, 25 count', price: 11.99 }
],
'Default': [
{ name: 'Office Chair', desc: 'Standard office chair', price: 149.99 },
{ name: 'Desk', desc: 'Standard office desk', price: 199.99 },
{ name: 'Monitor', desc: '24-inch HD monitor', price: 179.99 },
{ name: 'Keyboard', desc: 'USB keyboard', price: 29.99 },
{ name: 'Mouse', desc: 'Optical mouse', price: 19.99 }
]
};
const sourceItems = supplierItems[supplier] || supplierItems['Default'];
for (let i = 0; i < count; i++) {
const sourceItem = sourceItems[i % sourceItems.length];
const variation = Math.random() * 0.3 - 0.15; // -15% to +15% variation
items.push({
id: 'item-' + Date.now() + '-' + i,
documentId: docId,
name: sourceItem.name,
description: sourceItem.desc,
supplier: supplier,
price: sourceItem.price * (1 + variation),
date: new Date().toISOString().split('T')[0],
confidence: Math.floor(Math.random() * 10) + 85
});
}
return items;
}
function generateMockResponse(query) {
const lowerQuery = query.toLowerCase();
let answer = '';
let relevantItems = [];
if (lowerQuery.includes('average') || lowerQuery.includes('price')) {
// Price statistics question
const product = lowerQuery.includes('laptop') ? 'laptops' :
lowerQuery.includes('chair') ? 'office chairs' :
lowerQuery.includes('desk') ? 'desks' : 'items';
const supplierFilter = lowerQuery.includes('ikea') ? 'IKEA' :
lowerQuery.includes('dell') ? 'Dell' :
lowerQuery.includes('staples') ? 'Staples' : null;
const filteredItems = mockDB.items.filter(item => {
const matchesProduct = item.name.toLowerCase().includes(product.replace(' ', ''));
const matchesSupplier = supplierFilter ? item.supplier === supplierFilter : true;
return matchesProduct && matchesSupplier;
});
if (filteredItems.length > 0) {
const total = filteredItems.reduce((sum, item) => sum + item.price, 0);
const average = total / filteredItems.length;
const min = Math.min(...filteredItems.map(item => item.price));
const max = Math.max(...filteredItems.map(item => item.price));
answer = `The average price for ${supplierFilter ? supplierFilter + ' ' : ''}${product} is $${average.toFixed(2)}. `;
answer += `Prices range from $${min.toFixed(2)} to $${max.toFixed(2)}.`;
relevantItems = filteredItems.slice(0, 3);
} else {
answer = `I couldn't find any ${supplierFilter ? supplierFilter + ' ' : ''}${product} in your quotation data.`;
}
} else if (lowerQuery.includes('cheap') || lowerQuery.includes('lowest')) {
// Cheapest items question
const product = lowerQuery.includes('laptop') ? 'laptop' :
lowerQuery.includes('chair') ? 'chair' :
lowerQuery.includes('desk') ? 'desk' : 'item';
const supplierFilter = lowerQuery.includes('ikea') ? 'IKEA' :
lowerQuery.includes('dell') ? 'Dell' :
lowerQuery.includes('staples') ? 'Staples' : null;
const filteredItems = mockDB.items.filter(item => {
const matchesProduct = item.name.toLowerCase().includes(product);
const matchesSupplier = supplierFilter ? item.supplier === supplierFilter : true;
return matchesProduct && matchesSupplier;
}).sort((a, b) => a.price - b.price);
if (filteredItems.length > 0) {
answer = `The cheapest ${supplierFilter ? supplierFilter + ' ' : ''}${product}s in your quotations are:`;
relevantItems = filteredItems.slice(0, 3);
relevantItems.forEach((item, index) => {
answer += `<br>${index + 1}. ${item.name} - $${item.price.toFixed(2)}`;
});
} else {
answer = `I couldn't find any ${supplierFilter ? supplierFilter + ' ' : ''}${product}s in your quotation data.`;
}
} else if (lowerQuery.includes('ikea') || lowerQuery.includes('dell') || lowerQuery.includes('staples')) {
// Supplier-specific question
const supplier = lowerQuery.includes('ikea') ? 'IKEA' :
lowerQuery.includes('dell') ? 'Dell' : 'Staples';
const filteredItems = mockDB.items.filter(item => item.supplier === supplier);
if (filteredItems.length > 0) {
answer = `You have ${filteredItems.length} items from ${supplier} in your quotations. `;
answer += `Here are some examples:`;
relevantItems = filteredItems.slice(0, 3);
} else {
answer = `I couldn't find any items from ${supplier} in your quotation data.`;
}
} else {
// General question
answer = `I found some relevant items in your quotations:`;
relevantItems = mockDB.items.slice(0, 3);
}
return {
answer: answer,
sources: relevantItems.map(item => ({
name: item.name,
description: item.description,
supplier: item.supplier,
price: item.price
}))
};
}
// Initialize the app
document.addEventListener('DOMContentLoaded', init);
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Ultronprime/quote-for" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>