gitignore issues

This commit is contained in:
2025-09-18 07:08:07 +02:00
parent f088ed5bc9
commit 994fcdc4bf
7 changed files with 922 additions and 127 deletions

107
.gitignore vendored
View File

@@ -1,108 +1,3 @@
*.backup
backend/storage/rag_documents/*
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
venv/
env/
ENV/
env.bak/
venv.bak/
.venv/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.npm
.eslintcache
.next/
.nuxt/
out/
dist/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Docker
*.log
docker-compose.override.yml
# Database
*.db
*.sqlite
# Redis
dump.rdb
# Logs
logs/
*.log
# Coverage
coverage/
.coverage
.nyc_output
# Cache
.cache/
.pytest_cache/
.mypy_cache/
.ruff_cache/
# Temporary files
*.tmp
*.temp
.tmp/
# Security - sensitive files
backend/.config_encryption_key
*.key
*.pem
*.crt
# Generated files
backend/performance_report.json
performance_report*.json
backend/storage/

View File

@@ -1,28 +1,11 @@
import { NextRequest, NextResponse } from 'next/server'
import { proxyRequest, handleProxyResponse } from '@/lib/proxy-auth'
import { apiClient } from '@/lib/api-client'
export async function GET() {
try {
// Direct fetch instead of proxyRequest (proxyRequest had caching issues)
const baseUrl = process.env.INTERNAL_API_URL || `http://enclava-backend:${process.env.BACKEND_INTERNAL_PORT || '8000'}`
const url = `${baseUrl}/api/modules/`
const adminToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsImlzX3N1cGVydXNlciI6dHJ1ZSwicm9sZSI6InN1cGVyX2FkbWluIiwiZXhwIjoxNzg0Nzk2NDI2LjA0NDYxOX0.YOTlUY8nowkaLAXy5EKfnZEpbDgGCabru5R0jdq_DOQ'
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${adminToken}`,
'Content-Type': 'application/json'
},
// Disable caching to ensure fresh data
cache: 'no-store'
})
if (!response.ok) {
throw new Error(`Backend responded with ${response.status}: ${response.statusText}`)
}
const data = await response.json()
// Use the authenticated API client which handles JWT tokens automatically
const data = await apiClient.get('/modules/')
return NextResponse.json(data)
} catch (error) {
return NextResponse.json(

View File

@@ -0,0 +1,174 @@
/**
* Utility functions for error handling and user feedback
*/
export interface AppError {
code: string
message: string
details?: string
retryable?: boolean
}
export const ERROR_CODES = {
NETWORK_ERROR: 'NETWORK_ERROR',
UNAUTHORIZED: 'UNAUTHORIZED',
VALIDATION_ERROR: 'VALIDATION_ERROR',
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
SERVER_ERROR: 'SERVER_ERROR',
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
} as const
/**
* Converts various error types into standardized AppError format
*/
export function normalizeError(error: unknown): AppError {
if (error instanceof Error) {
// Network or fetch errors
if (error.name === 'TypeError' && error.message.includes('fetch')) {
return {
code: ERROR_CODES.NETWORK_ERROR,
message: 'Unable to connect to server. Please check your internet connection.',
retryable: true
}
}
// Timeout errors
if (error.name === 'AbortError' || error.message.includes('timeout')) {
return {
code: ERROR_CODES.TIMEOUT_ERROR,
message: 'Request timed out. Please try again.',
retryable: true
}
}
return {
code: ERROR_CODES.UNKNOWN_ERROR,
message: error.message || 'An unexpected error occurred',
details: error.stack,
retryable: false
}
}
if (typeof error === 'string') {
return {
code: ERROR_CODES.UNKNOWN_ERROR,
message: error,
retryable: false
}
}
return {
code: ERROR_CODES.UNKNOWN_ERROR,
message: 'An unknown error occurred',
retryable: false
}
}
/**
* Handles HTTP response errors
*/
export async function handleHttpError(response: Response): Promise<AppError> {
let errorDetails: string
try {
const errorData = await response.json()
errorDetails = errorData.error || errorData.message || 'Unknown error'
} catch {
try {
// Use the cloned response for text reading since original body was consumed
const responseClone = response.clone()
errorDetails = await responseClone.text()
} catch {
errorDetails = `HTTP ${response.status} error`
}
}
switch (response.status) {
case 401:
return {
code: ERROR_CODES.UNAUTHORIZED,
message: 'You need to log in to continue',
details: errorDetails,
retryable: false
}
case 400:
return {
code: ERROR_CODES.VALIDATION_ERROR,
message: 'Invalid request. Please check your input.',
details: errorDetails,
retryable: false
}
case 429:
return {
code: ERROR_CODES.SERVER_ERROR,
message: 'Too many requests. Please wait a moment and try again.',
details: errorDetails,
retryable: true
}
case 500:
case 502:
case 503:
case 504:
return {
code: ERROR_CODES.SERVER_ERROR,
message: 'Server error. Please try again in a moment.',
details: errorDetails,
retryable: true
}
default:
return {
code: ERROR_CODES.SERVER_ERROR,
message: `Request failed (${response.status}): ${errorDetails}`,
details: errorDetails,
retryable: response.status >= 500
}
}
}
/**
* Retry wrapper with exponential backoff
*/
export async function withRetry<T>(
fn: () => Promise<T>,
options: {
maxAttempts?: number
initialDelay?: number
maxDelay?: number
backoffMultiplier?: number
} = {}
): Promise<T> {
const {
maxAttempts = 3,
initialDelay = 1000,
maxDelay = 10000,
backoffMultiplier = 2
} = options
let lastError: unknown
let delay = initialDelay
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn()
} catch (error) {
lastError = error
const appError = normalizeError(error)
// Don't retry non-retryable errors
if (!appError.retryable || attempt === maxAttempts) {
throw error
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delay))
delay = Math.min(delay * backoffMultiplier, maxDelay)
}
}
throw lastError
}

