QuerySphere / frontend /index.html
satyakimitra's picture
Fix: Frontend url handling changed
49838f7
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QuerySphere</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
body {
background-color: #f8fafc;
}
/* Glass morphism effects */
.glass-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* Smooth transitions */
.transition-all {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Chat message animations */
.message-enter {
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Chat bubbles */
.user-message {
background: linear-gradient(135deg, #4f46e5, #6366f1);
color: white;
border-radius: 10px 10px 4px 10px;
margin-left: auto;
max-width: fit-content;
min-width:10px;
max-height: fit-content;
}
.ai-message {
background: white;
border: 1px solid #e2e8f0;
border-radius: 10px 10px 10px 4px;
max-width: fit-content;
min-width: 10px;
max-height: fit-content;
}
.ai-message:hover {
border-color: #c7d2fe;
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.1);
}
/* Custom scrollbar */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Tag styles */
.tag {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
}
.tag-blue {
background: #dbeafe;
color: #1e40af;
}
.tag-green {
background: #d1fae5;
color: #065f46;
}
.tag-purple {
background: #f3e8ff;
color: #6b21a8;
}
.tag-yellow {
background: #fef3c7;
color: #92400e;
}
.tag-red {
background: #fee2e2;
color: #991b1b;
}
/* Source cards */
.source-card {
transition: all 0.2s ease;
border-left: 3px solid transparent;
}
.source-card:hover {
border-left-color: #4f46e5;
transform: translateX(2px);
}
/* Loading animation */
.typing-dots {
display: flex;
align-items: center;
gap: 4px;
}
.typing-dots span {
width: 6px;
height: 6px;
border-radius: 50%;
background: #94a3b8;
animation: typing 1.4s infinite ease-in-out;
}
.typing-dots span:nth-child(1) { animation-delay: -0.32s; }
.typing-dots span:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing {
0%, 80%, 100% { transform: scale(0); opacity: 0.5; }
40% { transform: scale(1); opacity: 1; }
}
/* Gradient backgrounds */
.gradient-bg-primary {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
}
/* Feature card hover effects */
.feature-card {
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.feature-card:hover {
transform: translateY(-4px);
}
.feature-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #4f46e5, #7c3aed);
transform: scaleX(0);
transition: transform 0.3s ease;
}
.feature-card:hover::before {
transform: scaleX(1);
}
/* Step card styles */
.step-card {
position: relative;
padding-left: 60px;
}
.step-number {
position: absolute;
left: 0;
top: 0;
width: 44px;
height: 44px;
background: linear-gradient(135deg, #4f46e5, #7c3aed);
color: white;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 18px;
}
/* RAGAS table styling */
.ragas-good {
background-color: #d1fae5;
color: #065f46;
}
.ragas-fair {
background-color: #fef3c7;
color: #92400e;
}
.ragas-poor {
background-color: #fee2e2;
color: #991b1b;
}
.ragas-na {
background-color: #f1f5f9;
color: #64748b;
}
.ragas-cell {
border-radius: 4px;
padding: 4px 8px;
font-weight: 600;
font-size: 0.75rem;
text-align: center;
min-width: 60px;
}
/* Query type tags */
.query-type-rag {
background-color: #dbeafe;
color: #1e40af;
}
.query-type-non-rag {
background-color: #f3e8ff;
color: #6b21a8;
}
/* File upload zone */
.upload-zone-active {
border-color: #4f46e5;
background-color: #f5f3ff;
}
/* Collapsible sections */
.collapsible-header {
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
}
.collapsible-header:hover {
opacity: 0.8;
}
.collapsible-header i {
transition: transform 0.3s ease;
}
.collapsible-header.collapsed i {
transform: rotate(-90deg);
}
.collapsible-content {
overflow: hidden;
transition: max-height 0.3s ease, opacity 0.3s ease;
max-height: 1000px;
}
.collapsible-content.collapsed {
max-height: 0;
opacity: 0;
}
</style>
</head>
<body class="min-h-screen">
<!-- Landing Page -->
<div id="landing-page" class="min-h-screen">
<!-- Navigation -->
<nav class="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center space-x-3">
<div class="flex items-center space-x-2">
<div class="relative">
<div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
<i class="fas fa-brain text-white text-lg"></i>
<div class="absolute -top-1 -right-1 w-3 h-3 bg-green-400 rounded-full border-2 border-white"></div>
</div>
</div>
<div>
<div class="font-bold text-gray-900 text-lg">QuerySphere</div>
<div class="text-xs text-gray-500 -mt-1">RAG platform for document Q&A with local LLM or Cloud LLM API integration</div>
</div>
</div>
</div>
</div>
</div>
</nav>
<!-- Hero Section -->
<section class="gradient-bg-primary text-white pt-12 pb-20">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center">
<div class="inline-block mb-6">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl px-4 py-2 inline-flex items-center space-x-2">
<span class="text-sm font-medium">Enterprise RAG Platform</span>
<span class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></span>
</div>
</div>
<h1 class="text-5xl font-bold mb-6 leading-tight">
Break Down Information
<span class="block text-indigo-200">Silos Instantly</span>
</h1>
<p class="text-xl text-indigo-100 mb-8 max-w-3xl mx-auto leading-relaxed">
Transform how your organization accesses and utilizes knowledge across documents, folders, and archives with AI-powered search.
</p>
<div class="flex flex-col sm:flex-row justify-center gap-4">
<button onclick="startApp()" class="px-8 py-4 bg-white text-indigo-600 rounded-xl font-semibold hover:bg-gray-50 transition-all shadow-lg hover:shadow-xl text-lg">
<i class="fas fa-play mr-2"></i>
Try It Now
</button>
</div>
</div>
</div>
</section>
<!-- Content Tabs -->
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<!-- Tab Navigation -->
<div class="flex space-x-2 bg-gray-100 p-1 rounded-xl mb-12 max-w-2xl mx-auto">
<button id="tab-features" onclick="showTab('features')" class="flex-1 py-3 px-4 rounded-lg text-center font-medium transition-all nav-tab active">
<i class="fas fa-star mr-2"></i>Features
</button>
<button id="tab-how-to-use" onclick="showTab('how-to-use')" class="flex-1 py-3 px-4 rounded-lg text-center font-medium transition-all nav-tab">
<i class="fas fa-play-circle mr-2"></i>Quick Start
</button>
</div>
<!-- Tab Content -->
<div id="tab-content">
<!-- Features Tab -->
<div id="features-content" class="tab-panel active">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Feature 1 -->
<div class="feature-card bg-white rounded-2xl shadow-sm p-6 border border-gray-200 hover:border-indigo-200">
<div class="w-12 h-12 bg-gradient-to-br from-blue-100 to-blue-50 rounded-xl flex items-center justify-center mb-4">
<i class="fas fa-cloud-upload-alt text-blue-600 text-xl"></i>
</div>
<h3 class="text-lg font-semibold mb-3 text-gray-900">Multi-Format Support</h3>
<p class="text-gray-600 text-sm leading-relaxed mb-4">Upload PDFs, Word docs, text files, and ZIP archives with automatic extraction and processing.</p>
<div class="flex flex-wrap gap-2">
<span class="tag tag-blue">PDF</span>
<span class="tag tag-blue">DOCX</span>
<span class="tag tag-blue">TXT</span>
<span class="tag tag-blue">ZIP</span>
</div>
</div>
<!-- Feature 2 -->
<div class="feature-card bg-white rounded-2xl shadow-sm p-6 border border-gray-200 hover:border-green-200">
<div class="w-12 h-12 bg-gradient-to-br from-green-100 to-green-50 rounded-xl flex items-center justify-center mb-4">
<i class="fas fa-robot text-green-600 text-xl"></i>
</div>
<h3 class="text-lg font-semibold mb-3 text-gray-900">Intelligent Processing</h3>
<p class="text-gray-600 text-sm leading-relaxed mb-4">Adaptive chunking strategies and semantic understanding for optimal information retrieval.</p>
<div class="flex flex-wrap gap-2">
<span class="tag tag-green">AI Chunking</span>
<span class="tag tag-green">Semantic Search</span>
</div>
</div>
<!-- Feature 3 -->
<div class="feature-card bg-white rounded-2xl shadow-sm p-6 border border-gray-200 hover:border-purple-200">
<div class="w-12 h-12 bg-gradient-to-br from-purple-100 to-purple-50 rounded-xl flex items-center justify-center mb-4">
<i class="fas fa-search text-purple-600 text-xl"></i>
</div>
<h3 class="text-lg font-semibold mb-3 text-gray-900">Hybrid Retrieval</h3>
<p class="text-gray-600 text-sm leading-relaxed mb-4">Combines vector similarity with BM25 keyword matching for superior search accuracy.</p>
<div class="flex flex-wrap gap-2">
<span class="tag tag-purple">Vector Search</span>
<span class="tag tag-purple">Keyword Search</span>
</div>
</div>
<!-- Feature 4 -->
<div class="feature-card bg-white rounded-2xl shadow-sm p-6 border border-gray-200 hover:border-indigo-200">
<div class="w-12 h-12 bg-gradient-to-br from-indigo-100 to-indigo-50 rounded-xl flex items-center justify-center mb-4">
<i class="fas fa-comments text-indigo-600 text-xl"></i>
</div>
<h3 class="text-lg font-semibold mb-3 text-gray-900">Smart Q&A</h3>
<p class="text-gray-600 text-sm leading-relaxed mb-4">Ask natural language questions and get precise answers with source citations and context.</p>
<div class="flex flex-wrap gap-2">
<span class="tag tag-blue">Context-Aware</span>
<span class="tag tag-blue">Source Tracking</span>
</div>
</div>
<!-- Feature 5 -->
<div class="feature-card bg-white rounded-2xl shadow-sm p-6 border border-gray-200 hover:border-yellow-200">
<div class="w-12 h-12 bg-gradient-to-br from-yellow-100 to-yellow-50 rounded-xl flex items-center justify-center mb-4">
<i class="fas fa-chart-line text-yellow-600 text-xl"></i>
</div>
<h3 class="text-lg font-semibold mb-3 text-gray-900">Quality Analytics</h3>
<p class="text-gray-600 text-sm leading-relaxed mb-4">Comprehensive evaluation using RAGAS metrics to monitor and improve response quality.</p>
<div class="flex flex-wrap gap-2">
<span class="tag tag-yellow">RAGAS Metrics</span>
<span class="tag tag-yellow">Performance</span>
</div>
</div>
<!-- Feature 6 -->
<div class="feature-card bg-white rounded-2xl shadow-sm p-6 border border-gray-200 hover:border-red-200">
<div class="w-12 h-12 bg-gradient-to-br from-red-100 to-red-50 rounded-xl flex items-center justify-center mb-4">
<i class="fas fa-cog text-red-600 text-xl"></i>
</div>
<h3 class="text-lg font-semibold mb-3 text-gray-900">Flexible Configuration</h3>
<p class="text-gray-600 text-sm leading-relaxed mb-4">Customize every aspect of the pipeline with real-time configuration and monitoring.</p>
<div class="flex flex-wrap gap-2">
<span class="tag tag-red">Customizable</span>
<span class="tag tag-red">Real-time</span>
</div>
</div>
</div>
</div>
<!-- How to Use Tab -->
<div id="how-to-use-content" class="tab-panel hidden">
<div class="bg-white rounded-2xl shadow-sm p-8">
<div class="text-center mb-10">
<h2 class="text-3xl font-bold text-gray-900 mb-4">Get Started in 4 Simple Steps</h2>
<p class="text-gray-600 max-w-2xl mx-auto">Transform your documents into an intelligent knowledge base with our guided workflow.</p>
</div>
<div class="space-y-8">
<!-- Step 1 - Upload -->
<div class="step-card">
<div class="step-number">1</div>
<div>
<h3 class="text-xl font-semibold mb-3 text-gray-900">Upload Your Content</h3>
<p class="text-gray-600 mb-4">Drag and drop your documents or use the file browser to upload.</p>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
<div class="bg-blue-50 rounded-lg p-3 flex items-center justify-center">
<i class="fas fa-file-pdf text-red-500 text-xl"></i>
<span class="ml-2 text-sm font-medium">PDF</span>
</div>
<div class="bg-blue-50 rounded-lg p-3 flex items-center justify-center">
<i class="fas fa-file-word text-blue-500 text-xl"></i>
<span class="ml-2 text-sm font-medium">Word</span>
</div>
<div class="bg-blue-50 rounded-lg p-3 flex items-center justify-center">
<i class="fas fa-file-alt text-gray-500 text-xl"></i>
<span class="ml-2 text-sm font-medium">Text</span>
</div>
<div class="bg-blue-50 rounded-lg p-3 flex items-center justify-center">
<i class="fas fa-file-archive text-orange-500 text-xl"></i>
<span class="ml-2 text-sm font-medium">ZIP</span>
</div>
</div>
<div class="bg-gray-50 rounded-xl p-4 border border-gray-200">
<p class="text-sm text-gray-700">
<i class="fas fa-lightbulb text-yellow-500 mr-2"></i>
<span class="font-medium">Tip:</span> Upload multiple files at once or ZIP archives for batch processing.
</p>
</div>
</div>
</div>
<!-- Step 2 - Process -->
<div class="step-card">
<div class="step-number">2</div>
<div>
<h3 class="text-xl font-semibold mb-3 text-gray-900">Build Knowledge Base</h3>
<p class="text-gray-600 mb-4">Click "Start Building" to process documents through the AI pipeline.</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl p-4">
<div class="flex items-center mb-2">
<div class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-brain text-blue-600"></i>
</div>
<div>
<div class="font-medium text-gray-900">AI Processing</div>
<div class="text-xs text-gray-600">Semantic understanding</div>
</div>
</div>
</div>
<div class="bg-gradient-to-r from-green-50 to-emerald-50 rounded-xl p-4">
<div class="flex items-center mb-2">
<div class="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-database text-green-600"></i>
</div>
<div>
<div class="font-medium text-gray-900">Vector Store</div>
<div class="text-xs text-gray-600">Fast retrieval engine</div>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 rounded-xl p-4 border border-gray-200">
<p class="text-sm text-gray-700">
<i class="fas fa-lightbulb text-yellow-500 mr-2"></i>
<span class="font-medium">Tip:</span> Monitor progress in real-time with our visual pipeline tracker.
</p>
</div>
</div>
</div>
<!-- Step 3 - Chat -->
<div class="step-card">
<div class="step-number">3</div>
<div>
<h3 class="text-xl font-semibold mb-3 text-gray-900">Chat with AI Assistant</h3>
<p class="text-gray-600 mb-4">Ask questions in natural language and get intelligent responses.</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
<div class="bg-white border border-gray-200 rounded-xl p-4">
<div class="flex items-start">
<div class="w-8 h-8 bg-indigo-100 rounded-lg flex items-center justify-center flex-shrink-0 mr-3">
<i class="fas fa-question text-indigo-600"></i>
</div>
<div>
<div class="font-medium text-gray-900">Ask Questions</div>
<div class="text-xs text-gray-600">Get factual answers</div>
</div>
</div>
</div>
<div class="bg-white border border-gray-200 rounded-xl p-4">
<div class="flex items-start">
<div class="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center flex-shrink-0 mr-3">
<i class="fas fa-search text-green-600"></i>
</div>
<div>
<div class="font-medium text-gray-900">Find Insights</div>
<div class="text-xs text-gray-600">Discover patterns</div>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 rounded-xl p-4 border border-gray-200">
<p class="text-sm text-gray-700">
<i class="fas fa-lightbulb text-yellow-500 mr-2"></i>
<span class="font-medium">Tip:</span> Each response includes source citations for verification.
</p>
</div>
</div>
</div>
<!-- Step 4 - Analyze -->
<div class="step-card">
<div class="step-number">4</div>
<div>
<h3 class="text-xl font-semibold mb-3 text-gray-900">Optimize & Export</h3>
<p class="text-gray-600 mb-4">Analyze performance and export results for reporting.</p>
<div class="flex flex-wrap gap-3 mb-4">
<div class="flex items-center bg-gray-50 rounded-lg px-3 py-2">
<i class="fas fa-chart-bar text-indigo-600 mr-2"></i>
<span class="text-sm font-medium">Analytics</span>
</div>
<div class="flex items-center bg-gray-50 rounded-lg px-3 py-2">
<i class="fas fa-download text-green-600 mr-2"></i>
<span class="text-sm font-medium">Export</span>
</div>
<div class="flex items-center bg-gray-50 rounded-lg px-3 py-2">
<i class="fas fa-sliders-h text-purple-600 mr-2"></i>
<span class="text-sm font-medium">Tune</span>
</div>
</div>
<div class="bg-gray-50 rounded-xl p-4 border border-gray-200">
<p class="text-sm text-gray-700">
<i class="fas fa-lightbulb text-yellow-500 mr-2"></i>
<span class="font-medium">Tip:</span> Experiment with different configurations to optimize for your specific use case.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="bg-gray-900 text-white py-12">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div class="flex flex-col items-center mb-8">
<div class="w-14 h-14 bg-white rounded-xl flex items-center justify-center mb-4">
<i class="fas fa-brain text-gray-900 text-xl"></i>
</div>
<div class="text-2xl font-bold">QuerySphere</div>
<div class="text-gray-400 mt-2">RAG platform for document Q&A with local LLM or cloud deployment integration.</div>
</div>
<p class="text-gray-400 mb-8 max-w-4xl mx-auto">
Transforming organizational knowledge management with AI-powered search and analytics.
</p>
<div class="text-gray-400 text-sm">
© 2025 QuerySphere. All rights reserved.
</div>
</div>
</footer>
</div>
<!-- Main Application -->
<div id="main-app" class="min-h-screen hidden">
<!-- App Layout -->
<div class="h-screen flex">
<!-- Sidebar -->
<div class="w-64 bg-white border-r border-gray-200 flex flex-col">
<!-- Logo -->
<div class="p-5 border-b border-gray-200">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg flex items-center justify-center">
<i class="fas fa-brain text-white"></i>
</div>
<div>
<div class="font-bold text-gray-900">AI Knowledge</div>
<div class="text-xs text-gray-500 -mt-1">Enterprise Platform</div>
</div>
</div>
</div>
<!-- Navigation -->
<nav class="flex-1 p-4">
<ul class="space-y-2">
<li>
<button onclick="showAppSection('upload')" class="app-nav-btn w-full text-left flex items-center p-3 rounded-lg hover:bg-gray-100 text-gray-700">
<i class="fas fa-cloud-upload-alt mr-3 text-gray-500"></i>
Upload
</button>
</li>
<li>
<button onclick="showAppSection('chat')" class="app-nav-btn w-full text-left flex items-center p-3 rounded-lg hover:bg-gray-100 text-gray-700">
<i class="fas fa-comments mr-3 text-gray-500"></i>
Chat
</button>
</li>
<li>
<button onclick="showAppSection('analytics')" class="app-nav-btn w-full text-left flex items-center p-3 rounded-lg hover:bg-gray-100 text-gray-700">
<i class="fas fa-chart-bar mr-3 text-gray-500"></i>
Analytics
</button>
</li>
<li>
<button onclick="showAppSection('configuration')" class="app-nav-btn w-full text-left flex items-center p-3 rounded-lg hover:bg-gray-100 text-gray-700">
<i class="fas fa-cog mr-3 text-gray-500"></i>
Settings
</button>
</li>
</ul>
</nav>
<!-- System Status -->
<div class="p-4 border-t border-gray-200">
<div class="text-sm">
<div class="flex justify-between items-center mb-2">
<span class="text-gray-600">System Status</span>
<span id="system-status" class="font-medium text-gray-500">Checking...</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-1.5 mb-1">
<div id="progress-bar" class="bg-gray-400 h-1.5 rounded-full" style="width: 0%"></div>
</div>
<div id="status-details" class="text-xs text-gray-500">Initializing...</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col">
<!-- Header -->
<header class="bg-white border-b border-gray-200 p-4">
<div class="flex justify-between items-center">
<div>
<h2 id="page-title" class="text-lg font-semibold text-gray-900">Document Upload</h2>
<p class="text-sm text-gray-600">Upload documents to build your knowledge base</p>
</div>
<button onclick="backToHome()" class="flex items-center px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-all">
<i class="fas fa-home mr-2"></i>Home
</button>
</div>
</header>
<!-- Content Area -->
<main class="flex-1 p-6 overflow-auto bg-gray-50">
<!-- Upload Section -->
<div id="upload-section" class="mb-8">
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
<div class="mb-6">
<h3 class="text-lg font-semibold mb-2 text-gray-900">Upload Documents</h3>
<p class="text-gray-600 text-sm">Upload your documents to build an intelligent knowledge base</p>
</div>
<div class="border-2 border-dashed border-gray-300 rounded-xl p-12 text-center hover:border-indigo-400 transition-all cursor-pointer bg-gray-50/50"
id="drop-zone">
<div class="w-16 h-16 bg-gradient-to-br from-blue-100 to-indigo-100 rounded-full flex items-center justify-center mx-auto mb-4">
<i class="fas fa-cloud-upload-alt text-indigo-600 text-2xl"></i>
</div>
<p class="text-gray-700 font-medium mb-1">Drag & drop files here</p>
<p class="text-gray-500 text-sm mb-6">or click to browse your computer</p>
<div class="flex justify-center">
<button onclick="document.getElementById('file-input').click()" class="px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-all font-medium">
<i class="fas fa-folder-open mr-2"></i>Browse Files
</button>
</div>
<p class="text-xs text-gray-500 mt-6">Supports PDF, DOCX, TXT, ZIP • Max 2GB per file</p>
<input type="file" id="file-input" multiple class="hidden" accept=".pdf,.docx,.txt,.zip,.doc,.md,.csv">
</div>
<div id="file-list" class="mt-6 space-y-3 max-h-60 overflow-y-auto">
<p class="text-center text-gray-500 py-4">No files uploaded yet</p>
</div>
<!-- Action Buttons -->
<div class="mt-8 flex justify-end space-x-4">
<button id="process-btn" class="px-6 py-3 bg-gradient-to-r from-green-500 to-emerald-600 text-white rounded-lg hover:from-green-600 hover:to-emerald-700 transition-all font-medium disabled:opacity-50 disabled:cursor-not-allowed" disabled>
<i class="fas fa-play mr-2"></i>Start Building
</button>
</div>
</div>
</div>
<!-- Processing Status -->
<div id="processing-section" class="hidden mb-8">
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
<h3 class="text-lg font-semibold mb-6 text-gray-900">Building Knowledge Base</h3>
<div class="space-y-6">
<div>
<div class="flex justify-between mb-3">
<span id="current-step" class="font-medium text-gray-900">Initializing pipeline...</span>
<span id="progress-text" class="font-medium text-indigo-600">0%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div id="processing-progress" class="bg-gradient-to-r from-green-500 to-emerald-600 h-2 rounded-full transition-all duration-500" style="width: 0%"></div>
</div>
</div>
<div id="processing-details" class="text-sm text-gray-600 space-y-3">
<div class="flex justify-between items-center">
<span>Documents processed:</span>
<span id="processed-docs" class="font-medium">0/0</span>
</div>
<div class="flex justify-between items-center">
<span>Current operation:</span>
<span id="current-operation" class="font-medium text-gray-900">Waiting to start</span>
</div>
</div>
</div>
</div>
</div>
<!-- Chat Section -->
<div id="chat-section" class="hidden h-full flex flex-col">
<div class="bg-white rounded-xl shadow-sm h-full flex flex-col border border-gray-200">
<!-- Chat Header -->
<div class="border-b border-gray-200 p-4 bg-gray-50 rounded-t-xl">
<div class="flex justify-between items-center">
<div>
<h3 class="text-lg font-semibold text-gray-900">Chat with Documents</h3>
<p class="text-sm text-gray-600">Ask questions about your knowledge base</p>
</div>
<div class="flex space-x-2">
<button onclick="exportChat('json')" class="text-sm bg-gray-100 text-gray-700 px-3 py-1.5 rounded-lg hover:bg-gray-200 transition-all">
<i class="fas fa-download mr-1"></i>Export
</button>
<button onclick="clearChat()" class="text-sm bg-gray-100 text-gray-700 px-3 py-1.5 rounded-lg hover:bg-gray-200 transition-all">
<i class="fas fa-trash mr-1"></i>Clear
</button>
</div>
</div>
</div>
<!-- Chat Messages -->
<div id="chat-messages" class="flex-1 p-6 overflow-y-auto custom-scrollbar space-y-4">
<!-- Welcome message will be added here -->
</div>
<!-- Chat Input -->
<div class="border-t border-gray-200 p-4 bg-gray-50 rounded-b-xl">
<div class="flex space-x-3">
<div class="flex-1 relative">
<input type="text" id="chat-input" placeholder="Ask a question about your documents..."
class="w-full border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-100 transition-all pr-12"
onkeypress="handleChatInputKeypress(event)">
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
<span id="token-count" class="text-xs text-gray-500">0</span>
</div>
</div>
<button id="send-btn" onclick="sendMessage()" class="px-6 py-3 bg-gradient-to-r from-indigo-500 to-purple-600 text-white rounded-lg hover:from-indigo-600 hover:to-purple-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed font-medium" disabled>
<i class="fas fa-paper-plane"></i>
</button>
</div>
<div class="flex justify-between items-center mt-3">
<p class="text-xs text-gray-500">
<i class="fas fa-lightbulb mr-1"></i>
Press Enter to send • Shift+Enter for new line
</p>
<div class="flex space-x-3">
<button onclick="insertExample('summary')" class="text-xs text-indigo-600 hover:text-indigo-700">
Get Summary
</button>
<button onclick="insertExample('compare')" class="text-xs text-indigo-600 hover:text-indigo-700">
Compare
</button>
<button onclick="insertExample('find')" class="text-xs text-indigo-600 hover:text-indigo-700">
Find Details
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Analytics Section -->
<div id="analytics-section" class="hidden">
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
<div class="flex justify-between items-center mb-6">
<h3 class="text-lg font-semibold text-gray-900">Analytics & Quality Metrics</h3>
<div class="flex space-x-2">
<button onclick="refreshRagasTable()" class="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition-all font-medium">
<i class="fas fa-sync-alt mr-2"></i>Refresh
</button>
<button onclick="clearRagasHistory()" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-all font-medium">
<i class="fas fa-trash mr-2"></i>Clear Session
</button>
<button onclick="exportRagasData()" class="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-all font-medium">
<i class="fas fa-download mr-2"></i>Export Data
</button>
</div>
</div>
<!-- Session Statistics -->
<div class="mb-8">
<h4 class="font-semibold mb-4 text-gray-700">Session Statistics</h4>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-4 border border-blue-200">
<div class="text-sm font-medium text-blue-700 mb-1">Total Evaluations</div>
<div class="text-2xl font-bold text-blue-600" id="ragas-total-count">0</div>
</div>
<div class="bg-gradient-to-br from-green-50 to-green-100 rounded-lg p-4 border border-green-200">
<div class="text-sm font-medium text-green-700 mb-1">Average Answer Relevancy</div>
<div class="text-2xl font-bold text-green-600" id="ragas-avg-relevancy">-</div>
</div>
<div class="bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg p-4 border border-purple-200">
<div class="text-sm font-medium text-purple-700 mb-1">Average Faithfulness</div>
<div class="text-2xl font-bold text-purple-600" id="ragas-avg-faithfulness">-</div>
</div>
<div class="bg-gradient-to-br from-orange-50 to-orange-100 rounded-lg p-4 border border-orange-200">
<div class="text-sm font-medium text-orange-700 mb-1">Average Context Utilization</div>
<div class="text-2xl font-bold text-orange-600" id="ragas-avg-precision">-</div>
</div>
</div>
</div>
<!-- RAGAS Evaluation Table -->
<div class="mb-8">
<h4 class="font-semibold mb-4 text-gray-700 flex items-center">
<i class="fas fa-table mr-2 text-indigo-600"></i>
RAGAS Evaluation History
<span id="ragas-table-count" class="ml-2 text-sm font-normal text-gray-500">(0 entries)</span>
</h4>
<div class="border rounded-lg overflow-hidden">
<div class="overflow-x-auto overflow-y-auto" style="max-height: 600px;">
<table class="min-w-full divide-y divide-gray-200" id="ragas-table">
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">#</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Timestamp</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" style="min-width: 250px;">Query</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" style="min-width: 250px;">Answer</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
Query Type
</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="flex flex-col items-center">
<span>Answer</span>
<span>Relevancy</span>
</div>
</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="flex flex-col items-center">
<span>Faithfulness</span>
</div>
</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="flex flex-col items-center">
<span>Context</span>
<span>Precision</span>
</div>
</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="flex flex-col items-center">
<span>Context</span>
<span>Relevancy</span>
</div>
</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="flex flex-col items-center">
<span>Retrieval</span>
<span>Time (ms)</span>
</div>
</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="flex flex-col items-center">
<span>Generation</span>
<span>Time (ms)</span>
</div>
</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="flex flex-col items-center">
<span>Total</span>
<span>Time (ms)</span>
</div>
</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="flex flex-col items-center">
<span>Chunks</span>
<span>Retrieved</span>
</div>
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200" id="ragas-table-body">
<tr>
<td colspan="13" class="px-4 py-8 text-center text-gray-500">
<i class="fas fa-info-circle text-3xl mb-2 text-gray-400"></i>
<p>No RAGAS evaluations yet. Start chatting to see quality metrics.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Table Legend -->
<div class="mt-4 p-4 bg-gray-50 rounded-lg border border-gray-200">
<div class="text-sm text-gray-800">
<p class="font-semibold mb-2">Metric Descriptions:</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<div>
<span class="font-medium">Query Type:</span> RAG (uses documents) vs GENERAL (no documents)
</div>
<div>
<span class="font-medium">Answer Relevancy:</span> How well the answer addresses the question (0-1)
</div>
<div>
<span class="font-medium">Faithfulness:</span> Is the answer grounded in the retrieved context (0-1)
</div>
<div>
<span class="font-medium">Context Utilization:</span> How well the retrieved context is utilized (0-1)
</div>
</div>
<p class="mt-2 text-xs text-gray-700">
<i class="fas fa-lightbulb mr-1"></i>
Color coding:
<span class="text-green-600 font-medium">≥0.7 (Good)</span>,
<span class="text-yellow-600 font-medium">0.4-0.7 (Fair)</span>,
<span class="text-red-600 font-medium">&lt;0.4 (Poor)</span>,
<span class="text-gray-600 font-medium">N/A (Not Applicable)</span>
</p>
</div>
</div>
</div>
<!-- Refresh Controls -->
<div class="mt-6 flex justify-between items-center">
<div class="text-sm text-gray-600">
<i class="fas fa-info-circle mr-2"></i>
Table automatically refreshes when you switch to this tab or send a new message
</div>
<button onclick="refreshRagasTable()" class="bg-indigo-100 text-indigo-700 px-4 py-2 rounded-lg hover:bg-indigo-200 transition duration-200">
<i class="fas fa-redo-alt mr-2"></i>Manual Refresh
</button>
</div>
</div>
</div>
<!-- Configuration Section -->
<div id="configuration-section" class="hidden">
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
<h3 class="text-lg font-semibold mb-6 text-gray-900">System Configuration</h3>
<form id="config-form" class="space-y-8">
<!-- Inference Model -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div>
<label class="block text-sm font-medium mb-3 text-gray-700">Inference Model</label>
<select name="inference_model" class="w-full border rounded-lg px-4 py-3 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 transition duration-200">
<option value="mistral:7b">Mistral-7B (Ollama)</option>
<option value="gpt-3.5-turbo">GPT-3.5 Turbo (OpenAI)</option>
</select>
<p class="text-xs text-gray-500 mt-2">Select the LLM provider and model for text generation</p>
</div>
<div>
<label class="block text-sm font-medium mb-3 text-gray-700">Chunking Strategy</label>
<select name="chunking_strategy" class="w-full border rounded-lg px-4 py-3 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 transition duration-200">
<option value="fixed">Fixed Size Chunking</option>
<option value="semantic">Semantic Chunking</option>
<option value="hierarchical">Hierarchical Chunking</option>
</select>
<p class="text-xs text-gray-500 mt-2">Choose how documents are split into chunks for processing</p>
</div>
</div>
<!-- Chunking Parameters -->
<div>
<h4 class="font-medium mb-4 text-gray-700">Chunking Parameters</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium mb-2 text-gray-600">Chunk Size (tokens)</label>
<input type="number" name="chunk_size" value="512" min="100" max="2000"
class="w-full border rounded-lg px-4 py-2 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 transition duration-200">
<p class="text-xs text-gray-500 mt-1">Larger chunks capture more context but may reduce precision</p>
</div>
<div>
<label class="block text-sm font-medium mb-2 text-gray-600">Chunk Overlap (tokens)</label>
<input type="number" name="chunk_overlap" value="50" min="0" max="200"
class="w-full border rounded-lg px-4 py-2 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 transition duration-200">
<p class="text-xs text-gray-500 mt-1">Overlap between chunks to maintain context continuity</p>
</div>
</div>
</div>
<!-- Retrieval Parameters -->
<div>
<h4 class="font-medium mb-4 text-gray-700">Retrieval Parameters</h4>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label class="block text-sm font-medium mb-2 text-gray-600">Retrieval Top K</label>
<input type="number" name="retrieval_top_k" value="10" min="1" max="50"
class="w-full border rounded-lg px-4 py-2 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 transition duration-200">
<p class="text-xs text-gray-500 mt-1">Number of chunks to retrieve for each query</p>
</div>
<div>
<label class="block text-sm font-medium mb-2 text-gray-600">Vector Weight</label>
<input type="number" name="vector_weight" value="0.6" step="0.1" min="0" max="1"
class="w-full border rounded-lg px-4 py-2 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 transition duration-200">
<p class="text-xs text-gray-500 mt-1">Weight for vector similarity search (0.0-1.0)</p>
</div>
<div>
<label class="block text-sm font-medium mb-2 text-gray-600">BM25 Weight</label>
<input type="number" name="bm25_weight" value="0.4" step="0.1" min="0" max="1"
class="w-full border rounded-lg px-4 py-2 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 transition duration-200">
<p class="text-xs text-gray-500 mt-1">Weight for keyword search (0.0-1.0)</p>
</div>
</div>
<div class="mt-4">
<input type="checkbox" name="enable_reranking" id="enable_reranking" class="mr-3 w-4 h-4 text-indigo-600 focus:ring-indigo-500">
<label for="enable_reranking" class="text-sm font-medium text-gray-700">Enable Cross-Encoder Reranking</label>
<p class="text-xs text-gray-500 mt-1 ml-7">Use transformer model to rerank results for better precision (slower but more accurate)</p>
</div>
</div>
<!-- Generation Parameters -->
<div>
<h4 class="font-medium mb-4 text-gray-700">Generation Parameters</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium mb-2 text-gray-600">Temperature</label>
<input type="number" name="temperature" value="0.1" step="0.1" min="0" max="1"
class="w-full border rounded-lg px-4 py-2 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 transition duration-200">
<p class="text-xs text-gray-500 mt-1">Lower for factual answers, higher for creative responses</p>
</div>
<div>
<label class="block text-sm font-medium mb-2 text-gray-600">Max Tokens</label>
<input type="number" name="max_tokens" value="1000" min="100" max="4000"
class="w-full border rounded-lg px-4 py-2 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 transition duration-200">
<p class="text-xs text-gray-500 mt-1">Maximum length of generated responses</p>
</div>
</div>
</div>
<!-- Save Button -->
<div class="flex justify-end pt-4 border-t">
<button type="submit" class="bg-indigo-600 text-white px-8 py-3 rounded-lg hover:bg-indigo-700 transition duration-200 font-medium">
<i class="fas fa-save mr-2"></i>Save Configuration
</button>
</div>
<!-- Configuration Status -->
<div class="mt-8 pt-6 border-t">
<h4 class="font-medium mb-4 text-gray-700">Current System Status</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div class="bg-gray-50 p-4 rounded-lg">
<div class="flex justify-between mb-2">
<span class="text-gray-600">LLM Health:</span>
<span id="config-llm-health" class="font-medium">-</span>
</div>
<div class="flex justify-between mb-2">
<span class="text-gray-600">Vector Store:</span>
<span id="config-vector-store" class="font-medium">-</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Embeddings:</span>
<span id="config-embeddings" class="font-medium">-</span>
</div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="flex justify-between mb-2">
<span class="text-gray-600">Retrieval:</span>
<span id="config-retrieval" class="font-medium">-</span>
</div>
<div class="flex justify-between mb-2">
<span class="text-gray-600">Generation:</span>
<span id="config-generation" class="font-medium">-</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Overall Status:</span>
<span id="config-overall" class="font-medium">-</span>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</main>
</div>
</div>
</div>
<script>
// API Service for backend communication
class APIService {
constructor() {
// AUTO-DETECT the correct base URL
const currentOrigin = window.location.origin;
// Check if we're in HuggingFace Spaces or localhost
if (currentOrigin.includes('hf.space') || currentOrigin.includes('huggingface.co')) {
// HuggingFace Spaces
this.baseURL = currentOrigin;
} else if (currentOrigin.includes('localhost') || currentOrigin.includes('127.0.0.1')) {
// Local development
this.baseURL = 'http://localhost:8000';
} else {
// Any other deployment
this.baseURL = currentOrigin;
}
console.log('API Service initialized with baseURL:', this.baseURL);
}
async cleanupSession(sessionId) {
try {
const response = await fetch(`${this.baseURL}/api/cleanup/session/${sessionId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Cleanup failed: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('Session cleanup error:', error);
throw error;
}
}
async uploadFiles(files) {
const formData = new FormData();
for (let file of files) {
formData.append('files', file);
}
console.log('Uploading files to:', `${this.baseURL}/api/upload`);
try {
const response = await fetch(`${this.baseURL}/api/upload`, {
method: 'POST',
body: formData
});
console.log('Upload response status:', response.status);
if (!response.ok) {
let errorMessage = `Upload failed: ${response.status} ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.detail || errorData.message || errorMessage;
} catch (e) {
const text = await response.text();
errorMessage = `${errorMessage} - ${text.substring(0, 100)}`;
}
throw new Error(errorMessage);
}
const result = await response.json();
console.log('Upload successful:', result);
return result;
} catch (error) {
console.error('Upload error:', error);
// Better error message for network issues
if (error.message.includes('Failed to fetch')) {
throw new Error(`Cannot connect to server at ${this.baseURL}. Check if backend is running.`);
}
throw error;
}
}
async startProcessing() {
const response = await fetch(`${this.baseURL}/api/start-processing`, {
method: 'POST'
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || `Processing start failed: ${response.statusText}`);
}
return await response.json();
}
async getProcessingStatus() {
const response = await fetch(`${this.baseURL}/api/processing-status`);
if (!response.ok) {
throw new Error(`Status check failed: ${response.statusText}`);
}
return await response.json();
}
async sendChatMessage(message, sessionId = null) {
console.log('Sending chat message to backend:', this.baseURL);
try {
const response = await fetch(`${this.baseURL}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message,
session_id: sessionId
})
});
console.log('Response status:', response.status);
if (!response.ok) {
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.detail || errorData.message || errorMessage;
} catch (e) {
// If response is not JSON, get text
const text = await response.text();
errorMessage = `${errorMessage} - ${text.substring(0, 100)}`;
}
throw new Error(errorMessage);
}
const result = await response.json();
console.log('Chat API response received:', result);
return result;
} catch (error) {
console.error('Chat API error:', error);
// Better error messages for common issues
if (error.message.includes('Failed to fetch')) {
throw new Error(`Cannot connect to server at ${this.baseURL}. Please make sure the backend is running.`);
}
throw error;
}
}
async updateConfiguration(config) {
const response = await fetch(`${this.baseURL}/api/configuration`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || `Configuration update failed: ${response.statusText}`);
}
return await response.json();
}
async getConfiguration() {
const response = await fetch(`${this.baseURL}/api/configuration`);
if (!response.ok) {
throw new Error(`Configuration fetch failed: ${response.statusText}`);
}
return await response.json();
}
async getAnalytics() {
const response = await fetch(`${this.baseURL}/api/analytics`);
if (!response.ok) {
throw new Error(`Analytics fetch failed: ${response.statusText}`);
}
return await response.json();
}
async getSystemInfo() {
const response = await fetch(`${this.baseURL}/api/system-info`);
if (!response.ok) {
throw new Error(`System info fetch failed: ${response.statusText}`);
}
return await response.json();
}
async getHealth() {
const response = await fetch(`${this.baseURL}/api/health`);
if (!response.ok) {
throw new Error(`Health check failed: ${response.statusText}`);
}
return await response.json();
}
async exportChat(sessionId, format) {
const response = await fetch(`${this.baseURL}/api/export-chat/${sessionId}?format=${format}`);
if (!response.ok) {
throw new Error(`Export failed: ${response.statusText}`);
}
return await response.blob();
}
// RAGAS specific API methods
async getRagasHistory() {
const response = await fetch(`${this.baseURL}/api/ragas/history`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || `RAGAS history fetch failed: ${response.statusText}`);
}
return await response.json();
}
async getRagasStatistics() {
const response = await fetch(`${this.baseURL}/api/ragas/statistics`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || `RAGAS statistics fetch failed: ${response.statusText}`);
}
return await response.json();
}
async clearRagasHistory() {
const response = await fetch(`${this.baseURL}/api/ragas/clear`, {
method: 'POST'
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || `RAGAS clear failed: ${response.statusText}`);
}
return await response.json();
}
async exportRagasData() {
const response = await fetch(`${this.baseURL}/api/ragas/export`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || `RAGAS export failed: ${response.statusText}`);
}
const blob = await response.blob();
return blob;
}
}
// Global state
const apiService = new APIService();
let uploadedFiles = [];
let currentSessionId = null;
let systemReady = false;
let processingInterval = null;
// Add cleanup tracking
let cleanupSent = false;
let heartbeatInterval = null;
// Landing page functions
function showTab(tabName) {
// Update tab buttons
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.classList.remove('bg-white', 'text-indigo-700', 'shadow-sm');
});
document.getElementById(`tab-${tabName}`).classList.add('bg-white', 'text-indigo-700', 'shadow-sm');
// Update tab content
document.querySelectorAll('.tab-panel').forEach(panel => {
panel.classList.add('hidden');
panel.classList.remove('active');
});
document.getElementById(`${tabName}-content`).classList.remove('hidden');
document.getElementById(`${tabName}-content`).classList.add('active');
}
function startApp() {
document.getElementById('landing-page').classList.add('hidden');
document.getElementById('main-app').classList.remove('hidden');
initializeApp();
// Start at Upload section
showAppSection('upload');
}
function backToHome() {
// Clean up current session before leaving
if (currentSessionId) {
try {
apiService.cleanupSession(currentSessionId)
.then(() => console.log('Session cleaned up on home navigation'))
.catch(err => console.error('Session cleanup failed:', err));
} catch (error) {
console.log('Session cleanup error:', error);
}
// Stop heartbeat
stopSessionHeartbeat();
cleanupSent = false;
}
document.getElementById('main-app').classList.add('hidden');
document.getElementById('landing-page').classList.remove('hidden');
// Reset app state if needed
uploadedFiles = [];
currentSessionId = null;
systemReady = false;
// Clear file list
document.getElementById('file-list').innerHTML = '<p class="text-center text-gray-500 py-4">No files uploaded yet</p>';
updateProcessButton();
console.log('Returned to home page');
}
// Main app functions
async function initializeApp() {
setupEventListeners();
// Show current API endpoint in console
console.log('🔧 QuerySphere initialized');
console.log('🔧 Frontend URL:', window.location.origin);
console.log('🔧 API Base URL:', apiService.baseURL);
// Test backend connection
try {
const health = await apiService.getHealth();
console.log('✅ Backend connection successful:', health);
showNotification('Connected to backend', 'success');
} catch (error) {
console.error('❌ Backend connection failed:', error);
showNotification(`Cannot connect to backend at ${apiService.baseURL}`, 'error');
}
await checkSystemStatus();
startStatusPolling();
}
function startSessionHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
// Only start heartbeat if we have a session
if (!currentSessionId) {
return;
}
heartbeatInterval = setInterval(() => {
if (currentSessionId) {
// Simple ping to keep session alive (optional)
fetch(`${apiService.baseURL}/api/health`)
.catch(() => {
// Silently fail - it's just a heartbeat
});
}
}, 30000); // Every 30 seconds
console.log('Session heartbeat started');
}
function stopSessionHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
console.log('Session heartbeat stopped');
}
}
function setupEventListeners() {
// File input
const fileInput = document.getElementById('file-input');
fileInput.addEventListener('change', handleFileSelect);
// Process button
document.getElementById('process-btn').addEventListener('click', startProcessing);
// Chat input
document.getElementById('chat-input').addEventListener('input', updateTokenCount);
// Configuration form
document.getElementById('config-form').addEventListener('submit', saveConfiguration);
// Drag and drop
const dropZone = document.getElementById('drop-zone');
dropZone.addEventListener('dragover', handleDragOver);
dropZone.addEventListener('dragleave', handleDragLeave);
dropZone.addEventListener('drop', handleDrop);
// Browse button click
dropZone.querySelector('button').addEventListener('click', () => {
fileInput.click();
});
}
function handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
const dropZone = e.currentTarget;
dropZone.classList.add('upload-zone-active');
}
function handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
const dropZone = e.currentTarget;
dropZone.classList.remove('upload-zone-active');
}
async function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
const dropZone = e.currentTarget;
dropZone.classList.remove('upload-zone-active');
const files = e.dataTransfer.files;
if (files.length > 0) {
await handleFiles(files);
}
}
function handleFileSelect(e) {
const files = e.target.files;
if (files.length > 0) {
handleFiles(files);
// Reset input so same file can be selected again
e.target.value = '';
}
}
async function handleFiles(files) {
const validFiles = [];
for (let file of files) {
if (isValidFileType(file)) {
validFiles.push(file);
} else {
showNotification(`Unsupported file type: ${file.name}`, 'error');
}
}
if (validFiles.length > 0) {
await uploadFilesToBackend(validFiles);
}
}
function isValidFileType(file) {
const validExtensions = ['.pdf', '.docx', '.txt', '.zip', '.doc', '.md', '.csv'];
const fileName = file.name.toLowerCase();
// Check extension
const hasValidExtension = validExtensions.some(ext => fileName.endsWith(ext));
// Check MIME type
const validMimeTypes = [
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
'application/zip',
'application/x-zip-compressed',
'application/msword',
'text/markdown',
'text/csv'
];
const hasValidMimeType = validMimeTypes.includes(file.type) || file.type === '';
return hasValidExtension || hasValidMimeType;
}
async function uploadFilesToBackend(files) {
try {
showNotification('Uploading files...', 'info');
// First, check if we can connect to the backend
try {
console.log('Testing connection to:', apiService.baseURL);
await fetch(`${apiService.baseURL}/api/health`);
} catch (connectionError) {
console.error('Connection test failed:', connectionError);
showNotification(`Cannot connect to backend at ${apiService.baseURL}. Please check if the server is running.`, 'error');
return;
}
// Check file size limits for HuggingFace Spaces
const MAX_SIZE_MB = 50;
const MAX_SIZE_BYTES = MAX_SIZE_MB * 1024 * 1024;
for (let file of files) {
if (file.size > MAX_SIZE_BYTES) {
showNotification(`File ${file.name} is too large (max ${MAX_SIZE_MB}MB for HuggingFace Spaces)`, 'error');
return;
}
}
// Update UI immediately
for (let file of files) {
addFileToList(file, 'uploading');
}
const result = await apiService.uploadFiles(files);
// Update local state with uploaded files
uploadedFiles = result.files || [];
updateFileList();
updateProcessButton();
showNotification(`Successfully uploaded ${files.length} files`, 'success');
} catch (error) {
console.error('Upload error:', error);
// More specific error messages
if (error.message.includes('Cannot connect to server')) {
showNotification(error.message, 'error');
} else if (error.message.includes('Failed to fetch')) {
showNotification(`Network error: Cannot connect to ${apiService.baseURL}`, 'error');
} else {
showNotification(`Upload failed: ${error.message}`, 'error');
}
// Remove failed files from list
updateFileList();
}
}
function addFileToList(file, status = 'uploaded') {
const fileList = document.getElementById('file-list');
// Remove the "no files" message if it exists
const noFilesMessage = fileList.querySelector('p.text-center');
if (noFilesMessage) {
noFilesMessage.remove();
}
const fileElement = document.createElement('div');
fileElement.className = 'flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200';
fileElement.innerHTML = `
<div class="flex items-center">
<i class="fas fa-file text-gray-400 mr-3"></i>
<div>
<div class="font-medium text-sm">${file.name}</div>
<div class="text-xs text-gray-500">${formatFileSize(file.size)}${status === 'uploading' ? 'Uploading...' : 'Ready'}</div>
</div>
</div>
${status === 'uploading' ?
'<div class="text-blue-500"><i class="fas fa-spinner fa-spin"></i></div>' :
'<button class="text-red-500 hover:text-red-700"><i class="fas fa-times"></i></button>'
}
`;
fileList.appendChild(fileElement);
}
function updateFileList() {
const fileList = document.getElementById('file-list');
fileList.innerHTML = '';
if (uploadedFiles.length === 0) {
fileList.innerHTML = '<p class="text-center text-gray-500 py-4">No files uploaded yet</p>';
return;
}
uploadedFiles.forEach((file, index) => {
const fileElement = document.createElement('div');
fileElement.className = 'flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200';
fileElement.innerHTML = `
<div class="flex items-center">
<i class="fas fa-file text-gray-400 mr-3"></i>
<div>
<div class="font-medium text-sm">${file.original_name || file.filename}</div>
<div class="text-xs text-gray-500">${formatFileSize(file.size)}${new Date(file.upload_time).toLocaleTimeString()}</div>
</div>
</div>
<button onclick="removeFile(${index})" class="text-red-500 hover:text-red-700">
<i class="fas fa-times"></i>
</button>
`;
fileList.appendChild(fileElement);
});
}
function removeFile(index) {
uploadedFiles.splice(index, 1);
updateFileList();
updateProcessButton();
}
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 updateProcessButton() {
const processBtn = document.getElementById('process-btn');
processBtn.disabled = uploadedFiles.length === 0;
}
async function startProcessing() {
if (uploadedFiles.length === 0) {
showNotification('Please upload files first', 'warning');
return;
}
try {
// Show processing section
document.getElementById('upload-section').classList.add('hidden');
document.getElementById('processing-section').classList.remove('hidden');
// Start processing via API
const result = await apiService.startProcessing();
showNotification('Processing started successfully', 'success');
// Start polling for status updates
startProcessingPolling();
} catch (error) {
console.error('Processing start error:', error);
showNotification(`Failed to start processing: ${error.message}`, 'error');
}
}
function startProcessingPolling() {
if (processingInterval) {
clearInterval(processingInterval);
}
processingInterval = setInterval(async () => {
try {
const status = await apiService.getProcessingStatus();
updateProcessingUI(status);
if (status.status === 'ready' || status.status === 'error') {
clearInterval(processingInterval);
if (status.status === 'ready') {
processingComplete();
}
}
} catch (error) {
console.error('Status polling error:', error);
}
}, 2000);
}
function updateProcessingUI(status) {
document.getElementById('processing-progress').style.width = `${status.progress}%`;
document.getElementById('progress-text').textContent = `${status.progress}%`;
document.getElementById('current-step').textContent = status.current_step || 'Processing...';
document.getElementById('processed-docs').textContent = `${status.processed_documents || 0}/${status.total_documents || 0}`;
// Update system status
const systemStatus = document.getElementById('system-status');
if (status.status === 'processing') {
systemStatus.textContent = 'Processing';
systemStatus.className = 'font-medium text-yellow-600';
} else if (status.status === 'ready') {
systemStatus.textContent = 'Ready';
systemStatus.className = 'font-medium text-green-600';
} else if (status.status === 'error') {
systemStatus.textContent = 'Error';
systemStatus.className = 'font-medium text-red-600';
}
}
function processingComplete() {
systemReady = true;
// Enable chat
document.getElementById('send-btn').disabled = false;
document.getElementById('chat-input').disabled = false;
document.getElementById('system-status').textContent = 'Ready';
document.getElementById('system-status').className = 'font-medium text-green-600';
document.getElementById('progress-bar').style.width = '100%';
document.getElementById('progress-bar').className = 'bg-green-600 h-1.5 rounded-full';
document.getElementById('status-details').textContent = 'System ready for queries';
// Show chat section automatically after processing
showAppSection('chat');
addWelcomeMessage();
showNotification('Knowledge base setup complete! You can now chat with your documents.', 'success');
}
async function checkSystemStatus() {
try {
const health = await apiService.getHealth();
// Update system status
const systemStatus = document.getElementById('system-status');
if (health.status === 'healthy') {
systemStatus.textContent = 'Ready';
systemStatus.className = 'font-medium text-green-600';
systemReady = true;
} else {
systemStatus.textContent = 'Not Ready';
systemStatus.className = 'font-medium text-red-600';
systemReady = false;
}
// Update status details
const statusDetails = document.getElementById('status-details');
statusDetails.textContent = health.components ? 'All systems operational' : 'Checking components...';
} catch (error) {
console.error('System status check error:', error);
document.getElementById('system-status').textContent = 'Offline';
document.getElementById('system-status').className = 'font-medium text-red-600';
document.getElementById('progress-bar').style.width = '0%';
document.getElementById('status-details').textContent = 'Cannot connect to backend';
systemReady = false;
}
}
function startStatusPolling() {
setInterval(async () => {
try {
await checkSystemStatus();
} catch (error) {
console.error('Status polling error:', error);
}
}, 30000); // Check every 30 seconds
}
function showAppSection(section) {
// Hide all sections
document.querySelectorAll('[id$="-section"]').forEach(sec => {
sec.classList.add('hidden');
});
// Show selected section
document.getElementById(`${section}-section`).classList.remove('hidden');
// Update navigation
document.querySelectorAll('.app-nav-btn').forEach(btn => {
btn.classList.remove('active', 'bg-indigo-50', 'text-indigo-700');
btn.classList.add('text-gray-700', 'hover:bg-gray-100');
});
const activeBtn = document.querySelector(`[onclick="showAppSection('${section}')"]`);
activeBtn.classList.add('active', 'bg-indigo-50', 'text-indigo-700');
activeBtn.classList.remove('text-gray-700', 'hover:bg-gray-100');
// Update page title
const titles = {
'upload': 'Document Upload',
'chat': 'Chat with Documents',
'analytics': 'Analytics & Quality',
'configuration': 'System Configuration'
};
document.getElementById('page-title').textContent = titles[section] || section;
// Load section-specific data
if (section === 'analytics') {
refreshRagasTable();
} else if (section === 'configuration') {
loadConfigurationUI();
} else if (section === 'chat') {
// Only add welcome message if chat is empty
const chatMessages = document.getElementById('chat-messages');
if (chatMessages.children.length === 0) {
addWelcomeMessage();
}
}
}
function addWelcomeMessage() {
const chatMessages = document.getElementById('chat-messages');
// Clear existing messages
chatMessages.innerHTML = '';
const welcomeMsg = document.createElement('div');
welcomeMsg.className = 'message-enter';
welcomeMsg.innerHTML = `
<div class="ai-message p-4">
<div class="flex items-start space-x-3">
<div class="w-8 h-8 bg-gradient-to-br from-indigo-100 to-purple-100 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-robot text-indigo-600"></i>
</div>
<div class="flex-1">
<div class="flex items-center mb-2">
<span class="font-medium text-indigo-700">AI Assistant</span>
<span class="ml-2 text-xs bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded-full">Online</span>
</div>
<div class="text-gray-700 mb-3">
<p class="mb-3">Hello! I'm ready to help you explore your documents. You can ask me questions like:</p>
<ul class="space-y-2 text-sm">
<li class="flex items-start">
<i class="fas fa-chevron-right text-indigo-500 text-xs mt-1 mr-2"></i>
<span>What are the main points in the uploaded documents?</span>
</li>
<li class="flex items-start">
<i class="fas fa-chevron-right text-indigo-500 text-xs mt-1 mr-2"></i>
<span>Can you summarize the key findings?</span>
</li>
<li class="flex items-start">
<i class="fas fa-chevron-right text-indigo-500 text-xs mt-1 mr-2"></i>
<span>Compare information across different documents</span>
</li>
<li class="flex items-start">
<i class="fas fa-chevron-right text-indigo-500 text-xs mt-1 mr-2"></i>
<span>Find specific details or statistics</span>
</li>
</ul>
<p class="mt-4 font-medium">What would you like to know?</p>
</div>
<div class="text-xs text-gray-500">
<i class="far fa-clock mr-1"></i>System ready • ${new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
</div>
</div>
</div>
</div>
`;
chatMessages.appendChild(welcomeMsg);
}
function handleChatInputKeypress(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
async function sendMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) return;
// Add user message
addChatMessage('user', message);
input.value = '';
updateTokenCount();
// Show loading indicator
const loadingMsg = addLoadingMessage();
try {
console.log('Sending chat message to backend:', message);
// Send to backend
const response = await apiService.sendChatMessage(message, currentSessionId);
console.log('Chat API response received:', response);
// Remove loading message
loadingMsg.remove();
// Update session ID if this is the first message
if (!currentSessionId) {
currentSessionId = response.session_id;
console.log('New session ID:', currentSessionId);
startSessionHeartbeat();
}
// Add AI response
addChatMessage('assistant', response.response, response.sources, response.metrics, response.ragas_metrics, response.query_type);
// Refresh RAGAS table if analytics section is active
if (document.getElementById('analytics-section') && !document.getElementById('analytics-section').classList.contains('hidden')) {
setTimeout(() => refreshRagasTable(), 500);
}
} catch (error) {
console.error('Chat API error details:', error);
loadingMsg.remove();
// More specific error message
let errorMessage = `Sorry, I encountered an error: ${error.message}`;
if (error.message.includes('Failed to fetch')) {
errorMessage = 'Unable to connect to the server. Please check if the backend is running.';
} else if (error.message.includes('500')) {
errorMessage = 'Server error. Please try again later.';
}
addChatMessage('assistant', errorMessage, [], {});
showNotification(`Chat failed: ${error.message}`, 'error');
}
}
function addLoadingMessage() {
const chatMessages = document.getElementById('chat-messages');
const loadingMsg = document.createElement('div');
loadingMsg.className = 'message-enter ai-message p-4';
loadingMsg.innerHTML = `
<div class="flex items-start space-x-3">
<div class="w-8 h-8 bg-gradient-to-br from-indigo-100 to-purple-100 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-robot text-indigo-600"></i>
</div>
<div class="flex-1">
<div class="font-medium text-indigo-700 mb-2">AI Assistant</div>
<div class="text-gray-700 flex items-center">
<div class="typing-dots mr-2">
<span></span>
<span></span>
<span></span>
</div>
Thinking...
</div>
</div>
</div>
`;
chatMessages.appendChild(loadingMsg);
chatMessages.scrollTop = chatMessages.scrollHeight;
return loadingMsg;
}
function addChatMessage(role, content, sources = [], metrics = {}, ragasMetrics = {}, queryType = 'rag') {
const chatMessages = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
messageDiv.className = `message-enter ${role === 'user' ? 'user-message' : 'ai-message'} p-4 mb-4`;
if (role === 'user') {
messageDiv.innerHTML = `
<div class="flex items-start space-x-3">
<div class="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-user text-white text-sm"></i>
</div>
<div class="flex-1 min-w-0">
<div class="font-medium text-white mb-2">You</div>
<div class="text-white leading-relaxed whitespace-pre-wrap">
${formatChatContent(content)}
</div>
<div class="text-white/80 text-xs mt-3">
<i class="far fa-clock mr-1"></i>${new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
</div>
</div>
</div>
`;
} else {
// Determine if this is a RAG or non-RAG response
const isRAG = sources && sources.length > 0;
const actualQueryType = queryType || (isRAG ? 'rag' : 'non-rag');
// AI message with collapsible sources (only for RAG queries)
let sourcesHTML = '';
if (isRAG) {
sourcesHTML = `
<div class="mt-4 pt-4 border-t border-gray-200">
<div class="collapsible-header collapsed flex items-center justify-between mb-3 cursor-pointer" onclick="toggleSources(this)">
<div class="flex items-center">
<i class="fas fa-link text-gray-400 mr-2 text-sm"></i>
<span class="text-sm font-medium text-gray-700">Sources (${sources.length})</span>
</div>
<i class="fas fa-chevron-down text-gray-400 text-xs transition-transform"></i>
</div>
<div class="collapsible-content collapsed space-y-2">
${sources.map((source, index) => `
<div class="source-card bg-gray-50 rounded-lg p-3 border border-gray-200">
<div class="flex justify-between items-start mb-1">
<div class="flex items-center">
<span class="bg-blue-100 text-blue-700 text-xs font-medium px-2 py-0.5 rounded mr-2">${index + 1}</span>
<span class="text-xs font-medium text-gray-600 truncate">${source.document_id || 'Document'}</span>
</div>
<span class="text-xs font-medium text-gray-500">${source.score ? source.score.toFixed(2) : 'N/A'}</span>
</div>
<div class="text-sm text-gray-600 leading-snug">
${escapeHtml(source.text_preview || source.text || 'Source content')}
</div>
</div>
`).join('')}
</div>
</div>
`;
}
// Add query type badge
const queryTypeBadge = actualQueryType === 'rag' ?
'<span class="ml-2 text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">RAG</span>' :
'<span class="ml-2 text-xs bg-purple-100 text-purple-700 px-2 py-0.5 rounded-full">General</span>';
messageDiv.innerHTML = `
<div class="flex items-start space-x-3">
<div class="w-8 h-8 bg-gradient-to-br from-indigo-100 to-purple-100 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-robot text-indigo-600"></i>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center mb-2">
<span class="font-medium text-indigo-700">AI Assistant</span>
${queryTypeBadge}
${ragasMetrics && ragasMetrics.answer_relevancy ? `
<span class="ml-2 text-xs ${ragasMetrics.answer_relevancy >= 0.7 ? 'bg-green-100 text-green-700' : ragasMetrics.answer_relevancy >= 0.4 ? 'bg-yellow-100 text-yellow-700' : 'bg-red-100 text-red-700'} px-2 py-0.5 rounded-full">
${ragasMetrics.answer_relevancy.toFixed(1)} relevancy
</span>
` : ''}
</div>
<div class="text-gray-700 leading-relaxed whitespace-pre-wrap">
${formatChatContent(content)}
</div>
${sourcesHTML}
<div class="flex justify-between items-center mt-3 pt-3 border-t border-gray-200">
<div class="text-xs text-gray-500">
<i class="far fa-clock mr-1"></i>${new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
</div>
${metrics.total_time ? `
<div class="text-xs text-gray-500">
<i class="fas fa-bolt mr-1"></i>${metrics.total_time}ms
</div>
` : ''}
</div>
</div>
</div>
`;
}
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// Helper functions
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatChatContent(content) {
if (!content) return '';
// Escape HTML first
let formatted = escapeHtml(content);
// Format lists (lines starting with numbers or bullets)
formatted = formatted.replace(/^(\d+\.\s+.+)$/gm, '<li class="ml-4 mb-1">$1</li>');
formatted = formatted.replace(/^(•\s+.+)$/gm, '<li class="ml-4 mb-1">$1</li>');
formatted = formatted.replace(/^(-\s+.+)$/gm, '<li class="ml-4 mb-1">$1</li>');
// Wrap lists in UL tags if we have list items
if (formatted.includes('<li class="ml-4 mb-1">')) {
formatted = formatted.replace(/(<li class="ml-4 mb-1">[^<]+<\/li>)+/g, '<ul class="list-disc pl-5 my-2 space-y-1">$&</ul>');
}
// Format bold text (markdown **text**)
formatted = formatted.replace(/\*\*([^*]+)\*\*/g, '<strong class="font-semibold">$1</strong>');
// Add paragraph spacing for multiple newlines
const paragraphs = formatted.split('\n\n');
if (paragraphs.length > 1) {
formatted = paragraphs.map(p => {
const trimmed = p.trim();
if (trimmed && !trimmed.includes('<ul') && !trimmed.includes('<li')) {
return `<p class="mb-3">${trimmed}</p>`;
}
return trimmed;
}).join('');
}
return formatted;
}
function insertExample(type) {
const examples = {
summary: "Summarize the main points from all uploaded documents.",
compare: "Compare the information across different documents.",
find: "Find specific details or statistics about the topic."
};
const input = document.getElementById('chat-input');
input.value = examples[type];
input.focus();
updateTokenCount();
}
function clearChat() {
if (confirm('Clear all chat messages and reset session?')) {
const chatMessages = document.getElementById('chat-messages');
chatMessages.innerHTML = '';
addWelcomeMessage();
// Also cleanup the session on backend
if (currentSessionId) {
apiService.cleanupSession(currentSessionId)
.then(() => {
console.log('Session cleaned up after chat clear');
currentSessionId = null;
cleanupSent = false;
stopSessionHeartbeat();
})
.catch(err => console.error('Session cleanup failed:', err));
}
}
}
function updateTokenCount() {
const input = document.getElementById('chat-input');
const tokenCount = Math.ceil(input.value.length / 4); // Rough estimate
document.getElementById('token-count').textContent = `${tokenCount}`;
}
// RAGAS Table Functions
async function refreshRagasTable() {
try {
console.log('Refreshing RAGAS table...');
const response = await apiService.getRagasHistory();
updateRagasTable(response.history || [], response.statistics || {});
showNotification('RAGAS table updated', 'success');
} catch (error) {
console.error('RAGAS table refresh error:', error);
showNotification(`Failed to refresh RAGAS table: ${error.message}`, 'error');
// Show empty state
updateRagasTable([], {});
}
}
function updateRagasTable(history, statistics) {
const tableBody = document.getElementById('ragas-table-body');
// Update statistics
document.getElementById('ragas-total-count').textContent = statistics.total_evaluations || history.length || 0;
document.getElementById('ragas-avg-relevancy').textContent = statistics.avg_answer_relevancy ? statistics.avg_answer_relevancy.toFixed(3) : '-';
document.getElementById('ragas-avg-faithfulness').textContent = statistics.avg_faithfulness ? statistics.avg_faithfulness.toFixed(3) : '-';
document.getElementById('ragas-avg-precision').textContent = statistics.avg_context_utilization ? statistics.avg_context_utilization.toFixed(3) : '-';
// Update table count
document.getElementById('ragas-table-count').textContent = `(${history.length} entries)`;
if (history.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="13" class="px-4 py-8 text-center text-gray-500">
<i class="fas fa-info-circle text-3xl mb-2 text-gray-400"></i>
<p>No RAGAS evaluations yet. Start chatting to see quality metrics.</p>
</td>
</tr>
`;
return;
}
// Clear existing rows
tableBody.innerHTML = '';
// Helper function to get RAGAS score class
const getRagasScoreClass = (score, isRAG) => {
if (score === null || score === undefined || !isRAG) return 'ragas-na';
if (score >= 0.7) return 'ragas-good';
if (score >= 0.4) return 'ragas-fair';
return 'ragas-poor';
};
// Helper function to format score value
const formatScoreValue = (score, isRAG) => {
if (score === null || score === undefined || !isRAG) return 'N/A';
return score.toFixed(3);
};
// Helper function to get query type class
const getQueryTypeClass = (queryType) => {
return queryType === 'rag' ? 'query-type-rag' : 'query-type-non-rag';
};
// Helper function to truncate text
const truncateText = (text, maxLength = 100) => {
if (!text) return '';
if (text.length <= maxLength) return escapeHtml(text);
return escapeHtml(text.substring(0, maxLength)) + '...';
};
// Add rows
history.forEach((item, index) => {
const row = document.createElement('tr');
row.className = index % 2 === 0 ? 'bg-white' : 'bg-gray-50';
const timestamp = item.timestamp ? new Date(item.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : '';
const date = item.timestamp ? new Date(item.timestamp).toLocaleDateString() : '';
// Use the query_type from backend, don't try to infer it
const queryType = item.query_type || 'rag'; // Default to 'rag' if missing
const isRAG = queryType === 'rag';
// Display RAG or GENERAL (not NON-RAG)
const queryTypeDisplay = queryType.toUpperCase();
row.innerHTML = `
<td class="px-4 py-3 text-sm text-gray-900">${index + 1}</td>
<td class="px-4 py-3 text-sm text-gray-600">
<div class="font-medium">${date}</div>
<div class="text-xs">${timestamp}</div>
</td>
<td class="px-4 py-3 text-sm text-gray-800">${truncateText(item.query, 120)}</td>
<td class="px-4 py-3 text-sm text-gray-800">${truncateText(item.answer, 120)}</td>
<td class="px-4 py-3 text-center">
<span class="ragas-cell ${getQueryTypeClass(queryType)}">${queryTypeDisplay}</span>
</td>
<td class="px-4 py-3 text-center">
<span class="ragas-cell ${getRagasScoreClass(item.answer_relevancy, isRAG)}">
${formatScoreValue(item.answer_relevancy, isRAG)}
</span>
</td>
<td class="px-4 py-3 text-center">
<span class="ragas-cell ${getRagasScoreClass(item.faithfulness, isRAG)}">
${formatScoreValue(item.faithfulness, isRAG)}
</span>
</td>
<td class="px-4 py-3 text-center">
<span class="ragas-cell ${getRagasScoreClass(item.context_utilization || item.context_precision, isRAG)}">
${formatScoreValue(item.context_utilization || item.context_precision, isRAG)}
</span>
</td>
<td class="px-4 py-3 text-center">
<span class="ragas-cell ${getRagasScoreClass(item.context_relevancy, isRAG)}">
${formatScoreValue(item.context_relevancy, isRAG)}
</span>
</td>
<td class="px-4 py-3 text-center text-sm text-gray-600">${item.retrieval_time_ms || (isRAG ? '0' : 'N/A')}</td>
<td class="px-4 py-3 text-center text-sm text-gray-600">${item.generation_time_ms || 0}</td>
<td class="px-4 py-3 text-center text-sm text-gray-600">${item.total_time_ms || 0}</td>
<td class="px-4 py-3 text-center text-sm text-gray-600">${item.chunks_retrieved || (isRAG ? '0' : 'N/A')}</td>
`;
tableBody.appendChild(row);
});
}
async function clearRagasHistory() {
try {
if (!confirm('Are you sure you want to clear all RAGAS evaluation history? This cannot be undone.')) {
return;
}
const response = await apiService.clearRagasHistory();
if (response.success) {
showNotification(response.message, 'success');
refreshRagasTable();
} else {
throw new Error('Failed to clear RAGAS history');
}
} catch (error) {
console.error('Clear RAGAS history error:', error);
showNotification(`Failed to clear RAGAS history: ${error.message}`, 'error');
}
}
async function exportRagasData() {
try {
const blob = await apiService.exportRagasData();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `ragas-export-${new Date().toISOString().slice(0, 10)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showNotification('RAGAS data exported successfully', 'success');
} catch (error) {
console.error('Export RAGAS data error:', error);
showNotification(`Failed to export RAGAS data: ${error.message}`, 'error');
}
}
async function loadConfigurationUI() {
try {
const config = await apiService.getConfiguration();
const health = await apiService.getHealth();
if (config.configuration) {
// Populate form with current configuration
const formElements = document.getElementById('config-form').elements;
Object.keys(config.configuration).forEach(key => {
const element = formElements[key];
if (element) {
if (element.type === 'checkbox') {
element.checked = config.configuration[key];
} else if (element.type === 'select-one') {
element.value = config.configuration[key];
} else {
element.value = config.configuration[key];
}
}
});
}
if (health) {
updateConfigurationStatus(health);
}
} catch (error) {
console.error('Configuration UI load error:', error);
}
}
function updateConfigurationStatus(health) {
if (!health || !health.components) return;
const statusMap = {
'config-llm-health': health.components.llm,
'config-vector-store': health.components.vector_store,
'config-embeddings': health.components.embeddings,
'config-retrieval': health.components.retrieval,
'config-generation': health.components.generation,
'config-overall': health.status
};
Object.keys(statusMap).forEach(id => {
const element = document.getElementById(id);
if (element) {
const value = statusMap[id];
if (typeof value === 'boolean') {
element.textContent = value ? '✓ Healthy' : '✗ Unhealthy';
element.className = value ? 'font-medium text-green-600' : 'font-medium text-red-600';
} else {
element.textContent = value || '-';
element.className = value === 'healthy' ? 'font-medium text-green-600' :
value === 'degraded' ? 'font-medium text-yellow-600' :
'font-medium text-gray-600';
}
}
});
}
async function saveConfiguration(e) {
e.preventDefault();
const formData = new FormData(e.target);
const config = {};
for (let [key, value] of formData.entries()) {
// Convert number fields to appropriate types
if (['chunk_size', 'chunk_overlap', 'retrieval_top_k', 'max_tokens'].includes(key)) {
config[key] = parseInt(value);
} else if (['vector_weight', 'bm25_weight', 'temperature'].includes(key)) {
config[key] = parseFloat(value);
} else if (key === 'enable_reranking') {
config[key] = value === 'on';
} else {
config[key] = value;
}
}
try {
await apiService.updateConfiguration(config);
showNotification('Configuration saved successfully!', 'success');
// Update system with new configuration
systemReady = true;
} catch (error) {
console.error('Configuration save error:', error);
showNotification(`Configuration save failed: ${error.message}`, 'error');
}
}
async function exportChat(format) {
if (!currentSessionId) {
showNotification('No chat session to export', 'warning');
return;
}
try {
const blob = await apiService.exportChat(currentSessionId, format);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `chat-export-${currentSessionId}-${Date.now()}.${format}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showNotification(`Chat exported as ${format.toUpperCase()}`, 'success');
} catch (error) {
console.error('Export error:', error);
showNotification(`Export failed: ${error.message}`, 'error');
}
}
function toggleSources(element) {
const header = element.closest('.collapsible-header');
const content = element.closest('.collapsible-header').nextElementSibling;
header.classList.toggle('collapsed');
content.classList.toggle('collapsed');
// If we're expanding, set max-height to scrollHeight
if (!content.classList.contains('collapsed')) {
content.style.maxHeight = content.scrollHeight + 'px';
} else {
content.style.maxHeight = '0';
}
}
function showNotification(message, type = 'info') {
// Remove existing notifications
document.querySelectorAll('.notification').forEach(n => n.remove());
const notification = document.createElement('div');
notification.className = `notification fixed top-4 right-4 p-4 rounded-lg shadow-lg text-white z-50 ${
type === 'success' ? 'bg-green-500' :
type === 'error' ? 'bg-red-500' :
type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500'
}`;
notification.innerHTML = `
<div class="flex items-center">
<i class="fas ${
type === 'success' ? 'fa-check-circle' :
type === 'error' ? 'fa-exclamation-circle' :
type === 'warning' ? 'fa-exclamation-triangle' : 'fa-info-circle'
} mr-2"></i>
<span>${message}</span>
</div>
`;
document.body.appendChild(notification);
// Auto-remove after 5 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 5000);
}
// Initialize when page loads
document.addEventListener('DOMContentLoaded', function() {
// SESSION CLEANUP ON PAGE UNLOAD
window.addEventListener('beforeunload', function(e) {
// Only cleanup if we have an active session and haven't sent cleanup yet
if (!cleanupSent && currentSessionId) {
cleanupSent = true;
try {
// Use sendBeacon for reliability (works even during page close)
const data = JSON.stringify({
session_id: currentSessionId,
timestamp: new Date().toISOString()
});
const blob = new Blob([data], { type: 'application/json' });
// Send cleanup request
navigator.sendBeacon(
`${apiService.baseURL}/api/cleanup/session/${currentSessionId}`,
blob
);
console.log('✅ Session cleanup sent via sendBeacon');
} catch (error) {
console.log('❌ Session cleanup failed:', error);
// Fallback: try regular fetch (might not work on page close)
try {
apiService.cleanupSession(currentSessionId)
.then(() => console.log('✅ Fallback cleanup succeeded'))
.catch(err => console.log('❌ Fallback cleanup failed:', err));
} catch (fetchError) {
console.log('❌ Both cleanup methods failed');
}
}
// Stop heartbeat
stopSessionHeartbeat();
}
});
// Also cleanup when page becomes hidden (tab switch, minimize)
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
// Page is now hidden (user switched tabs or minimized)
console.log('Page hidden - session may become inactive');
}
});
showTab('features');
});
</script>
</body>
</html>