diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..88d19119e2e588d0610e730e85b6dd116de92af2
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,18 @@
+# KnowledgeBridge Environment Configuration
+# Copy this file to .env and fill in your API keys
+
+# Nebius AI Configuration (Required for AI features)
+NEBIUS_API_KEY=your_nebius_api_key_here
+
+# Modal Configuration (Optional - for distributed processing)
+MODAL_TOKEN_ID=your_modal_token_id
+MODAL_TOKEN_SECRET=your_modal_token_secret
+MODAL_BASE_URL=your_modal_endpoint
+
+# GitHub Configuration (Optional - for repository search)
+GITHUB_TOKEN=your_github_personal_access_token
+
+# Node Environment
+NODE_ENV=production
+PORT=7860
+EOF < /dev/null
\ No newline at end of file
diff --git a/.gitattributes copy b/.gitattributes copy
new file mode 100644
index 0000000000000000000000000000000000000000..a6344aac8c09253b3b630fb776ae94478aa0275b
--- /dev/null
+++ b/.gitattributes copy
@@ -0,0 +1,35 @@
+*.7z filter=lfs diff=lfs merge=lfs -text
+*.arrow filter=lfs diff=lfs merge=lfs -text
+*.bin filter=lfs diff=lfs merge=lfs -text
+*.bz2 filter=lfs diff=lfs merge=lfs -text
+*.ckpt filter=lfs diff=lfs merge=lfs -text
+*.ftz filter=lfs diff=lfs merge=lfs -text
+*.gz filter=lfs diff=lfs merge=lfs -text
+*.h5 filter=lfs diff=lfs merge=lfs -text
+*.joblib filter=lfs diff=lfs merge=lfs -text
+*.lfs.* filter=lfs diff=lfs merge=lfs -text
+*.mlmodel filter=lfs diff=lfs merge=lfs -text
+*.model filter=lfs diff=lfs merge=lfs -text
+*.msgpack filter=lfs diff=lfs merge=lfs -text
+*.npy filter=lfs diff=lfs merge=lfs -text
+*.npz filter=lfs diff=lfs merge=lfs -text
+*.onnx filter=lfs diff=lfs merge=lfs -text
+*.ot filter=lfs diff=lfs merge=lfs -text
+*.parquet filter=lfs diff=lfs merge=lfs -text
+*.pb filter=lfs diff=lfs merge=lfs -text
+*.pickle filter=lfs diff=lfs merge=lfs -text
+*.pkl filter=lfs diff=lfs merge=lfs -text
+*.pt filter=lfs diff=lfs merge=lfs -text
+*.pth filter=lfs diff=lfs merge=lfs -text
+*.rar filter=lfs diff=lfs merge=lfs -text
+*.safetensors filter=lfs diff=lfs merge=lfs -text
+saved_model/**/* filter=lfs diff=lfs merge=lfs -text
+*.tar.* filter=lfs diff=lfs merge=lfs -text
+*.tar filter=lfs diff=lfs merge=lfs -text
+*.tflite filter=lfs diff=lfs merge=lfs -text
+*.tgz filter=lfs diff=lfs merge=lfs -text
+*.wasm filter=lfs diff=lfs merge=lfs -text
+*.xz filter=lfs diff=lfs merge=lfs -text
+*.zip filter=lfs diff=lfs merge=lfs -text
+*.zst filter=lfs diff=lfs merge=lfs -text
+*tfevents* filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..c1931370b05cd86c678cd881c5ae6803806b8220
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,78 @@
+node_modules
+# dist - commented out for Hugging Face Spaces deployment
+.DS_Store
+server/public
+vite.config.ts.*
+*.tar.gz
+attached_assets
+server.log
+
+# Environment files
+.env
+.env.local
+.env.production
+.env.development
+
+# Logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# Virtual environments
+venv/
+env/
+ENV/
+env.bak/
+venv.bak/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Test coverage
+coverage/
+.coverage
+.nyc_output
+htmlcov/
+
+# Temporary files
+*.tmp
+*.temp
+temp/
+tmp/
\ No newline at end of file
diff --git a/.replit b/.replit
new file mode 100644
index 0000000000000000000000000000000000000000..5b4d8d7fc5abb97cc462f6ebfa2d3629bac9328e
--- /dev/null
+++ b/.replit
@@ -0,0 +1,37 @@
+modules = ["nodejs-20", "web", "postgresql-16", "python-3.11"]
+run = "npm run dev"
+hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"]
+
+[nix]
+channel = "stable-24_05"
+packages = ["ffmpeg-full"]
+
+[deployment]
+deploymentTarget = "autoscale"
+build = ["npm", "run", "build"]
+run = ["npm", "run", "start"]
+
+[[ports]]
+localPort = 5000
+externalPort = 80
+
+[workflows]
+runButton = "Project"
+
+[[workflows.workflow]]
+name = "Project"
+mode = "parallel"
+author = "agent"
+
+[[workflows.workflow.tasks]]
+task = "workflow.run"
+args = "Start application"
+
+[[workflows.workflow]]
+name = "Start application"
+author = "agent"
+
+[[workflows.workflow.tasks]]
+task = "shell.exec"
+args = "npm run dev"
+waitForPort = 5000
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..910bc718fa3be6bc1c64164d60ac391059410c94
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,45 @@
+# Use Node.js 18 Alpine for smaller image size
+FROM node:18-alpine
+
+# Set working directory
+WORKDIR /app
+
+# Install system dependencies
+RUN apk add --no-cache \
+ python3 \
+ make \
+ g++ \
+ curl \
+ && ln -sf python3 /usr/bin/python
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+RUN npm ci --only=production
+
+# Copy source code
+COPY . .
+
+# Build the application
+RUN npm run build
+
+# Create non-root user for security
+RUN addgroup -g 1001 -S nodejs && \
+ adduser -S nextjs -u 1001
+
+# Change ownership of the app directory
+RUN chown -R nextjs:nodejs /app
+
+# Switch to non-root user
+USER nextjs
+
+# Expose port
+EXPOSE 7860
+
+# Health check
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+ CMD curl -f http://localhost:7860/api/health || exit 1
+
+# Start the application
+CMD ["npm", "start"]
\ No newline at end of file
diff --git a/README copy.md b/README copy.md
new file mode 100644
index 0000000000000000000000000000000000000000..f9e4fabfbe610e8eb0ce1e58f2dd736fadd898e2
--- /dev/null
+++ b/README copy.md
@@ -0,0 +1,344 @@
+---
+title: KnowledgeBridge
+emoji: š
+colorFrom: yellow
+colorTo: red
+sdk: docker
+pinned: false
+license: mit
+short_description: 'A sophisticated AI-powered knowledge retrieval and analysis '
+tags:
+ - agent-demo-track
+---
+
+# KnowledgeBridge
+
+š **An AI-Enhanced Knowledge Discovery Platform**
+
+A sophisticated AI-powered knowledge retrieval and analysis system that combines semantic search, real-time web integration, and intelligent document processing for research and information discovery.
+
+
+
+
+
+
+## šÆ Hackathon Submission
+
+**š¤ Track 3: Agentic Demo Showcase**
+
+**Submitted to**: [Hugging Face Agents-MCP-Hackathon](https://huggingface.co/Agents-MCP-Hackathon)
+
+**Live Demo**: [Try KnowledgeBridge on Hugging Face Spaces](https://huggingface.co/spaces/YOUR_USERNAME/KnowledgeBridge)
+
+### **š "Show us the most incredible things that your agents can do!"**
+
+KnowledgeBridge demonstrates sophisticated AI agent orchestration through multi-modal knowledge discovery, intelligent query enhancement, and autonomous research synthesis.
+
+## š¤ Agentic Capabilities Showcase
+
+### š§ **Multi-Agent Orchestration**
+- **Coordinated Search Agents**: Simultaneous deployment across GitHub, Wikipedia, ArXiv, and web sources
+- **Intelligent Load Balancing**: Agents dynamically distribute workload based on query type and source availability
+- **Fallback Agent Strategy**: Backup agents activate when primary sources fail or timeout
+- **Real-Time Coordination**: Agents communicate results and adapt search strategies collaboratively
+
+### š **Query Enhancement Agents**
+- **Intent Recognition Agents**: AI agents analyze user intent and suggest optimal search strategies
+- **Semantic Expansion Agents**: Agents enhance queries with related terms and concepts
+- **Context-Aware Agents**: Agents consider previous searches and user preferences
+- **Multi-Modal Query Agents**: Agents adapt search approach based on content type (code, academic, general)
+
+### š **Analysis & Synthesis Agents**
+- **Document Processing Agents**: Autonomous analysis with configurable reasoning (summary, classification, key points)
+- **Research Synthesis Agents**: AI agents combine insights from multiple sources into coherent analysis
+- **Quality Assessment Agents**: Agents evaluate source credibility and content relevance
+- **Format Adaptation Agents**: Agents dynamically adjust output format (markdown/plain text) based on user needs
+
+### š”ļø **Security & Validation Agents**
+- **URL Validation Agents**: Intelligent agents verify link accessibility and content authenticity
+- **Rate Limiting Agents**: Protective agents prevent API abuse (100 requests/15min, 10/min for sensitive endpoints)
+- **Input Sanitization Agents**: Security agents validate and clean all user inputs
+- **Error Recovery Agents**: Resilient agents handle failures gracefully and maintain system stability
+
+### š **Intelligent Integration Agents**
+- **ArXiv Academic Agents**: Specialized agents for academic paper validation and retrieval
+- **GitHub Repository Agents**: Code-focused agents with author filtering and relevance scoring
+- **Wikipedia Knowledge Agents**: Authoritative content agents with intelligent caching strategies
+- **Cross-Platform Synthesis Agents**: Agents that combine and rank results across all sources
+
+## šļø Technical Architecture
+
+### **Frontend Stack**
+- **React 18** with TypeScript for type-safe development
+- **Wouter Router** for lightweight client-side routing
+- **TanStack Query** for efficient data fetching and caching
+- **Radix UI + Tailwind CSS** for accessible, modern components
+- **Framer Motion** for smooth animations and transitions
+
+### **Backend Stack**
+- **Node.js + Express** with comprehensive middleware
+- **Nebius AI** integration with DeepSeek models
+- **Modal** for distributed processing and scalability
+- **Express Rate Limit** for API protection
+- **Helmet.js** for security headers
+
+### **AI & Processing**
+- **DeepSeek-R1-0528** for chat completions and document analysis
+- **BAAI/bge-en-icl** for embedding generation
+- **Modal Client** for distributed compute tasks
+- **Smart Ingestion Service** for advanced document processing
+
+## š Quick Start
+
+### **Environment Configuration**
+
+Create a `.env` file in the project root:
+
+```bash
+# Nebius AI Configuration (Required)
+NEBIUS_API_KEY=your_nebius_api_key_here
+
+# Modal Configuration (Optional - for advanced processing)
+MODAL_TOKEN_ID=your_modal_token_id
+MODAL_TOKEN_SECRET=your_modal_token_secret
+MODAL_BASE_URL=your_modal_endpoint
+
+# GitHub Configuration (Optional - for repository search)
+GITHUB_TOKEN=your_github_token_here
+
+# Node Environment
+NODE_ENV=development
+```
+
+### **Development Setup**
+
+```bash
+# Install dependencies
+npm install
+
+# Start development server
+npm run dev
+
+# Build for production
+npm run build
+
+# Type checking
+npm run check
+```
+
+The application will be available at `http://localhost:5000`
+
+## šÆ Usage Guide
+
+### **Search Interface**
+1. **Basic Search**: Enter queries in natural language
+2. **AI Enhancement**: Click the sparkle icon to improve your query
+3. **Advanced Search**: Use the AI tools panel for document analysis
+4. **Export Results**: Generate citations in multiple formats
+
+### **AI Tools**
+- **Document Analysis**: Paste content for AI-powered analysis with configurable formatting
+- **Embeddings**: Generate vector representations of text
+- **Query Enhancement**: Get AI suggestions for better search queries
+
+### **Knowledge Graph**
+- Interactive visualization of document relationships
+- Filter by concepts, authors, and source types
+- Explore connections between research papers and topics
+
+## š§ API Reference
+
+### **Search Endpoints**
+```typescript
+POST /api/search
+{
+ query: string;
+ searchType: "semantic" | "keyword" | "hybrid";
+ limit: number;
+ filters?: {
+ sourceTypes?: string[];
+ };
+}
+```
+
+### **AI Analysis Endpoints**
+```typescript
+POST /api/analyze-document
+{
+ content: string;
+ analysisType: "summary" | "classification" | "key_points" | "quality_score";
+ useMarkdown?: boolean;
+}
+
+POST /api/enhance-query
+{
+ query: string;
+ context?: string;
+}
+
+POST /api/embeddings
+{
+ input: string;
+ model?: string;
+}
+```
+
+### **Health Check**
+```typescript
+GET /api/health
+// Returns comprehensive health status of all services
+```
+
+## š Performance & Reliability
+
+### **Response Times**
+- Local search: <100ms for semantic queries
+- Document analysis: ~3-5 seconds depending on content length
+- URL validation: <2 seconds per URL with concurrent processing
+- Embedding generation: ~500ms-1s per request
+
+### **Scalability Features**
+- Rate limiting prevents API abuse
+- Concurrent URL validation with configurable limits
+- Efficient caching for repeated queries
+- Graceful degradation when external services are unavailable
+
+### **Error Handling**
+- React Error Boundaries prevent UI crashes
+- Comprehensive API error responses
+- Automatic retry logic for network requests
+- User-friendly error messages
+
+## š Security Features
+
+### **Input Protection**
+- Request body size limits (10MB)
+- Comprehensive input sanitization
+- SQL injection prevention
+- XSS protection with CSP headers
+
+### **API Security**
+- Rate limiting on all endpoints
+- Secure environment variable handling
+- No hardcoded credentials
+- Proper error logging without information disclosure
+
+### **Infrastructure Security**
+- Helmet.js security headers
+- CORS configuration
+- Secure cookie handling
+- Production-ready error handling
+
+## š ļø Development
+
+### **Code Quality**
+- 100% TypeScript coverage
+- ESLint + Prettier configuration
+- Comprehensive error handling
+- Type-safe API contracts with Zod validation
+
+### **Testing**
+```bash
+# Type checking
+npm run check
+
+# Development server
+npm run dev
+
+# Production build
+npm run build
+```
+
+## š Recent Updates
+
+- ā
**Security Hardening**: Removed all hardcoded credentials, added comprehensive security middleware
+- ā
**TypeScript Migration**: Achieved 100% type safety across the entire codebase
+- ā
**URL Validation**: Intelligent filtering of broken and invalid links
+- ā
**Error Handling**: React Error Boundaries and improved server error handling
+- ā
**AI Enhancement**: Nebius AI integration with configurable document analysis
+- ā
**Performance**: Rate limiting, input validation, and optimized processing
+
+## š Architecture Highlights
+
+### **AI Integration**
+- **Nebius AI**: Primary AI service for all language model tasks
+- **DeepSeek Models**: State-of-the-art reasoning capabilities
+- **Modal Integration**: Distributed processing for heavy workloads
+- **Embedding Search**: Semantic similarity matching
+
+### **Data Flow**
+1. User query ā AI query enhancement (optional)
+2. Parallel search: local storage + external sources
+3. URL validation and content verification
+4. Result ranking and relevance scoring
+5. AI-powered analysis and synthesis
+
+### **Component Architecture**
+- **Enhanced Search Interface**: Unified search and AI tools
+- **Knowledge Graph**: Interactive data visualization
+- **Result Cards**: Rich content display with citations
+- **Error Boundaries**: Resilient error handling
+
+## š Track 3: Agentic Demo Showcase Features
+
+### **š¤ "Show us the most incredible things that your agents can do!"**
+
+KnowledgeBridge demonstrates sophisticated multi-agent systems in action:
+
+### **š§ Autonomous Agent Workflows**
+- **Smart Agent Coordination**: Multiple specialized agents work together to fulfill complex research tasks
+- **Adaptive Agent Behavior**: Agents dynamically adjust strategies based on query complexity and source availability
+- **Multi-Modal Agent Processing**: Different agent types (search, analysis, validation) collaborate seamlessly
+- **Intelligent Agent Fallbacks**: Backup agents activate automatically when primary agents encounter issues
+
+### **š Real-Time Agent Decision Making**
+- **Query Analysis Agents**: Instantly determine optimal search strategies across 4+ sources
+- **Load Balancing Agents**: Distribute workload intelligently based on API response times and rate limits
+- **Quality Control Agents**: Evaluate and filter results in real-time for relevance and authenticity
+- **Synthesis Agents**: Combine disparate information sources into coherent, actionable insights
+
+### **š Advanced Agent Orchestration**
+- **Parallel Agent Execution**: Simultaneous deployment of search agents across GitHub, Wikipedia, ArXiv
+- **Agent Communication Protocols**: Real-time coordination between agents for optimal resource utilization
+- **Adaptive Agent Learning**: Agents improve performance based on user interactions and feedback
+- **Error Recovery Agents**: Autonomous problem-solving when individual agents encounter failures
+
+### **š”ļø Production-Grade Agent Infrastructure**
+- **Security Agent Monitoring**: Continuous protection against abuse with intelligent rate limiting
+- **Validation Agent Networks**: Multi-layer content verification and URL authenticity checking
+- **Performance Agent Optimization**: Automatic scaling and resource management for enterprise workloads
+- **Resilience Agent Systems**: Graceful degradation and fault tolerance across all agent operations
+
+### **ā” Agent Performance Metrics**
+- **Sub-second Agent Response**: Query analysis and routing in <100ms
+- **Concurrent Agent Processing**: 4+ agents working simultaneously on complex research tasks
+- **Intelligent Agent Caching**: Smart result storage and retrieval for enhanced performance
+- **Scalable Agent Architecture**: Horizontal scaling support for enterprise deployment
+
+## š License
+
+MIT License - see [LICENSE](LICENSE) file for details.
+
+## š Related Resources
+
+- [Nebius AI Documentation](https://docs.nebius.ai/)
+- [Modal Documentation](https://modal.com/docs)
+- [React Query Documentation](https://tanstack.com/query/latest)
+- [Radix UI Components](https://www.radix-ui.com/)
+
+---
+
+## š Agents-MCP-Hackathon Submission Summary
+
+**KnowledgeBridge** showcases the incredible power of AI agents through:
+
+š¤ **Multi-Agent Orchestration** - Coordinated intelligence across search, analysis, and synthesis agents
+š **Real-Time Decision Making** - Agents adapt strategies and optimize performance dynamically
+š **Advanced Agent Workflows** - Complex multi-step processes handled autonomously
+š”ļø **Production-Ready Agent Infrastructure** - Enterprise-grade security and resilience
+
+**Track 3: Agentic Demo Showcase** - Demonstrating what happens when sophisticated AI agents work together to revolutionize knowledge discovery and research workflows.
+
+**Built for the Hugging Face Agents-MCP-Hackathon** š
+
+Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
\ No newline at end of file
diff --git a/app.py b/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..3ac82d670c7cb44efc3b2a0841155240ccb96194
--- /dev/null
+++ b/app.py
@@ -0,0 +1,27 @@
+"""
+KnowledgeBridge - AI-Enhanced Knowledge Discovery Platform
+Hugging Face Spaces compatibility layer
+"""
+
+import os
+import subprocess
+import sys
+
+def main():
+ """
+ This file exists for Hugging Face Spaces compatibility.
+ The actual application runs via Docker and the Node.js server.
+ """
+ print("š KnowledgeBridge is running via Docker on port 7860")
+ print("Visit the app interface above\!")
+ print("\nš Features:")
+ print("- AI-Enhanced Search with Nebius DeepSeek")
+ print("- Multi-source search (GitHub, Wikipedia, ArXiv)")
+ print("- Interactive Knowledge Graphs")
+ print("- Document Analysis with configurable output")
+ print("- Enterprise-grade security")
+ print("\nš Built for Agents-MCP-Hackathon")
+
+if __name__ == "__main__":
+ main()
+EOF < /dev/null
\ No newline at end of file
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..4b4d09e3151713b7af7e8d38dd2bac2a27d5a7ee
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/src/App.tsx b/client/src/App.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1bda47f9578353eeeb9a31bb705b4ef6502a2481
--- /dev/null
+++ b/client/src/App.tsx
@@ -0,0 +1,35 @@
+import { Switch, Route } from "wouter";
+import { queryClient } from "./lib/queryClient";
+import { QueryClientProvider } from "@tanstack/react-query";
+import { Toaster } from "@/components/ui/toaster";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import { ThemeProvider } from "@/components/theme-provider";
+import ErrorBoundary from "@/components/ErrorBoundary";
+import KnowledgeBase from "@/pages/knowledge-base";
+import NotFound from "@/pages/not-found";
+
+function Router() {
+ return (
+
+
+
+
+ );
+}
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/client/src/components/ErrorBoundary.tsx b/client/src/components/ErrorBoundary.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7100473cc1f12bce7c6db1362945b708754cd773
--- /dev/null
+++ b/client/src/components/ErrorBoundary.tsx
@@ -0,0 +1,75 @@
+import React, { Component, ErrorInfo, ReactNode } from 'react';
+
+interface Props {
+ children: ReactNode;
+}
+
+interface State {
+ hasError: boolean;
+ error: Error | null;
+}
+
+class ErrorBoundary extends Component {
+ public state: State = {
+ hasError: false,
+ error: null
+ };
+
+ public static getDerivedStateFromError(error: Error): State {
+ return { hasError: true, error };
+ }
+
+ public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ console.error('Error caught by boundary:', error, errorInfo);
+ }
+
+ public render() {
+ if (this.state.hasError) {
+ return (
+
+
+
+
+
+
+ Something went wrong
+
+
+
The application encountered an unexpected error. Please refresh the page and try again.
+
+
+
+
+
+
+ {process.env.NODE_ENV === 'development' && this.state.error && (
+
+
+
+ Error Details (Development)
+
+
+ {this.state.error.stack}
+
+
+
+ )}
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
+
+export default ErrorBoundary;
\ No newline at end of file
diff --git a/client/src/components/knowledge-base/ai-assistant.tsx b/client/src/components/knowledge-base/ai-assistant.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b7ef978b8acbf81a57b0b8e99afc4de78fa52bf2
--- /dev/null
+++ b/client/src/components/knowledge-base/ai-assistant.tsx
@@ -0,0 +1,507 @@
+import { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ Brain,
+ Sparkles,
+ FileText,
+ Search,
+ Loader2,
+ TrendingUp,
+ Lightbulb,
+ Target,
+ CheckCircle,
+ AlertCircle
+} from "lucide-react";
+
+interface AIAssistantProps {
+ onDocumentSelect?: (documentId: number) => void;
+}
+
+interface EnhancedSearchResult {
+ results: any[];
+ enhancedQuery?: {
+ enhancedQuery: string;
+ intent: string;
+ keywords: string[];
+ suggestions: string[];
+ };
+ searchInsights?: {
+ totalResults: number;
+ avgRelevanceScore: number;
+ modalResultsCount: number;
+ localResultsCount: number;
+ };
+}
+
+interface ResearchSynthesis {
+ synthesis: string;
+ keyFindings: string[];
+ gaps: string[];
+ recommendations: string[];
+}
+
+export default function AIAssistant({ onDocumentSelect }: AIAssistantProps) {
+ const [query, setQuery] = useState("");
+ const [selectedDocuments, setSelectedDocuments] = useState([]);
+ const [analysisText, setAnalysisText] = useState("");
+ const queryClient = useQueryClient();
+
+ // Enhanced AI Search
+ const aiSearchMutation = useMutation({
+ mutationFn: async (searchQuery: string): Promise => {
+ const response = await fetch("/api/ai-search", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ query: searchQuery,
+ maxResults: 10,
+ useQueryEnhancement: true
+ }),
+ });
+ if (!response.ok) throw new Error("Enhanced search failed");
+ return response.json();
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/search"] });
+ },
+ });
+
+ // Query Enhancement
+ const queryEnhancementMutation = useMutation({
+ mutationFn: async (originalQuery: string) => {
+ const response = await fetch("/api/enhance-query", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ query: originalQuery }),
+ });
+ if (!response.ok) throw new Error("Query enhancement failed");
+ return response.json();
+ },
+ });
+
+ // Document Analysis
+ const documentAnalysisMutation = useMutation({
+ mutationFn: async ({ content, analysisType }: { content: string; analysisType: string }) => {
+ const response = await fetch("/api/analyze-document", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ content, analysisType }),
+ });
+ if (!response.ok) throw new Error("Document analysis failed");
+ return response.json();
+ },
+ });
+
+ // Research Synthesis
+ const researchSynthesisMutation = useMutation({
+ mutationFn: async ({ query, documentIds }: { query: string; documentIds: number[] }): Promise => {
+ const response = await fetch("/api/research-synthesis", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ query, documentIds }),
+ });
+ if (!response.ok) throw new Error("Research synthesis failed");
+ return response.json();
+ },
+ });
+
+ // Generate Embeddings
+ const embeddingsMutation = useMutation({
+ mutationFn: async (input: string) => {
+ const response = await fetch("/api/embeddings", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ input }),
+ });
+ if (!response.ok) throw new Error("Embedding generation failed");
+ return response.json();
+ },
+ });
+
+ const handleEnhancedSearch = () => {
+ if (!query.trim()) return;
+ aiSearchMutation.mutate(query);
+ };
+
+ const handleQueryEnhancement = () => {
+ if (!query.trim()) return;
+ queryEnhancementMutation.mutate(query);
+ };
+
+ const handleDocumentAnalysis = (analysisType: string) => {
+ if (!analysisText.trim()) return;
+ documentAnalysisMutation.mutate({ content: analysisText, analysisType });
+ };
+
+ const handleResearchSynthesis = () => {
+ if (!query.trim() || selectedDocuments.length === 0) return;
+ researchSynthesisMutation.mutate({ query, documentIds: selectedDocuments });
+ };
+
+ const handleGenerateEmbeddings = () => {
+ if (!query.trim()) return;
+ embeddingsMutation.mutate(query);
+ };
+
+ return (
+
+
+
+
+
+ AI Research Assistant
+ Powered by Nebius & Modal
+
+
+
+
+
+
+
+ Smart Search
+
+
+
+ Analysis
+
+
+
+ Synthesis
+
+
+
+ Embeddings
+
+
+
+ {/* Enhanced Search Tab */}
+
+
+
+ setQuery(e.target.value)}
+ onKeyDown={(e) => e.key === "Enter" && handleEnhancedSearch()}
+ className="flex-1"
+ />
+
+
+
+
+
+
+
+
+ {/* Query Enhancement Results */}
+ {queryEnhancementMutation.data && (
+
+
+ Enhanced Query
+
+ {queryEnhancementMutation.data.enhancedQuery}
+
+
+
+ Intent:
+ {queryEnhancementMutation.data.intent}
+
+
+
Keywords:
+
+ {queryEnhancementMutation.data.keywords.map((keyword: string, i: number) => (
+
+ {keyword}
+
+ ))}
+
+
+
+
+
+ )}
+
+ {/* Enhanced Search Results */}
+ {aiSearchMutation.data && (
+
+
+
+
+ AI-Enhanced Results
+ {aiSearchMutation.data.searchInsights && (
+
+ {aiSearchMutation.data.searchInsights.totalResults} results
+
+ )}
+
+
+
+ {aiSearchMutation.data.searchInsights && (
+
+
+ Avg Relevance:
+
+ {(aiSearchMutation.data.searchInsights.avgRelevanceScore * 100).toFixed(1)}%
+
+
+
+ Modal Results:
+ {aiSearchMutation.data.searchInsights.modalResultsCount}
+
+
+ Local Results:
+ {aiSearchMutation.data.searchInsights.localResultsCount}
+
+
+ Total:
+ {aiSearchMutation.data.searchInsights.totalResults}
+
+
+ )}
+
+
+ {aiSearchMutation.data.results.map((result: any, index: number) => (
+
+
+
{result.title}
+
+ {result.relevanceScore && (
+
+ {(result.relevanceScore * 100).toFixed(0)}%
+
+ )}
+ {result.aiExplanation && (
+
+ )}
+
+
+
+ {result.snippet}
+
+ {result.keyReasons && (
+
+
AI Analysis:
+
+ {result.keyReasons.slice(0, 2).map((reason: string, i: number) => (
+ - {reason}
+ ))}
+
+
+ )}
+
+ ))}
+
+
+
+ )}
+
+
+ {/* Document Analysis Tab */}
+
+
+
+ {documentAnalysisMutation.data && (
+
+
+
+
+ Analysis Result
+
+
+
+
+ {documentAnalysisMutation.data.analysis}
+
+
+
+ )}
+
+
+ {/* Research Synthesis Tab */}
+
+
+
setQuery(e.target.value)}
+ />
+
+ Selected Documents:
+ {selectedDocuments.length}
+
+
+
+
+
+ {researchSynthesisMutation.data && (
+
+
+
+
Synthesis
+
{researchSynthesisMutation.data.synthesis}
+
+
+ {researchSynthesisMutation.data.keyFindings.length > 0 && (
+
+
Key Findings
+
+ {researchSynthesisMutation.data.keyFindings.map((finding: string, i: number) => (
+ - {finding}
+ ))}
+
+
+ )}
+
+ {researchSynthesisMutation.data.recommendations.length > 0 && (
+
+
Recommendations
+
+ {researchSynthesisMutation.data.recommendations.map((rec: string, i: number) => (
+ - {rec}
+ ))}
+
+
+ )}
+
+
+ )}
+
+
+ {/* Embeddings Tab */}
+
+
+ setQuery(e.target.value)}
+ />
+
+
+
+ {embeddingsMutation.data && (
+
+
+
+
+ Model:
+ {embeddingsMutation.data.model}
+
+
+ Dimensions:
+ {embeddingsMutation.data.data[0].embedding.length}
+
+
+
+
Vector (first 10 dimensions):
+
+ [{embeddingsMutation.data.data[0].embedding.slice(0, 10).map((val: number) => val.toFixed(4)).join(', ')}...]
+
+
+
+ Token usage: {embeddingsMutation.data.usage.total_tokens} tokens
+
+
+
+ )}
+
+
+
+
+
+ {/* Error States */}
+ {(aiSearchMutation.error || documentAnalysisMutation.error || researchSynthesisMutation.error || embeddingsMutation.error) && (
+
+
+
+
+ {(aiSearchMutation.error || documentAnalysisMutation.error || researchSynthesisMutation.error || embeddingsMutation.error)?.message}
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/knowledge-base/citation-panel.tsx b/client/src/components/knowledge-base/citation-panel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..56cb92c58ee0d9127f1fa8d95c7c7afefc040b7e
--- /dev/null
+++ b/client/src/components/knowledge-base/citation-panel.tsx
@@ -0,0 +1,133 @@
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { X, Download, Trash2 } from "lucide-react";
+import { type Citation } from "@shared/schema";
+
+interface CitationPanelProps {
+ citations: Citation[];
+ isVisible: boolean;
+ onClose: () => void;
+ onRemoveCitation: (citationId: number) => void;
+}
+
+export default function CitationPanel({
+ citations,
+ isVisible,
+ onClose,
+ onRemoveCitation
+}: CitationPanelProps) {
+ if (!isVisible) return null;
+
+ const handleExportCitations = () => {
+ const citationText = citations
+ .map((citation, index) => {
+ return `[${index + 1}] ${citation.citationText}${citation.section ? ` (${citation.section})` : ''}${citation.pageNumber ? ` - Page ${citation.pageNumber}` : ''}`;
+ })
+ .join('\n\n');
+
+ const blob = new Blob([citationText], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'citations.txt';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
Active Citations
+
+ {citations.length}
+
+
+
+
+
+ {/* Citations List */}
+
+ {citations.length === 0 ? (
+
+
+
No citations added yet
+
+ Click "Add Citation" on search results to build your reference list
+
+
+ ) : (
+
+ {citations.map((citation, index) => (
+
+
+ {index + 1}
+
+
+
+ {citation.citationText}
+
+ {(citation.section || citation.pageNumber) && (
+
+ {citation.section && (
+
+ {citation.section}
+
+ )}
+ {citation.pageNumber && (
+
+ Page {citation.pageNumber}
+
+ )}
+
+ )}
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Footer */}
+ {citations.length > 0 && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/client/src/components/knowledge-base/enhanced-search-interface.tsx b/client/src/components/knowledge-base/enhanced-search-interface.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f26f13659935efcef3c0141ba3eb34daefa9a887
--- /dev/null
+++ b/client/src/components/knowledge-base/enhanced-search-interface.tsx
@@ -0,0 +1,603 @@
+import { useState } from "react";
+import { useMutation } from "@tanstack/react-query";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Textarea } from "@/components/ui/textarea";
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
+import {
+ Search,
+ Loader2,
+ Brain,
+ Sparkles,
+ Target,
+ FileText,
+ Lightbulb,
+ ChevronDown,
+ ChevronUp,
+ Wand2,
+ AlertCircle
+} from "lucide-react";
+import { type SearchRequest } from "@shared/schema";
+
+interface SearchInterfaceProps {
+ onSearch: (request: SearchRequest) => void;
+ onAISearch?: (query: string) => void;
+ isLoading?: boolean;
+ onDocumentSelect?: (documentId: number) => void;
+}
+
+interface EnhancedSearchResult {
+ results: any[];
+ enhancedQuery?: {
+ enhancedQuery: string;
+ intent: string;
+ keywords: string[];
+ suggestions: string[];
+ };
+ searchInsights?: {
+ totalResults: number;
+ avgRelevanceScore: number;
+ modalResultsCount: number;
+ localResultsCount: number;
+ };
+}
+
+export default function EnhancedSearchInterface({ onSearch, onAISearch, isLoading, onDocumentSelect }: SearchInterfaceProps) {
+ const [query, setQuery] = useState("");
+ const [searchType, setSearchType] = useState<"semantic" | "keyword" | "hybrid">("semantic");
+ const [sourceTypes, setSourceTypes] = useState(["pdf", "web", "academic", "code"]);
+ const [showAITools, setShowAITools] = useState(false);
+ const [analysisText, setAnalysisText] = useState("");
+ const [selectedDocuments, setSelectedDocuments] = useState([]);
+ const [useMarkdown, setUseMarkdown] = useState(true);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!query.trim()) return;
+
+ onSearch({
+ query: query.trim(),
+ searchType,
+ filters: {
+ sourceTypes: sourceTypes.length > 0 ? sourceTypes : undefined,
+ },
+ limit: 10,
+ offset: 0,
+ });
+ };
+
+ const handleSourceTypeChange = (sourceType: string, checked: boolean) => {
+ setSourceTypes(prev =>
+ checked
+ ? [...prev, sourceType]
+ : prev.filter(type => type !== sourceType)
+ );
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSubmit(e);
+ } else if (e.key === "Escape") {
+ setQuery("");
+ }
+ };
+
+ // Enhanced AI Search
+ const aiSearchMutation = useMutation({
+ mutationFn: async (searchQuery: string): Promise => {
+ const response = await fetch("/api/ai-search", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ query: searchQuery,
+ maxResults: 10,
+ useQueryEnhancement: true
+ }),
+ });
+ if (!response.ok) throw new Error("Enhanced search failed");
+ return response.json();
+ },
+ });
+
+ // Query Enhancement
+ const queryEnhancementMutation = useMutation({
+ mutationFn: async (originalQuery: string) => {
+ const response = await fetch("/api/enhance-query", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ query: originalQuery }),
+ });
+ if (!response.ok) throw new Error("Query enhancement failed");
+ return response.json();
+ },
+ });
+
+ // Document Analysis
+ const documentAnalysisMutation = useMutation({
+ mutationFn: async ({ content, analysisType, useMarkdown }: { content: string; analysisType: string; useMarkdown?: boolean }) => {
+ const response = await fetch("/api/analyze-document", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ content, analysisType, useMarkdown }),
+ });
+ if (!response.ok) throw new Error("Document analysis failed");
+ return response.json();
+ },
+ });
+
+ // Generate Embeddings
+ const embeddingsMutation = useMutation({
+ mutationFn: async (input: string) => {
+ const response = await fetch("/api/embeddings", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ input }),
+ });
+ if (!response.ok) throw new Error("Embedding generation failed");
+ return response.json();
+ },
+ });
+
+ const handleEnhancedSearch = () => {
+ if (!query.trim()) return;
+ aiSearchMutation.mutate(query);
+ if (onAISearch) onAISearch(query);
+ };
+
+ const handleQueryEnhancement = () => {
+ if (!query.trim()) return;
+ queryEnhancementMutation.mutate(query);
+ };
+
+ const handleDocumentAnalysis = (analysisType: string) => {
+ if (!analysisText.trim()) return;
+ documentAnalysisMutation.mutate({
+ content: analysisText,
+ analysisType,
+ useMarkdown
+ });
+ };
+
+ const handleGenerateEmbeddings = () => {
+ if (!query.trim()) return;
+ embeddingsMutation.mutate(query);
+ };
+
+ const applyEnhancedQuery = (enhancedQuery: string) => {
+ setQuery(enhancedQuery);
+ onSearch({
+ query: enhancedQuery,
+ searchType,
+ filters: {
+ sourceTypes: sourceTypes.length > 0 ? sourceTypes : undefined,
+ },
+ limit: 10,
+ offset: 0,
+ });
+ };
+
+ return (
+
+
+
+
+
AI-Enhanced Search
+ Powered by Nebius & Modal
+
+
+
+
+
+
+ {/* Query Enhancement Results */}
+ {queryEnhancementMutation.data && (
+
+
+
+
+
+
+ Enhanced Query Suggestion
+
+
+
+
+ {queryEnhancementMutation.data.enhancedQuery}
+
+
+
+ Intent:
+ {queryEnhancementMutation.data.intent}
+
+
+
Keywords:
+
+ {queryEnhancementMutation.data.keywords.map((keyword: string, i: number) => (
+
+ {keyword}
+
+ ))}
+
+
+
+
+
+
+ )}
+
+ {/* AI Enhanced Search Results */}
+ {aiSearchMutation.data && (
+
+
+
+
+
+ AI-Enhanced Results
+ {aiSearchMutation.data.searchInsights && (
+
+ {aiSearchMutation.data.searchInsights.totalResults} results
+
+ )}
+
+
+
+ {aiSearchMutation.data.searchInsights && (
+
+
+ Avg Relevance:
+
+ {(aiSearchMutation.data.searchInsights.avgRelevanceScore * 100).toFixed(1)}%
+
+
+
+ Modal Results:
+ {aiSearchMutation.data.searchInsights.modalResultsCount}
+
+
+ Local Results:
+ {aiSearchMutation.data.searchInsights.localResultsCount}
+
+
+ Total:
+ {aiSearchMutation.data.searchInsights.totalResults}
+
+
+ )}
+
+ ⨠AI-enhanced search completed. Results are ranked by semantic relevance and include additional context.
+
+
+
+
+ )}
+
+ {/* Collapsible AI Tools */}
+
+
+
+
+
+
+ Analysis
+
+
+
+ Embeddings
+
+
+
+ External Tools
+
+
+
+ {/* Document Analysis Tab */}
+
+
+
+ {documentAnalysisMutation.data && (
+
+
+
+
+ Analysis Result
+
+
+
+
+ {documentAnalysisMutation.data.analysis}
+
+
+
+ )}
+
+
+ {/* Embeddings Tab */}
+
+
+
+
+ setQuery(e.target.value)}
+ className="flex-1"
+ />
+
+
+
+
+ {embeddingsMutation.data && (
+
+
+
+
+ Model:
+ {embeddingsMutation.data.model}
+
+
+ Dimensions:
+ {embeddingsMutation.data.data[0].embedding.length}
+
+
+
+
Vector (first 10 dimensions):
+
+ [{embeddingsMutation.data.data[0].embedding.slice(0, 10).map((val: number) => val.toFixed(4)).join(', ')}...]
+
+
+
+
+ )}
+
+
+ {/* External Tools Tab */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Error States */}
+ {(aiSearchMutation.error || documentAnalysisMutation.error || embeddingsMutation.error || queryEnhancementMutation.error) && (
+
+
+
+
+
+ {(aiSearchMutation.error || documentAnalysisMutation.error || embeddingsMutation.error || queryEnhancementMutation.error)?.message}
+
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/knowledge-base/knowledge-graph.tsx b/client/src/components/knowledge-base/knowledge-graph.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e810cf415b23c7b7500e21d8b5a24527c3ed941d
--- /dev/null
+++ b/client/src/components/knowledge-base/knowledge-graph.tsx
@@ -0,0 +1,811 @@
+import { useEffect, useRef, useState } from "react";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Slider } from "@/components/ui/slider";
+import { Switch } from "@/components/ui/switch";
+import { Label } from "@/components/ui/label";
+import { Network, GitBranch, Zap, Eye, Play, Pause, Search } from "lucide-react";
+import { Input } from "@/components/ui/input";
+import ForceGraph2D from "react-force-graph-2d";
+import ForceGraph3D from "react-force-graph-3d";
+import * as d3 from "d3";
+
+interface GraphNode {
+ id: string;
+ label: string;
+ type: "document" | "concept" | "author" | "topic" | "query";
+ size: number;
+ color: string;
+ metadata?: any;
+ x?: number;
+ y?: number;
+ z?: number;
+}
+
+interface GraphLink {
+ source: string | GraphNode;
+ target: string | GraphNode;
+ relationship: string;
+ strength: number;
+ color: string;
+}
+
+interface GraphData {
+ nodes: GraphNode[];
+ links: GraphLink[];
+}
+
+interface GraphStats {
+ totalDocuments: number;
+ totalConcepts: number;
+ totalResearchTeams: number;
+ totalSourceTypes: number;
+}
+
+const NODE_COLORS = {
+ document: "#3b82f6", // blue
+ concept: "#10b981", // emerald
+ author: "#f59e0b", // amber
+ topic: "#8b5cf6", // violet
+ query: "#ef4444", // red
+};
+
+const RELATIONSHIP_COLORS = {
+ "cites": "#64748b",
+ "authored_by": "#f59e0b",
+ "contains_concept": "#10b981",
+ "related_to": "#8b5cf6",
+ "searched_for": "#ef4444",
+ "similar_to": "#06b6d4",
+};
+
+export function KnowledgeGraph() {
+ const [graphData, setGraphData] = useState({ nodes: [], links: [] });
+ const [view3D, setView3D] = useState(false);
+ const [isAnimating, setIsAnimating] = useState(true);
+ const [selectedNode, setSelectedNode] = useState(null);
+ const [linkDistance, setLinkDistance] = useState([150]);
+ const [chargeStrength, setChargeStrength] = useState([-300]);
+ const [showLabels, setShowLabels] = useState(true);
+ const [isLoading, setIsLoading] = useState(true);
+ const [stats, setStats] = useState({ totalDocuments: 0, totalConcepts: 0, totalResearchTeams: 0, totalSourceTypes: 0 });
+ const [searchTerm, setSearchTerm] = useState("");
+ const [filteredGraphData, setFilteredGraphData] = useState({ nodes: [], links: [] });
+ const [hoveredNode, setHoveredNode] = useState(null);
+ const [highlightNodes, setHighlightNodes] = useState(new Set());
+ const [highlightLinks, setHighlightLinks] = useState(new Set());
+ const fgRef = useRef();
+
+ useEffect(() => {
+ fetchKnowledgeGraph();
+ }, []);
+
+ useEffect(() => {
+ filterGraphData();
+ }, [searchTerm, graphData]);
+
+ const filterGraphData = () => {
+ if (!searchTerm) {
+ setFilteredGraphData({ nodes: [], links: [] });
+ return;
+ }
+
+ const term = searchTerm.toLowerCase();
+ const filteredNodes = graphData.nodes.filter(node =>
+ node.label.toLowerCase().includes(term) ||
+ node.type.toLowerCase().includes(term) ||
+ (node.metadata?.concept && node.metadata.concept.toLowerCase().includes(term)) ||
+ (node.metadata?.title && node.metadata.title.toLowerCase().includes(term))
+ );
+
+ const nodeIds = new Set(filteredNodes.map(n => n.id));
+ const filteredLinks = graphData.links.filter(link => {
+ const sourceId = typeof link.source === 'string' ? link.source : (link.source as GraphNode).id;
+ const targetId = typeof link.target === 'string' ? link.target : (link.target as GraphNode).id;
+ return nodeIds.has(sourceId) && nodeIds.has(targetId);
+ });
+
+ setFilteredGraphData({ nodes: filteredNodes, links: filteredLinks });
+ };
+
+ const fetchKnowledgeGraph = async () => {
+ try {
+ setIsLoading(true);
+ const response = await fetch('/api/knowledge-graph');
+ const data = await response.json();
+
+ if (data.nodes && data.links) {
+ setGraphData({ nodes: data.nodes, links: data.links });
+ setStats(data.stats || { totalDocuments: 0, totalConcepts: 0, totalResearchTeams: 0, totalSourceTypes: 0 });
+ } else {
+ // Fallback to sample data if API fails
+ generateKnowledgeGraph();
+ }
+ } catch (error) {
+ console.error('Failed to fetch knowledge graph:', error);
+ // Fallback to sample data
+ generateKnowledgeGraph();
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const generateKnowledgeGraph = () => {
+ // Extract concepts from KnowledgeBridge sample documents
+ const documents = [
+ { id: "deepseek-r1", title: "DeepSeek-R1: Advanced Reasoning Model", concepts: ["reasoning", "thinking", "chain-of-thought"], authors: ["DeepSeek Team"], year: 2024, sourceType: "arxiv" },
+ { id: "nebius-ai", title: "Nebius AI Platform", concepts: ["cloud-ai", "scaling", "embeddings"], authors: ["Nebius Team"], year: 2024, sourceType: "github" },
+ { id: "semantic-search", title: "Semantic Search with BGE Models", concepts: ["embeddings", "similarity", "retrieval"], authors: ["BAAI Team"], year: 2024, sourceType: "arxiv" },
+ { id: "modal-compute", title: "Modal Distributed Computing", concepts: ["serverless", "scaling", "distributed"], authors: ["Modal Team"], year: 2024, sourceType: "github" },
+ { id: "url-validation", title: "Smart URL Validation Systems", concepts: ["validation", "filtering", "content-verification"], authors: ["Web Research Group"], year: 2024, sourceType: "web" },
+ { id: "multi-source", title: "Multi-Source Information Retrieval", concepts: ["search", "aggregation", "ranking"], authors: ["IR Conference"], year: 2024, sourceType: "academic" },
+ { id: "ai-security", title: "AI Application Security", concepts: ["rate-limiting", "validation", "middleware"], authors: ["Security Researchers"], year: 2024, sourceType: "github" },
+ { id: "react-query", title: "TanStack Query for Data Fetching", concepts: ["caching", "state-management", "performance"], authors: ["TanStack Team"], year: 2024, sourceType: "github" },
+ { id: "typescript-safety", title: "TypeScript for Type Safety", concepts: ["type-safety", "compilation", "validation"], authors: ["TypeScript Team"], year: 2024, sourceType: "web" },
+ { id: "knowledge-graphs", title: "Interactive Knowledge Graphs", concepts: ["visualization", "relationships", "d3js"], authors: ["DataViz Community"], year: 2024, sourceType: "github" },
+ { id: "github-api", title: "GitHub API for Repository Search", concepts: ["api-integration", "search", "filtering"], authors: ["GitHub Team"], year: 2024, sourceType: "web" },
+ { id: "arxiv-papers", title: "ArXiv Academic Paper Search", concepts: ["academic-search", "paper-validation", "research"], authors: ["arXiv Team"], year: 2024, sourceType: "academic" },
+ ];
+
+ const nodes: GraphNode[] = [];
+ const links: GraphLink[] = [];
+
+ // Add document nodes
+ documents.forEach(doc => {
+ nodes.push({
+ id: doc.id,
+ label: doc.title,
+ type: "document",
+ size: 12,
+ color: NODE_COLORS.document,
+ metadata: doc
+ });
+ });
+
+ // Extract unique concepts and create concept nodes
+ const allConcepts = new Set();
+ documents.forEach(doc => doc.concepts.forEach(concept => allConcepts.add(concept)));
+
+ allConcepts.forEach(concept => {
+ const relatedDocs = documents.filter(doc => doc.concepts.includes(concept));
+ nodes.push({
+ id: `concept_${concept}`,
+ label: concept,
+ type: "concept",
+ size: 8 + relatedDocs.length * 2,
+ color: NODE_COLORS.concept,
+ metadata: { relatedDocuments: relatedDocs.length }
+ });
+
+ // Link concepts to documents
+ relatedDocs.forEach(doc => {
+ links.push({
+ source: doc.id,
+ target: `concept_${concept}`,
+ relationship: "contains_concept",
+ strength: 1,
+ color: RELATIONSHIP_COLORS.contains_concept
+ });
+ });
+ });
+
+ // Extract unique authors and create author nodes
+ const allAuthors = new Set();
+ documents.forEach(doc => doc.authors.forEach(author => allAuthors.add(author)));
+
+ allAuthors.forEach(author => {
+ const authoredDocs = documents.filter(doc => doc.authors.includes(author));
+ nodes.push({
+ id: `author_${author}`,
+ label: author,
+ type: "author",
+ size: 6 + authoredDocs.length,
+ color: NODE_COLORS.author,
+ metadata: { publications: authoredDocs.length }
+ });
+
+ // Link authors to documents
+ authoredDocs.forEach(doc => {
+ links.push({
+ source: `author_${author}`,
+ target: doc.id,
+ relationship: "authored_by",
+ strength: 0.8,
+ color: RELATIONSHIP_COLORS.authored_by
+ });
+ });
+ });
+
+ // Create topic clusters for KnowledgeBridge features
+ const topics = [
+ { id: "ai_models", name: "AI Models & Processing", docs: ["deepseek-r1", "nebius-ai", "semantic-search"] },
+ { id: "infrastructure", name: "Infrastructure & Scaling", docs: ["modal-compute", "ai-security", "react-query"] },
+ { id: "search_systems", name: "Search & Retrieval", docs: ["multi-source", "url-validation", "github-api"] },
+ { id: "data_visualization", name: "Data & Visualization", docs: ["knowledge-graphs", "typescript-safety"] },
+ { id: "academic_integration", name: "Academic Integration", docs: ["arxiv-papers", "semantic-search"] },
+ { id: "web_technologies", name: "Web Technologies", docs: ["react-query", "typescript-safety", "ai-security"] }
+ ];
+
+ topics.forEach(topic => {
+ nodes.push({
+ id: topic.id,
+ label: topic.name,
+ type: "topic",
+ size: 10,
+ color: NODE_COLORS.topic,
+ metadata: { documentCount: topic.docs.length }
+ });
+
+ // Link topics to documents
+ topic.docs.forEach(docId => {
+ if (documents.find(d => d.id === docId)) {
+ links.push({
+ source: topic.id,
+ target: docId,
+ relationship: "related_to",
+ strength: 0.6,
+ color: RELATIONSHIP_COLORS.related_to
+ });
+ }
+ });
+ });
+
+ // Add similarity links between documents with shared concepts
+ documents.forEach(doc1 => {
+ documents.forEach(doc2 => {
+ if (doc1.id !== doc2.id) {
+ const sharedConcepts = doc1.concepts.filter(c => doc2.concepts.includes(c));
+ if (sharedConcepts.length >= 2) {
+ links.push({
+ source: doc1.id,
+ target: doc2.id,
+ relationship: "similar_to",
+ strength: sharedConcepts.length * 0.3,
+ color: RELATIONSHIP_COLORS.similar_to
+ });
+ }
+ }
+ });
+ });
+
+ // Add sample search queries for KnowledgeBridge
+ const sampleQueries = [
+ { id: "query_semantic", text: "semantic search with embeddings", relatedDocs: ["semantic-search", "nebius-ai"] },
+ { id: "query_security", text: "AI application security middleware", relatedDocs: ["ai-security", "url-validation"] },
+ { id: "query_distributed", text: "distributed AI processing", relatedDocs: ["modal-compute", "nebius-ai"] }
+ ];
+
+ sampleQueries.forEach(query => {
+ nodes.push({
+ id: query.id,
+ label: query.text,
+ type: "query",
+ size: 6,
+ color: NODE_COLORS.query,
+ metadata: { searchText: query.text }
+ });
+
+ query.relatedDocs.forEach(docId => {
+ links.push({
+ source: query.id,
+ target: docId,
+ relationship: "searched_for",
+ strength: 0.4,
+ color: RELATIONSHIP_COLORS.searched_for
+ });
+ });
+ });
+
+ setGraphData({ nodes, links });
+ };
+
+ const handleNodeClick = (node: any) => {
+ setSelectedNode(node);
+ if (fgRef.current) {
+ // Center the camera on the clicked node
+ if (view3D) {
+ fgRef.current.cameraPosition(
+ { x: node.x, y: node.y, z: node.z + 100 },
+ node,
+ 3000
+ );
+ } else {
+ fgRef.current.centerAt(node.x, node.y, 1000);
+ fgRef.current.zoom(2, 1000);
+ }
+ }
+ };
+
+ const updateHighlight = () => {
+ const currentData = filteredGraphData.nodes.length > 0 ? filteredGraphData : graphData;
+
+ setHighlightNodes(highlightNodes);
+ setHighlightLinks(highlightLinks);
+ };
+
+ const handleNodeHover = (node: any) => {
+ if (!node) {
+ setHoveredNode(null);
+ setHighlightNodes(new Set());
+ setHighlightLinks(new Set());
+ return;
+ }
+
+ setHoveredNode(node.id);
+
+ const newHighlightNodes = new Set();
+ const newHighlightLinks = new Set();
+
+ newHighlightNodes.add(node.id);
+
+ const currentData = filteredGraphData.nodes.length > 0 ? filteredGraphData : graphData;
+
+ currentData.links.forEach(link => {
+ const sourceId = typeof link.source === 'string' ? link.source : link.source.id;
+ const targetId = typeof link.target === 'string' ? link.target : link.target.id;
+
+ if (sourceId === node.id || targetId === node.id) {
+ newHighlightLinks.add(`${sourceId}-${targetId}`);
+ newHighlightNodes.add(sourceId);
+ newHighlightNodes.add(targetId);
+ }
+ });
+
+ setHighlightNodes(newHighlightNodes);
+ setHighlightLinks(newHighlightLinks);
+ };
+
+ const resetView = () => {
+ if (fgRef.current) {
+ if (view3D) {
+ fgRef.current.cameraPosition({ x: 0, y: 0, z: 300 }, { x: 0, y: 0, z: 0 }, 2000);
+ } else {
+ fgRef.current.zoomToFit(2000);
+ }
+ }
+ setSelectedNode(null);
+ };
+
+ const toggleAnimation = () => {
+ setIsAnimating(!isAnimating);
+ if (fgRef.current) {
+ if (isAnimating) {
+ fgRef.current.pauseAnimation();
+ } else {
+ fgRef.current.resumeAnimation();
+ }
+ }
+ };
+
+ const getNodeLabel = (node: any) => {
+ if (!showLabels) return "";
+ return node.label.length > 20 ? node.label.substring(0, 20) + "..." : node.label;
+ };
+
+ const getNodeSize = (node: any) => {
+ return node.size || 8;
+ };
+
+ const getNodeColor = (node: any) => {
+ if (hoveredNode === node.id) return "#ff6b6b"; // Red for hovered node
+ if (highlightNodes.has(node.id)) return "#4ecdc4"; // Teal for connected nodes
+ return node.color; // Original color
+ };
+
+ const getLinkWidth = (link: any) => {
+ const baseWidth = Math.max(0.5, link.strength * 3);
+ const linkId = `${link.source}-${link.target}`;
+ return highlightLinks.has(linkId) ? baseWidth * 2 : baseWidth;
+ };
+
+ const getLinkColor = (link: any) => {
+ const linkId = `${link.source}-${link.target}`;
+ return highlightLinks.has(linkId) ? "#ff6b6b" : link.color;
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ KnowledgeBridge Knowledge Graph
+
+
+ Explore relationships between technologies, research sources, and concepts in KnowledgeBridge
+
+
+
+
+
+
+
+
+
+
+ {/* Controls Panel */}
+
+
+ Graph Controls
+
+
+ {/* Search Filter */}
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-8"
+ />
+
+ {filteredGraphData.nodes.length > 0 && (
+
+ Showing {filteredGraphData.nodes.length} of {graphData.nodes.length} nodes
+
+ )}
+
+
+ {/* View Toggle */}
+
+
+
+
+
+ {/* Show Labels Toggle */}
+
+
+
+
+
+ {/* Link Distance */}
+
+
+
+
+ {linkDistance[0]}px
+
+
+
+ {/* Charge Strength */}
+
+
+
+
+ {chargeStrength[0]}
+
+
+
+ {/* Legend */}
+
+
+
+ {Object.entries(NODE_COLORS).map(([type, color]) => (
+
+ ))}
+
+
+
+ {/* Graph Stats */}
+
+
+ Nodes:
+ {graphData.nodes.length}
+
+
+ Links:
+ {graphData.links.length}
+
+
+
+
+
+ {/* Graph Visualization */}
+
+
+
+ {isLoading && (
+
+
+
+
Building knowledge graph...
+
+
+ )}
+ {view3D ? (
+
0 ? filteredGraphData : graphData}
+ nodeLabel={getNodeLabel}
+ nodeColor={getNodeColor}
+ nodeVal={getNodeSize}
+ linkColor={getLinkColor}
+ linkWidth={getLinkWidth}
+ linkDirectionalParticles={2}
+ linkDirectionalParticleSpeed={0.01}
+ onNodeClick={handleNodeClick}
+ onNodeHover={handleNodeHover}
+ d3AlphaDecay={0.01}
+ d3VelocityDecay={0.1}
+ enableNodeDrag={true}
+ enableNavigationControls={true}
+ controlType="orbit"
+ backgroundColor="rgba(0,0,0,0)"
+ onEngineStop={() => {
+ if (fgRef.current) {
+ // Center the 3D graph with proper camera positioning
+ setTimeout(() => {
+ const distance = Math.max(400, graphData.nodes.length * 20);
+ fgRef.current.cameraPosition({ x: 0, y: 0, z: distance }, { x: 0, y: 0, z: 0 }, 1500);
+ }, 100);
+ }
+ }}
+ />
+ ) : (
+ 0 ? filteredGraphData : graphData}
+ nodeLabel={getNodeLabel}
+ nodeColor={getNodeColor}
+ nodeVal={getNodeSize}
+ linkColor={getLinkColor}
+ linkWidth={getLinkWidth}
+ linkDirectionalParticles={1}
+ linkDirectionalParticleSpeed={0.005}
+ onNodeClick={handleNodeClick}
+ onNodeHover={handleNodeHover}
+ d3AlphaDecay={0.01}
+ d3VelocityDecay={0.1}
+ enableNodeDrag={true}
+ enableZoomInteraction={true}
+ backgroundColor="rgba(0,0,0,0)"
+ linkDirectionalArrowLength={3}
+ linkDirectionalArrowRelPos={1}
+ onEngineStop={() => {
+ if (fgRef.current) {
+ // Center the graph with proper zoom
+ setTimeout(() => {
+ fgRef.current.zoomToFit(400, 80);
+ }, 100);
+ }
+ }}
+ />
+ )}
+
+
+
+
+ {/* Node Details Panel */}
+
+
+ Node Details
+
+ Click on a node to view details
+
+
+
+ {selectedNode ? (
+
+
+
+ {selectedNode.type}
+
+
{selectedNode.label}
+
+
+ {selectedNode.metadata && (
+
+ {selectedNode.type === "document" && (
+ <>
+ {selectedNode.metadata.year && (
+
+ Year: {selectedNode.metadata.year}
+
+ )}
+ {selectedNode.metadata.authors && Array.isArray(selectedNode.metadata.authors) && (
+
+
Authors:
+
+ {selectedNode.metadata.authors.map((author: string) => (
+
+ {author}
+
+ ))}
+
+
+ )}
+ {selectedNode.metadata.venue && (
+
+ Venue: {selectedNode.metadata.venue}
+
+ )}
+ {selectedNode.metadata.sourceType && (
+
+ Source Type: {selectedNode.metadata.sourceType}
+
+ )}
+ {selectedNode.metadata.title && selectedNode.metadata.title !== selectedNode.label && (
+
+
Full Title:
+
+ {selectedNode.metadata.title}
+
+
+ )}
+ >
+ )}
+
+ {selectedNode.type === "concept" && (
+
+ Related Documents: {selectedNode.metadata.relatedDocuments}
+
+ )}
+
+ {selectedNode.type === "author" && (
+ <>
+ {selectedNode.metadata.teamName && (
+
+ Team: {selectedNode.metadata.teamName}
+
+ )}
+ {selectedNode.metadata.publicationCount && (
+
+ Publications: {selectedNode.metadata.publicationCount}
+
+ )}
+ {selectedNode.metadata.publications && (
+
+ Publications: {selectedNode.metadata.publications}
+
+ )}
+ >
+ )}
+
+ {selectedNode.type === "topic" && (
+
+ Documents: {selectedNode.metadata.documentCount}
+
+ )}
+
+ {selectedNode.type === "query" && (
+
+ Search Text: {selectedNode.metadata.searchText}
+
+ )}
+
+ )}
+
+ {/* Connected Nodes */}
+
+
Connected Nodes
+
+ {graphData.links
+ .filter(link => {
+ // Handle both string and object references for source/target
+ const sourceId = typeof link.source === 'string' ? link.source : (link.source as GraphNode).id;
+ const targetId = typeof link.target === 'string' ? link.target : (link.target as GraphNode).id;
+ return sourceId === selectedNode.id || targetId === selectedNode.id;
+ })
+ .map((link, idx) => {
+ const sourceId = typeof link.source === 'string' ? link.source : (link.source as GraphNode).id;
+ const targetId = typeof link.target === 'string' ? link.target : (link.target as GraphNode).id;
+ const connectedNodeId = sourceId === selectedNode.id ? targetId : sourceId;
+ const connectedNode = graphData.nodes.find(n => n.id === connectedNodeId);
+ return connectedNode ? (
+
setSelectedNode(connectedNode)}>
+
{connectedNode.label}
+
+ {link.relationship?.replace(/_/g, ' ') || 'related'}
+
+
+ ) : null;
+ })}
+ {graphData.links.filter(link => {
+ const sourceId = typeof link.source === 'string' ? link.source : (link.source as GraphNode).id;
+ const targetId = typeof link.target === 'string' ? link.target : (link.target as GraphNode).id;
+ return sourceId === selectedNode.id || targetId === selectedNode.id;
+ }).length === 0 && (
+
+ No connected nodes found
+
+ )}
+
+
+
+ ) : (
+
+
+
Select a node to view its details and connections
+
+ )}
+
+
+
+
+ {/* Insights Panel */}
+
+
+
+
+ Graph Insights
+
+
+
+
+
+
+ {stats.totalDocuments || graphData.nodes.filter(n => n.type === "document").length}
+
+
Knowledge Sources
+
+
+
+ {stats.totalConcepts || graphData.nodes.filter(n => n.type === "concept").length}
+
+
Technical Concepts
+
+
+
+ {stats.totalResearchTeams || graphData.nodes.filter(n => n.type === "author").length}
+
+
Technology Teams
+
+
+
+ {stats.totalSourceTypes || graphData.nodes.filter(n => n.type === "topic").length}
+
+
Technology Areas
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/knowledge-base/result-card.tsx b/client/src/components/knowledge-base/result-card.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..acc98248102c7f14b58b58d4ae56b3e1531a9d55
--- /dev/null
+++ b/client/src/components/knowledge-base/result-card.tsx
@@ -0,0 +1,509 @@
+import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { ChevronDown, ChevronUp, ExternalLink, Quote, Bookmark, FileText, Globe, Github, GraduationCap, Brain, Copy, Check, Volume2, VolumeX } from "lucide-react";
+import { type DocumentWithContext } from "@shared/schema";
+
+// Helper function to safely render metadata
+const renderMetadataValue = (value: unknown): string => {
+ if (Array.isArray(value)) {
+ return value.map(String).join(", ");
+ }
+ return String(value);
+};
+
+interface ResultCardProps {
+ document: DocumentWithContext;
+ isExpanded: boolean;
+ isSaved?: boolean;
+ onToggleExpanded: () => void;
+ onAddCitation: (documentId: number, citationText: string, section?: string, pageNumber?: number) => void;
+ onSaveDocument?: (documentId: number) => void;
+}
+
+const sourceTypeIcons = {
+ pdf: FileText,
+ web: Globe,
+ code: Github,
+ academic: GraduationCap,
+};
+
+const sourceTypeColors = {
+ pdf: "text-red-500",
+ web: "text-blue-500",
+ code: "text-slate-700",
+ academic: "text-purple-500",
+};
+
+export default function ResultCard({
+ document,
+ isExpanded,
+ isSaved = false,
+ onToggleExpanded,
+ onAddCitation,
+ onSaveDocument
+}: ResultCardProps) {
+ const [isAddingCitation, setIsAddingCitation] = useState(false);
+ const [isExplaining, setIsExplaining] = useState(false);
+ const [explanation, setExplanation] = useState("");
+ const [copiedFormat, setCopiedFormat] = useState(null);
+ const [isPlayingAudio, setIsPlayingAudio] = useState(false);
+ const [selectedVoice, setSelectedVoice] = useState(null);
+
+ const IconComponent = sourceTypeIcons[document.sourceType as keyof typeof sourceTypeIcons] || FileText;
+ const iconColor = sourceTypeColors[document.sourceType as keyof typeof sourceTypeColors] || "text-gray-500";
+
+ useEffect(() => {
+ const loadVoices = () => {
+ if ('speechSynthesis' in window) {
+ const voices = window.speechSynthesis.getVoices();
+ if (voices.length > 0 && !selectedVoice) {
+ // Prefer calm, soothing voices
+ const preferredVoice = voices.find(voice =>
+ voice.name.includes('Samantha') ||
+ voice.name.includes('Victoria') ||
+ voice.name.includes('Google UK English Female') ||
+ voice.name.includes('Microsoft Zira') ||
+ voice.name.includes('Karen') ||
+ voice.name.includes('Fiona') ||
+ voice.name.includes('Serena')
+ ) || voices.find(voice => voice.lang.startsWith('en') && voice.name.includes('Female')) || voices.find(voice => voice.lang.startsWith('en')) || voices[0];
+
+ setSelectedVoice(preferredVoice);
+ }
+ }
+ };
+
+ loadVoices();
+ if ('speechSynthesis' in window) {
+ window.speechSynthesis.onvoiceschanged = loadVoices;
+ }
+ }, [selectedVoice]);
+
+ const getRelevanceColor = (score: number) => {
+ if (score >= 0.9) return "bg-emerald-100 text-emerald-700";
+ if (score >= 0.8) return "bg-blue-100 text-blue-700";
+ if (score >= 0.7) return "bg-amber-100 text-amber-700";
+ return "bg-slate-100 text-slate-700";
+ };
+
+ const getSourceTypeLabel = (type: string) => {
+ const labels = {
+ pdf: "PDF Document",
+ web: "Web Page",
+ code: "GitHub Repository",
+ academic: "Academic Paper",
+ };
+ return labels[type as keyof typeof labels] || "Document";
+ };
+
+ const handleAddCitation = async () => {
+ setIsAddingCitation(true);
+ try {
+ await onAddCitation(
+ document.id,
+ document.snippet,
+ "Main Content",
+ (document.metadata && typeof document.metadata === 'object' && 'pageNumber' in document.metadata ? Number(document.metadata.pageNumber) || undefined : undefined)
+ );
+ } finally {
+ setIsAddingCitation(false);
+ }
+ };
+
+ const handleViewSource = () => {
+ if (document.url) {
+ window.open(document.url, '_blank', 'noopener,noreferrer');
+ }
+ };
+
+ const handleSaveDocument = () => {
+ if (onSaveDocument) {
+ onSaveDocument(document.id);
+ }
+ };
+
+ const highlightSearchHits = (text: string, query: string) => {
+ if (!query.trim()) return text;
+
+ const words = query.toLowerCase().split(/\s+/).filter(word => word.length > 2);
+ let highlightedText = text;
+
+ words.forEach(word => {
+ const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const regex = new RegExp(`(${escapedWord})`, 'gi');
+ highlightedText = highlightedText.replace(regex, '$1');
+ });
+
+ return highlightedText;
+ };
+
+ const handleExplain = async () => {
+ setIsExplaining(true);
+ try {
+ const response = await fetch('/api/explain', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ title: document.title,
+ snippet: document.snippet,
+ content: document.content.substring(0, 1000)
+ })
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setExplanation(data.explanation);
+
+ // Automatically play audio explanation
+ playExplanationAudio(data.explanation);
+ } else {
+ setExplanation("Unable to generate explanation at this time.");
+ }
+ } catch (error) {
+ setExplanation("Error generating explanation.");
+ } finally {
+ setIsExplaining(false);
+ }
+ };
+
+ const playExplanationAudio = (text: string) => {
+ if ('speechSynthesis' in window) {
+ // Stop any current speech
+ window.speechSynthesis.cancel();
+
+ // Add friendly, engaging intro phrase
+ const engagingText = `Here's what I discovered: ${text}. Quite fascinating stuff!`;
+ const utterance = new SpeechSynthesisUtterance(engagingText);
+
+ // Use selected voice or get a more engaging default
+ if (selectedVoice) {
+ utterance.voice = selectedVoice;
+ } else {
+ const voices = window.speechSynthesis.getVoices();
+ const preferredVoice = voices.find(voice =>
+ voice.name.includes('Samantha') ||
+ voice.name.includes('Victoria') ||
+ voice.name.includes('Google UK English Female') ||
+ voice.name.includes('Microsoft Zira') ||
+ voice.name.includes('Karen') ||
+ voice.name.includes('Fiona') ||
+ voice.name.includes('Serena')
+ ) || voices.find(voice => voice.lang.startsWith('en') && voice.name.includes('Female')) || voices.find(voice => voice.lang.startsWith('en')) || voices[0];
+
+ if (preferredVoice) {
+ utterance.voice = preferredVoice;
+ }
+ }
+
+ // Engaging yet pleasant voice settings
+ utterance.rate = 1.05; // Slightly faster, more engaging
+ utterance.pitch = 1.1; // Warm, friendly pitch
+ utterance.volume = 0.9; // Clear but not overwhelming
+
+ utterance.onstart = () => setIsPlayingAudio(true);
+ utterance.onend = () => setIsPlayingAudio(false);
+ utterance.onerror = () => setIsPlayingAudio(false);
+
+ window.speechSynthesis.speak(utterance);
+ }
+ };
+
+ const toggleAudio = () => {
+ if (isPlayingAudio) {
+ window.speechSynthesis.cancel();
+ setIsPlayingAudio(false);
+ } else if (explanation) {
+ playExplanationAudio(explanation);
+ }
+ };
+
+ const copyToClipboard = async (format: 'markdown' | 'bibtex') => {
+ let text = '';
+
+ if (format === 'markdown') {
+ text = `[${document.title}](${document.url || '#'}) - ${document.source}`;
+ } else if (format === 'bibtex') {
+ const year = (document.metadata && typeof document.metadata === 'object' && 'year' in document.metadata ? Number(document.metadata.year) || new Date().getFullYear() : new Date().getFullYear());
+ const authors = (document.metadata && typeof document.metadata === 'object' && 'authors' in document.metadata ? (Array.isArray(document.metadata.authors) ? document.metadata.authors as string[] : [String(document.metadata.authors)]) : ['Unknown']);
+ text = `@article{doc${document.id},
+ title={${document.title}},
+ author={${Array.isArray(authors) ? authors.join(' and ') : authors}},
+ year={${year}},
+ url={${document.url || ''}},
+ note={${document.source}}
+}`;
+ }
+
+ try {
+ await navigator.clipboard.writeText(text);
+ setCopiedFormat(format);
+ setTimeout(() => setCopiedFormat(null), 2000);
+ } catch (error) {
+ console.error('Failed to copy:', error);
+ }
+ };
+
+ const getTrustBadge = () => {
+ const sourceType = document.sourceType;
+ const source = document.source.toLowerCase();
+
+ if (sourceType === 'academic' || source.includes('arxiv') || source.includes('acm') || source.includes('ieee')) {
+ return { icon: 'šµ', label: 'Peer-reviewed', color: 'text-blue-600 bg-blue-50' };
+ } else if (sourceType === 'web' && (source.includes('docs.') || source.includes('official') || source.includes('.org'))) {
+ return { icon: 'š¢', label: 'Official docs', color: 'text-green-600 bg-green-50' };
+ } else {
+ return { icon: 'āŖ', label: 'Web source', color: 'text-gray-600 bg-gray-50' };
+ }
+ };
+
+ const trustBadge = getTrustBadge();
+
+ return (
+
+
+
+
+
+
+
+
+ {getSourceTypeLabel(document.sourceType)}
+
+
+
+ {Math.round(document.relevanceScore * 100)}% Relevance
+
+
+ {trustBadge.icon} {trustBadge.label}
+
+
+
+ {document.title}
+
+
+ {document.source}
+
+
+
+
+
+ {/* Snippet Preview with Highlighted Hits */}
+
+
+ {/* AI Explanation */}
+ {explanation && (
+
+
+
+
+ š¤ AI Assistant
+
+
+
+
+ Here's what I discovered: {explanation} Quite fascinating stuff!
+
+ {isPlayingAudio && (
+
+
+
+
+ Playing engaging audio explanation...
+
+ )}
+
+ )}
+
+ {/* Expanded Content */}
+ {isExpanded && (
+
+
+
Additional Context
+
+ {document.additionalContext?.map((context, index) => (
+
+
+ {context.section}: {context.text}
+
+ {context.pageNumber && (
+
+ Page {context.pageNumber}
+
+ )}
+
+ )) || (
+
+
+ Full Content: {document.content.substring(0, 300)}
+ {document.content.length > 300 && "..."}
+
+
+ )}
+
+
+
+ {/* Metadata */}
+ {document.metadata && typeof document.metadata === 'object' ? (
+
+
Metadata
+
+ {(() => {
+ const metadata = document.metadata as Record
;
+ return (
+ <>
+ {metadata.authors && (
+
+ Authors:
+
+ {renderMetadataValue(metadata.authors)}
+
+
+ )}
+ {metadata.year && (
+
+ Year:
+ {renderMetadataValue(metadata.year)}
+
+ )}
+ {metadata.citations && (
+
+ Citations:
+ {renderMetadataValue(metadata.citations)}
+
+ )}
+ {metadata.language && (
+
+ Language:
+ {renderMetadataValue(metadata.language)}
+
+ )}
+ >
+ );
+ })()}
+
+
+ ) : null}
+
+ {/* Enhanced Actions */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {document.url && (
+
+ )}
+
+
+
+
+ {/* Retrieval Metrics */}
+
+ Retrieved in {((document as any).retrievalTime || Math.random() * 0.3 + 0.1).toFixed(2)}s ā¢
+ {((document as any).tokenCount || Math.floor(document.content.length / 4))} tokens
+
+
+
+ )}
+
+
+ );
+}
diff --git a/client/src/components/knowledge-base/search-interface.tsx b/client/src/components/knowledge-base/search-interface.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2dc1dd96be79cab77d650c106a900ecd4ad17fc9
--- /dev/null
+++ b/client/src/components/knowledge-base/search-interface.tsx
@@ -0,0 +1,181 @@
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Search, Loader2 } from "lucide-react";
+import { type SearchRequest } from "@shared/schema";
+
+interface SearchInterfaceProps {
+ onSearch: (request: SearchRequest) => void;
+ isLoading?: boolean;
+}
+
+export default function SearchInterface({ onSearch, isLoading }: SearchInterfaceProps) {
+ const [query, setQuery] = useState("");
+ const [searchType, setSearchType] = useState<"semantic" | "keyword" | "hybrid">("semantic");
+ const [sourceTypes, setSourceTypes] = useState(["pdf", "web", "academic", "code"]);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!query.trim()) return;
+
+ onSearch({
+ query: query.trim(),
+ searchType,
+ filters: {
+ sourceTypes: sourceTypes.length > 0 ? sourceTypes : undefined,
+ },
+ limit: 10,
+ offset: 0,
+ });
+ };
+
+ const handleSourceTypeChange = (sourceType: string, checked: boolean) => {
+ setSourceTypes(prev =>
+ checked
+ ? [...prev, sourceType]
+ : prev.filter(type => type !== sourceType)
+ );
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSubmit(e);
+ } else if (e.key === "Escape") {
+ setQuery("");
+ }
+ };
+
+ return (
+
+
+
+ {/* AI Development Tools */}
+
+
+
+
AI Development Tools
+
Access external AI platforms and services
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/knowledge-base/search-results.tsx b/client/src/components/knowledge-base/search-results.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..56c47fe06da28ee30b9e966141f2e18e0bd822ee
--- /dev/null
+++ b/client/src/components/knowledge-base/search-results.tsx
@@ -0,0 +1,145 @@
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { SortAsc, Filter, Circle } from "lucide-react";
+import ResultCard from "./result-card";
+import { type SearchResponse, type DocumentWithContext } from "@shared/schema";
+
+interface SearchResultsProps {
+ results?: SearchResponse;
+ expandedResults: Set;
+ savedDocuments?: Set;
+ onToggleExpanded: (resultId: number) => void;
+ onAddCitation: (documentId: number, citationText: string, section?: string, pageNumber?: number) => void;
+ onSaveDocument?: (documentId: number) => void;
+ isLoading?: boolean;
+ error?: Error | null;
+}
+
+export default function SearchResults({
+ results,
+ expandedResults,
+ savedDocuments,
+ onToggleExpanded,
+ onAddCitation,
+ onSaveDocument,
+ isLoading,
+ error
+}: SearchResultsProps) {
+ const [sortBy, setSortBy] = useState<"relevance" | "date" | "title">("relevance");
+
+ if (error) {
+ return null; // Error is handled in parent component
+ }
+
+ if (isLoading) {
+ return (
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+ );
+ }
+
+ const sortedResults = results?.results ? [...results.results].sort((a, b) => {
+ switch (sortBy) {
+ case "relevance":
+ return b.relevanceScore - a.relevanceScore;
+ case "date":
+ return new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime();
+ case "title":
+ return a.title.localeCompare(b.title);
+ default:
+ return 0;
+ }
+ }) : [];
+
+ if (!results) {
+ return null;
+ }
+
+ return (
+
+ {/* Results Statistics */}
+
+
+
+ {results?.totalCount || 0} results found in{" "}
+ {results?.searchTime?.toFixed(2) || '0.00'} seconds
+
+
+
+ Vector index active
+
+
+
+
+
+
+
+
+ {/* Search Results */}
+
+ {sortedResults.map((result) => (
+ onToggleExpanded(result.id)}
+ onAddCitation={onAddCitation}
+ onSaveDocument={onSaveDocument}
+ />
+ ))}
+
+
+ {/* Load More */}
+ {results?.results && results.results.length > 0 && results.totalCount > results.results.length && (
+
+
+
+ )}
+
+ );
+}
diff --git a/client/src/components/knowledge-base/system-flow-diagram.tsx b/client/src/components/knowledge-base/system-flow-diagram.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..82f3d38fb4b119eaf21c61a5620d4865ed6a337d
--- /dev/null
+++ b/client/src/components/knowledge-base/system-flow-diagram.tsx
@@ -0,0 +1,491 @@
+
+import React, { useState, useEffect } from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Progress } from '@/components/ui/progress';
+import {
+ Search,
+ Database,
+ Brain,
+ ArrowRight,
+ FileText,
+ Zap,
+ GitBranch,
+ Target,
+ Layers,
+ RotateCcw
+} from 'lucide-react';
+
+interface FlowStep {
+ id: string;
+ title: string;
+ description: string;
+ icon: React.ReactNode;
+ details: string[];
+ tech: string[];
+ active: boolean;
+ completed: boolean;
+}
+
+const SystemFlowDiagram: React.FC = () => {
+ const [currentStep, setCurrentStep] = useState(0);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [userQuery, setUserQuery] = useState("How does semantic search work?");
+ const [queryEmbedding, setQueryEmbedding] = useState([]);
+ const [similarityScores, setSimilarityScores] = useState<{doc: string, score: number}[]>([]);
+
+ // Generate realistic embedding values for demonstration
+ const generateEmbedding = (text: string) => {
+ const seed = text.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
+ const random = (s: number) => {
+ const x = Math.sin(s) * 10000;
+ return x - Math.floor(x);
+ };
+
+ return Array.from({length: 8}, (_, i) =>
+ Number((random(seed + i) * 2 - 1).toFixed(3))
+ );
+ };
+
+ const flowSteps: FlowStep[] = [
+ {
+ id: 'input',
+ title: '1. User Input',
+ description: 'User enters search query',
+ icon: ,
+ details: [
+ `User types: "${userQuery}"`,
+ 'Enhanced search interface with unified AI tools',
+ 'React frontend with security validation',
+ 'Express.js backend with rate limiting and security middleware'
+ ],
+ tech: ['React', 'TypeScript', 'Express.js', 'Security Middleware'],
+ active: false,
+ completed: false
+ },
+ {
+ id: 'enhancement',
+ title: '2. AI Query Enhancement',
+ description: 'Optionally enhance query with AI',
+ icon: ,
+ details: [
+ `Nebius AI analyzes "${userQuery}"`,
+ 'DeepSeek-R1-0528 model provides query improvements',
+ 'Suggests keywords and alternative phrasings',
+ 'Falls back to original query if enhancement fails'
+ ],
+ tech: ['Nebius AI', 'DeepSeek-R1-0528', 'Query Analysis'],
+ active: false,
+ completed: false
+ },
+ {
+ id: 'search',
+ title: '3. Multi-Source Search',
+ description: 'Search across multiple knowledge sources',
+ icon: ,
+ details: [
+ 'Parallel search across GitHub, Wikipedia, ArXiv',
+ 'Local storage with sample academic data',
+ 'Enhanced GitHub search with author filtering',
+ 'Smart query routing to appropriate sources'
+ ],
+ tech: ['GitHub API', 'Wikipedia API', 'ArXiv API', 'Parallel Processing'],
+ active: false,
+ completed: false
+ },
+ {
+ id: 'validation',
+ title: '4. URL Validation',
+ description: 'Validate and verify result URLs',
+ icon: ,
+ details: [
+ 'Smart URL validation with ArXiv format checking',
+ 'Content verification to detect error pages',
+ 'Concurrent processing with rate limits',
+ 'Trusted domain fast-path for reliable sources'
+ ],
+ tech: ['URL Validation', 'Content Verification', 'Rate Limiting'],
+ active: false,
+ completed: false
+ },
+ {
+ id: 'embeddings',
+ title: '5. Embedding Generation',
+ description: 'Generate semantic embeddings with Nebius',
+ icon: ,
+ details: [
+ 'BAAI/bge-en-icl model for vector generation',
+ 'High-dimensional semantic representations',
+ 'Fallback to mock embeddings for demos',
+ 'Prepare embeddings for similarity matching'
+ ],
+ tech: ['Nebius AI', 'BAAI/bge-en-icl', 'Vector Embeddings'],
+ active: false,
+ completed: false
+ },
+ {
+ id: 'analysis',
+ title: '6. Document Analysis',
+ description: 'AI-powered document analysis (optional)',
+ icon: ,
+ details: [
+ 'Nebius DeepSeek-R1 analyzes document content',
+ 'Configurable output: markdown or plain text',
+ 'Analysis types: summary, classification, key points',
+ 'Clean output with thinking tag removal'
+ ],
+ tech: ['Nebius AI', 'DeepSeek-R1', 'Document Analysis'],
+ active: false,
+ completed: false
+ },
+ {
+ id: 'display',
+ title: '7. Results Display',
+ description: 'Present results to user',
+ icon: ,
+ details: [
+ 'Format results in user-friendly cards',
+ 'Show relevance scores and snippets',
+ 'Enable citation tracking',
+ 'Provide AI explanations on demand'
+ ],
+ tech: ['React', 'UI Components', 'State Management'],
+ active: false,
+ completed: false
+ }
+ ];
+
+ const [steps, setSteps] = useState(flowSteps);
+
+ useEffect(() => {
+ if (isPlaying) {
+ const interval = setInterval(() => {
+ setCurrentStep((prev) => {
+ if (prev < steps.length - 1) {
+ return prev + 1;
+ } else {
+ setIsPlaying(false);
+ return prev;
+ }
+ });
+ }, 2000);
+
+ return () => clearInterval(interval);
+ }
+ }, [isPlaying, steps.length]);
+
+ useEffect(() => {
+ setSteps(prevSteps =>
+ prevSteps.map((step, index) => ({
+ ...step,
+ active: index === currentStep,
+ completed: index < currentStep
+ }))
+ );
+ setProgress(((currentStep + 1) / steps.length) * 100);
+ }, [currentStep, steps.length]);
+
+ const resetAnimation = () => {
+ setCurrentStep(0);
+ setIsPlaying(false);
+ setProgress(0);
+ };
+
+ const playAnimation = () => {
+ if (currentStep === steps.length - 1) {
+ resetAnimation();
+ }
+ setIsPlaying(true);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ KnowledgeBridge System Flow
+
+
+ How your query becomes intelligent multi-source research with AI enhancement
+
+
+ {/* Query Input */}
+
+
+ setUserQuery(e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:text-white"
+ placeholder="Enter your query to see the process"
+ />
+
+
+ {/* Controls */}
+
+
+
+
+
+ {/* Progress Bar */}
+
+
+
+ Step {currentStep + 1} of {steps.length}
+
+
+
+
+ {/* Flow Diagram */}
+
+ {steps.map((step, index) => (
+
+ {/* Connection Arrow */}
+ {index < steps.length - 1 && (
+
+ )}
+
+ {/* Step Card */}
+
setCurrentStep(index)}
+ >
+
+
+
+ {step.icon}
+
+ {step.completed && (
+
+ )}
+
+
+ {step.title}
+
+
+
+
+ {step.description}
+
+
+ {/* Technology Tags */}
+
+ {step.tech.map((tech) => (
+
+ {tech}
+
+ ))}
+
+
+ {/* Details (shown when active) */}
+ {step.active && (
+
+
+ Process Details:
+
+
+ {step.details.map((detail, i) => (
+ -
+ ā¢
+ {detail}
+
+ ))}
+
+
+ {/* Special visualization for embeddings step */}
+ {step.id === 'embeddings' && (
+
+
+ Vector: [{generateEmbedding(userQuery).slice(0, 4).join(', ')}, ...]
+
+
+ Dimensions: 1536 | Magnitude: {Math.sqrt(generateEmbedding(userQuery).reduce((sum, val) => sum + val * val, 0)).toFixed(3)}
+
+
+ )}
+
+ {/* Special visualization for validation step */}
+ {step.id === 'validation' && (
+
+ {[
+ { doc: 'github.com/research/ai', status: 'valid' },
+ { doc: 'arxiv.org/abs/2024.12345', status: 'verified' },
+ { doc: 'invalid-url.broken', status: 'filtered' }
+ ].map((result, i) => (
+
+ {result.doc}
+ {result.status}
+
+ ))}
+
+ )}
+
+ )}
+
+
+
+ ))}
+
+
+ {/* Live Embedding Demo */}
+
+
+
+ Live Embedding Calculator
+
+
+
+ {/* Text to Vector */}
+
+
Text ā Vector Conversion
+
+
+
+ setUserQuery(e.target.value)}
+ className="w-full font-mono text-sm bg-gray-100 dark:bg-gray-700 p-2 rounded border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 dark:text-gray-100"
+ placeholder="Enter text to generate embeddings..."
+ />
+
+
+
Embedding (first 8 dims):
+
+ [{generateEmbedding(userQuery).join(', ')}]
+
+
+
+ Vector magnitude: {Math.sqrt(generateEmbedding(userQuery).reduce((sum, val) => sum + val * val, 0)).toFixed(3)}
+
+
+
+
+ {/* Similarity Calculations */}
+
+
Similarity Scores
+
+ {[
+ { doc: 'AI Research Paper', vector: [0.2, 0.8, -0.1, 0.5, 0.3, -0.4, 0.7, 0.1] },
+ { doc: 'GitHub Repository', vector: [0.1, 0.6, 0.2, -0.3, 0.8, 0.4, -0.2, 0.5] },
+ { doc: 'Wikipedia Article', vector: [-0.3, 0.4, 0.7, 0.2, -0.1, 0.6, 0.3, -0.5] }
+ ].map((doc, i) => {
+ const queryVec = generateEmbedding(userQuery);
+ const dotProduct = queryVec.reduce((sum, val, idx) => sum + val * doc.vector[idx], 0);
+ const queryMag = Math.sqrt(queryVec.reduce((sum, val) => sum + val * val, 0));
+ const docMag = Math.sqrt(doc.vector.reduce((sum, val) => sum + val * val, 0));
+ const similarity = dotProduct / (queryMag * docMag);
+
+ return (
+
+
{doc.doc}
+
+
0.3 ? 'bg-green-400' : similarity > 0.1 ? 'bg-yellow-400' : 'bg-gray-300'}`}
+ style={{width: `${Math.max(20, Math.abs(similarity) * 60)}px`}}>
+
{similarity.toFixed(2)}
+
+
+ );
+ })}
+
+
+
+
+
+ {/* Key Concepts */}
+
+
+
+
+
+ Embeddings
+
+
+
+
+ Nebius BAAI/bge-en-icl generates semantic vectors. Similar concepts have similar vector values.
+
+
+ "AI research" ā [0.1, 0.3, 0.8, ...]
+ "machine learning" ā [0.2, 0.4, 0.7, ...]
+
+
+
+
+
+
+
+
+ Vector Search
+
+
+
+
+ Multi-source search across GitHub, ArXiv, Wikipedia with smart URL validation.
+
+
+ GitHub API: repositories + code
+ ArXiv API: academic papers
+ Wikipedia: authoritative content
+
+
+
+
+
+
+
+
+ AI Pipeline
+
+
+
+
+ KnowledgeBridge combines multi-source search with Nebius AI for intelligent research synthesis.
+
+
+ Query ā Enhance ā Search ā Validate ā Analyze
+
+
+
+
+
+ );
+};
+
+export default SystemFlowDiagram;
diff --git a/client/src/components/theme-provider.tsx b/client/src/components/theme-provider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..725a25c36eb0a2c5169073045e78fbed66b5af98
--- /dev/null
+++ b/client/src/components/theme-provider.tsx
@@ -0,0 +1,64 @@
+import { createContext, useContext, useEffect, useState } from "react";
+
+type Theme = "dark" | "light";
+
+type ThemeProviderProps = {
+ children: React.ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+};
+
+type ThemeProviderState = {
+ theme: Theme;
+ setTheme: (theme: Theme) => void;
+};
+
+const initialState: ThemeProviderState = {
+ theme: "light",
+ setTheme: () => null,
+};
+
+const ThemeProviderContext = createContext(initialState);
+
+export function ThemeProvider({
+ children,
+ defaultTheme = "light",
+ storageKey = "ui-theme",
+ ...props
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(() => {
+ if (typeof window !== "undefined") {
+ return (localStorage.getItem(storageKey) as Theme) || defaultTheme;
+ }
+ return defaultTheme;
+ });
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+ root.classList.remove("light", "dark");
+ root.classList.add(theme);
+ }, [theme]);
+
+ const value = {
+ theme,
+ setTheme: (theme: Theme) => {
+ localStorage.setItem(storageKey, theme);
+ setTheme(theme);
+ },
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useTheme = () => {
+ const context = useContext(ThemeProviderContext);
+
+ if (context === undefined)
+ throw new Error("useTheme must be used within a ThemeProvider");
+
+ return context;
+};
\ No newline at end of file
diff --git a/client/src/components/theme-toggle.tsx b/client/src/components/theme-toggle.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0d9bd854bafe362da3f2b25f0d446603b6eba9ce
--- /dev/null
+++ b/client/src/components/theme-toggle.tsx
@@ -0,0 +1,20 @@
+import { Moon, Sun } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { useTheme } from "./theme-provider";
+
+export function ThemeToggle() {
+ const { theme, setTheme } = useTheme();
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/ui/accordion.tsx b/client/src/components/ui/accordion.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e6a723d06574ee5cec8b00759b98f3fbe1ac7cc9
--- /dev/null
+++ b/client/src/components/ui/accordion.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/client/src/components/ui/alert-dialog.tsx b/client/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8722561cf6bda62d62f9a0c67730aefda971873a
--- /dev/null
+++ b/client/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,139 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/client/src/components/ui/alert.tsx b/client/src/components/ui/alert.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..41fa7e0561a3fdb5f986c1213a35e563de740e96
--- /dev/null
+++ b/client/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/client/src/components/ui/aspect-ratio.tsx b/client/src/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c4abbf37f217c715a0eaade7f45ac78600df419f
--- /dev/null
+++ b/client/src/components/ui/aspect-ratio.tsx
@@ -0,0 +1,5 @@
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/client/src/components/ui/avatar.tsx b/client/src/components/ui/avatar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..51e507ba9d08bcdbb1fb630498f1cbdf2bf50093
--- /dev/null
+++ b/client/src/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..12daad7dcdb0091a814ff09aa257681a3f42d18c
--- /dev/null
+++ b/client/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
\ No newline at end of file
diff --git a/client/src/components/ui/breadcrumb.tsx b/client/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..60e6c96f72f0350d08b47e4730cab8f3975dc853
--- /dev/null
+++ b/client/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..36496a28727a3643b4212a14225d4f6cbd50bda5
--- /dev/null
+++ b/client/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/client/src/components/ui/calendar.tsx b/client/src/components/ui/calendar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2174f7101ba18ec1385eed03f69dbd619feb98aa
--- /dev/null
+++ b/client/src/components/ui/calendar.tsx
@@ -0,0 +1,68 @@
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ (
+
+ ),
+ IconRight: ({ className, ...props }) => (
+
+ ),
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/client/src/components/ui/card.tsx b/client/src/components/ui/card.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f62edea578d50058bef5e6bcc178b88d145564e9
--- /dev/null
+++ b/client/src/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/client/src/components/ui/carousel.tsx b/client/src/components/ui/carousel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9c2b9bf3705d8421bef00704c0c52e83d371ca11
--- /dev/null
+++ b/client/src/components/ui/carousel.tsx
@@ -0,0 +1,260 @@
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/client/src/components/ui/chart.tsx b/client/src/components/ui/chart.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..39fba6d6fc8509b968824b5076a8791c46650294
--- /dev/null
+++ b/client/src/components/ui/chart.tsx
@@ -0,0 +1,365 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+
+
+
+
+
+
+