View File

@@ -0,0 +1,301 @@
/**
* Performance monitoring and optimization utilities
*/
import React from 'react'
export interface PerformanceMetric {
name: string
value: number
timestamp: number
metadata?: Record<string, any>
}
export interface PerformanceReport {
metrics: PerformanceMetric[]
summary: {
averageResponseTime: number
totalRequests: number
errorRate: number
slowestRequests: PerformanceMetric[]
}
}
class PerformanceMonitor {
private metrics: PerformanceMetric[] = []
private maxMetrics = 1000 // Keep last 1000 metrics
private enabled = process.env.NODE_ENV === 'development'
/**
* Start timing an operation
*/
startTiming(name: string, metadata?: Record<string, any>): () => void {
if (!this.enabled) {
return () => {} // No-op in production
}
const startTime = performance.now()
return () => {
const duration = performance.now() - startTime
this.recordMetric(name, duration, metadata)
}
}
/**
* Record a performance metric
*/
recordMetric(name: string, value: number, metadata?: Record<string, any>): void {
if (!this.enabled) return
const metric: PerformanceMetric = {
name,
value,
timestamp: Date.now(),
metadata
}
this.metrics.push(metric)
// Keep only the most recent metrics
if (this.metrics.length > this.maxMetrics) {
this.metrics = this.metrics.slice(-this.maxMetrics)
}
// Log slow operations
if (value > 1000) { // Slower than 1 second
}
}
/**
* Measure and track API calls
*/
async trackApiCall<T>(
name: string,
apiCall: () => Promise<T>,
metadata?: Record<string, any>
): Promise<T> {
const endTiming = this.startTiming(`api_${name}`, metadata)
try {
const result = await apiCall()
endTiming()
return result
} catch (error) {
endTiming()
this.recordMetric(`api_${name}_error`, 1, {
...metadata,
error: error instanceof Error ? error.message : 'Unknown error'
})
throw error
}
}
/**
* Track React component render times
*/
trackComponentRender(componentName: string, renderCount: number = 1): void {
this.recordMetric(`render_${componentName}`, renderCount)
}
/**
* Get performance report
*/
getReport(): PerformanceReport {
const apiMetrics = this.metrics.filter(m => m.name.startsWith('api_'))
const errorMetrics = this.metrics.filter(m => m.name.includes('_error'))
const totalRequests = apiMetrics.length
const errorRate = totalRequests > 0 ? (errorMetrics.length / totalRequests) * 100 : 0
const responseTimes = apiMetrics.map(m => m.value)
const averageResponseTime = responseTimes.length > 0
? responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length
: 0
const slowestRequests = [...apiMetrics]
.sort((a, b) => b.value - a.value)
.slice(0, 10)
return {
metrics: this.metrics,
summary: {
averageResponseTime,
totalRequests,
errorRate,
slowestRequests
}
}
}
/**
* Clear all metrics
*/
clear(): void {
this.metrics = []
}
/**
* Enable/disable monitoring
*/
setEnabled(enabled: boolean): void {
this.enabled = enabled
}
/**
* Export metrics for analysis
*/
exportMetrics(): string {
return JSON.stringify({
timestamp: Date.now(),
userAgent: navigator.userAgent,
metrics: this.metrics,
summary: this.getReport().summary
}, null, 2)
}
}
// Global performance monitor instance
export const performanceMonitor = new PerformanceMonitor()
/**
* React hook for component performance tracking
*/
export function usePerformanceTracking(componentName: string) {
const [renderCount, setRenderCount] = React.useState(0)
React.useEffect(() => {
const newCount = renderCount + 1
setRenderCount(newCount)
performanceMonitor.trackComponentRender(componentName, newCount)
})
return {
renderCount,
trackOperation: (name: string, metadata?: Record<string, any>) =>
performanceMonitor.startTiming(`${componentName}_${name}`, metadata),
trackApiCall: <T>(name: string, apiCall: () => Promise<T>) =>
performanceMonitor.trackApiCall(`${componentName}_${name}`, apiCall)
}
}
/**
* Debounce utility for performance optimization
*/
export function debounce<Args extends any[]>(
func: (...args: Args) => void,
delay: number
): (...args: Args) => void {
let timeoutId: NodeJS.Timeout | null = null
return (...args: Args) => {
if (timeoutId) {
clearTimeout(timeoutId)
}
timeoutId = setTimeout(() => {
func.apply(null, args)
}, delay)
}
}
/**
* Throttle utility for performance optimization
*/
export function throttle<Args extends any[]>(
func: (...args: Args) => void,
limit: number
): (...args: Args) => void {
let inThrottle = false
return (...args: Args) => {
if (!inThrottle) {
func.apply(null, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
/**
* Memoization utility with performance tracking
*/
export function memoizeWithTracking<Args extends any[], Return>(
fn: (...args: Args) => Return,
keyGenerator?: (...args: Args) => string
): (...args: Args) => Return {
const cache = new Map<string, { result: Return; timestamp: number }>()
const cacheTimeout = 5 * 60 * 1000 // 5 minutes
return (...args: Args) => {
const key = keyGenerator ? keyGenerator(...args) : JSON.stringify(args)
const now = Date.now()
// Check cache
const cached = cache.get(key)
if (cached && (now - cached.timestamp) < cacheTimeout) {
performanceMonitor.recordMetric('memoize_hit', 1, { function: fn.name })
return cached.result
}
// Compute result
const endTiming = performanceMonitor.startTiming('memoize_compute', { function: fn.name })
const result = fn(...args)
endTiming()
// Store in cache
cache.set(key, { result, timestamp: now })
performanceMonitor.recordMetric('memoize_miss', 1, { function: fn.name })
// Clean up old entries
if (cache.size > 100) {
const entries = Array.from(cache.entries())
entries
.filter(([, value]) => (now - value.timestamp) > cacheTimeout)
.forEach(([key]) => cache.delete(key))
}
return result
}
}
/**
* Web Vitals tracking
*/
export function trackWebVitals() {
if (typeof window === 'undefined') return
// Track Largest Contentful Paint
if ('PerformanceObserver' in window) {
try {
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.entryType === 'largest-contentful-paint') {
performanceMonitor.recordMetric('lcp', entry.startTime)
}
if (entry.entryType === 'first-input') {
performanceMonitor.recordMetric('fid', (entry as any).processingStart - entry.startTime)
}
})
}).observe({ entryTypes: ['largest-contentful-paint', 'first-input'] })
} catch (error) {
}
}
// Track Cumulative Layout Shift
if ('PerformanceObserver' in window) {
try {
let clsValue = 0
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (!(entry as any).hadRecentInput) {
clsValue += (entry as any).value
performanceMonitor.recordMetric('cls', clsValue)
}
})
}).observe({ entryTypes: ['layout-shift'] })
} catch (error) {
}
}
}

