diff --git a/backend/api_server.py b/backend/api_server.py
new file mode 100644
index 0000000..914d497
--- /dev/null
+++ b/backend/api_server.py
@@ -0,0 +1,1026 @@
+#!/usr/bin/env python3
+"""
+FastAPI wrapper for NeuraX backend integration
+Provides REST API endpoints that the Next.js frontend can consume
+"""
+import sys
+import os
+import asyncio
+import logging
+from pathlib import Path
+from typing import List, Optional, Dict, Any
+from datetime import datetime, timedelta
+import json
+import uuid
+from fastapi import FastAPI, File, UploadFile, HTTPException, Depends, BackgroundTasks
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse, FileResponse
+from fastapi.staticfiles import StaticFiles
+from pydantic import BaseModel, Field
+import uvicorn
+from loguru import logger
+
+# Add current directory to Python path
+sys.path.insert(0, str(Path(__file__).parent))
+
+# Import existing NeuraX components
+try:
+ from ingestion.ingestion_manager import IngestionManager
+ from indexing.embedding_manager import EmbeddingManager
+ from indexing.vector_store import VectorStore
+ from retrieval.query_processor import QueryProcessor
+ from retrieval.speech_to_text_processor import SpeechToTextProcessor
+ from generation.llm_factory import create_llm_generator
+ from generation.citation_generator import CitationGenerator
+ from feedback.feedback_system import FeedbackSystem
+ from config import (
+ CHROMA_CONFIG, LM_STUDIO_CONFIG, LLM_CONFIG,
+ SECURITY_CONFIG, FEEDBACK_CONFIG
+ )
+except ImportError as e:
+ logger.error(f"Failed to import NeuraX components: {e}")
+ logger.warning("Running in standalone mode without full NeuraX backend")
+
+# Pydantic models for API
+class QueryRequest(BaseModel):
+ text: Optional[str] = None
+ similarity_threshold: float = Field(default=0.5, ge=0.0, le=1.0)
+ options: Dict[str, Any] = Field(default_factory=dict)
+
+class ImageQueryRequest(BaseModel):
+ text_query: Optional[str] = None
+ similarity_threshold: float = Field(default=0.5, ge=0.0, le=1.0)
+ max_results: int = Field(default=10, ge=1, le=100)
+
+class VoiceQueryRequest(BaseModel):
+ language: str = Field(default="en")
+ similarity_threshold: float = Field(default=0.5, ge=0.0, le=1.0)
+ max_results: int = Field(default=10, ge=1, le=100)
+
+class MultimodalQueryRequest(BaseModel):
+ text: str
+ similarity_threshold: float = Field(default=0.5, ge=0.0, le=1.0)
+ max_results: int = Field(default=10, ge=1, le=100)
+
+class ResponseGenerationRequest(BaseModel):
+ query: str
+ context: List[Dict[str, Any]]
+ model: Optional[str] = "gemma-3n"
+ max_tokens: int = Field(default=1024, ge=1, le=4096)
+ temperature: float = Field(default=0.7, ge=0.0, le=2.0)
+ include_citations: bool = True
+
+class FeedbackRequest(BaseModel):
+ query_id: str
+ response_id: Optional[str] = None
+ rating: int = Field(ge=1, le=5)
+ comments: Optional[str] = None
+ is_helpful: Optional[bool] = None
+ metadata: Optional[Dict[str, Any]] = None
+
+class ConfigUpdateRequest(BaseModel):
+ api_url: Optional[str] = None
+ lm_studio_url: Optional[str] = None
+ max_file_size: Optional[int] = None
+ default_similarity_threshold: Optional[float] = None
+ enable_analytics: Optional[bool] = None
+ enable_voice_input: Optional[bool] = None
+ models: Optional[Dict[str, str]] = None
+ performance: Optional[Dict[str, Any]] = None
+ security: Optional[Dict[str, Any]] = None
+
+# Initialize FastAPI app
+app = FastAPI(
+ title="NeuraX API",
+ description="REST API wrapper for NeuraX RAG System",
+ version="1.0.0",
+ docs_url="/api/docs",
+ redoc_url="/api/redoc"
+)
+
+# CORS middleware
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=[
+ "http://localhost:3000", # Next.js dev server
+ "http://localhost:3001", # Next.js production
+ "https://localhost:3000",
+ ],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Global variables for NeuraX components
+ingestion_manager = None
+embedding_manager = None
+vector_store = None
+query_processor = None
+stt_processor = None
+llm_generator = None
+citation_generator = None
+feedback_system = None
+
+# Initialize components
+def initialize_components():
+ """Initialize NeuraX components"""
+ global ingestion_manager, embedding_manager, vector_store, query_processor, stt_processor, llm_generator, citation_generator, feedback_system
+
+ try:
+ # Initialize core components
+ ingestion_manager = IngestionManager()
+ logger.info("Ingestion manager initialized")
+
+ # Initialize embedding manager and vector store
+ embedding_manager = EmbeddingManager()
+ vector_store = VectorStore(
+ persist_directory=CHROMA_CONFIG['persist_directory'],
+ collection_name=CHROMA_CONFIG['collection_name']
+ )
+
+ # Initialize query processor
+ query_processor = QueryProcessor(
+ embedding_manager,
+ vector_store,
+ {
+ 'similarity_threshold': 0.5,
+ 'max_results': 10
+ }
+ )
+
+ # Initialize STT processor
+ stt_processor = SpeechToTextProcessor()
+
+ # Initialize LLM generator
+ llm_generator = create_llm_generator(LLM_CONFIG)
+
+ # Initialize citation generator
+ citation_generator = CitationGenerator()
+
+ # Initialize feedback system
+ feedback_system = FeedbackSystem()
+
+ logger.info("All NeuraX components initialized successfully")
+
+ except Exception as e:
+ logger.error(f"Failed to initialize components: {e}")
+ # Continue with mock implementations for development
+
+@app.on_event("startup")
+async def startup_event():
+ """Initialize components on startup"""
+ initialize_components()
+
+# API Response models
+class ApiResponse(BaseModel):
+ success: bool
+ data: Optional[Any] = None
+ error: Optional[str] = None
+ message: Optional[str] = None
+ timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
+ request_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
+
+class PaginatedResponse(BaseModel):
+ success: bool
+ data: List[Any]
+ pagination: Dict[str, Any]
+ timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
+ request_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
+
+# Health and status endpoints
+@app.get("/health", response_model=ApiResponse)
+async def health_check():
+ """Health check endpoint"""
+ return ApiResponse(
+ success=True,
+ message="NeuraX API is running",
+ data={
+ "status": "healthy",
+ "timestamp": datetime.now().isoformat(),
+ "version": "1.0.0"
+ }
+ )
+
+@app.get("/api/status", response_model=ApiResponse)
+async def system_status():
+ """Get system status"""
+ try:
+ status = {
+ "status": "online",
+ "components": {
+ "ingestion_manager": ingestion_manager is not None,
+ "embedding_manager": embedding_manager is not None,
+ "vector_store": vector_store is not None,
+ "query_processor": query_processor is not None,
+ "stt_processor": stt_processor is not None,
+ "llm_generator": llm_generator is not None,
+ "citation_generator": citation_generator is not None,
+ "feedback_system": feedback_system is not None,
+ },
+ "lm_studio": {
+ "url": LM_STUDIO_CONFIG.get('base_url', 'Not configured'),
+ "available": True # Would check actual connectivity
+ },
+ "uptime": "N/A", # Would calculate from startup time
+ "timestamp": datetime.now().isoformat()
+ }
+
+ return ApiResponse(success=True, data=status)
+
+ except Exception as e:
+ return ApiResponse(
+ success=False,
+ error=str(e),
+ message="Failed to get system status"
+ )
+
+# File upload endpoints
+@app.post("/api/upload", response_model=ApiResponse)
+async def upload_files(files: List[UploadFile] = File(...)):
+ """Upload and process files"""
+ if not files:
+ raise HTTPException(status_code=400, detail="No files provided")
+
+ uploaded_files = []
+ total_size = 0
+ errors = []
+
+ try:
+ for file in files:
+ # Validate file
+ if file.size and file.size > SECURITY_CONFIG['max_upload_size_mb'] * 1024 * 1024:
+ errors.append({
+ "fileName": file.filename,
+ "error": f"File too large: {file.size / 1024 / 1024:.1f}MB"
+ })
+ continue
+
+ # Save file temporarily
+ file_id = str(uuid.uuid4())
+ file_path = Path(f"uploads/{file_id}_{file.filename}")
+ file_path.parent.mkdir(exist_ok=True)
+
+ with open(file_path, "wb") as buffer:
+ content = await file.read()
+ buffer.write(content)
+
+ total_size += len(content)
+
+ # Process file if components are available
+ if ingestion_manager:
+ try:
+ result = ingestion_manager.process_file(str(file_path))
+ uploaded_files.append({
+ "id": file_id,
+ "fileName": file.filename,
+ "filePath": str(file_path),
+ "fileType": result.get('file_type', 'unknown'),
+ "fileSize": len(content),
+ "mimeType": file.content_type,
+ "status": "completed",
+ "progress": 100,
+ "metadata": result,
+ "uploadedAt": datetime.now().isoformat(),
+ "processedAt": datetime.now().isoformat()
+ })
+ except Exception as e:
+ uploaded_files.append({
+ "id": file_id,
+ "fileName": file.filename,
+ "filePath": str(file_path),
+ "fileType": "unknown",
+ "fileSize": len(content),
+ "mimeType": file.content_type,
+ "status": "error",
+ "progress": 0,
+ "error": str(e),
+ "uploadedAt": datetime.now().isoformat()
+ })
+ else:
+ # Mock response for development
+ uploaded_files.append({
+ "id": file_id,
+ "fileName": file.filename,
+ "filePath": str(file_path),
+ "fileType": "unknown",
+ "fileSize": len(content),
+ "mimeType": file.content_type,
+ "status": "completed",
+ "progress": 100,
+ "uploadedAt": datetime.now().isoformat()
+ })
+
+ return ApiResponse(
+ success=True,
+ message=f"Successfully uploaded {len(uploaded_files)} files",
+ data={
+ "files": uploaded_files,
+ "totalFiles": len(uploaded_files),
+ "totalSize": total_size,
+ "errors": errors
+ }
+ )
+
+ except Exception as e:
+ logger.error(f"Upload error: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.get("/api/files", response_model=PaginatedResponse)
+async def get_uploaded_files(page: int = 1, limit: int = 20):
+ """Get list of uploaded files"""
+ try:
+ # This would typically query a database
+ # For now, return mock data
+ files = []
+ total = 0
+
+ return PaginatedResponse(
+ success=True,
+ data=files,
+ pagination={
+ "page": page,
+ "limit": limit,
+ "total": total,
+ "totalPages": (total + limit - 1) // limit,
+ "hasNext": page * limit < total,
+ "hasPrev": page > 1
+ }
+ )
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.delete("/api/files/{file_id}", response_model=ApiResponse)
+async def delete_file(file_id: str):
+ """Delete uploaded file"""
+ try:
+ # Implementation would remove file from storage and database
+ return ApiResponse(success=True, message="File deleted successfully")
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+# Query endpoints
+@app.post("/api/query/text", response_model=ApiResponse)
+async def process_text_query(request: QueryRequest):
+ """Process text query"""
+ if not request.text:
+ raise HTTPException(status_code=400, detail="Text query is required")
+
+ try:
+ if query_processor:
+ query_result = query_processor.process_text_query(request.text)
+
+ # Transform results to match frontend expectations
+ results = []
+ for result in query_result.results:
+ results.append({
+ "id": str(uuid.uuid4()),
+ "filePath": result.get('file_path', ''),
+ "fileName": Path(result.get('file_path', '')).name,
+ "fileType": result.get('file_type', 'unknown'),
+ "similarityScore": result.get('similarity_score', 0.0),
+ "confidence": result.get('confidence', 0.0),
+ "contentPreview": result.get('content_preview', ''),
+ "metadata": result.get('metadata', {}),
+ "timestamp": datetime.now().isoformat()
+ })
+
+ return ApiResponse(
+ success=True,
+ data={
+ "query": {
+ "id": str(uuid.uuid4()),
+ "type": "text",
+ "text": request.text,
+ "timestamp": datetime.now().isoformat(),
+ "similarityThreshold": request.similarity_threshold,
+ "status": "completed"
+ },
+ "results": results,
+ "totalResults": len(results),
+ "processingTime": getattr(query_result, 'processing_time', 0.0)
+ }
+ )
+ else:
+ # Mock response for development
+ return ApiResponse(
+ success=True,
+ data={
+ "query": {
+ "id": str(uuid.uuid4()),
+ "type": "text",
+ "text": request.text,
+ "timestamp": datetime.now().isoformat(),
+ "similarityThreshold": request.similarity_threshold,
+ "status": "completed"
+ },
+ "results": [],
+ "totalResults": 0,
+ "processingTime": 0.1
+ }
+ )
+
+ except Exception as e:
+ logger.error(f"Text query error: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/api/query/image", response_model=ApiResponse)
+async def process_image_query(
+ image: UploadFile = File(...),
+ text_query: Optional[str] = None,
+ similarity_threshold: float = 0.5,
+ max_results: int = 10
+):
+ """Process image query"""
+ try:
+ # Save uploaded image temporarily
+ image_id = str(uuid.uuid4())
+ image_path = Path(f"temp/{image_id}_{image.filename}")
+ image_path.parent.mkdir(exist_ok=True)
+
+ with open(image_path, "wb") as buffer:
+ content = await image.read()
+ buffer.write(content)
+
+ if query_processor:
+ query_result = query_processor.process_image_query(str(image_path))
+
+ # Transform results
+ results = []
+ for result in query_result.results:
+ results.append({
+ "id": str(uuid.uuid4()),
+ "filePath": result.get('file_path', ''),
+ "fileName": Path(result.get('file_path', '')).name,
+ "fileType": result.get('file_type', 'unknown'),
+ "similarityScore": result.get('similarity_score', 0.0),
+ "confidence": result.get('confidence', 0.0),
+ "contentPreview": result.get('content_preview', ''),
+ "metadata": result.get('metadata', {}),
+ "timestamp": datetime.now().isoformat()
+ })
+
+ return ApiResponse(
+ success=True,
+ data={
+ "query": {
+ "id": str(uuid.uuid4()),
+ "type": "image",
+ "text": text_query,
+ "timestamp": datetime.now().isoformat(),
+ "similarityThreshold": similarity_threshold,
+ "status": "completed"
+ },
+ "results": results,
+ "totalResults": len(results),
+ "processingTime": getattr(query_result, 'processing_time', 0.0)
+ }
+ )
+ else:
+ # Mock response
+ return ApiResponse(
+ success=True,
+ data={
+ "query": {
+ "id": str(uuid.uuid4()),
+ "type": "image",
+ "text": text_query,
+ "timestamp": datetime.now().isoformat(),
+ "similarityThreshold": similarity_threshold,
+ "status": "completed"
+ },
+ "results": [],
+ "totalResults": 0,
+ "processingTime": 0.1
+ }
+ )
+
+ except Exception as e:
+ logger.error(f"Image query error: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/api/query/voice", response_model=ApiResponse)
+async def process_voice_query(
+ audio: UploadFile = File(...),
+ language: str = "en",
+ similarity_threshold: float = 0.5,
+ max_results: int = 10
+):
+ """Process voice query with speech-to-text"""
+ try:
+ # Save uploaded audio temporarily
+ audio_id = str(uuid.uuid4())
+ audio_path = Path(f"temp/{audio_id}_{audio.filename}")
+ audio_path.parent.mkdir(exist_ok=True)
+
+ with open(audio_path, "wb") as buffer:
+ content = await audio.read()
+ buffer.write(content)
+
+ if stt_processor and query_processor:
+ # Transcribe audio
+ stt_result = stt_processor.process_voice_query_with_fallback(str(audio_path))
+
+ if not stt_result.get('success'):
+ raise HTTPException(status_code=400, detail=stt_result.get('error', 'Transcription failed'))
+
+ transcribed_text = stt_result.get('transcribed_text', '')
+
+ # Process as text query
+ query_result = query_processor.process_text_query(transcribed_text)
+
+ # Transform results
+ results = []
+ for result in query_result.results:
+ results.append({
+ "id": str(uuid.uuid4()),
+ "filePath": result.get('file_path', ''),
+ "fileName": Path(result.get('file_path', '')).name,
+ "fileType": result.get('file_type', 'unknown'),
+ "similarityScore": result.get('similarity_score', 0.0),
+ "confidence": result.get('confidence', 0.0),
+ "contentPreview": result.get('content_preview', ''),
+ "metadata": result.get('metadata', {}),
+ "timestamp": datetime.now().isoformat()
+ })
+
+ return ApiResponse(
+ success=True,
+ data={
+ "query": {
+ "id": str(uuid.uuid4()),
+ "type": "voice",
+ "text": transcribed_text,
+ "timestamp": datetime.now().isoformat(),
+ "similarityThreshold": similarity_threshold,
+ "status": "completed"
+ },
+ "results": results,
+ "totalResults": len(results),
+ "processingTime": getattr(query_result, 'processing_time', 0.0)
+ }
+ )
+ else:
+ # Mock response
+ return ApiResponse(
+ success=True,
+ data={
+ "query": {
+ "id": str(uuid.uuid4()),
+ "type": "voice",
+ "text": "Mock transcription",
+ "timestamp": datetime.now().isoformat(),
+ "similarityThreshold": similarity_threshold,
+ "status": "completed"
+ },
+ "results": [],
+ "totalResults": 0,
+ "processingTime": 0.1
+ }
+ )
+
+ except Exception as e:
+ logger.error(f"Voice query error: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/api/query/multimodal", response_model=ApiResponse)
+async def process_multimodal_query(
+ text: str = Form(...),
+ image: Optional[UploadFile] = File(None),
+ similarity_threshold: float = 0.5,
+ max_results: int = 10
+):
+ """Process multimodal query combining text and image"""
+ try:
+ image_path = None
+
+ # Save uploaded image if provided
+ if image:
+ image_id = str(uuid.uuid4())
+ image_path = Path(f"temp/{image_id}_{image.filename}")
+ image_path.parent.mkdir(exist_ok=True)
+
+ with open(image_path, "wb") as buffer:
+ content = await image.read()
+ buffer.write(content)
+
+ if query_processor:
+ if image_path:
+ query_result = query_processor.process_multimodal_query(text, str(image_path))
+ else:
+ query_result = query_processor.process_text_query(text)
+
+ # Transform results
+ results = []
+ for result in query_result.results:
+ results.append({
+ "id": str(uuid.uuid4()),
+ "filePath": result.get('file_path', ''),
+ "fileName": Path(result.get('file_path', '')).name,
+ "fileType": result.get('file_type', 'unknown'),
+ "similarityScore": result.get('similarity_score', 0.0),
+ "confidence": result.get('confidence', 0.0),
+ "contentPreview": result.get('content_preview', ''),
+ "metadata": result.get('metadata', {}),
+ "timestamp": datetime.now().isoformat()
+ })
+
+ return ApiResponse(
+ success=True,
+ data={
+ "query": {
+ "id": str(uuid.uuid4()),
+ "type": "multimodal",
+ "text": text,
+ "timestamp": datetime.now().isoformat(),
+ "similarityThreshold": similarity_threshold,
+ "status": "completed"
+ },
+ "results": results,
+ "totalResults": len(results),
+ "processingTime": getattr(query_result, 'processing_time', 0.0)
+ }
+ )
+ else:
+ # Mock response
+ return ApiResponse(
+ success=True,
+ data={
+ "query": {
+ "id": str(uuid.uuid4()),
+ "type": "multimodal",
+ "text": text,
+ "timestamp": datetime.now().isoformat(),
+ "similarityThreshold": similarity_threshold,
+ "status": "completed"
+ },
+ "results": [],
+ "totalResults": 0,
+ "processingTime": 0.1
+ }
+ )
+
+ except Exception as e:
+ logger.error(f"Multimodal query error: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+# Response generation endpoint
+@app.post("/api/generate-response", response_model=ApiResponse)
+async def generate_response(request: ResponseGenerationRequest):
+ """Generate AI response with citations"""
+ try:
+ if llm_generator and citation_generator:
+ # Generate response using LLM
+ generated_response = llm_generator.generate_grounded_response(
+ query=request.query,
+ context=request.context
+ )
+
+ # Generate citations
+ citations = citation_generator.generate_citations(
+ response=generated_response.response_text,
+ sources=request.context,
+ citation_indices=None
+ )
+
+ # Transform citations
+ citation_list = []
+ for citation in citations:
+ citation_list.append({
+ "id": str(uuid.uuid4()),
+ "citationId": getattr(citation, 'citation_id', ''),
+ "filePath": getattr(citation, 'file_path', ''),
+ "fileName": Path(getattr(citation, 'file_path', '')).name,
+ "sourceType": getattr(citation, 'source_type', 'document'),
+ "pageNumber": getattr(citation, 'page_number', None),
+ "contentSnippet": getattr(citation, 'content_snippet', ''),
+ "confidenceScore": getattr(citation, 'confidence_score', 0.0),
+ "timestamp": datetime.now().isoformat()
+ })
+
+ return ApiResponse(
+ success=True,
+ data={
+ "id": str(uuid.uuid4()),
+ "queryId": str(uuid.uuid4()),
+ "responseText": generated_response.response_text,
+ "confidence": getattr(generated_response, 'confidence_score', 0.0),
+ "citations": citation_list,
+ "processingTime": 0.0, # Would calculate actual time
+ "modelUsed": request.model or "gemma-3n",
+ "timestamp": datetime.now().isoformat()
+ },
+ message="Response generated successfully"
+ )
+ else:
+ # Mock response
+ return ApiResponse(
+ success=True,
+ data={
+ "id": str(uuid.uuid4()),
+ "queryId": str(uuid.uuid4()),
+ "responseText": f"This is a mock response to: {request.query}",
+ "confidence": 0.8,
+ "citations": [],
+ "processingTime": 0.1,
+ "modelUsed": request.model or "gemma-3n",
+ "timestamp": datetime.now().isoformat()
+ },
+ message="Mock response generated"
+ )
+
+ except Exception as e:
+ logger.error(f"Response generation error: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+# Query history endpoint
+@app.get("/api/query/history", response_model=PaginatedResponse)
+async def get_query_history(page: int = 1, limit: int = 20):
+ """Get query history"""
+ try:
+ # This would typically query a database
+ # For now, return mock data
+ queries = []
+ total = 0
+
+ return PaginatedResponse(
+ success=True,
+ data=queries,
+ pagination={
+ "page": page,
+ "limit": limit,
+ "total": total,
+ "totalPages": (total + limit - 1) // limit,
+ "hasNext": page * limit < total,
+ "hasPrev": page > 1
+ }
+ )
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+# Analytics endpoints
+@app.get("/api/analytics", response_model=ApiResponse)
+async def get_analytics(start: Optional[str] = None, end: Optional[str] = None):
+ """Get analytics data"""
+ try:
+ # Mock analytics data
+ analytics_data = {
+ "queryStats": {
+ "totalQueries": 150,
+ "textQueries": 100,
+ "imageQueries": 30,
+ "voiceQueries": 15,
+ "multimodalQueries": 5,
+ "avgProcessingTime": 1.2,
+ "successRate": 95.3
+ },
+ "fileStats": {
+ "totalFiles": 45,
+ "documentFiles": 25,
+ "imageFiles": 15,
+ "audioFiles": 5,
+ "totalSize": 125.6, # MB
+ "avgProcessingTime": 2.8
+ },
+ "systemStats": {
+ "uptime": 86400, # seconds
+ "memoryUsage": 45.2, # percentage
+ "cpuUsage": 23.1, # percentage
+ "diskUsage": 67.8 # percentage
+ },
+ "usageTrends": [
+ {"date": "2024-01-01", "queries": 12, "uploads": 3},
+ {"date": "2024-01-02", "queries": 18, "uploads": 5},
+ {"date": "2024-01-03", "queries": 15, "uploads": 2},
+ {"date": "2024-01-04", "queries": 22, "uploads": 7},
+ {"date": "2024-01-05", "queries": 19, "uploads": 4},
+ {"date": "2024-01-06", "queries": 25, "uploads": 6},
+ {"date": "2024-01-07", "queries": 21, "uploads": 3}
+ ],
+ "popularQueries": [
+ {"query": "security protocols", "count": 12, "avgRating": 4.2},
+ {"query": "network configuration", "count": 8, "avgRating": 4.5},
+ {"query": "user authentication", "count": 6, "avgRating": 4.0}
+ ]
+ }
+
+ return ApiResponse(success=True, data=analytics_data)
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.get("/api/analytics/metrics", response_model=ApiResponse)
+async def get_system_metrics():
+ """Get system performance metrics"""
+ try:
+ metrics = {
+ "cpu": {"usage": 23.1, "cores": 4, "frequency": "2.4GHz"},
+ "memory": {"usage": 45.2, "total": "16GB", "available": "8.7GB"},
+ "disk": {"usage": 67.8, "total": "500GB", "available": "161GB"},
+ "network": {"rx": "1.2MB/s", "tx": "0.8MB/s"},
+ "timestamp": datetime.now().isoformat()
+ }
+
+ return ApiResponse(success=True, data=metrics)
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+# Security endpoints
+@app.get("/api/security/events", response_model=PaginatedResponse)
+async def get_security_events(page: int = 1, limit: int = 20):
+ """Get security events"""
+ try:
+ # Mock security events
+ events = [
+ {
+ "id": str(uuid.uuid4()),
+ "type": "audit",
+ "severity": "low",
+ "title": "User Login",
+ "description": "User logged in successfully",
+ "source": "auth_system",
+ "timestamp": datetime.now().isoformat(),
+ "resolved": True,
+ "resolvedAt": datetime.now().isoformat()
+ }
+ ]
+ total = len(events)
+
+ return PaginatedResponse(
+ success=True,
+ data=events,
+ pagination={
+ "page": page,
+ "limit": limit,
+ "total": total,
+ "totalPages": (total + limit - 1) // limit,
+ "hasNext": page * limit < total,
+ "hasPrev": page > 1
+ }
+ )
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+# Feedback endpoint
+@app.post("/api/feedback", response_model=ApiResponse)
+async def submit_feedback(feedback: FeedbackRequest):
+ """Submit user feedback"""
+ try:
+ if feedback_system:
+ feedback_id = feedback_system.collect_feedback(
+ query="", # Would get from database
+ response="", # Would get from database
+ rating=feedback.rating,
+ comments=feedback.comments,
+ metadata=feedback.metadata
+ )
+ else:
+ feedback_id = str(uuid.uuid4())
+
+ return ApiResponse(
+ success=True,
+ data={"feedbackId": feedback_id},
+ message="Feedback submitted successfully"
+ )
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+# Configuration endpoints
+@app.get("/api/config", response_model=ApiResponse)
+async def get_system_config():
+ """Get system configuration"""
+ try:
+ config = {
+ "apiUrl": "http://localhost:8000",
+ "wsUrl": "ws://localhost:8000",
+ "lmStudioUrl": LM_STUDIO_CONFIG.get('base_url', 'http://localhost:1234'),
+ "maxFileSize": SECURITY_CONFIG['max_upload_size_mb'] * 1024 * 1024,
+ "allowedFileTypes": [".pdf", ".docx", ".doc", ".txt", ".jpg", ".png", ".mp3"],
+ "enableAnalytics": True,
+ "enableDarkMode": True,
+ "defaultSimilarityThreshold": 0.5,
+ "maxQueryHistory": 50,
+ "enableVoiceInput": True,
+ "models": {
+ "primary": "gemma-3n",
+ "fallback": "qwen3-4b"
+ },
+ "performance": {
+ "batchSize": 10,
+ "maxConcurrency": 4,
+ "cacheEnabled": True,
+ "cacheTimeout": 3600
+ },
+ "security": {
+ "auditLogging": True,
+ "anomalyDetection": True,
+ "rateLimiting": True,
+ "maxUploadsPerHour": 100
+ }
+ }
+
+ return ApiResponse(success=True, data=config)
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.put("/api/config", response_model=ApiResponse)
+async def update_system_config(config: ConfigUpdateRequest):
+ """Update system configuration"""
+ try:
+ # In a real implementation, this would save to a config file or database
+ return ApiResponse(
+ success=True,
+ message="Configuration updated successfully",
+ data=config.dict()
+ )
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/api/config/validate", response_model=ApiResponse)
+async def validate_config(config: ConfigUpdateRequest):
+ """Validate configuration"""
+ try:
+ # Basic validation
+ if config.max_file_size and config.max_file_size < 1024:
+ raise ValueError("Max file size must be at least 1024 bytes")
+
+ if config.default_similarity_threshold and not (0.0 <= config.default_similarity_threshold <= 1.0):
+ raise ValueError("Similarity threshold must be between 0.0 and 1.0")
+
+ return ApiResponse(
+ success=True,
+ message="Configuration is valid"
+ )
+
+ except Exception as e:
+ return ApiResponse(
+ success=False,
+ error=str(e),
+ message="Configuration validation failed"
+ )
+
+# Export endpoints
+@app.post("/api/export")
+async def export_results(format: str, data: Dict[str, Any]):
+ """Export results in various formats"""
+ try:
+ if format == "json":
+ return JSONResponse(content=data)
+ elif format == "csv":
+ # Simple CSV conversion (would be more sophisticated in real implementation)
+ csv_content = "File Name,File Type,Similarity Score,Content Preview\n"
+ for result in data.get("results", []):
+ csv_content += f'"{result.get("fileName", "")}","{result.get("fileType", "")}",{result.get("similarityScore", 0)},"{result.get("contentPreview", "")}"\n'
+
+ return FileResponse(
+ content=csv_content,
+ media_type="text/csv",
+ filename=f"neurax-export-{datetime.now().strftime('%Y%m%d-%H%M%S')}.csv"
+ )
+ else:
+ raise HTTPException(status_code=400, detail="Unsupported export format")
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+# Serve static files (for uploaded files, etc.)
+if Path("uploads").exists():
+ app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
+
+if Path("temp").exists():
+ app.mount("/temp", StaticFiles(directory="temp"), name="temp")
+
+# Error handlers
+@app.exception_handler(404)
+async def not_found_handler(request, exc):
+ return JSONResponse(
+ status_code=404,
+ content={"success": False, "error": "Endpoint not found", "timestamp": datetime.now().isoformat()}
+ )
+
+@app.exception_handler(500)
+async def internal_error_handler(request, exc):
+ return JSONResponse(
+ status_code=500,
+ content={"success": False, "error": "Internal server error", "timestamp": datetime.now().isoformat()}
+ )
+
+if __name__ == "__main__":
+ # Configure logging
+ logging.basicConfig(level=logging.INFO)
+
+ # Run the server
+ uvicorn.run(
+ "api_server:app",
+ host="0.0.0.0",
+ port=8000,
+ reload=True,
+ log_level="info"
+ )
\ No newline at end of file
diff --git a/backend/requirements-api.txt b/backend/requirements-api.txt
new file mode 100644
index 0000000..b86bb02
--- /dev/null
+++ b/backend/requirements-api.txt
@@ -0,0 +1,8 @@
+# Backend API Server Requirements
+fastapi==0.104.1
+uvicorn[standard]==0.24.0
+python-multipart==0.0.6
+pydantic==2.5.0
+python-jose[cryptography]==3.3.0
+passlib[bcrypt]==1.7.4
+aiofiles==23.2.0
\ No newline at end of file
diff --git a/neurax-frontend/.env.example b/neurax-frontend/.env.example
new file mode 100644
index 0000000..796b646
--- /dev/null
+++ b/neurax-frontend/.env.example
@@ -0,0 +1,28 @@
+# Environment variables for NeuraX Frontend
+# Copy this file to .env.local and configure as needed
+
+# API Configuration
+NEXT_PUBLIC_API_URL=http://localhost:8000
+NEXT_PUBLIC_WS_URL=ws://localhost:8000
+NEXT_PUBLIC_LM_STUDIO_URL=http://localhost:1234
+
+# File Upload Settings
+NEXT_PUBLIC_MAX_FILE_SIZE=104857600
+NEXT_PUBLIC_ALLOWED_FILE_TYPES=.pdf,.docx,.doc,.txt,.jpg,.png,.mp3,.wav,.m4a,.flac,.ogg,.bmp,.tiff,.webp
+
+# Feature Flags
+NEXT_PUBLIC_ENABLE_ANALYTICS=true
+NEXT_PUBLIC_ENABLE_DARK_MODE=true
+NEXT_PUBLIC_ENABLE_VOICE_INPUT=true
+
+# Query Settings
+NEXT_PUBLIC_DEFAULT_SIMILARITY_THRESHOLD=0.5
+NEXT_PUBLIC_MAX_QUERY_HISTORY=50
+
+# Development Settings
+NEXT_PUBLIC_DEBUG_MODE=false
+NEXT_PUBLIC_LOG_LEVEL=info
+
+# Optional: Custom model configurations
+# NEXT_PUBLIC_CUSTOM_MODELS_PATH=/path/to/models
+# NEXT_PUBLIC_ENABLE_EXPERIMENTAL_FEATURES=false
\ No newline at end of file
diff --git a/neurax-frontend/.env.local b/neurax-frontend/.env.local
new file mode 100644
index 0000000..9142603
--- /dev/null
+++ b/neurax-frontend/.env.local
@@ -0,0 +1,11 @@
+# NeuraX Frontend Environment Variables
+NEXT_PUBLIC_API_URL=http://localhost:8000
+NEXT_PUBLIC_WS_URL=ws://localhost:8000
+NEXT_PUBLIC_LM_STUDIO_URL=http://localhost:1234
+NEXT_PUBLIC_MAX_FILE_SIZE=104857600
+NEXT_PUBLIC_ALLOWED_FILE_TYPES=.pdf,.docx,.doc,.txt,.jpg,.png,.mp3,.wav,.m4a,.flac,.ogg,.bmp,.tiff,.webp
+NEXT_PUBLIC_ENABLE_ANALYTICS=true
+NEXT_PUBLIC_ENABLE_DARK_MODE=true
+NEXT_PUBLIC_DEFAULT_SIMILARITY_THRESHOLD=0.5
+NEXT_PUBLIC_MAX_QUERY_HISTORY=50
+NEXT_PUBLIC_ENABLE_VOICE_INPUT=true
\ No newline at end of file
diff --git a/neurax-frontend/Dockerfile b/neurax-frontend/Dockerfile
new file mode 100644
index 0000000..7f269d5
--- /dev/null
+++ b/neurax-frontend/Dockerfile
@@ -0,0 +1,62 @@
+# Use the official Node.js 18 image
+FROM node:18-alpine AS base
+
+# Install dependencies only when needed
+FROM base AS deps
+# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
+RUN apk add --no-cache libc6-compat
+WORKDIR /app
+
+# Install dependencies based on the preferred package manager
+COPY package.json package-lock.json* ./
+RUN npm ci --only=production && npm cache clean --force
+
+# Rebuild the source code only when needed
+FROM base AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+
+# Next.js collects completely anonymous telemetry data about general usage.
+# Learn more here: https://nextjs.org/telemetry
+# Uncomment the following line in case you want to disable telemetry during the build.
+ENV NEXT_TELEMETRY_DISABLED 1
+
+RUN npm run build
+
+# If using npm comment out above and use below instead
+# RUN npm run build
+
+# Production image, copy all the files and run next
+FROM base AS runner
+WORKDIR /app
+
+ENV NODE_ENV production
+# Uncomment the following line in case you want to disable telemetry during runtime.
+ENV NEXT_TELEMETRY_DISABLED 1
+
+RUN addgroup --system --gid 1001 nodejs
+RUN adduser --system --uid 1001 nextjs
+
+COPY --from=builder /app/public ./public
+
+# Set the correct permission for prerender cache
+RUN mkdir .next
+RUN chown nextjs:nodejs .next
+
+# Automatically leverage output traces to reduce image size
+# https://nextjs.org/docs/advanced-features/output-file-tracing
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+
+USER nextjs
+
+EXPOSE 3000
+
+ENV PORT 3000
+# set hostname to localhost
+ENV HOSTNAME "0.0.0.0"
+
+# server.js is created by next build from the standalone output
+# https://nextjs.org/docs/pages/api-reference/next-config-js/output
+CMD ["node", "server.js"]
\ No newline at end of file
diff --git a/neurax-frontend/app/layout.tsx b/neurax-frontend/app/layout.tsx
new file mode 100644
index 0000000..9deda44
--- /dev/null
+++ b/neurax-frontend/app/layout.tsx
@@ -0,0 +1,50 @@
+import type { Metadata } from 'next'
+import { Inter } from 'next/font/google'
+import './globals.css'
+import { Toaster } from 'react-hot-toast'
+
+const inter = Inter({ subsets: ['latin'] })
+
+export const metadata: Metadata = {
+ title: 'NeuraX - Multimodal RAG System',
+ description: 'Secure, offline multimodal retrieval-augmented generation system with document intelligence and analytics.',
+ keywords: 'RAG, multimodal, document intelligence, AI, security, offline',
+ authors: [{ name: 'NeuraX Team' }],
+ viewport: 'width=device-width, initial-scale=1',
+}
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+
+
+ {children}
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/neurax-frontend/app/page.tsx b/neurax-frontend/app/page.tsx
new file mode 100644
index 0000000..2d6a1f5
--- /dev/null
+++ b/neurax-frontend/app/page.tsx
@@ -0,0 +1,162 @@
+'use client'
+
+import { useState, useCallback } from 'react'
+import { Header } from '@/components/common/Header'
+import { FileUploader } from '@/components/upload/FileUploader'
+import { QueryInterface } from '@/components/query/QueryInterface'
+import { ResultsDisplay } from '@/components/results/ResultsDisplay'
+import { AnalyticsDashboard } from '@/components/analytics/AnalyticsDashboard'
+import { QueryHistory } from '@/components/query/QueryHistory'
+import { Settings } from '@/components/common/Settings'
+import { Card, CardContent } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { FileText, Search, BarChart3, History, Settings as SettingsIcon, Upload } from 'lucide-react'
+import type { Query, SearchResult, FileUpload } from '@/types'
+
+export default function HomePage() {
+ const [activeTab, setActiveTab] = useState<'query' | 'upload' | 'analytics' | 'history' | 'settings'>('query')
+ const [currentQuery, setCurrentQuery] = useState(null)
+ const [searchResults, setSearchResults] = useState([])
+ const [uploadedFiles, setUploadedFiles] = useState([])
+
+ const handleSearch = useCallback((query: Query) => {
+ setCurrentQuery(query)
+ // Search results will be set by the QueryInterface component
+ }, [])
+
+ const handleSearchResults = useCallback((results: SearchResult[]) => {
+ setSearchResults(results)
+ }, [])
+
+ const handleFileUpload = useCallback((files: FileUpload[]) => {
+ setUploadedFiles(prev => [...prev, ...files])
+ }, [])
+
+ const renderActiveTab = () => {
+ switch (activeTab) {
+ case 'upload':
+ return (
+
+ )
+ case 'analytics':
+ return
+ case 'history':
+ return
+ case 'settings':
+ return
+ case 'query':
+ default:
+ return (
+
+
+ {searchResults.length > 0 && (
+
+ )}
+
+ )
+ }
+ }
+
+ return (
+
+
setActiveTab('query')}
+ onUpload={() => setActiveTab('upload')}
+ onAnalytics={() => setActiveTab('analytics')}
+ onHistory={() => setActiveTab('history')}
+ onSettings={() => setActiveTab('settings')}
+ />
+
+
+ {/* Tab Navigation */}
+
+
+ setActiveTab('query')}
+ className="gap-2"
+ >
+
+ Query & Search
+
+ setActiveTab('upload')}
+ className="gap-2"
+ >
+
+ File Upload
+
+ setActiveTab('analytics')}
+ className="gap-2"
+ >
+
+ Analytics
+
+ setActiveTab('history')}
+ className="gap-2"
+ >
+
+ History
+
+ setActiveTab('settings')}
+ className="gap-2"
+ >
+
+ Settings
+
+
+
+
+ {/* Tab Content */}
+
+ {renderActiveTab()}
+
+
+ {/* Quick Stats Footer */}
+ {activeTab === 'query' && (
+
+
+
+
+
+
+
{uploadedFiles.length} files uploaded
+
+
+
+
{searchResults.length} results found
+
+
+
+ System Status: Online
+
+
+
+
+ )}
+
+
+ )
+}
\ No newline at end of file
diff --git a/neurax-frontend/components/analytics/AnalyticsDashboard.tsx b/neurax-frontend/components/analytics/AnalyticsDashboard.tsx
new file mode 100644
index 0000000..36bec04
--- /dev/null
+++ b/neurax-frontend/components/analytics/AnalyticsDashboard.tsx
@@ -0,0 +1,507 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import {
+ BarChart3,
+ TrendingUp,
+ TrendingDown,
+ Activity,
+ Database,
+ Clock,
+ Users,
+ FileText,
+ Image,
+ Music,
+ Shield,
+ AlertTriangle,
+ CheckCircle,
+ Download
+} from 'lucide-react'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Badge } from '@/components/ui/badge'
+import { cn } from '@/lib/utils'
+import { apiClient } from '@/lib/api/client'
+import toast from 'react-hot-toast'
+import type { Analytics, SecurityEvent } from '@/types'
+
+export function AnalyticsDashboard() {
+ const [analytics, setAnalytics] = useState(null)
+ const [securityEvents, setSecurityEvents] = useState([])
+ const [isLoading, setIsLoading] = useState(true)
+ const [timeRange, setTimeRange] = useState<'24h' | '7d' | '30d' | '90d'>('7d')
+
+ useEffect(() => {
+ loadAnalytics()
+ loadSecurityEvents()
+ }, [timeRange])
+
+ const loadAnalytics = async () => {
+ try {
+ setIsLoading(true)
+ const end = new Date()
+ const start = new Date()
+
+ switch (timeRange) {
+ case '24h':
+ start.setDate(start.getDate() - 1)
+ break
+ case '7d':
+ start.setDate(start.getDate() - 7)
+ break
+ case '30d':
+ start.setDate(start.getDate() - 30)
+ break
+ case '90d':
+ start.setDate(start.getDate() - 90)
+ break
+ }
+
+ const data = await apiClient.getAnalytics({
+ start: start.toISOString(),
+ end: end.toISOString()
+ })
+ setAnalytics(data)
+ } catch (error) {
+ console.error('Failed to load analytics:', error)
+ toast.error('Failed to load analytics data')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const loadSecurityEvents = async () => {
+ try {
+ const events = await apiClient.getSecurityEvents(1, 10)
+ if (events.success && events.data) {
+ setSecurityEvents(events.data)
+ }
+ } catch (error) {
+ console.error('Failed to load security events:', error)
+ }
+ }
+
+ const exportAnalytics = async () => {
+ try {
+ const blob = await apiClient.exportAnalytics('json', {
+ timeRange,
+ timestamp: new Date().toISOString()
+ })
+
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `neurax-analytics-${timeRange}-${Date.now()}.json`
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+ URL.revokeObjectURL(url)
+ toast.success('Analytics exported successfully')
+ } catch (error) {
+ console.error('Export failed:', error)
+ toast.error('Failed to export analytics')
+ }
+ }
+
+ if (isLoading) {
+ return (
+
+
+ {[...Array(4)].map((_, i) => (
+
+
+
+
+
+ ))}
+
+
+ )
+ }
+
+ if (!analytics) {
+ return (
+
+
+
+
+
No Analytics Data
+
+ Analytics data is not available. The system may not have been running long enough to generate meaningful statistics.
+
+
Retry
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
Analytics Dashboard
+
System performance and usage analytics
+
+
+ setTimeRange(e.target.value as any)}
+ className="px-3 py-1 border rounded-md text-sm"
+ >
+ Last 24 hours
+ Last 7 days
+ Last 30 days
+ Last 90 days
+
+
+
+ Export
+
+
+
+
+ {/* Overview Cards */}
+
+ }
+ trend={analytics.queryStats.successRate > 80 ? 'up' : 'down'}
+ trendValue={`${analytics.queryStats.successRate.toFixed(1)}% success rate`}
+ />
+ }
+ trend="up"
+ trendValue={`${(analytics.fileStats.totalSize / 1024 / 1024).toFixed(1)} MB total`}
+ />
+ }
+ trend={analytics.queryStats.avgProcessingTime < 2 ? 'up' : 'down'}
+ trendValue={analytics.queryStats.avgProcessingTime < 2 ? 'Fast' : 'Needs optimization'}
+ />
+ }
+ trend="up"
+ trendValue={`${((analytics.systemStats.uptime / 86400) * 100).toFixed(1)}% of time`}
+ />
+
+
+ {/* Query Statistics */}
+
+
+
+
+
+ Query Types
+
+
+
+
+
+
+
+ Text Queries
+
+
+
{analytics.queryStats.textQueries}
+
+
+
+
+
+
+ Image Queries
+
+
+
{analytics.queryStats.imageQueries}
+
+
+
+
+
+
+ Voice Queries
+
+
+
{analytics.queryStats.voiceQueries}
+
+
+
+
+
+
+ Multimodal Queries
+
+
+
{analytics.queryStats.multimodalQueries}
+
+
+
+
+
+
+
+
+
+
+
+ File Types
+
+
+
+
+
+
+
+ Documents
+
+
+ {analytics.fileStats.documentFiles}
+ {((analytics.fileStats.documentFiles / analytics.fileStats.totalFiles) * 100).toFixed(1)}%
+
+
+
+
+
+ Images
+
+
+ {analytics.fileStats.imageFiles}
+ {((analytics.fileStats.imageFiles / analytics.fileStats.totalFiles) * 100).toFixed(1)}%
+
+
+
+
+
+ Audio
+
+
+ {analytics.fileStats.audioFiles}
+ {((analytics.fileStats.audioFiles / analytics.fileStats.totalFiles) * 100).toFixed(1)}%
+
+
+
+
+
+
+
+ {/* System Performance */}
+
+
+
+
+
+ System Performance
+
+
+
+
+
80 ? 'red' : analytics.systemStats.cpuUsage > 60 ? 'yellow' : 'green'}
+ />
+ 80 ? 'red' : analytics.systemStats.memoryUsage > 60 ? 'yellow' : 'green'}
+ />
+ 90 ? 'red' : analytics.systemStats.diskUsage > 75 ? 'yellow' : 'green'}
+ />
+
+
+
+
+
+
+
+
+ Usage Trends
+
+
+
+
+ {analytics.usageTrends.slice(-7).map((trend, index) => (
+
+
+ {new Date(trend.date).toLocaleDateString()}
+
+
+ {trend.queries} queries
+ {trend.uploads} uploads
+
+
+ ))}
+
+
+
+
+
+
+
+
+ Popular Queries
+
+
+
+
+ {analytics.popularQueries.slice(0, 5).map((query, index) => (
+
+
+ {query.query}
+
+
+ {query.count}
+ •
+ {query.avgRating.toFixed(1)}★
+
+
+ ))}
+
+
+
+
+
+ {/* Security Events */}
+ {securityEvents.length > 0 && (
+
+
+
+
+ Recent Security Events
+
+
+
+
+ {securityEvents.map((event) => (
+
+
+
+
+
{event.title}
+
{event.description}
+
+
+
+
+ {event.severity}
+
+
+ {event.resolved ? 'Resolved' : 'Active'}
+
+
+
+ ))}
+
+
+
+ )}
+
+ )
+}
+
+interface StatCardProps {
+ title: string
+ value: string
+ icon: React.ReactNode
+ trend: 'up' | 'down'
+ trendValue: string
+}
+
+function StatCard({ title, value, icon, trend, trendValue }: StatCardProps) {
+ return (
+
+
+
+
+
{title}
+
{value}
+
+ {trend === 'up' ? (
+
+ ) : (
+
+ )}
+ {trendValue}
+
+
+
+ {icon}
+
+
+
+
+ )
+}
+
+interface PerformanceMetricProps {
+ label: string
+ value: number
+ unit: string
+ color: 'green' | 'yellow' | 'red'
+}
+
+function PerformanceMetric({ label, value, unit, color }: PerformanceMetricProps) {
+ const colorClasses = {
+ green: 'bg-green-500',
+ yellow: 'bg-yellow-500',
+ red: 'bg-red-500'
+ }
+
+ return (
+
+
+ {label}
+ {value.toFixed(1)}{unit}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/neurax-frontend/components/common/Header.tsx b/neurax-frontend/components/common/Header.tsx
new file mode 100644
index 0000000..e3a9175
--- /dev/null
+++ b/neurax-frontend/components/common/Header.tsx
@@ -0,0 +1,146 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { Moon, Sun, Settings, Search, Upload, FileText, BarChart3, History, Shield } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { cn } from '@/lib/utils'
+
+interface HeaderProps {
+ className?: string
+ onThemeToggle?: () => void
+ onSearch?: () => void
+ onUpload?: () => void
+ onAnalytics?: () => void
+ onHistory?: () => void
+ onSettings?: () => void
+}
+
+export function Header({
+ className,
+ onThemeToggle,
+ onSearch,
+ onUpload,
+ onAnalytics,
+ onHistory,
+ onSettings
+}: HeaderProps) {
+ const [isDarkMode, setIsDarkMode] = useState(false)
+
+ useEffect(() => {
+ // Check system preference or stored preference
+ const savedTheme = localStorage.getItem('theme')
+ const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
+
+ setIsDarkMode(savedTheme === 'dark' || (!savedTheme && systemPrefersDark))
+ }, [])
+
+ useEffect(() => {
+ // Apply theme to document
+ if (isDarkMode) {
+ document.documentElement.classList.add('dark')
+ localStorage.setItem('theme', 'dark')
+ } else {
+ document.documentElement.classList.remove('dark')
+ localStorage.setItem('theme', 'light')
+ }
+ }, [isDarkMode])
+
+ const handleThemeToggle = () => {
+ setIsDarkMode(!isDarkMode)
+ onThemeToggle?.()
+ }
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/neurax-frontend/components/common/Settings.tsx b/neurax-frontend/components/common/Settings.tsx
new file mode 100644
index 0000000..78d0ca6
--- /dev/null
+++ b/neurax-frontend/components/common/Settings.tsx
@@ -0,0 +1,570 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import {
+ Settings as SettingsIcon,
+ Save,
+ RotateCcw,
+ Upload,
+ Database,
+ Shield,
+ Bell,
+ Monitor,
+ Palette,
+ Globe,
+ HardDrive,
+ Cpu,
+ MemoryStick
+} from 'lucide-react'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Badge } from '@/components/ui/badge'
+import { cn } from '@/lib/utils'
+import { apiClient } from '@/lib/api/client'
+import toast from 'react-hot-toast'
+import type { SystemConfig } from '@/types'
+
+interface SettingsProps {
+ onConfigChange?: (config: Partial) => void
+}
+
+export function Settings({ onConfigChange }: SettingsProps) {
+ const [config, setConfig] = useState>({})
+ const [originalConfig, setOriginalConfig] = useState>({})
+ const [isLoading, setIsLoading] = useState(true)
+ const [isSaving, setIsSaving] = useState(false)
+ const [hasChanges, setHasChanges] = useState(false)
+
+ useEffect(() => {
+ loadConfig()
+ }, [])
+
+ useEffect(() => {
+ // Check if there are changes compared to original config
+ const hasChanges = JSON.stringify(config) !== JSON.stringify(originalConfig)
+ setHasChanges(hasChanges)
+ }, [config, originalConfig])
+
+ const loadConfig = async () => {
+ try {
+ setIsLoading(true)
+
+ // Try to load from backend API
+ try {
+ const backendConfig = await apiClient.getSystemConfig()
+ setConfig(backendConfig)
+ setOriginalConfig(backendConfig)
+ } catch (apiError) {
+ // Fallback to environment variables and defaults
+ const fallbackConfig: Partial = {
+ apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
+ wsUrl: process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000',
+ lmStudioUrl: process.env.NEXT_PUBLIC_LM_STUDIO_URL || 'http://localhost:1234',
+ maxFileSize: parseInt(process.env.NEXT_PUBLIC_MAX_FILE_SIZE || '104857600'),
+ allowedFileTypes: (process.env.NEXT_PUBLIC_ALLOWED_FILE_TYPES || '.pdf,.docx,.doc,.txt,.jpg,.png,.mp3,.wav,.m4a,.flac,.ogg,.bmp,.tiff,.webp').split(','),
+ enableAnalytics: process.env.NEXT_PUBLIC_ENABLE_ANALYTICS === 'true',
+ enableDarkMode: process.env.NEXT_PUBLIC_ENABLE_DARK_MODE === 'true',
+ defaultSimilarityThreshold: parseFloat(process.env.NEXT_PUBLIC_DEFAULT_SIMILARITY_THRESHOLD || '0.5'),
+ maxQueryHistory: parseInt(process.env.NEXT_PUBLIC_MAX_QUERY_HISTORY || '50'),
+ enableVoiceInput: process.env.NEXT_PUBLIC_ENABLE_VOICE_INPUT === 'true',
+ models: {
+ primary: 'gemma-3n',
+ fallback: 'qwen3-4b'
+ },
+ performance: {
+ batchSize: 10,
+ maxConcurrency: 4,
+ cacheEnabled: true,
+ cacheTimeout: 3600
+ },
+ security: {
+ auditLogging: true,
+ anomalyDetection: true,
+ rateLimiting: true,
+ maxUploadsPerHour: 100
+ }
+ }
+ setConfig(fallbackConfig)
+ setOriginalConfig(fallbackConfig)
+ }
+ } catch (error) {
+ console.error('Failed to load configuration:', error)
+ toast.error('Failed to load configuration')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const saveConfig = async () => {
+ try {
+ setIsSaving(true)
+
+ // Validate configuration
+ const validationResponse = await apiClient.validateConfig(config)
+ if (!validationResponse.success) {
+ toast.error('Configuration validation failed: ' + validationResponse.error)
+ return
+ }
+
+ // Save to backend
+ try {
+ const savedConfig = await apiClient.updateSystemConfig(config)
+ setConfig(savedConfig)
+ setOriginalConfig(savedConfig)
+ onConfigChange?.(savedConfig)
+ toast.success('Configuration saved successfully')
+ } catch (apiError) {
+ // Save to localStorage as fallback
+ localStorage.setItem('neurax_config', JSON.stringify(config))
+ setOriginalConfig(config)
+ onConfigChange?.(config)
+ toast.success('Configuration saved locally (backend not available)')
+ }
+ } catch (error) {
+ console.error('Failed to save configuration:', error)
+ toast.error('Failed to save configuration')
+ } finally {
+ setIsSaving(false)
+ }
+ }
+
+ const resetConfig = () => {
+ setConfig(originalConfig)
+ toast.info('Configuration reset to original values')
+ }
+
+ const updateConfig = (path: string, value: any) => {
+ setConfig(prev => {
+ const newConfig = { ...prev }
+ const keys = path.split('.')
+ let current: any = newConfig
+
+ for (let i = 0; i < keys.length - 1; i++) {
+ if (!current[keys[i]]) {
+ current[keys[i]] = {}
+ }
+ current = current[keys[i]]
+ }
+
+ current[keys[keys.length - 1]] = value
+ return newConfig
+ })
+ }
+
+ if (isLoading) {
+ return (
+
+
+
+ {[...Array(6)].map((_, i) => (
+
+ ))}
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+ System Settings
+
+
+ Configure NeuraX system preferences and behavior
+
+
+
+ {hasChanges && (
+
Unsaved Changes
+ )}
+
+
+ Reset
+
+
+ {isSaving ? (
+
+ ) : (
+
+ )}
+ {isSaving ? 'Saving...' : 'Save Changes'}
+
+
+
+
+
+
+
+ {/* General Settings */}
+
+
+
+
+ General Settings
+
+
+
+
+
Default Similarity Threshold
+
+
updateConfig('defaultSimilarityThreshold', parseFloat(e.target.value))}
+ className="w-full"
+ />
+
+ Lenient (0.0)
+ {(config.defaultSimilarityThreshold || 0.5).toFixed(2)}
+ Strict (1.0)
+
+
+
+
+
+
Max Query History
+
updateConfig('maxQueryHistory', parseInt(e.target.value))}
+ min="10"
+ max="1000"
+ />
+
+ Number of queries to keep in history
+
+
+
+
+
Max File Size (MB)
+
updateConfig('maxFileSize', parseInt(e.target.value) * 1024 * 1024)}
+ min="1"
+ max="1000"
+ />
+
+ Maximum file size for uploads
+
+
+
+
+
+
+
+ {/* Model Configuration */}
+
+
+
+
+ Model Configuration
+
+
+
+
+
Primary Model
+
updateConfig('models.primary', e.target.value)}
+ className="w-full px-3 py-2 border rounded-md"
+ >
+ Gemma 3n (Multimodal)
+ Qwen3 4b (Reasoning)
+
+
+ Main model for most queries
+
+
+
+
+
Fallback Model
+
updateConfig('models.fallback', e.target.value)}
+ className="w-full px-3 py-2 border rounded-md"
+ >
+ Gemma 3n (Multimodal)
+ Qwen3 4b (Reasoning)
+
+
+ Backup model if primary fails
+
+
+
+
+
LM Studio URL
+
updateConfig('lmStudioUrl', e.target.value)}
+ placeholder="http://localhost:1234"
+ />
+
+ URL where LM Studio is running
+
+
+
+
+
Performance Settings
+
+
+
+
+
+ {/* Security Settings */}
+
+
+
+
+ Security & Privacy
+
+
+
+
+
+
+
Max Uploads Per Hour
+
updateConfig('security.maxUploadsPerHour', parseInt(e.target.value))}
+ min="10"
+ max="1000"
+ />
+
+ Rate limit for file uploads
+
+
+
+
+
Allowed File Types
+
+
+ {(config.allowedFileTypes || []).map((type, index) => (
+
+ {type}
+
+ ))}
+
+
updateConfig('allowedFileTypes', e.target.value.split(',').map(t => t.trim()).filter(Boolean))}
+ />
+
+
+
+
+
+ {/* System Information */}
+
+
+
+
+ System Information
+
+
+
+
+
+
+
+ Browser
+
+
+ {typeof navigator !== 'undefined' ? navigator.userAgent.split(' ')[0] : 'Unknown'}
+
+
+
+
+
+
+ Memory
+
+
+ {typeof navigator !== 'undefined' && 'memory' in performance ?
+ `${Math.round((performance as any).memory.usedJSHeapSize / 1024 / 1024)}MB` :
+ 'N/A'
+ }
+
+
+
+
+
+
+ Connection
+
+
+ {typeof navigator !== 'undefined' && 'connection' in navigator ?
+ (navigator as any).connection.effectiveType :
+ 'Unknown'
+ }
+
+
+
+
+
+
+ Screen
+
+
+ {typeof window !== 'undefined' ? `${window.screen.width}x${window.screen.height}` : 'N/A'}
+
+
+
+
+
+
Environment
+
+
API URL: {config.apiUrl || 'Not configured'}
+
WebSocket URL: {config.wsUrl || 'Not configured'}
+
LM Studio: {config.lmStudioUrl || 'Not configured'}
+
+
+
+
+ {
+ // Export configuration
+ const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `neurax-config-${Date.now()}.json`
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+ URL.revokeObjectURL(url)
+ toast.success('Configuration exported')
+ }}
+ className="w-full gap-2"
+ >
+
+ Export Configuration
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/neurax-frontend/components/feedback/FeedbackSystem.tsx b/neurax-frontend/components/feedback/FeedbackSystem.tsx
new file mode 100644
index 0000000..211bf3f
--- /dev/null
+++ b/neurax-frontend/components/feedback/FeedbackSystem.tsx
@@ -0,0 +1,189 @@
+'use client'
+
+import { useState } from 'react'
+import { MessageSquare, ThumbsUp, ThumbsDown, Star, Send } from 'lucide-react'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Badge } from '@/components/ui/badge'
+import { apiClient } from '@/lib/api/client'
+import toast from 'react-hot-toast'
+
+interface FeedbackSystemProps {
+ queryId?: string
+ responseId?: string
+ onFeedbackSubmitted?: () => void
+}
+
+export function FeedbackSystem({ queryId, responseId, onFeedbackSubmitted }: FeedbackSystemProps) {
+ const [rating, setRating] = useState(0)
+ const [comments, setComments] = useState('')
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [submitted, setSubmitted] = useState(false)
+
+ const handleSubmit = async () => {
+ if (rating === 0) {
+ toast.error('Please provide a rating')
+ return
+ }
+
+ setIsSubmitting(true)
+ try {
+ await apiClient.submitFeedback({
+ queryId: queryId || '',
+ responseId: responseId,
+ rating,
+ comments: comments.trim() || undefined,
+ isHelpful: rating >= 4,
+ metadata: {
+ userAgent: navigator.userAgent,
+ timestamp: new Date().toISOString()
+ }
+ })
+
+ setSubmitted(true)
+ onFeedbackSubmitted?.()
+ toast.success('Thank you for your feedback!')
+ } catch (error) {
+ console.error('Feedback submission error:', error)
+ toast.error('Failed to submit feedback')
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ const resetForm = () => {
+ setRating(0)
+ setComments('')
+ setSubmitted(false)
+ }
+
+ if (submitted) {
+ return (
+
+
+
+
+
Thank you for your feedback!
+
+ Your input helps us improve NeuraX for everyone.
+
+
+ Submit Another Feedback
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ Share Your Feedback
+
+
+
+ {/* Rating */}
+
+
How would you rate this result?
+
+ {[1, 2, 3, 4, 5].map((star) => (
+ setRating(star)}
+ className={`p-1 rounded transition-colors ${
+ star <= rating
+ ? 'text-yellow-500 hover:text-yellow-600'
+ : 'text-muted-foreground hover:text-yellow-400'
+ }`}
+ disabled={isSubmitting}
+ >
+
+
+ ))}
+
+
+ {rating === 0 && 'Click to rate'}
+ {rating === 1 && 'Very poor'}
+ {rating === 2 && 'Poor'}
+ {rating === 3 && 'Average'}
+ {rating === 4 && 'Good'}
+ {rating === 5 && 'Excellent'}
+
+
+
+ {/* Comments */}
+
+ Additional comments (optional)
+
+
+ {/* Quick feedback buttons */}
+
+
Quick feedback
+
+ {
+ setRating(5)
+ setComments('Very helpful result')
+ }}
+ disabled={isSubmitting}
+ className="gap-2"
+ >
+
+ Helpful
+
+ {
+ setRating(2)
+ setComments('Not very helpful')
+ }}
+ disabled={isSubmitting}
+ className="gap-2"
+ >
+
+ Not Helpful
+
+
+
+
+ {/* Submit */}
+
+
+ Reset
+
+
+ {isSubmitting ? (
+
+ ) : (
+
+ )}
+ Submit Feedback
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/neurax-frontend/components/query/QueryHistory.tsx b/neurax-frontend/components/query/QueryHistory.tsx
new file mode 100644
index 0000000..a913bb3
--- /dev/null
+++ b/neurax-frontend/components/query/QueryHistory.tsx
@@ -0,0 +1,458 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import {
+ History,
+ Search,
+ FileText,
+ Image,
+ Music,
+ Clock,
+ Trash2,
+ Eye,
+ Play,
+ RotateCcw,
+ Star,
+ StarOff
+} from 'lucide-react'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Badge } from '@/components/ui/badge'
+import { Input } from '@/components/ui/input'
+import { cn } from '@/lib/utils'
+import { apiClient } from '@/lib/api/client'
+import toast from 'react-hot-toast'
+import type { Query } from '@/types'
+
+interface QueryHistoryProps {
+ onQuerySelect?: (query: Query) => void
+ onSearchFromHistory?: (queryText: string) => void
+}
+
+export function QueryHistory({ onQuerySelect, onSearchFromHistory }: QueryHistoryProps) {
+ const [queries, setQueries] = useState([])
+ const [filteredQueries, setFilteredQueries] = useState([])
+ const [isLoading, setIsLoading] = useState(true)
+ const [searchFilter, setSearchFilter] = useState('')
+ const [typeFilter, setTypeFilter] = useState<'all' | 'text' | 'image' | 'voice' | 'multimodal'>('all')
+ const [sortBy, setSortBy] = useState<'date' | 'relevance' | 'popularity'>('date')
+
+ useEffect(() => {
+ loadQueryHistory()
+ }, [])
+
+ useEffect(() => {
+ filterAndSortQueries()
+ }, [queries, searchFilter, typeFilter, sortBy])
+
+ const loadQueryHistory = async () => {
+ try {
+ setIsLoading(true)
+ const response = await apiClient.getQueryHistory(1, 100)
+ if (response.success && response.data) {
+ // Transform backend data to our Query type
+ const transformedQueries: Query[] = response.data.map((item: any) => ({
+ id: item.id || `query_${Date.now()}`,
+ type: item.type || 'text',
+ text: item.query || item.text,
+ timestamp: item.timestamp || item.created_at,
+ similarityThreshold: item.similarity_threshold || 0.5,
+ status: item.status || 'completed',
+ processingTime: item.processing_time || 0,
+ results: item.results || []
+ }))
+ setQueries(transformedQueries)
+ }
+ } catch (error) {
+ console.error('Failed to load query history:', error)
+ toast.error('Failed to load query history')
+ // Use localStorage as fallback
+ const localHistory = localStorage.getItem('neurax_query_history')
+ if (localHistory) {
+ try {
+ const localQueries = JSON.parse(localHistory)
+ setQueries(localQueries)
+ } catch (e) {
+ console.error('Failed to parse local query history:', e)
+ }
+ }
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const filterAndSortQueries = () => {
+ let filtered = [...queries]
+
+ // Apply search filter
+ if (searchFilter) {
+ filtered = filtered.filter(query =>
+ query.text?.toLowerCase().includes(searchFilter.toLowerCase()) ||
+ query.type.toLowerCase().includes(searchFilter.toLowerCase())
+ )
+ }
+
+ // Apply type filter
+ if (typeFilter !== 'all') {
+ filtered = filtered.filter(query => query.type === typeFilter)
+ }
+
+ // Apply sorting
+ filtered.sort((a, b) => {
+ switch (sortBy) {
+ case 'date':
+ return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
+ case 'relevance':
+ const avgScoreA = a.results?.reduce((sum, r) => sum + r.similarityScore, 0) / (a.results?.length || 1) || 0
+ const avgScoreB = b.results?.reduce((sum, r) => sum + r.similarityScore, 0) / (b.results?.length || 1) || 0
+ return avgScoreB - avgScoreA
+ case 'popularity':
+ // Sort by number of results (more results = more popular)
+ return (b.results?.length || 0) - (a.results?.length || 0)
+ default:
+ return 0
+ }
+ })
+
+ setFilteredQueries(filtered)
+ }
+
+ const clearHistory = async () => {
+ try {
+ // Clear from backend (if API exists)
+ // await apiClient.clearQueryHistory()
+
+ // Clear from localStorage
+ localStorage.removeItem('neurax_query_history')
+ setQueries([])
+ setFilteredQueries([])
+ toast.success('Query history cleared')
+ } catch (error) {
+ console.error('Failed to clear history:', error)
+ toast.error('Failed to clear history')
+ }
+ }
+
+ const deleteQuery = (queryId: string) => {
+ setQueries(prev => prev.filter(q => q.id !== queryId))
+ setFilteredQueries(prev => prev.filter(q => q.id !== queryId))
+
+ // Update localStorage
+ const updatedQueries = queries.filter(q => q.id !== queryId)
+ localStorage.setItem('neurax_query_history', JSON.stringify(updatedQueries))
+
+ toast.success('Query removed from history')
+ }
+
+ const runQueryAgain = (query: Query) => {
+ if (onSearchFromHistory && query.text) {
+ onSearchFromHistory(query.text)
+ } else if (onQuerySelect) {
+ onQuerySelect(query)
+ }
+ }
+
+ const getQueryTypeIcon = (type: string) => {
+ switch (type) {
+ case 'text':
+ return
+ case 'image':
+ return
+ case 'voice':
+ return
+ case 'multimodal':
+ return
+ default:
+ return
+ }
+ }
+
+ const getQueryTypeBadgeVariant = (type: string) => {
+ switch (type) {
+ case 'text':
+ return 'default'
+ case 'image':
+ return 'secondary'
+ case 'voice':
+ return 'destructive'
+ case 'multimodal':
+ return 'outline'
+ default:
+ return 'outline'
+ }
+ }
+
+ if (isLoading) {
+ return (
+
+
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ Query History
+
+
+
+ Clear All
+
+
+
+
+ {/* Filters */}
+
+
+
+ setSearchFilter(e.target.value)}
+ className="w-full"
+ />
+
+
+ setTypeFilter(e.target.value as any)}
+ className="px-3 py-2 border rounded-md text-sm"
+ >
+ All Types
+ Text
+ Image
+ Voice
+ Multimodal
+
+ setSortBy(e.target.value as any)}
+ className="px-3 py-2 border rounded-md text-sm"
+ >
+ Latest First
+ Most Relevant
+ Most Results
+
+
+
+
+ {/* Results Count */}
+
+ Showing {filteredQueries.length} of {queries.length} queries
+
+
+
+
+
+ {/* Query List */}
+ {filteredQueries.length === 0 ? (
+
+
+
+
+
No Query History
+
+ {queries.length === 0
+ ? "You haven't made any queries yet. Start searching to build your history."
+ : "No queries match your current filters."
+ }
+
+ {(searchFilter || typeFilter !== 'all') && (
+
{
+ setSearchFilter('')
+ setTypeFilter('all')
+ }}
+ >
+ Clear Filters
+
+ )}
+
+
+
+ ) : (
+
+ {filteredQueries.map((query) => (
+ runQueryAgain(query)}
+ onDelete={() => deleteQuery(query.id)}
+ onViewDetails={() => onQuerySelect?.(query)}
+ />
+ ))}
+
+ )}
+
+ )
+}
+
+interface QueryHistoryCardProps {
+ query: Query
+ onRunAgain: () => void
+ onDelete: () => void
+ onViewDetails: () => void
+}
+
+function QueryHistoryCard({ query, onRunAgain, onDelete, onViewDetails }: QueryHistoryCardProps) {
+ const [isExpanded, setIsExpanded] = useState(false)
+ const [isFavorite, setIsFavorite] = useState(false)
+
+ const getQueryTypeIcon = (type: string) => {
+ switch (type) {
+ case 'text':
+ return
+ case 'image':
+ return
+ case 'voice':
+ return
+ case 'multimodal':
+ return
+ default:
+ return
+ }
+ }
+
+ const getQueryTypeBadgeVariant = (type: string) => {
+ switch (type) {
+ case 'text':
+ return 'default'
+ case 'image':
+ return 'secondary'
+ case 'voice':
+ return 'destructive'
+ case 'multimodal':
+ return 'outline'
+ default:
+ return 'outline'
+ }
+ }
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+ {getQueryTypeIcon(query.type)}
+
+
+
+
+ {query.type}
+
+
+ {new Date(query.timestamp).toLocaleDateString()}
+
+
+
+ {query.text || `${query.type} query`}
+
+
+
+
+ {query.processingTime?.toFixed(2) || '0.00'}s
+
+ {query.results && (
+ {query.results.length} results
+ )}
+
+
+
+
+
+ setIsFavorite(!isFavorite)}
+ className="h-8 w-8"
+ >
+ {isFavorite ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Query Details (expandable) */}
+ {query.text && query.text.length > 100 && (
+
+
setIsExpanded(!isExpanded)}
+ className="gap-1"
+ >
+
+ {isExpanded ? 'Show Less' : 'Show More'}
+
+ {isExpanded && (
+
+ {query.text}
+
+ )}
+
+ )}
+
+ {/* Actions */}
+
+
+
+
+ Run Again
+
+
+
+ Details
+
+
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/neurax-frontend/components/query/QueryInterface.tsx b/neurax-frontend/components/query/QueryInterface.tsx
new file mode 100644
index 0000000..d159a9f
--- /dev/null
+++ b/neurax-frontend/components/query/QueryInterface.tsx
@@ -0,0 +1,539 @@
+'use client'
+
+import { useState, useCallback, useRef } from 'react'
+import { Search, Mic, Image, Type, X, Loader2, MicOff } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { cn } from '@/lib/utils'
+import { apiClient } from '@/lib/api/client'
+import toast from 'react-hot-toast'
+import type { Query, SearchResult } from '@/types'
+
+interface QueryInterfaceProps {
+ onSearch: (query: Query) => void
+ onResults: (results: SearchResult[]) => void
+ uploadedFiles: any[]
+}
+
+type QueryType = 'text' | 'image' | 'voice' | 'multimodal'
+
+export function QueryInterface({ onSearch, onResults, uploadedFiles }: QueryInterfaceProps) {
+ const [queryType, setQueryType] = useState('text')
+ const [textQuery, setTextQuery] = useState('')
+ const [imageFile, setImageFile] = useState(null)
+ const [audioFile, setAudioFile] = useState(null)
+ const [isProcessing, setIsProcessing] = useState(false)
+ const [similarityThreshold, setSimilarityThreshold] = useState(0.5)
+ const [isRecording, setIsRecording] = useState(false)
+ const [transcription, setTranscription] = useState('')
+ const fileInputRef = useRef(null)
+ const audioInputRef = useRef(null)
+
+ const handleImageUpload = useCallback((event: React.ChangeEvent) => {
+ const file = event.target.files?.[0]
+ if (file) {
+ setImageFile(file)
+ }
+ }, [])
+
+ const handleAudioUpload = useCallback((event: React.ChangeEvent) => {
+ const file = event.target.files?.[0]
+ if (file) {
+ setAudioFile(file)
+ }
+ }, [])
+
+ const removeImage = useCallback(() => {
+ setImageFile(null)
+ if (fileInputRef.current) {
+ fileInputRef.current.value = ''
+ }
+ }, [])
+
+ const removeAudio = useCallback(() => {
+ setAudioFile(null)
+ setTranscription('')
+ if (audioInputRef.current) {
+ audioInputRef.current.value = ''
+ }
+ }, [])
+
+ const handleVoiceRecording = useCallback(() => {
+ if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
+ toast.error('Voice recognition not supported in this browser')
+ return
+ }
+
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
+ const recognition = new SpeechRecognition()
+
+ recognition.continuous = false
+ recognition.interimResults = true
+ recognition.lang = 'en-US'
+
+ recognition.onstart = () => {
+ setIsRecording(true)
+ toast.success('Recording... Speak now')
+ }
+
+ recognition.onresult = (event) => {
+ let finalTranscript = ''
+
+ for (let i = event.resultIndex; i < event.results.length; i++) {
+ if (event.results[i].isFinal) {
+ finalTranscript += event.results[i][0].transcript
+ }
+ }
+
+ if (finalTranscript) {
+ setTextQuery(finalTranscript)
+ setTranscription(finalTranscript)
+ }
+ }
+
+ recognition.onerror = (event) => {
+ setIsRecording(false)
+ toast.error('Voice recognition error: ' + event.error)
+ }
+
+ recognition.onend = () => {
+ setIsRecording(false)
+ }
+
+ recognition.start()
+ }, [])
+
+ const processQuery = useCallback(async () => {
+ if (!textQuery.trim() && !imageFile && !audioFile) {
+ toast.error('Please enter a query or upload files')
+ return
+ }
+
+ setIsProcessing(true)
+
+ try {
+ let response
+
+ switch (queryType) {
+ case 'text':
+ response = await apiClient.processTextQuery(
+ textQuery,
+ similarityThreshold,
+ {
+ includeImages: true,
+ includeDocuments: true,
+ includeAudio: true,
+ maxResults: 10
+ }
+ )
+ break
+
+ case 'image':
+ if (!imageFile) {
+ toast.error('Please upload an image')
+ return
+ }
+ response = await apiClient.processImageQuery(
+ imageFile,
+ similarityThreshold,
+ {
+ maxResults: 10,
+ textQuery: textQuery || undefined
+ }
+ )
+ break
+
+ case 'voice':
+ if (!audioFile) {
+ toast.error('Please upload an audio file')
+ return
+ }
+ response = await apiClient.processVoiceQuery(
+ audioFile,
+ similarityThreshold,
+ {
+ maxResults: 10,
+ language: 'en'
+ }
+ )
+
+ if (response.query?.type === 'voice' && response.query?.text) {
+ setTextQuery(response.query.text)
+ setTranscription(response.query.text)
+ }
+ break
+
+ case 'multimodal':
+ if (!textQuery.trim() && !imageFile) {
+ toast.error('Please enter text or upload an image')
+ return
+ }
+ response = await apiClient.processMultimodalQuery(
+ textQuery,
+ imageFile || undefined,
+ similarityThreshold,
+ { maxResults: 10 }
+ )
+ break
+
+ default:
+ throw new Error('Invalid query type')
+ }
+
+ // Create query object
+ const query: Query = {
+ id: `query_${Date.now()}`,
+ type: queryType,
+ text: textQuery,
+ image: imageFile || undefined,
+ audio: audioFile || undefined,
+ timestamp: new Date().toISOString(),
+ similarityThreshold,
+ results: response.results,
+ processingTime: response.processingTime,
+ status: 'completed'
+ }
+
+ onSearch(query)
+ onResults(response.results)
+ toast.success(`Found ${response.results.length} results in ${response.processingTime.toFixed(2)}s`)
+
+ } catch (error) {
+ console.error('Query processing error:', error)
+ toast.error('Query failed: ' + (error instanceof Error ? error.message : 'Unknown error'))
+ } finally {
+ setIsProcessing(false)
+ }
+ }, [queryType, textQuery, imageFile, audioFile, similarityThreshold, onSearch, onResults])
+
+ const clearQuery = useCallback(() => {
+ setTextQuery('')
+ setImageFile(null)
+ setAudioFile(null)
+ setTranscription('')
+ if (fileInputRef.current) fileInputRef.current.value = ''
+ if (audioInputRef.current) audioInputRef.current.value = ''
+ }, [])
+
+ const renderQueryInput = () => {
+ switch (queryType) {
+ case 'text':
+ return (
+
+
+ Search Query
+ setTextQuery(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && processQuery()}
+ disabled={isProcessing}
+ />
+
+
+ )
+
+ case 'image':
+ return (
+
+
+ Optional Text Query
+ setTextQuery(e.target.value)}
+ disabled={isProcessing}
+ />
+
+
+
Image
+
+ {imageFile ? (
+
+
+
+ {imageFile.name}
+
+
+
+
+
+ ) : (
+
+
+ fileInputRef.current?.click()}
+ disabled={isProcessing}
+ >
+ Choose Image
+
+
+
+ )}
+
+
+
+ )
+
+ case 'voice':
+ return (
+
+
+
Voice Recording
+
+ {audioFile ? (
+
+
+
+
+ {audioFile.name}
+
+
+
+
+
+ {transcription && (
+
+ Transcription: {transcription}
+
+ )}
+
+ ) : (
+
+
+
+ audioInputRef.current?.click()}
+ disabled={isProcessing}
+ >
+ Upload Audio File
+
+
+ {process.env.NEXT_PUBLIC_ENABLE_VOICE_INPUT === 'true' && (
+
+ {isRecording ? : }
+ {isRecording ? 'Recording...' : 'Record Voice'}
+
+ )}
+
+
+ )}
+
+
+
+ )
+
+ case 'multimodal':
+ return (
+
+
+ Text Description
+ setTextQuery(e.target.value)}
+ disabled={isProcessing}
+ />
+
+
+
Reference Image (Optional)
+
+ {imageFile ? (
+
+
+
+ {imageFile.name}
+
+
+
+
+
+ ) : (
+
+
+ fileInputRef.current?.click()}
+ disabled={isProcessing}
+ >
+ Choose Reference Image
+
+
+
+ )}
+
+
+
+ )
+
+ default:
+ return null
+ }
+ }
+
+ return (
+
+
+
+
+ Multimodal Query Interface
+
+
+
+ {/* Query Type Selection */}
+
+ setQueryType('text')}
+ className="gap-2"
+ >
+
+ Text
+
+ setQueryType('image')}
+ className="gap-2"
+ >
+
+ Image
+
+ setQueryType('voice')}
+ className="gap-2"
+ >
+
+ Voice
+
+ setQueryType('multimodal')}
+ className="gap-2"
+ >
+
+ Multimodal
+
+
+
+ {/* Query Input */}
+ {renderQueryInput()}
+
+ {/* Advanced Settings */}
+
+
+
+ Similarity Threshold: {similarityThreshold.toFixed(2)}
+
+
setSimilarityThreshold(parseFloat(e.target.value))}
+ className="w-full"
+ disabled={isProcessing}
+ />
+
+ Lenient
+ Strict
+
+
+
+
+
Status
+
+ {isProcessing ? (
+ <>
+
+
Processing...
+ >
+ ) : (
+ <>
+
+
Ready
+ >
+ )}
+
+
+
+
+ {/* Action Buttons */}
+
+
+ {isProcessing ? (
+
+ ) : (
+
+ )}
+ {isProcessing ? 'Processing...' : 'Search'}
+
+
+
+ Clear
+
+
+
+ {/* Uploaded Files Info */}
+ {uploadedFiles.length > 0 && (
+
+
+ 📁 Searching through {uploadedFiles.length} uploaded files
+
+
+ )}
+
+
+ )
+}
\ No newline at end of file
diff --git a/neurax-frontend/components/results/ResultsDisplay.tsx b/neurax-frontend/components/results/ResultsDisplay.tsx
new file mode 100644
index 0000000..a9b1cce
--- /dev/null
+++ b/neurax-frontend/components/results/ResultsDisplay.tsx
@@ -0,0 +1,400 @@
+'use client'
+
+import { useState, useCallback } from 'react'
+import {
+ FileText,
+ Image,
+ Music,
+ Copy,
+ Download,
+ ExternalLink,
+ Star,
+ StarOff,
+ ThumbsUp,
+ ThumbsDown,
+ MessageSquare,
+ Eye,
+ Clock,
+ Target
+} from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Badge } from '@/components/ui/badge'
+import { cn, formatBytes, getFileType, getFileIcon, formatSimilarityScore, copyToClipboard } from '@/lib/utils'
+import { apiClient } from '@/lib/api/client'
+import toast from 'react-hot-toast'
+import type { Query, SearchResult } from '@/types'
+
+interface ResultsDisplayProps {
+ query: Query | null
+ results: SearchResult[]
+ onFeedback?: (resultId: string, feedback: any) => void
+}
+
+export function ResultsDisplay({ query, results, onFeedback }: ResultsDisplayProps) {
+ const [favorites, setFavorites] = useState>(new Set())
+ const [feedback, setFeedback] = useState>({})
+
+ const toggleFavorite = useCallback((resultId: string) => {
+ setFavorites(prev => {
+ const newSet = new Set(prev)
+ if (newSet.has(resultId)) {
+ newSet.delete(resultId)
+ toast.success('Removed from favorites')
+ } else {
+ newSet.add(resultId)
+ toast.success('Added to favorites')
+ }
+ return newSet
+ })
+ }, [])
+
+ const handleFeedback = useCallback((resultId: string, type: 'positive' | 'negative') => {
+ setFeedback(prev => ({ ...prev, [resultId]: type }))
+ onFeedback?.(resultId, { type, timestamp: new Date().toISOString() })
+
+ toast.success(type === 'positive' ? 'Thanks for your feedback!' : 'Feedback recorded')
+ }, [onFeedback])
+
+ const copyToClipboardHandler = useCallback(async (text: string) => {
+ try {
+ await copyToClipboard(text)
+ toast.success('Copied to clipboard')
+ } catch (error) {
+ toast.error('Failed to copy')
+ }
+ }, [])
+
+ const exportResults = useCallback(() => {
+ const exportData = {
+ query: query?.text,
+ timestamp: new Date().toISOString(),
+ results: results.map(result => ({
+ fileName: result.fileName,
+ fileType: result.fileType,
+ similarityScore: result.similarityScore,
+ contentPreview: result.contentPreview,
+ metadata: result.metadata
+ }))
+ }
+
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `neurax-results-${Date.now()}.json`
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+ URL.revokeObjectURL(url)
+ toast.success('Results exported')
+ }, [query, results])
+
+ if (!results || results.length === 0) {
+ return (
+
+
+
+
+
No Results Found
+
+ Try adjusting your query or similarity threshold to find more results.
+
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Results Header */}
+
+
+
+
+
+
+ Search Results
+
+
+ {query?.text && `Query: "${query.text}"`}
+ {query?.type && ` • Type: ${query.type}`}
+ {query?.processingTime && ` • Time: ${query.processingTime.toFixed(2)}s`}
+
+
+
+
+ {results.length} result{results.length !== 1 ? 's' : ''}
+
+
+
+ Export
+
+
+
+
+
+
+ {/* Results List */}
+
+ {results.map((result, index) => (
+ toggleFavorite(result.id)}
+ onFeedback={(type) => handleFeedback(result.id, type)}
+ onCopy={() => copyToClipboardHandler(result.contentPreview)}
+ />
+ ))}
+
+
+ {/* Export Options */}
+
+
+
+
Export Options
+
+ {
+ // Export as CSV
+ const csvContent = [
+ ['File Name', 'File Type', 'Similarity Score', 'Content Preview'],
+ ...results.map(r => [r.fileName, r.fileType, r.similarityScore.toString(), r.contentPreview])
+ ].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n')
+
+ const blob = new Blob([csvContent], { type: 'text/csv' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `neurax-results-${Date.now()}.csv`
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+ URL.revokeObjectURL(url)
+ toast.success('Results exported as CSV')
+ }}
+ >
+ Export as CSV
+
+ {
+ // Export as PDF would require a library, for now export as JSON
+ exportResults()
+ }}
+ >
+ Export as JSON
+
+
+
+
+
+
+ )
+}
+
+interface ResultCardProps {
+ result: SearchResult
+ index: number
+ isFavorite: boolean
+ feedback: 'positive' | 'negative' | null
+ onToggleFavorite: () => void
+ onFeedback: (type: 'positive' | 'negative') => void
+ onCopy: () => void
+}
+
+function ResultCard({
+ result,
+ index,
+ isFavorite,
+ feedback,
+ onToggleFavorite,
+ onFeedback,
+ onCopy
+}: ResultCardProps) {
+ const [isExpanded, setIsExpanded] = useState(false)
+ const similarityInfo = formatSimilarityScore(result.similarityScore)
+ const fileTypeInfo = getFileType(result.fileName)
+
+ const getFileTypeIcon = () => {
+ switch (fileTypeInfo) {
+ case 'document':
+ return
+ case 'image':
+ return
+ case 'audio':
+ return
+ default:
+ return
+ }
+ }
+
+ const getSimilarityBadgeVariant = () => {
+ if (result.similarityScore >= 0.8) return 'default' // Green
+ if (result.similarityScore >= 0.6) return 'secondary' // Yellow
+ return 'destructive' // Red
+ }
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+ {getFileTypeIcon()}
+
+
+
+
{result.fileName}
+
+ {result.similarityScore.toFixed(3)}
+
+
+
+
+ {getFileIcon(result.fileName)}
+ {fileTypeInfo}
+
+ {result.pageNumber && (
+ Page {result.pageNumber}
+ )}
+
+
+ {new Date(result.timestamp).toLocaleDateString()}
+
+
+
+
+
+
+
+ {isFavorite ? : }
+
+
+
+
+ {/* Content Preview */}
+
+
+ Content Preview
+ setIsExpanded(!isExpanded)}
+ className="gap-1"
+ >
+
+ {isExpanded ? 'Show Less' : 'Show More'}
+
+
+
+ {result.contentPreview}
+ {!isExpanded && result.contentPreview.length > 200 && (
+
+ setIsExpanded(true)}
+ className="text-xs"
+ >
+ Read more...
+
+
+ )}
+
+
+
+ {/* Metadata */}
+ {result.metadata && Object.keys(result.metadata).length > 0 && (
+
+
Metadata
+
+ {Object.entries(result.metadata).map(([key, value]) => (
+
+ {key}:
+ {String(value)}
+
+ ))}
+
+
+ )}
+
+ {/* Actions */}
+
+
+
+
+
+ onFeedback('negative')}
+ className={cn("gap-1", feedback === 'negative' && "text-red-600")}
+ disabled={feedback !== null}
+ >
+
+
+
+
+ Copy
+
+
+
+
+ {result.confidence && (
+
+ Confidence: {(result.confidence * 100).toFixed(1)}%
+
+ )}
+ {
+ // Open file preview (would need file viewer implementation)
+ toast.info('File preview feature coming soon')
+ }}
+ >
+
+ Preview
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/neurax-frontend/components/ui/badge.tsx b/neurax-frontend/components/ui/badge.tsx
new file mode 100644
index 0000000..12daad7
--- /dev/null
+++ b/neurax-frontend/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/neurax-frontend/components/ui/button.tsx b/neurax-frontend/components/ui/button.tsx
new file mode 100644
index 0000000..a84e714
--- /dev/null
+++ b/neurax-frontend/components/ui/button.tsx
@@ -0,0 +1,64 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center 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",
+ {
+ 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) => {
+ if (asChild) {
+ return React.cloneElement(
+ props.children as React.ReactElement,
+ {
+ className: cn(buttonVariants({ variant, size, className })),
+ ref,
+ ...props
+ }
+ )
+ }
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
\ No newline at end of file
diff --git a/neurax-frontend/components/ui/card.tsx b/neurax-frontend/components/ui/card.tsx
new file mode 100644
index 0000000..938aa22
--- /dev/null
+++ b/neurax-frontend/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<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ 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 }
\ No newline at end of file
diff --git a/neurax-frontend/components/ui/input.tsx b/neurax-frontend/components/ui/input.tsx
new file mode 100644
index 0000000..522915b
--- /dev/null
+++ b/neurax-frontend/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
\ No newline at end of file
diff --git a/neurax-frontend/components/ui/progress.tsx b/neurax-frontend/components/ui/progress.tsx
new file mode 100644
index 0000000..e5ae975
--- /dev/null
+++ b/neurax-frontend/components/ui/progress.tsx
@@ -0,0 +1,26 @@
+import * as React from "react"
+import * as ProgressPrimitive from "@radix-ui/react-progress"
+
+import { cn } from "@/lib/utils"
+
+const Progress = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, value, ...props }, ref) => (
+
+
+
+))
+Progress.displayName = ProgressPrimitive.Root.displayName
+
+export { Progress }
\ No newline at end of file
diff --git a/neurax-frontend/components/upload/FileUploader.tsx b/neurax-frontend/components/upload/FileUploader.tsx
new file mode 100644
index 0000000..a0166e1
--- /dev/null
+++ b/neurax-frontend/components/upload/FileUploader.tsx
@@ -0,0 +1,286 @@
+'use client'
+
+import { useState, useCallback } from 'react'
+import { useDropzone } from 'react-dropzone'
+import { Upload, X, File, CheckCircle, AlertCircle, Loader2 } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Progress } from '@/components/ui/progress'
+import { cn, formatBytes, getFileType, getFileIcon } from '@/lib/utils'
+import { apiClient } from '@/lib/api/client'
+import toast from 'react-hot-toast'
+import type { FileUpload } from '@/types'
+
+interface FileUploaderProps {
+ onFileUpload: (files: FileUpload[]) => void
+ uploadedFiles: FileUpload[]
+}
+
+export function FileUploader({ onFileUpload, uploadedFiles }: FileUploaderProps) {
+ const [isUploading, setIsUploading] = useState(false)
+ const [uploadProgress, setUploadProgress] = useState(0)
+ const [dragActive, setDragActive] = useState(false)
+
+ const allowedTypes = process.env.NEXT_PUBLIC_ALLOWED_FILE_TYPES?.split(',') || [
+ '.pdf', '.docx', '.doc', '.txt', '.jpg', '.png', '.mp3', '.wav', '.m4a', '.flac', '.ogg', '.bmp', '.tiff', '.webp'
+ ]
+
+ const maxFileSize = parseInt(process.env.NEXT_PUBLIC_MAX_FILE_SIZE || '104857600') // 100MB
+
+ const onDrop = useCallback(async (acceptedFiles: File[]) => {
+ setIsUploading(true)
+ setUploadProgress(0)
+
+ try {
+ // Validate files
+ const validFiles = acceptedFiles.filter(file => {
+ const fileType = getFileType(file.name)
+ if (fileType === 'unknown') {
+ toast.error(`Unsupported file type: ${file.name}`)
+ return false
+ }
+ if (file.size > maxFileSize) {
+ toast.error(`File too large: ${file.name} (${formatBytes(file.size)})`)
+ return false
+ }
+ return true
+ })
+
+ if (validFiles.length === 0) {
+ toast.error('No valid files to upload')
+ setIsUploading(false)
+ return
+ }
+
+ // Simulate progress
+ const progressInterval = setInterval(() => {
+ setUploadProgress(prev => Math.min(prev + 10, 90))
+ }, 200)
+
+ try {
+ const uploadResponse = await apiClient.uploadFiles(validFiles)
+ setUploadProgress(100)
+
+ if (uploadResponse.success) {
+ onFileUpload(uploadResponse.files)
+ toast.success(`Successfully uploaded ${uploadResponse.totalFiles} files`)
+ } else {
+ toast.error('Upload failed')
+ }
+ } catch (error) {
+ console.error('Upload error:', error)
+ toast.error('Upload failed: ' + (error instanceof Error ? error.message : 'Unknown error'))
+ } finally {
+ clearInterval(progressInterval)
+ setTimeout(() => {
+ setIsUploading(false)
+ setUploadProgress(0)
+ }, 1000)
+ }
+ } catch (error) {
+ console.error('Drop error:', error)
+ setIsUploading(false)
+ setUploadProgress(0)
+ }
+ }, [maxFileSize, onFileUpload])
+
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
+ onDrop,
+ accept: allowedTypes.reduce((acc, type) => {
+ acc[type] = []
+ return acc
+ }, {} as Record),
+ maxSize: maxFileSize,
+ multiple: true,
+ disabled: isUploading
+ })
+
+ const removeFile = async (fileId: string) => {
+ try {
+ await apiClient.deleteFile(fileId)
+ toast.success('File removed')
+ } catch (error) {
+ toast.error('Failed to remove file')
+ }
+ }
+
+ return (
+
+ {/* Upload Area */}
+
+
+
+
+ File Upload & Processing
+
+
+
+
+
+
+ {isUploading ? (
+
+
+
+
Uploading files...
+
+
{uploadProgress}% complete
+
+
+ ) : (
+
+
+
+
+ {isDragActive ? "Drop files here" : "Upload files"}
+
+
+ Drag and drop files here, or click to browse
+
+
+
Supported formats: PDF, DOCX, DOC, TXT, Images, Audio
+
Maximum file size: {formatBytes(maxFileSize)}
+
+
+
+ )}
+
+
+
+
+ {/* Uploaded Files List */}
+ {uploadedFiles.length > 0 && (
+
+
+ Uploaded Files ({uploadedFiles.length})
+
+
+
+ {uploadedFiles.map((file) => (
+
+ ))}
+
+
+
+ )}
+
+ {/* Upload Guidelines */}
+
+
+
+
Upload Guidelines
+
+
+
📄 Documents
+
+ • PDF, DOCX, DOC, TXT
+ • Text extraction and indexing
+ • OCR for scanned documents
+
+
+
+
🖼️ Images
+
+ • JPG, PNG, BMP, TIFF, WEBP
+ • OCR text extraction
+ • Visual similarity search
+
+
+
+
🎵 Audio
+
+ • WAV, MP3, M4A, FLAC, OGG
+ • Speech-to-text transcription
+ • Audio content search
+
+
+
+
+
+
+
+ )
+}
+
+interface FileCardProps {
+ file: FileUpload
+ onRemove: (fileId: string) => void
+}
+
+function FileCard({ file, onRemove }: FileCardProps) {
+ const getStatusIcon = () => {
+ switch (file.status) {
+ case 'completed':
+ return
+ case 'error':
+ return
+ case 'processing':
+ return
+ default:
+ return
+ }
+ }
+
+ const getStatusColor = () => {
+ switch (file.status) {
+ case 'completed':
+ return 'text-green-600 dark:text-green-400'
+ case 'error':
+ return 'text-red-600 dark:text-red-400'
+ case 'processing':
+ return 'text-blue-600 dark:text-blue-400'
+ default:
+ return 'text-muted-foreground'
+ }
+ }
+
+ return (
+
+
+
+ {getStatusIcon()}
+
+
+
+
{getFileIcon(file.fileName)}
+
+
{file.fileName}
+
+ {formatBytes(file.fileSize)}
+ •
+ {file.status}
+ {file.status === 'processing' && (
+ <>
+ •
+ {file.progress}%
+ >
+ )}
+
+
+
+ {file.error && (
+
{file.error}
+ )}
+
+
+
+
onRemove(file.id)}
+ className="h-8 w-8 text-muted-foreground hover:text-foreground"
+ >
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/neurax-frontend/lib/api/client.ts b/neurax-frontend/lib/api/client.ts
new file mode 100644
index 0000000..553a4dd
--- /dev/null
+++ b/neurax-frontend/lib/api/client.ts
@@ -0,0 +1,401 @@
+import axios, { AxiosInstance, AxiosResponse } from 'axios';
+import {
+ ApiResponse,
+ PaginatedResponse,
+ UploadResponse,
+ QueryResponse,
+ ResponseGenerationResponse,
+ SystemConfig,
+ Analytics,
+ SecurityEvent,
+ Feedback,
+ ProcessingStatus,
+} from '@/types';
+
+// API Client Configuration
+class ApiClient {
+ private client: AxiosInstance;
+
+ constructor() {
+ this.client = axios.create({
+ baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
+ timeout: 120000, // 2 minutes for file processing
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ this.setupInterceptors();
+ }
+
+ private setupInterceptors() {
+ // Request interceptor
+ this.client.interceptors.request.use(
+ (config) => {
+ // Add auth token if available
+ const token = localStorage.getItem('auth_token');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => Promise.reject(error)
+ );
+
+ // Response interceptor
+ this.client.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ if (error.response?.status === 401) {
+ // Handle unauthorized
+ window.location.href = '/login';
+ }
+ return Promise.reject(error);
+ }
+ );
+ }
+
+ // Generic API methods
+ private async handleRequest(promise: Promise>): Promise {
+ try {
+ const response = await promise;
+ return response.data;
+ } catch (error: any) {
+ const message = error.response?.data?.message || error.message || 'API request failed';
+ throw new Error(message);
+ }
+ }
+
+ // File Upload API
+ async uploadFiles(files: File[]): Promise {
+ const formData = new FormData();
+ files.forEach((file) => {
+ formData.append('files', file);
+ });
+
+ const response = await this.client.post('/api/upload', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ onUploadProgress: (progressEvent) => {
+ if (progressEvent.total) {
+ const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
+ // Emit progress event for UI updates
+ window.dispatchEvent(new CustomEvent('upload-progress', { detail: progress }));
+ }
+ },
+ });
+
+ return response.data;
+ }
+
+ async getUploadedFiles(page = 1, limit = 20): Promise> {
+ return this.handleRequest(
+ this.client.get(`/api/files?page=${page}&limit=${limit}`)
+ );
+ }
+
+ async deleteFile(fileId: string): Promise {
+ return this.handleRequest(
+ this.client.delete(`/api/files/${fileId}`)
+ );
+ }
+
+ async getFileStatus(fileId: string): Promise {
+ return this.handleRequest(
+ this.client.get(`/api/processing-status/${fileId}`)
+ );
+ }
+
+ // Query API
+ async processTextQuery(
+ text: string,
+ similarityThreshold = 0.5,
+ options?: {
+ includeImages?: boolean;
+ includeDocuments?: boolean;
+ includeAudio?: boolean;
+ maxResults?: number;
+ }
+ ): Promise {
+ return this.handleRequest(
+ this.client.post('/api/query/text', {
+ text,
+ similarity_threshold: similarityThreshold,
+ options: {
+ include_images: options?.includeImages ?? true,
+ include_documents: options?.includeDocuments ?? true,
+ include_audio: options?.includeAudio ?? true,
+ max_results: options?.maxResults ?? 10,
+ },
+ })
+ );
+ }
+
+ async processImageQuery(
+ imageFile: File,
+ similarityThreshold = 0.5,
+ options?: {
+ maxResults?: number;
+ textQuery?: string;
+ }
+ ): Promise {
+ const formData = new FormData();
+ formData.append('image', imageFile);
+ formData.append('similarity_threshold', similarityThreshold.toString());
+
+ if (options?.textQuery) {
+ formData.append('text_query', options.textQuery);
+ }
+ if (options?.maxResults) {
+ formData.append('max_results', options.maxResults.toString());
+ }
+
+ return this.handleRequest(
+ this.client.post('/api/query/image', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ })
+ );
+ }
+
+ async processVoiceQuery(
+ audioFile: File,
+ similarityThreshold = 0.5,
+ options?: {
+ maxResults?: number;
+ language?: string;
+ }
+ ): Promise {
+ const formData = new FormData();
+ formData.append('audio', audioFile);
+ formData.append('similarity_threshold', similarityThreshold.toString());
+
+ if (options?.maxResults) {
+ formData.append('max_results', options.maxResults.toString());
+ }
+ if (options?.language) {
+ formData.append('language', options.language);
+ }
+
+ return this.handleRequest(
+ this.client.post('/api/query/voice', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ })
+ );
+ }
+
+ async processMultimodalQuery(
+ text: string,
+ imageFile?: File,
+ similarityThreshold = 0.5,
+ options?: {
+ maxResults?: number;
+ }
+ ): Promise {
+ const formData = new FormData();
+ formData.append('text', text);
+ formData.append('similarity_threshold', similarityThreshold.toString());
+
+ if (imageFile) {
+ formData.append('image', imageFile);
+ }
+ if (options?.maxResults) {
+ formData.append('max_results', options.maxResults.toString());
+ }
+
+ return this.handleRequest(
+ this.client.post('/api/query/multimodal', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ })
+ );
+ }
+
+ async getQueryHistory(page = 1, limit = 20): Promise> {
+ return this.handleRequest(
+ this.client.get(`/api/query/history?page=${page}&limit=${limit}`)
+ );
+ }
+
+ async getQuerySuggestions(query: string): Promise> {
+ return this.handleRequest(
+ this.client.get(`/api/query/suggestions?q=${encodeURIComponent(query)}`)
+ );
+ }
+
+ // Response Generation API
+ async generateResponse(request: {
+ query: string;
+ context: any[];
+ model?: 'gemma-3n' | 'qwen3-4b';
+ maxTokens?: number;
+ temperature?: number;
+ includeCitations?: boolean;
+ }): Promise {
+ return this.handleRequest(
+ this.client.post('/api/generate-response', {
+ query: request.query,
+ context: request.context,
+ model: request.model || 'gemma-3n',
+ max_tokens: request.maxTokens || 1024,
+ temperature: request.temperature || 0.7,
+ include_citations: request.includeCitations ?? true,
+ })
+ );
+ }
+
+ // Analytics API
+ async getAnalytics(timeRange?: {
+ start?: string;
+ end?: string;
+ }): Promise {
+ const params = new URLSearchParams();
+ if (timeRange?.start) params.append('start', timeRange.start);
+ if (timeRange?.end) params.append('end', timeRange.end);
+
+ return this.handleRequest(
+ this.client.get(`/api/analytics?${params.toString()}`)
+ );
+ }
+
+ async getSystemMetrics(): Promise {
+ return this.handleRequest(
+ this.client.get('/api/analytics/metrics')
+ );
+ }
+
+ async getUsageStatistics(timeRange?: {
+ start?: string;
+ end?: string;
+ }): Promise {
+ const params = new URLSearchParams();
+ if (timeRange?.start) params.append('start', timeRange.start);
+ if (timeRange?.end) params.append('end', timeRange.end);
+
+ return this.handleRequest(
+ this.client.get(`/api/analytics/usage?${params.toString()}`)
+ );
+ }
+
+ // Security API
+ async getSecurityEvents(page = 1, limit = 20): Promise> {
+ return this.handleRequest(
+ this.client.get(`/api/security/events?page=${page}&limit=${limit}`)
+ );
+ }
+
+ async getAnomalyDetection(): Promise {
+ return this.handleRequest(
+ this.client.get('/api/security/anomalies')
+ );
+ }
+
+ async getAuditLogs(page = 1, limit = 20): Promise> {
+ return this.handleRequest(
+ this.client.get(`/api/audit/logs?page=${page}&limit=${limit}`)
+ );
+ }
+
+ // Feedback API
+ async submitFeedback(feedback: {
+ queryId: string;
+ responseId?: string;
+ rating: number;
+ comments?: string;
+ isHelpful?: boolean;
+ metadata?: Record;
+ }): Promise {
+ return this.handleRequest(
+ this.client.post('/api/feedback', feedback)
+ );
+ }
+
+ async getFeedbackHistory(page = 1, limit = 20): Promise> {
+ return this.handleRequest(
+ this.client.get(`/api/feedback/history?page=${page}&limit=${limit}`)
+ );
+ }
+
+ async getFeedbackAnalytics(): Promise {
+ return this.handleRequest(
+ this.client.get('/api/feedback/analytics')
+ );
+ }
+
+ // Configuration API
+ async getSystemConfig(): Promise {
+ return this.handleRequest(
+ this.client.get('/api/config')
+ );
+ }
+
+ async updateSystemConfig(config: Partial): Promise {
+ return this.handleRequest(
+ this.client.put('/api/config', config)
+ );
+ }
+
+ async validateConfig(config: Partial): Promise {
+ return this.handleRequest(
+ this.client.post('/api/config/validate', config)
+ );
+ }
+
+ // Knowledge Graph API
+ async getKnowledgeGraph(): Promise {
+ return this.handleRequest(
+ this.client.get('/api/knowledge-graph')
+ );
+ }
+
+ // Export API
+ async exportResults(format: 'json' | 'csv' | 'pdf', data: any): Promise {
+ const response = await this.client.post('/api/export', {
+ format,
+ data,
+ }, {
+ responseType: 'blob',
+ });
+
+ return response.data;
+ }
+
+ async exportAnalytics(format: 'json' | 'csv' | 'pdf', timeRange?: {
+ start?: string;
+ end?: string;
+ }): Promise {
+ const params = new URLSearchParams();
+ params.append('format', format);
+ if (timeRange?.start) params.append('start', timeRange.start);
+ if (timeRange?.end) params.append('end', timeRange.end);
+
+ const response = await this.client.get(`/api/export/analytics?${params.toString()}`, {
+ responseType: 'blob',
+ });
+
+ return response.data;
+ }
+
+ // Health Check
+ async healthCheck(): Promise {
+ return this.handleRequest(
+ this.client.get('/health')
+ );
+ }
+
+ // System Status
+ async getSystemStatus(): Promise {
+ return this.handleRequest(
+ this.client.get('/api/status')
+ );
+ }
+}
+
+// Create singleton instance
+export const apiClient = new ApiClient();
+export default apiClient;
\ No newline at end of file
diff --git a/neurax-frontend/lib/types/index.ts b/neurax-frontend/lib/types/index.ts
new file mode 100644
index 0000000..49f2c6b
--- /dev/null
+++ b/neurax-frontend/lib/types/index.ts
@@ -0,0 +1,351 @@
+// Core types for NeuraX frontend
+
+export interface User {
+ id: string;
+ name: string;
+ email: string;
+ avatar?: string;
+ role: 'admin' | 'user' | 'viewer';
+ createdAt: string;
+ lastActive: string;
+}
+
+export interface FileUpload {
+ id: string;
+ fileName: string;
+ filePath: string;
+ fileType: 'document' | 'image' | 'audio' | 'unknown';
+ fileSize: number;
+ mimeType: string;
+ status: 'uploading' | 'processing' | 'completed' | 'error';
+ progress: number;
+ error?: string;
+ metadata?: Record;
+ uploadedAt: string;
+ processedAt?: string;
+}
+
+export interface SearchResult {
+ id: string;
+ filePath: string;
+ fileName: string;
+ fileType: 'document' | 'image' | 'audio' | 'unknown';
+ similarityScore: number;
+ confidence: number;
+ contentPreview: string;
+ metadata: Record;
+ pageNumber?: number;
+ timestamp: string;
+ highlights?: string[];
+ thumbnailUrl?: string;
+ textEmbedding?: number[];
+ imageEmbedding?: number[];
+}
+
+export interface Query {
+ id: string;
+ type: 'text' | 'image' | 'voice' | 'multimodal';
+ text?: string;
+ image?: File;
+ audio?: File;
+ timestamp: string;
+ similarityThreshold: number;
+ results?: SearchResult[];
+ processingTime?: number;
+ status: 'pending' | 'processing' | 'completed' | 'error';
+ error?: string;
+}
+
+export interface AIResponse {
+ id: string;
+ queryId: string;
+ responseText: string;
+ confidence: number;
+ citations: Citation[];
+ processingTime: number;
+ modelUsed: 'gemma-3n' | 'qwen3-4b' | 'unknown';
+ timestamp: string;
+ feedback?: Feedback;
+}
+
+export interface Citation {
+ id: string;
+ citationId: string;
+ filePath: string;
+ fileName: string;
+ sourceType: 'document' | 'image' | 'audio';
+ pageNumber?: number;
+ contentSnippet: string;
+ confidenceScore: number;
+ timestamp: string;
+ url?: string;
+ position?: {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ };
+}
+
+export interface Feedback {
+ id: string;
+ queryId: string;
+ responseId: string;
+ rating: number; // 1-5
+ comments?: string;
+ isHelpful: boolean;
+ timestamp: string;
+ metadata?: Record;
+}
+
+export interface Analytics {
+ queryStats: {
+ totalQueries: number;
+ textQueries: number;
+ imageQueries: number;
+ voiceQueries: number;
+ multimodalQueries: number;
+ avgProcessingTime: number;
+ successRate: number;
+ };
+ fileStats: {
+ totalFiles: number;
+ documentFiles: number;
+ imageFiles: number;
+ audioFiles: number;
+ totalSize: number;
+ avgProcessingTime: number;
+ };
+ systemStats: {
+ uptime: number;
+ memoryUsage: number;
+ cpuUsage: number;
+ diskUsage: number;
+ };
+ usageTrends: {
+ date: string;
+ queries: number;
+ uploads: number;
+ }[];
+ popularQueries: {
+ query: string;
+ count: number;
+ avgRating: number;
+ }[];
+}
+
+export interface SecurityEvent {
+ id: string;
+ type: 'anomaly' | 'audit' | 'alert' | 'error';
+ severity: 'low' | 'medium' | 'high' | 'critical';
+ title: string;
+ description: string;
+ source: string;
+ timestamp: string;
+ metadata?: Record;
+ resolved: boolean;
+ resolvedAt?: string;
+ resolvedBy?: string;
+}
+
+export interface SystemConfig {
+ apiUrl: string;
+ wsUrl: string;
+ lmStudioUrl: string;
+ maxFileSize: number;
+ allowedFileTypes: string[];
+ enableAnalytics: boolean;
+ enableDarkMode: boolean;
+ defaultSimilarityThreshold: number;
+ maxQueryHistory: number;
+ enableVoiceInput: boolean;
+ models: {
+ primary: 'gemma-3n' | 'qwen3-4b';
+ fallback: 'gemma-3n' | 'qwen3-4b';
+ };
+ performance: {
+ batchSize: number;
+ maxConcurrency: number;
+ cacheEnabled: boolean;
+ cacheTimeout: number;
+ };
+ security: {
+ auditLogging: boolean;
+ anomalyDetection: boolean;
+ rateLimiting: boolean;
+ maxUploadsPerHour: number;
+ };
+}
+
+export interface KnowledgeGraph {
+ nodes: KnowledgeNode[];
+ edges: KnowledgeEdge[];
+ metadata: {
+ totalNodes: number;
+ totalEdges: number;
+ createdAt: string;
+ lastUpdated: string;
+ };
+}
+
+export interface KnowledgeNode {
+ id: string;
+ label: string;
+ type: 'document' | 'concept' | 'entity' | 'topic';
+ properties: Record;
+ position?: {
+ x: number;
+ y: number;
+ z: number;
+ };
+}
+
+export interface KnowledgeEdge {
+ id: string;
+ source: string;
+ target: string;
+ type: 'similarity' | 'related' | 'contained_in' | 'mentions';
+ weight: number;
+ properties: Record;
+}
+
+export interface ProcessingStatus {
+ jobId: string;
+ type: 'file_upload' | 'query' | 'indexing';
+ status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
+ progress: number; // 0-100
+ message: string;
+ result?: any;
+ error?: string;
+ startedAt: string;
+ completedAt?: string;
+ metadata?: Record;
+}
+
+// UI-specific types
+export interface ThemeConfig {
+ mode: 'light' | 'dark' | 'system';
+ primary: string;
+ secondary: string;
+ accent: string;
+ background: string;
+ foreground: string;
+}
+
+export interface Notification {
+ id: string;
+ type: 'success' | 'error' | 'warning' | 'info';
+ title: string;
+ message: string;
+ timestamp: string;
+ read: boolean;
+ action?: {
+ label: string;
+ handler: () => void;
+ };
+ duration?: number; // Auto-dismiss duration in ms
+}
+
+// Form types
+export interface QueryFormData {
+ type: 'text' | 'image' | 'voice' | 'multimodal';
+ text?: string;
+ image?: File | null;
+ audio?: File | null;
+ similarityThreshold: number;
+ includeImages: boolean;
+ includeDocuments: boolean;
+ includeAudio: boolean;
+ maxResults: number;
+}
+
+export interface FeedbackFormData {
+ queryId: string;
+ responseId: string;
+ rating: number;
+ comments?: string;
+ tags?: string[];
+ isPublic: boolean;
+}
+
+export interface SettingsFormData {
+ general: {
+ defaultSimilarityThreshold: number;
+ maxResults: number;
+ enableNotifications: boolean;
+ enableSounds: boolean;
+ language: string;
+ };
+ models: {
+ primaryModel: 'gemma-3n' | 'qwen3-4b';
+ fallbackModel: 'gemma-3n' | 'qwen3-4b';
+ maxTokens: number;
+ temperature: number;
+ };
+ privacy: {
+ enableAnalytics: boolean;
+ enableUsageTracking: boolean;
+ dataRetentionDays: number;
+ anonymizeLogs: boolean;
+ };
+}
+
+// API Response types
+export interface ApiResponse {
+ success: boolean;
+ data?: T;
+ error?: string;
+ message?: string;
+ timestamp: string;
+ requestId: string;
+}
+
+export interface PaginatedResponse extends ApiResponse {
+ pagination: {
+ page: number;
+ limit: number;
+ total: number;
+ totalPages: number;
+ hasNext: boolean;
+ hasPrev: boolean;
+ };
+}
+
+export interface UploadResponse {
+ success: boolean;
+ files: FileUpload[];
+ totalFiles: number;
+ totalSize: number;
+ errors?: {
+ fileName: string;
+ error: string;
+ }[];
+}
+
+export interface QueryResponse {
+ query: Query;
+ results: SearchResult[];
+ totalResults: number;
+ processingTime: number;
+ similarQueries?: string[];
+ suggestions?: string[];
+}
+
+export interface ResponseGenerationRequest {
+ query: string;
+ context: SearchResult[];
+ model?: 'gemma-3n' | 'qwen3-4b';
+ maxTokens?: number;
+ temperature?: number;
+ includeCitations: boolean;
+}
+
+export interface ResponseGenerationResponse extends ApiResponse {
+ alternatives?: AIResponse[];
+ usedContext: SearchResult[];
+ modelInfo: {
+ name: string;
+ parameters: Record;
+ capabilities: string[];
+ };
+}
\ No newline at end of file
diff --git a/neurax-frontend/lib/utils/index.ts b/neurax-frontend/lib/utils/index.ts
new file mode 100644
index 0000000..28ada92
--- /dev/null
+++ b/neurax-frontend/lib/utils/index.ts
@@ -0,0 +1,232 @@
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
+
+export function formatBytes(bytes: number, decimals = 2): string {
+ if (bytes === 0) return '0 Bytes'
+
+ const k = 1024
+ const dm = decimals < 0 ? 0 : decimals
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
+
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
+}
+
+export function formatDuration(seconds: number): string {
+ if (seconds < 60) {
+ return `${Math.round(seconds)}s`
+ } else if (seconds < 3600) {
+ const minutes = Math.floor(seconds / 60)
+ const remainingSeconds = Math.round(seconds % 60)
+ return `${minutes}m ${remainingSeconds}s`
+ } else {
+ const hours = Math.floor(seconds / 3600)
+ const minutes = Math.floor((seconds % 3600) / 60)
+ const remainingSeconds = Math.round(seconds % 60)
+ return `${hours}h ${minutes}m ${remainingSeconds}s`
+ }
+}
+
+export function formatSimilarityScore(score: number): { label: string; color: string } {
+ if (score >= 0.8) {
+ return { label: 'Excellent', color: 'text-green-600 dark:text-green-400' }
+ } else if (score >= 0.6) {
+ return { label: 'Good', color: 'text-yellow-600 dark:text-yellow-400' }
+ } else {
+ return { label: 'Fair', color: 'text-red-600 dark:text-red-400' }
+ }
+}
+
+export function formatFileType(fileType: string): string {
+ const typeMap: Record = {
+ 'document': '📄 Document',
+ 'image': '🖼️ Image',
+ 'audio': '🎵 Audio',
+ 'unknown': '❓ Unknown'
+ }
+ return typeMap[fileType] || '❓ Unknown'
+}
+
+export function getFileIcon(fileName: string): string {
+ const extension = fileName.split('.').pop()?.toLowerCase()
+
+ const iconMap: Record = {
+ // Documents
+ 'pdf': '📄',
+ 'doc': '📝',
+ 'docx': '📝',
+ 'txt': '📄',
+ 'rtf': '📄',
+ 'odt': '📄',
+
+ // Images
+ 'jpg': '🖼️',
+ 'jpeg': '🖼️',
+ 'png': '🖼️',
+ 'gif': '🖼️',
+ 'bmp': '🖼️',
+ 'tiff': '🖼️',
+ 'webp': '🖼️',
+ 'svg': '🎨',
+
+ // Audio
+ 'mp3': '🎵',
+ 'wav': '🎵',
+ 'm4a': '🎵',
+ 'flac': '🎵',
+ 'ogg': '🎵',
+ 'aac': '🎵',
+
+ // Archives
+ 'zip': '📦',
+ 'rar': '📦',
+ '7z': '📦',
+ 'tar': '📦',
+ 'gz': '📦'
+ }
+
+ return iconMap[extension || ''] || '📄'
+}
+
+export function debounce any>(
+ func: T,
+ wait: number
+): (...args: Parameters) => void {
+ let timeout: NodeJS.Timeout | null = null
+
+ return (...args: Parameters) => {
+ if (timeout) {
+ clearTimeout(timeout)
+ }
+
+ timeout = setTimeout(() => {
+ func(...args)
+ }, wait)
+ }
+}
+
+export function throttle any>(
+ func: T,
+ limit: number
+): (...args: Parameters) => void {
+ let inThrottle: boolean
+
+ return (...args: Parameters) => {
+ if (!inThrottle) {
+ func(...args)
+ inThrottle = true
+ setTimeout(() => (inThrottle = false), limit)
+ }
+ }
+}
+
+export function generateId(): string {
+ return Math.random().toString(36).substr(2, 9)
+}
+
+export function truncateText(text: string, maxLength: number): string {
+ if (text.length <= maxLength) return text
+ return text.slice(0, maxLength) + '...'
+}
+
+export function highlightSearchTerms(text: string, searchTerm: string): string {
+ if (!searchTerm) return text
+
+ const regex = new RegExp(`(${searchTerm})`, 'gi')
+ return text.replace(regex, '$1 ')
+}
+
+export function validateEmail(email: string): boolean {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+ return emailRegex.test(email)
+}
+
+export function validateFileType(fileName: string, allowedTypes: string[]): boolean {
+ const extension = fileName.split('.').pop()?.toLowerCase()
+ return extension ? allowedTypes.includes(`.${extension}`) : false
+}
+
+export function validateFileSize(fileSize: number, maxSize: number): boolean {
+ return fileSize <= maxSize
+}
+
+export function isImageFile(fileName: string): boolean {
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp', 'svg']
+ const extension = fileName.split('.').pop()?.toLowerCase()
+ return extension ? imageExtensions.includes(extension) : false
+}
+
+export function isAudioFile(fileName: string): boolean {
+ const audioExtensions = ['mp3', 'wav', 'm4a', 'flac', 'ogg', 'aac']
+ const extension = fileName.split('.').pop()?.toLowerCase()
+ return extension ? audioExtensions.includes(extension) : false
+}
+
+export function isDocumentFile(fileName: string): boolean {
+ const documentExtensions = ['pdf', 'doc', 'docx', 'txt', 'rtf', 'odt']
+ const extension = fileName.split('.').pop()?.toLowerCase()
+ return extension ? documentExtensions.includes(extension) : false
+}
+
+export function getFileType(fileName: string): 'document' | 'image' | 'audio' | 'unknown' {
+ if (isImageFile(fileName)) return 'image'
+ if (isAudioFile(fileName)) return 'audio'
+ if (isDocumentFile(fileName)) return 'document'
+ return 'unknown'
+}
+
+export function downloadBlob(blob: Blob, filename: string): void {
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = filename
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+ URL.revokeObjectURL(url)
+}
+
+export function copyToClipboard(text: string): Promise {
+ if (navigator.clipboard && window.isSecureContext) {
+ return navigator.clipboard.writeText(text)
+ } else {
+ // Fallback for older browsers
+ const textArea = document.createElement('textarea')
+ textArea.value = text
+ textArea.style.position = 'absolute'
+ textArea.style.left = '-999999px'
+
+ document.body.prepend(textArea)
+ textArea.select()
+
+ try {
+ document.execCommand('copy')
+ } finally {
+ textArea.remove()
+ }
+
+ return Promise.resolve()
+ }
+}
+
+export function shareContent(data: {
+ title?: string;
+ text?: string;
+ url?: string;
+}): Promise {
+ if (navigator.share) {
+ return navigator.share(data)
+ } else {
+ // Fallback to clipboard
+ const shareText = [data.title, data.text, data.url]
+ .filter(Boolean)
+ .join('\n')
+
+ return copyToClipboard(shareText)
+ }
+}
\ No newline at end of file
diff --git a/neurax-frontend/next.config.js b/neurax-frontend/next.config.js
new file mode 100644
index 0000000..3776f02
--- /dev/null
+++ b/neurax-frontend/next.config.js
@@ -0,0 +1,33 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ experimental: {
+ appDir: true,
+ },
+ images: {
+ domains: ['localhost'],
+ unoptimized: true,
+ },
+ env: {
+ CUSTOM_KEY: process.env.CUSTOM_KEY,
+ },
+ webpack: (config, { isServer }) => {
+ if (!isServer) {
+ config.resolve.fallback = {
+ ...config.resolve.fallback,
+ fs: false,
+ path: false,
+ };
+ }
+ return config;
+ },
+ async rewrites() {
+ return [
+ {
+ source: '/api/backend/:path*',
+ destination: 'http://localhost:8000/:path*',
+ },
+ ];
+ },
+};
+
+module.exports = nextConfig;
\ No newline at end of file
diff --git a/neurax-frontend/package-lock.json b/neurax-frontend/package-lock.json
new file mode 100644
index 0000000..806c03b
--- /dev/null
+++ b/neurax-frontend/package-lock.json
@@ -0,0 +1,37 @@
+# Dependencies for Next.js Frontend
+# Production dependencies
+next@14.1.0
+react@^18.2.0
+react-dom@^18.2.0
+typescript@^5.3.3
+@types/node@^20.11.5
+@types/react@^18.2.48
+@types/react-dom@^18.2.18
+tailwindcss@^3.4.1
+autoprefixer@^10.4.17
+postcss@^8.4.33
+@tailwindcss/typography@^0.5.10
+class-variance-authority@^0.7.0
+clsx@^2.1.0
+tailwind-merge@^2.2.1
+lucide-react@^0.323.0
+react-dropzone@^14.2.3
+zustand@^4.5.0
+react-hook-form@^7.49.3
+@hookform/resolvers@^3.3.4
+zod@^3.22.4
+axios@^1.6.7
+recharts@^2.12.0
+@tanstack/react-query@^5.17.19
+socket.io-client@^4.7.4
+date-fns@^3.3.1
+react-hot-toast@^2.4.1
+framer-motion@^11.0.3
+react-speech-recognition@^3.10.0
+react-pdf@^7.7.1
+react-player@^2.14.1
+
+# Development dependencies
+eslint@^8.56.0
+eslint-config-next@14.1.0
+@types/react-speech-recognition@^3.9.5
\ No newline at end of file
diff --git a/neurax-frontend/package.json b/neurax-frontend/package.json
new file mode 100644
index 0000000..876f051
--- /dev/null
+++ b/neurax-frontend/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "neurax-frontend",
+ "version": "1.0.0",
+ "description": "Production-ready Next.js frontend for NeuraX RAG system",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint",
+ "type-check": "tsc --noEmit"
+ },
+ "dependencies": {
+ "next": "14.1.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "typescript": "^5.3.3",
+ "@types/node": "^20.11.5",
+ "@types/react": "^18.2.48",
+ "@types/react-dom": "^18.2.18",
+ "tailwindcss": "^3.4.1",
+ "autoprefixer": "^10.4.17",
+ "postcss": "^8.4.33",
+ "@tailwindcss/typography": "^0.5.10",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.0",
+ "tailwind-merge": "^2.2.1",
+ "lucide-react": "^0.323.0",
+ "react-dropzone": "^14.2.3",
+ "zustand": "^4.5.0",
+ "react-hook-form": "^7.49.3",
+ "@hookform/resolvers": "^3.3.4",
+ "zod": "^3.22.4",
+ "axios": "^1.6.7",
+ "recharts": "^2.12.0",
+ "react-query": "^3.39.3",
+ "@tanstack/react-query": "^5.17.19",
+ "socket.io-client": "^4.7.4",
+ "date-fns": "^3.3.1",
+ "react-hot-toast": "^2.4.1",
+ "framer-motion": "^11.0.3",
+ "react-speech-recognition": "^3.10.0",
+ "react-pdf": "^7.7.1",
+ "react-player": "^2.14.1"
+ },
+ "devDependencies": {
+ "eslint": "^8.56.0",
+ "eslint-config-next": "14.1.0",
+ "@types/react-speech-recognition": "^3.9.5"
+ }
+}
\ No newline at end of file
diff --git a/neurax-frontend/postcss.config.js b/neurax-frontend/postcss.config.js
new file mode 100644
index 0000000..96bb01e
--- /dev/null
+++ b/neurax-frontend/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
\ No newline at end of file
diff --git a/neurax-frontend/styles/globals.css b/neurax-frontend/styles/globals.css
new file mode 100644
index 0000000..5fe2157
--- /dev/null
+++ b/neurax-frontend/styles/globals.css
@@ -0,0 +1,233 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary: 221.2 83.2% 53.3%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96%;
+ --secondary-foreground: 222.2 84% 4.9%;
+ --muted: 210 40% 96%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96%;
+ --accent-foreground: 222.2 84% 4.9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 221.2 83.2% 53.3%;
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary: 217.2 91.2% 59.8%;
+ --primary-foreground: 222.2 84% 4.9%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 224.3 76.3% 94.1%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
+
+/* Custom scrollbar */
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ @apply bg-secondary;
+}
+
+::-webkit-scrollbar-thumb {
+ @apply bg-muted-foreground/30 rounded-full;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ @apply bg-muted-foreground/50;
+}
+
+/* Animations */
+@keyframes fade-in {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes slide-in-right {
+ from {
+ opacity: 0;
+ transform: translateX(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.animate-fade-in {
+ animation: fade-in 0.5s ease-out;
+}
+
+.animate-slide-in-right {
+ animation: slide-in-right 0.3s ease-out;
+}
+
+.animate-pulse {
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}
+
+.animate-spin {
+ animation: spin 1s linear infinite;
+}
+
+/* File upload styles */
+.file-upload-area {
+ @apply border-2 border-dashed border-border rounded-lg p-8 text-center transition-colors hover:border-primary/50;
+}
+
+.file-upload-area.drag-over {
+ @apply border-primary bg-primary/5;
+}
+
+/* Search result highlighting */
+.search-highlight {
+ @apply bg-yellow-200 dark:bg-yellow-800 px-1 rounded;
+}
+
+/* Citation styles */
+.citation-link {
+ @apply text-primary hover:text-primary/80 underline decoration-dotted underline-offset-4 cursor-pointer;
+}
+
+/* Loading skeleton */
+.skeleton {
+ @apply animate-pulse bg-muted rounded;
+}
+
+/* Status indicators */
+.status-pending {
+ @apply bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400;
+}
+
+.status-processing {
+ @apply bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400;
+}
+
+.status-completed {
+ @apply bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400;
+}
+
+.status-error {
+ @apply bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400;
+}
+
+/* Similarity score colors */
+.similarity-excellent {
+ @apply bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400;
+}
+
+.similarity-good {
+ @apply bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400;
+}
+
+.similarity-fair {
+ @apply bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400;
+}
+
+/* Modal and overlay styles */
+.modal-overlay {
+ @apply fixed inset-0 bg-black/50 backdrop-blur-sm z-50;
+}
+
+.modal-content {
+ @apply bg-background rounded-lg shadow-lg max-h-[90vh] overflow-hidden;
+}
+
+/* Toast styles */
+.toast-success {
+ @apply bg-green-50 border-green-200 text-green-800 dark:bg-green-900/30 dark:border-green-800 dark:text-green-400;
+}
+
+.toast-error {
+ @apply bg-red-50 border-red-200 text-red-800 dark:bg-red-900/30 dark:border-red-800 dark:text-red-400;
+}
+
+.toast-warning {
+ @apply bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/30 dark:border-yellow-800 dark:text-yellow-400;
+}
+
+.toast-info {
+ @apply bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-900/30 dark:border-blue-800 dark:text-blue-400;
+}
+
+/* Responsive design utilities */
+@media (max-width: 640px) {
+ .mobile-hidden {
+ @apply hidden;
+ }
+
+ .mobile-full {
+ @apply w-full;
+ }
+}
+
+/* Print styles */
+@media print {
+ .no-print {
+ display: none !important;
+ }
+
+ .print-break {
+ page-break-before: always;
+ }
+}
\ No newline at end of file
diff --git a/neurax-frontend/tailwind.config.js b/neurax-frontend/tailwind.config.js
new file mode 100644
index 0000000..ef3ba72
--- /dev/null
+++ b/neurax-frontend/tailwind.config.js
@@ -0,0 +1,76 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ darkMode: ["class"],
+ content: [
+ './pages/**/*.{ts,tsx}',
+ './components/**/*.{ts,tsx}',
+ './app/**/*.{ts,tsx}',
+ './src/**/*.{ts,tsx}',
+ ],
+ theme: {
+ container: {
+ center: true,
+ padding: "2rem",
+ screens: {
+ "2xl": "1400px",
+ },
+ },
+ extend: {
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
+ },
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ keyframes: {
+ "accordion-down": {
+ from: { height: 0 },
+ to: { height: "var(--radix-accordion-content-height)" },
+ },
+ "accordion-up": {
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: 0 },
+ },
+ },
+ animation: {
+ "accordion-down": "accordion-down 0.2s ease-out",
+ "accordion-up": "accordion-up 0.2s ease-out",
+ },
+ },
+ },
+ plugins: [require("@tailwindcss/typography")],
+}
\ No newline at end of file
diff --git a/neurax-frontend/tsconfig.json b/neurax-frontend/tsconfig.json
new file mode 100644
index 0000000..04d05dc
--- /dev/null
+++ b/neurax-frontend/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "es6"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./*"],
+ "@/components/*": ["./components/*"],
+ "@/lib/*": ["./lib/*"],
+ "@/types/*": ["./types/*"],
+ "@/utils/*": ["./utils/*"],
+ "@/hooks/*": ["./hooks/*"],
+ "@/store/*": ["./store/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
\ No newline at end of file
diff --git a/setup_neurax.sh b/setup_neurax.sh
new file mode 100755
index 0000000..1d114de
--- /dev/null
+++ b/setup_neurax.sh
@@ -0,0 +1,404 @@
+#!/bin/bash
+# NeuraX Frontend Setup Script
+# This script sets up the complete NeuraX RAG system with both frontend and backend
+
+set -e
+
+echo "🚀 Setting up NeuraX RAG System..."
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Function to print colored output
+print_status() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+print_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+print_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+print_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# Check if we're in the right directory
+if [ ! -f "main_launcher.py" ]; then
+ print_error "Please run this script from the NeuraX project root directory"
+ exit 1
+fi
+
+# Create directory structure
+print_status "Creating directory structure..."
+mkdir -p neurax-frontend
+mkdir -p logs
+mkdir -p data
+mkdir -p vector_db
+mkdir -p models
+
+# Setup Backend Dependencies
+print_status "Setting up Python backend dependencies..."
+pip3 install -r requirements.txt
+pip3 install -r backend/requirements-api.txt
+
+# Setup Frontend
+print_status "Setting up Next.js frontend..."
+cd neurax-frontend
+
+# Install Node.js dependencies
+if ! command -v npm &> /dev/null; then
+ print_error "Node.js and npm are required but not installed."
+ print_error "Please install Node.js 18+ from https://nodejs.org/"
+ exit 1
+fi
+
+print_status "Installing Node.js dependencies..."
+npm install
+
+# Setup environment variables
+print_status "Setting up environment configuration..."
+if [ ! -f ".env.local" ]; then
+ cp .env.example .env.local 2>/dev/null || true
+ print_warning "Created .env.local from template. Please configure it with your settings."
+fi
+
+# Build the frontend
+print_status "Building frontend for production..."
+npm run build
+
+cd ..
+
+# Setup LM Studio (optional)
+print_status "Checking LM Studio integration..."
+if [ ! -f "lm_studio_config.json" ]; then
+ print_warning "LM Studio configuration not found."
+ print_warning "Please ensure LM Studio is running on localhost:1234 for full functionality."
+ print_warning "You can download LM Studio from https://lmstudio.ai/"
+fi
+
+# Create startup scripts
+print_status "Creating startup scripts..."
+
+# Frontend startup script
+cat > start_frontend.sh << 'EOF'
+#!/bin/bash
+echo "🚀 Starting NeuraX Frontend..."
+cd neurax-frontend
+npm run dev
+EOF
+
+# Backend startup script
+cat > start_backend.sh << 'EOF'
+#!/bin/bash
+echo "🚀 Starting NeuraX Backend API..."
+cd backend
+python api_server.py
+EOF
+
+# Full system startup script
+cat > start_system.sh << 'EOF'
+#!/bin/bash
+echo "🚀 Starting Complete NeuraX RAG System..."
+
+# Function to cleanup background processes on exit
+cleanup() {
+ echo "Shutting down NeuraX system..."
+ kill $(jobs -p) 2>/dev/null || true
+ exit
+}
+
+trap cleanup SIGINT SIGTERM
+
+# Start backend in background
+echo "Starting backend API server..."
+./start_backend.sh &
+BACKEND_PID=$!
+
+# Wait a moment for backend to start
+sleep 3
+
+# Start frontend in background
+echo "Starting frontend application..."
+./start_frontend.sh &
+FRONTEND_PID=$!
+
+echo ""
+echo "✅ NeuraX system is starting up..."
+echo "📱 Frontend: http://localhost:3000"
+echo "🔧 Backend API: http://localhost:8000"
+echo "📚 API Docs: http://localhost:8000/api/docs"
+echo ""
+echo "Press Ctrl+C to stop all services"
+
+# Wait for background processes
+wait
+EOF
+
+# Make scripts executable
+chmod +x start_frontend.sh
+chmod +x start_backend.sh
+chmod +x start_system.sh
+
+# Create a comprehensive README for the complete system
+print_status "Creating system documentation..."
+cat > NEURAX_SETUP.md << 'EOF'
+# NeuraX RAG System - Complete Setup Guide
+
+## System Overview
+
+The NeuraX RAG (Retrieval-Augmented Generation) system consists of:
+
+1. **Python Backend** - Core RAG functionality with multimodal processing
+2. **FastAPI Wrapper** - REST API for frontend integration
+3. **Next.js Frontend** - Modern web interface
+
+## Quick Start
+
+### Option 1: Start Everything at Once
+```bash
+./start_system.sh
+```
+
+### Option 2: Start Components Separately
+
+**Terminal 1 - Backend:**
+```bash
+./start_backend.sh
+```
+
+**Terminal 2 - Frontend:**
+```bash
+./start_frontend.sh
+```
+
+## Access Points
+
+- **Frontend Application**: http://localhost:3000
+- **Backend API**: http://localhost:8000
+- **API Documentation**: http://localhost:8000/api/docs
+- **System Status**: http://localhost:8000/api/status
+
+## Features
+
+### 📁 File Upload
+- Drag-and-drop interface
+- Support for PDF, DOCX, DOC, TXT, Images, Audio
+- Real-time processing status
+- Batch upload capabilities
+
+### 🔍 Multimodal Search
+- **Text Queries**: Natural language search
+- **Image Search**: Visual similarity search
+- **Voice Search**: Speech-to-text queries
+- **Multimodal**: Combine text and image search
+
+### 📊 Analytics Dashboard
+- System performance metrics
+- Query statistics
+- Usage trends
+- Security monitoring
+
+### ⚙️ Configuration
+- Model selection (Gemma 3n, Qwen3 4b)
+- Performance tuning
+- Security settings
+- System preferences
+
+## Backend API Endpoints
+
+### File Operations
+- `POST /api/upload` - Upload files
+- `GET /api/files` - List uploaded files
+- `DELETE /api/files/{id}` - Delete file
+
+### Query Operations
+- `POST /api/query/text` - Text search
+- `POST /api/query/image` - Image search
+- `POST /api/query/voice` - Voice search
+- `POST /api/query/multimodal` - Combined search
+
+### Response Generation
+- `POST /api/generate-response` - AI response with citations
+
+### Analytics & Monitoring
+- `GET /api/analytics` - Analytics data
+- `GET /api/security/events` - Security events
+- `GET /api/audit/logs` - Audit logs
+
+### Configuration
+- `GET /api/config` - Get system config
+- `PUT /api/config` - Update configuration
+- `POST /api/config/validate` - Validate config
+
+## System Requirements
+
+### Minimum Requirements
+- Python 3.8+
+- Node.js 18+
+- 8GB RAM
+- 20GB disk space
+
+### Recommended Requirements
+- Python 3.10+
+- Node.js 20+
+- 16GB RAM
+- 50GB disk space
+- GPU (optional, for faster processing)
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Port Already in Use**
+ ```bash
+ # Find and kill processes using ports
+ lsof -ti:3000 | xargs kill -9 # Frontend
+ lsof -ti:8000 | xargs kill -9 # Backend
+ ```
+
+2. **Module Not Found Errors**
+ ```bash
+ # Reinstall dependencies
+ pip install -r requirements.txt
+ npm install
+ ```
+
+3. **Frontend Build Errors**
+ ```bash
+ # Clear cache and rebuild
+ cd neurax-frontend
+ rm -rf .next node_modules
+ npm install
+ npm run build
+ ```
+
+4. **Backend Import Errors**
+ ```bash
+ # Check Python path
+ export PYTHONPATH="${PYTHONPATH}:$(pwd)"
+ ```
+
+### Logs and Debugging
+
+- **Frontend Logs**: Browser developer console
+- **Backend Logs**: Check terminal output or log files
+- **API Logs**: Check FastAPI server logs
+- **System Logs**: Check logs/ directory
+
+## Configuration
+
+### Environment Variables (.env.local)
+
+```env
+# API Configuration
+NEXT_PUBLIC_API_URL=http://localhost:8000
+NEXT_PUBLIC_WS_URL=ws://localhost:8000
+NEXT_PUBLIC_LM_STUDIO_URL=http://localhost:1234
+
+# File Upload Settings
+NEXT_PUBLIC_MAX_FILE_SIZE=104857600
+NEXT_PUBLIC_ALLOWED_FILE_TYPES=.pdf,.docx,.doc,.txt,.jpg,.png,.mp3,.wav,.m4a,.flac,.ogg,.bmp,.tiff,.webp
+
+# Feature Flags
+NEXT_PUBLIC_ENABLE_ANALYTICS=true
+NEXT_PUBLIC_ENABLE_DARK_MODE=true
+NEXT_PUBLIC_ENABLE_VOICE_INPUT=true
+
+# Query Settings
+NEXT_PUBLIC_DEFAULT_SIMILARITY_THRESHOLD=0.5
+NEXT_PUBLIC_MAX_QUERY_HISTORY=50
+```
+
+### Backend Configuration (config.py)
+
+Key configuration sections:
+- **LM_STUDIO_CONFIG**: LM Studio connection settings
+- **CHROMA_CONFIG**: Vector database settings
+- **SECURITY_CONFIG**: Security and file upload limits
+- **FEEDBACK_CONFIG**: Feedback system settings
+
+## Development
+
+### Frontend Development
+```bash
+cd neurax-frontend
+npm run dev
+```
+
+### Backend Development
+```bash
+cd backend
+python api_server.py
+```
+
+### Adding New Features
+
+1. **Frontend**: Add components in `neurax-frontend/components/`
+2. **Backend**: Add endpoints in `backend/api_server.py`
+3. **Types**: Update type definitions in `neurax-frontend/lib/types/`
+
+## Production Deployment
+
+### Static Export
+```bash
+cd neurax-frontend
+npm run build
+# Static files will be in .next/
+```
+
+### Docker Deployment
+```bash
+# Build and run with Docker
+docker build -t neurax-frontend neurax-frontend/
+docker run -p 3000:3000 neurax-frontend
+```
+
+### Security Considerations
+
+1. **Environment Variables**: Never commit sensitive data
+2. **File Uploads**: Validate file types and sizes
+3. **API Endpoints**: Implement rate limiting
+4. **Network**: Use HTTPS in production
+5. **Authentication**: Add auth if needed
+
+## Support
+
+- **Issues**: GitHub Issues
+- **Documentation**: This README and inline comments
+- **API Docs**: http://localhost:8000/api/docs
+
+## License
+
+MIT License - see LICENSE file for details.
+EOF
+
+# Final setup completion
+print_success "✅ NeuraX RAG System setup completed!"
+echo ""
+echo "🚀 To start the system:"
+echo " ./start_system.sh"
+echo ""
+echo "📱 Access the application:"
+echo " Frontend: http://localhost:3000"
+echo " Backend: http://localhost:8000"
+echo " API Docs: http://localhost:8000/api/docs"
+echo ""
+echo "📚 Read NEURAX_SETUP.md for detailed documentation"
+echo ""
+echo "🎯 Key Features:"
+echo " • Multimodal file upload and processing"
+echo " • Text, image, voice, and multimodal search"
+echo " • AI response generation with citations"
+echo " • Real-time analytics dashboard"
+echo " • Query history and management"
+echo " • Comprehensive system settings"
+echo ""
+print_success "Happy exploring with NeuraX! 🎉"
\ No newline at end of file