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
JavaScript
/**
* 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()
};
}
}