View File

@@ -0,0 +1,44 @@
// Centralized playground configuration
export const playgroundConfig = {
// Working models (avoiding rate-limited ones)
availableModels: [
{
id: 'openrouter-gpt-4',
name: 'GPT-4 (OpenRouter)',
provider: 'OpenRouter',
category: 'chat',
status: 'available'
},
{
id: 'openrouter-claude-3-sonnet',
name: 'Claude 3 Sonnet (OpenRouter)',
provider: 'OpenRouter',
category: 'chat',
status: 'available'
}
],
// Rate limited models to avoid
rateLimitedModels: [
'ollama-qwen3-235b',
'ollama-gemini-2.0-flash',
'ollama-gemini-2.5-pro'
],
// Default settings
defaults: {
model: 'openrouter-gpt-4',
temperature: 0.7,
maxTokens: 150,
systemPrompt: 'You are a helpful AI assistant.'
},
// Error handling
errorMessages: {
rateLimited: 'Model is currently rate limited. Please try another model.',
authFailed: 'Authentication failed. Please refresh the page.',
networkError: 'Network error. Please check your connection.'
}
}
export default playgroundConfig

View File

@@ -0,0 +1,109 @@
/**
* URL utilities for handling HTTP/HTTPS protocol detection
*/
/**
* Get the base URL with proper protocol detection
* This ensures API calls use the same protocol as the page was loaded with
*/
export const getBaseUrl = (): string => {
if (typeof window !== 'undefined') {
// Client-side: detect current protocol
const protocol = window.location.protocol === 'https:' ? 'https' : 'http'
const host = process.env.NEXT_PUBLIC_BASE_URL || window.location.hostname
return `${protocol}://${host}`
}
// Server-side: default based on environment
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'
return `${protocol}://${process.env.NEXT_PUBLIC_BASE_URL || 'localhost'}`
}
/**
* Get the API URL with proper protocol detection
* This is the main function that should be used for all API calls
*/
export const getApiUrl = (): string => {
if (typeof window !== 'undefined') {
// Client-side: use the same protocol as the current page
const protocol = window.location.protocol.slice(0, -1) // Remove ':' from 'https:'
const host = window.location.hostname
return `${protocol}://${host}`
}
// Server-side: default to HTTP for internal requests
return `http://${process.env.NEXT_PUBLIC_BASE_URL || 'localhost'}`
}
/**
* Get the internal API URL for authenticated endpoints
* This ensures internal API calls use the same protocol as the page
*/
export const getInternalApiUrl = (): string => {
const baseUrl = getApiUrl()
return `${baseUrl}/api-internal`
}
/**
* Get the public API URL for external client endpoints
* This ensures public API calls use the same protocol as the page
*/
export const getPublicApiUrl = (): string => {
const baseUrl = getApiUrl()
return `${baseUrl}/api`
}
/**
* Helper function to make API calls with proper protocol
*/
export const apiFetch = async (
endpoint: string,
options: RequestInit = {}
): Promise<Response> => {
const baseUrl = getApiUrl()
const url = `${baseUrl}${endpoint}`
return fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
})
}
/**
* Helper function for internal API calls
*/
export const internalApiFetch = async (
endpoint: string,
options: RequestInit = {}
): Promise<Response> => {
const url = `${getInternalApiUrl()}${endpoint}`
return fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
})
}
/**
* Helper function for public API calls
*/
export const publicApiFetch = async (
endpoint: string,
options: RequestInit = {}
): Promise<Response> => {
const url = `${getPublicApiUrl()}${endpoint}`
return fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
})
}

