UNPKG

claritykit-svelte

Version:

A comprehensive Svelte component library focused on accessibility, ADHD-optimized design, developer experience, and full SSR compatibility

522 lines (521 loc) 18.6 kB
/** * Real-time AI Analysis Pipeline * * Handles streaming analysis, debounced requests, caching, and suggestion ranking * for the Personal AI Writing Assistant. */ import { createAIAnalysisService } from './AIAnalysisService'; export class AnalysisPipeline { constructor(config) { Object.defineProperty(this, "config", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "aiService", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "cache", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "requestQueue", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "activeRequests", { enumerable: true, configurable: true, writable: true, value: new Set() }); Object.defineProperty(this, "debounceTimers", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "personalizationData", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "analytics", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "isProcessing", { enumerable: true, configurable: true, writable: true, value: false }); this.config = config; this.aiService = createAIAnalysisService({ provider: config.provider, apiKey: config.apiKey, endpoint: config.endpoint, enabledTypes: config.enabledTypes, maxSuggestions: config.maxSuggestions, analysisDelay: config.debounceDelay, privacyMode: config.privacyMode === 'strict' ? 'private' : 'public', dataRetentionDays: config.dataRetention, allowTelemetry: config.allowTelemetry, encryptionEnabled: config.privacyMode === 'strict', cachingEnabled: true, offlineMode: config.enableOfflineMode, streamingEnabled: config.enableStreaming, writingStyle: 'casual', tonePreference: ['helpful', 'clear'], knowledgeGraphEnabled: true, chatContextEnabled: true, collaborativeAnalysis: false }); // Initialize periodic cleanup setInterval(() => this.cleanup(), 60000); // Every minute // Initialize processing loop this.startProcessingLoop(); } /** * Submit text for analysis with debouncing */ async analyzeText(text, context, priority = 'medium') { const requestId = this.generateRequestId(text, context); // Check cache first const cached = this.getCachedAnalysis(text, context); if (cached) { this.trackAnalytics('cache_hit', { requestId, textLength: text.length }); return this.rankSuggestions(cached.results, context); } // Debounce the request return new Promise((resolve, reject) => { // Clear existing debounce timer for this request const existingTimer = this.debounceTimers.get(requestId); if (existingTimer) { clearTimeout(existingTimer); } // Set new debounce timer const timer = setTimeout(async () => { try { const results = await this.processAnalysisRequest({ id: requestId, text, context, timestamp: Date.now(), priority, retry: 0 }); resolve(results); } catch (error) { reject(error); } finally { this.debounceTimers.delete(requestId); } }, this.config.debounceDelay); this.debounceTimers.set(requestId, timer); }); } /** * Process analysis request with queuing and batching */ async processAnalysisRequest(request) { // Check if already processing this request if (this.activeRequests.has(request.id)) { return []; } // Add to queue if too many concurrent requests if (this.activeRequests.size >= this.config.maxConcurrentRequests) { this.requestQueue.push(request); return []; } this.activeRequests.add(request.id); try { const startTime = Date.now(); this.trackAnalytics('analysis_start', { requestId: request.id, textLength: request.text.length, priority: request.priority }); // Call AI service const results = await this.aiService.analyzeText(request.text, request.context); // Process and rank results const rankedResults = this.rankSuggestions(results, request.context); // Cache results this.cacheAnalysis(request.text, request.context, rankedResults); // Update personalization data if (this.config.enablePersonalization) { this.updatePersonalization(request, rankedResults); } const processingTime = Date.now() - startTime; this.trackAnalytics('analysis_complete', { requestId: request.id, processingTime, suggestionCount: rankedResults.length, cacheHit: false }); return rankedResults; } catch (error) { // Handle retry logic if (request.retry < 3) { request.retry++; this.requestQueue.unshift(request); // High priority retry this.trackAnalytics('analysis_retry', { requestId: request.id, retry: request.retry, error: error.message }); } else { this.trackAnalytics('analysis_failed', { requestId: request.id, error: error.message }); throw error; } return []; } finally { this.activeRequests.delete(request.id); } } /** * Start the processing loop for queued requests */ startProcessingLoop() { const processNext = async () => { if (this.isProcessing || this.requestQueue.length === 0) { return; } this.isProcessing = true; // Sort queue by priority and timestamp this.requestQueue.sort((a, b) => { const priorityWeight = { high: 3, medium: 2, low: 1 }; const priorityDiff = priorityWeight[b.priority] - priorityWeight[a.priority]; if (priorityDiff !== 0) return priorityDiff; return a.timestamp - b.timestamp; // FIFO for same priority }); // Process batch const batch = this.requestQueue.splice(0, this.config.batchSize); const promises = batch.map(request => this.processAnalysisRequest(request)); try { await Promise.all(promises); } catch (error) { console.error('Batch processing error:', error); } this.isProcessing = false; }; // Process queue every 100ms setInterval(processNext, 100); } /** * Rank suggestions based on various factors */ rankSuggestions(suggestions, context) { const rankings = suggestions.map(suggestion => { const ranking = this.calculateSuggestionRanking(suggestion, context); return { suggestion, ranking }; }); // Sort by ranking score (descending) rankings.sort((a, b) => b.ranking.score - a.ranking.score); // Filter by confidence threshold const filtered = rankings.filter(({ ranking }) => ranking.factors.confidence >= this.config.confidenceThreshold / 100); return filtered.map(({ suggestion }) => suggestion); } /** * Calculate ranking score for a suggestion */ calculateSuggestionRanking(suggestion, context) { const factors = { // Base confidence from AI confidence: this.mapConfidence(suggestion.confidence), // Relevance to enabled types relevance: this.config.enabledTypes.includes(suggestion.type) ? 1.0 : 0.5, // User preference based on past acceptance userPreference: this.getUserPreference(suggestion.type), // Context matching contextMatch: this.calculateContextMatch(suggestion, context), // Freshness (prefer recent over stale) freshness: this.calculateFreshness(suggestion.created) }; // Weighted score calculation const weights = { confidence: 0.3, relevance: 0.25, userPreference: 0.2, contextMatch: 0.15, freshness: 0.1 }; const score = Object.entries(factors).reduce((sum, [key, value]) => sum + value * weights[key], 0); return { suggestionId: suggestion.id, score, factors, decayFactor: Math.exp(-0.1 * (Date.now() - suggestion.created.getTime()) / 1000 / 60) // Decay over time }; } /** * Get cached analysis if available and fresh */ getCachedAnalysis(text, context) { const key = this.generateCacheKey(text, context); const cached = this.cache.get(key); if (!cached) return null; // Check if cache is still fresh const age = Date.now() - cached.timestamp; if (age > this.config.cacheTTL) { this.cache.delete(key); return null; } // Update hit count cached.hits++; return cached; } /** * Cache analysis results */ cacheAnalysis(text, context, results) { const key = this.generateCacheKey(text, context); const textHash = this.hashText(text); const cached = { key, results, timestamp: Date.now(), hits: 0, context, textHash }; // Evict old entries if cache is full if (this.cache.size >= this.config.cacheSize) { this.evictLeastUsed(); } this.cache.set(key, cached); } /** * Update personalization data based on user interactions */ updatePersonalization(request, results) { if (!this.personalizationData) { this.personalizationData = { userId: 'anonymous', writingPatterns: [], preferredSuggestions: [], dismissedRules: [], acceptanceRates: {}, avgResponseTime: 0, languageLevel: 'intermediate', domainExpertise: [], lastUpdated: new Date() }; } // Extract writing patterns const patterns = this.extractWritingPatterns(request.text); this.personalizationData.writingPatterns = this.mergeWritingPatterns(this.personalizationData.writingPatterns, patterns); // Update last updated timestamp this.personalizationData.lastUpdated = new Date(); } /** * Track analytics events */ trackAnalytics(eventType, metadata) { if (!this.config.allowTelemetry) return; const event = { eventType, metadata, timestamp: new Date() }; this.analytics.push(event); // Keep only recent events (last 1000) if (this.analytics.length > 1000) { this.analytics = this.analytics.slice(-1000); } } /** * Cleanup old cache entries and analytics */ cleanup() { const now = Date.now(); // Clean expired cache entries for (const [key, cached] of this.cache.entries()) { if (now - cached.timestamp > this.config.cacheTTL) { this.cache.delete(key); } } // Clean old analytics (keep last 24 hours) const dayAgo = now - 24 * 60 * 60 * 1000; this.analytics = this.analytics.filter(event => event.timestamp.getTime() > dayAgo); } /** * Helper methods */ generateRequestId(text, context) { return `req-${this.hashText(text)}-${Date.now()}`; } generateCacheKey(text, context) { const textHash = this.hashText(text); const contextHash = this.hashText(JSON.stringify(context)); return `${textHash}-${contextHash}`; } hashText(text) { let hash = 0; for (let i = 0; i < text.length; i++) { const char = text.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash).toString(36); } mapConfidence(confidence) { const map = { low: 0.3, medium: 0.6, high: 0.9 }; return map[confidence]; } getUserPreference(type) { if (!this.personalizationData) return 0.5; return this.personalizationData.acceptanceRates[type] || 0.5; } calculateContextMatch(suggestion, context) { let score = 0.5; // Base score // Match writing style if (context.writingStyle && suggestion.metadata?.writingStyle === context.writingStyle) { score += 0.2; } // Match document type if (context.documentType && suggestion.metadata?.documentType === context.documentType) { score += 0.2; } // Match tone preference if (context.tonePreference && suggestion.metadata?.tone) { const toneMatch = context.tonePreference.includes(suggestion.metadata.tone); score += toneMatch ? 0.1 : -0.1; } return Math.max(0, Math.min(1, score)); } calculateFreshness(created) { const age = Date.now() - created.getTime(); const maxAge = 5 * 60 * 1000; // 5 minutes return Math.max(0, 1 - (age / maxAge)); } evictLeastUsed() { let leastUsed = null; for (const entry of this.cache.entries()) { if (!leastUsed || entry[1].hits < leastUsed[1].hits) { leastUsed = entry; } } if (leastUsed) { this.cache.delete(leastUsed[0]); } } extractWritingPatterns(text) { const patterns = []; // Simple pattern extraction (can be enhanced) const sentences = text.split(/[.!?]+/); const avgSentenceLength = sentences.reduce((sum, s) => sum + s.length, 0) / sentences.length; if (avgSentenceLength > 100) { patterns.push({ pattern: 'long_sentences', frequency: 1, context: ['writing'], lastUsed: new Date() }); } return patterns; } mergeWritingPatterns(existing, newPatterns) { const merged = [...existing]; newPatterns.forEach(newPattern => { const existingIndex = merged.findIndex(p => p.pattern === newPattern.pattern); if (existingIndex >= 0) { merged[existingIndex].frequency++; merged[existingIndex].lastUsed = newPattern.lastUsed; } else { merged.push(newPattern); } }); return merged; } /** * Public API methods */ /** * Get cache statistics */ getCacheStats() { return { size: this.cache.size, maxSize: this.config.cacheSize, hitRate: this.calculateHitRate(), avgAge: this.calculateAvgCacheAge() }; } /** * Get analytics data */ getAnalytics() { return { events: this.analytics.slice(-100), // Last 100 events summary: this.summarizeAnalytics() }; } /** * Clear cache */ clearCache() { this.cache.clear(); } /** * Shutdown pipeline */ shutdown() { // Clear all debounce timers for (const timer of this.debounceTimers.values()) { clearTimeout(timer); } this.debounceTimers.clear(); // Clear cache if not persistent if (!this.config.enablePersistentCache) { this.cache.clear(); } // Cleanup AI service this.aiService?.destroy(); } calculateHitRate() { const totalHits = Array.from(this.cache.values()).reduce((sum, cached) => sum + cached.hits, 0); const totalRequests = this.analytics.filter(e => e.eventType === 'analysis_start').length; return totalRequests > 0 ? totalHits / totalRequests : 0; } calculateAvgCacheAge() { const now = Date.now(); const ages = Array.from(this.cache.values()).map(cached => now - cached.timestamp); return ages.length > 0 ? ages.reduce((sum, age) => sum + age, 0) / ages.length : 0; } summarizeAnalytics() { const events = this.analytics; const analysisEvents = events.filter(e => e.eventType === 'analysis_complete'); return { totalAnalyses: analysisEvents.length, avgProcessingTime: analysisEvents.reduce((sum, e) => sum + (e.processingTime || 0), 0) / analysisEvents.length || 0, errorRate: events.filter(e => e.eventType === 'analysis_failed').length / Math.max(1, analysisEvents.length), cacheHitRate: this.calculateHitRate() }; } }