mirror of
https://github.com/aljazceru/enclava.git
synced 2026-02-11 02:44:23 +01:00
gitignore issues
This commit is contained in:
107
.gitignore
vendored
107
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -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(
|
||||
|
||||
174
frontend/src/lib/error-utils.ts
Normal file
174
frontend/src/lib/error-utils.ts
Normal 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
|
||||
}
|
||||
301
frontend/src/lib/performance.ts
Normal file
301
frontend/src/lib/performance.ts
Normal 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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
44
frontend/src/lib/playground-config.ts
Normal file
44
frontend/src/lib/playground-config.ts
Normal 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
|
||||
109
frontend/src/lib/url-utils.ts
Normal file
109
frontend/src/lib/url-utils.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
||||
289
frontend/src/lib/validation.ts
Normal file
289
frontend/src/lib/validation.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user