View File

@@ -0,0 +1,289 @@
/**
* Validation utilities with TypeScript support
*/
import type { ValidationRule, ValidationRules, ValidationResult } from '@/types/chatbot'
/**
* Validates a single field against its rules
*/
export function validateField<T>(
value: T,
rules: ValidationRule<T> = {}
): string | null {
const {
required = false,
minLength,
maxLength,
min,
max,
pattern,
custom
} = rules
// Required validation
if (required) {
if (value === null || value === undefined) {
return 'This field is required'
}
if (typeof value === 'string' && value.trim().length === 0) {
return 'This field is required'
}
if (Array.isArray(value) && value.length === 0) {
return 'This field is required'
}
}
// Skip other validations if value is empty and not required
if (!required && (value === null || value === undefined || value === '')) {
return null
}
// String length validation
if (typeof value === 'string') {
if (minLength !== undefined && value.length < minLength) {
return `Must be at least ${minLength} characters`
}
if (maxLength !== undefined && value.length > maxLength) {
return `Must be no more than ${maxLength} characters`
}
}
// Number range validation
if (typeof value === 'number') {
if (min !== undefined && value < min) {
return `Must be at least ${min}`
}
if (max !== undefined && value > max) {
return `Must be no more than ${max}`
}
}
// Array length validation
if (Array.isArray(value)) {
if (minLength !== undefined && value.length < minLength) {
return `Must have at least ${minLength} items`
}
if (maxLength !== undefined && value.length > maxLength) {
return `Must have no more than ${maxLength} items`
}
}
// Pattern validation
if (typeof value === 'string' && pattern) {
if (!pattern.test(value)) {
return 'Invalid format'
}
}
// Custom validation
if (custom) {
return custom(value)
}
return null
}
/**
* Validates an entire object against validation rules
*/
export function validateObject<T extends Record<string, any>>(
obj: T,
rules: ValidationRules<T>
): ValidationResult {
const errors: Record<string, string> = {}
for (const [key, rule] of Object.entries(rules)) {
if (rule && key in obj) {
const error = validateField(obj[key], rule as ValidationRule<any>)
if (error) {
errors[key] = error
}
}
}
return {
isValid: Object.keys(errors).length === 0,
errors
}
}
/**
* Common validation rules for chatbot fields
*/
export const chatbotValidationRules = {
name: {
required: true,
minLength: 1,
maxLength: 100,
custom: (value: string) => {
if (!/^[a-zA-Z0-9\s\-_]+$/.test(value)) {
return 'Name can only contain letters, numbers, spaces, hyphens, and underscores'
}
return null
}
},
model: {
required: true,
minLength: 1,
maxLength: 100
},
system_prompt: {
maxLength: 4000,
custom: (value: string) => {
if (value && value.trim().length === 0) {
return 'System prompt cannot be only whitespace'
}
return null
}
},
temperature: {
required: true,
min: 0,
max: 2
},
max_tokens: {
required: true,
min: 1,
max: 4000
},
memory_length: {
required: true,
min: 1,
max: 50
},
rag_top_k: {
required: true,
min: 1,
max: 20
},
fallback_responses: {
minLength: 1,
maxLength: 10,
custom: (responses: string[]) => {
if (responses.some(r => !r || r.trim().length === 0)) {
return 'All fallback responses must be non-empty'
}
return null
}
}
} as const
/**
* Email validation rule
*/
export const emailRule: ValidationRule<string> = {
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
custom: (value: string) => {
if (value && !emailRule.pattern?.test(value)) {
return 'Please enter a valid email address'
}
return null
}
}
/**
* URL validation rule
*/
export const urlRule: ValidationRule<string> = {
pattern: /^https?:\/\/.+/,
custom: (value: string) => {
if (value && !urlRule.pattern?.test(value)) {
return 'Please enter a valid URL starting with http:// or https://'
}
return null
}
}
/**
* Username validation rule
*/
export const usernameRule: ValidationRule<string> = {
minLength: 3,
maxLength: 30,
pattern: /^[a-zA-Z0-9_-]+$/,
custom: (value: string) => {
if (value && !usernameRule.pattern?.test(value)) {
return 'Username can only contain letters, numbers, hyphens, and underscores'
}
return null
}
}
/**
* Password validation rule
*/
export const passwordRule: ValidationRule<string> = {
minLength: 8,
maxLength: 128,
custom: (value: string) => {
if (!value) return null
if (!/(?=.*[a-z])/.test(value)) {
return 'Password must contain at least one lowercase letter'
}
if (!/(?=.*[A-Z])/.test(value)) {
return 'Password must contain at least one uppercase letter'
}
if (!/(?=.*\d)/.test(value)) {
return 'Password must contain at least one number'
}
if (!/(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\?])/.test(value)) {
return 'Password must contain at least one special character'
}
return null
}
}
/**
* Utility to create conditional validation rules
*/
export function when<T>(
condition: (obj: any) => boolean,
rules: ValidationRule<T>
): ValidationRule<T> {
return {
...rules,
custom: (value: T, obj?: any) => {
if (!condition(obj)) {
return null
}
const originalCustom = rules.custom
if (originalCustom) {
return originalCustom(value, obj)
}
return validateField(value, { ...rules, custom: undefined })
}
}
}
/**
* Debounced validation for real-time form validation
*/
export function createDebouncedValidator<T extends Record<string, any>>(
rules: ValidationRules<T>,
delay: number = 300
) {
let timeoutId: NodeJS.Timeout | null = null
return (obj: T, callback: (result: ValidationResult) => void) => {
if (timeoutId) {
clearTimeout(timeoutId)
}
timeoutId = setTimeout(() => {
const result = validateObject(obj, rules)
callback(result)
}, delay)
}
}