Spaces:
Running
Running
| <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 ; | |
| background-color: #eef2ff ; | |
| } | |
| .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> |