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 */} +
+
+ + + + + +
+
+ + {/* 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. +

+ +
+
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+

Analytics Dashboard

+

System performance and usage analytics

+
+
+ + +
+
+ + {/* 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 ( +
+
+ {/* Logo and Title */} +
+ +

NeuraX

+ RAG System +
+ + {/* Navigation */} + + + {/* Right side controls */} +
+ {/* System Status Indicator */} +
+
+ Online +
+ + {/* Theme Toggle */} + + + {/* Settings */} + +
+
+
+ ) +} \ 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 + )} + + +
+
+
+
+ +
+ {/* General Settings */} + + + + + General Settings + + + +
+ +
+ updateConfig('defaultSimilarityThreshold', parseFloat(e.target.value))} + className="w-full" + /> +
+ Lenient (0.0) + {(config.defaultSimilarityThreshold || 0.5).toFixed(2)} + Strict (1.0) +
+
+
+ +
+ + updateConfig('maxQueryHistory', parseInt(e.target.value))} + min="10" + max="1000" + /> +

+ Number of queries to keep in history +

+
+ +
+ + updateConfig('maxFileSize', parseInt(e.target.value) * 1024 * 1024)} + min="1" + max="1000" + /> +

+ Maximum file size for uploads +

+
+ +
+ +
+ + + +
+
+
+
+ + {/* Model Configuration */} + + + + + Model Configuration + + + +
+ + +

+ Main model for most queries +

+
+ +
+ + +

+ Backup model if primary fails +

+
+ +
+ + updateConfig('lmStudioUrl', e.target.value)} + placeholder="http://localhost:1234" + /> +

+ URL where LM Studio is running +

+
+ +
+ +
+
+ + updateConfig('performance.batchSize', parseInt(e.target.value))} + min="1" + max="50" + /> +
+
+ + updateConfig('performance.maxConcurrency', parseInt(e.target.value))} + min="1" + max="16" + /> +
+ +
+
+
+
+ + {/* Security Settings */} + + + + + Security & Privacy + + + +
+ +
+ + + +
+
+ +
+ + updateConfig('security.maxUploadsPerHour', parseInt(e.target.value))} + min="10" + max="1000" + /> +

+ Rate limit for file uploads +

+
+ +
+ +
+
+ {(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'}
+
+
+ +
+ +
+
+
+
+
+ ) +} \ 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. +

+ +
+
+
+ ) + } + + return ( + + + + + Share Your Feedback + + + + {/* Rating */} +
+ +
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+

+ {rating === 0 && 'Click to rate'} + {rating === 1 && 'Very poor'} + {rating === 2 && 'Poor'} + {rating === 3 && 'Average'} + {rating === 4 && 'Good'} + {rating === 5 && 'Excellent'} +

+
+ + {/* Comments */} +
+ +