import { useState, useEffect, useCallback } from 'react' import { getApiPort } from '@/utils/apiConfig' import { Memory } from '@/types' // Singleton cache for quality standards to prevent multiple fetches let cachedStandards: QualityStandards | null = null let fetchPromise: Promise | null = null let wsConnection: WebSocket | null = null let wsListeners: Set<() => void> = new Set() interface QualityStandards { titleMinLength: number titleMaxLength: number descriptionMinLength: number descriptionMaxLength: number forbiddenPatterns: string[] weakWords: string[] strongActions: string[] qualityThresholds: { excellent: number good: number fair: number poor: number critical: number passing: number } } interface QualityValidation { score: number level: 'excellent' | 'good' | 'fair' | 'poor' | 'critical' issues: string[] suggestions: string[] meetsStandards: boolean } export function useQualityStandards() { // Enable quality validation - using real implementation const [standards, setStandards] = useState(cachedStandards) const [loading, setLoading] = useState(!cachedStandards) const [error, setError] = useState(null) // Fetch standards from API with singleton pattern const fetchStandards = useCallback(async () => { // If already fetching, wait for existing promise if (fetchPromise) { try { const data = await fetchPromise setStandards(data) setError(null) return } catch (err) { console.error('Error waiting for standards fetch:', err) } } // Start new fetch fetchPromise = fetch(`http://${window.location.hostname}:${await getApiPort()}/api/quality/standards`) .then(response => { if (!response.ok) throw new Error('Failed to fetch standards') return response.json() }) .then(data => { cachedStandards = data fetchPromise = null return data }) try { const data = await fetchPromise setStandards(data) setError(null) } catch (err) { console.error('Error fetching quality standards:', err) setError(err.message) fetchPromise = null // Use default standards as fallback const defaultStandards = { titleMinLength: 15, titleMaxLength: 80, descriptionMinLength: 50, descriptionMaxLength: 300, forbiddenPatterns: [ 'dashboard improvements', 'session\\s*\\(', '\\(\\s*\\w+\\s+\\d{1,2},?\\s+\\d{4}\\s*\\)', 'major|complete|comprehensive', 'status|update|progress' ], weakWords: ['improvements', 'session', 'update', 'status', 'changes', 'modifications'], strongActions: ['implement', 'fix', 'add', 'create', 'configure', 'optimize', 'refactor'], qualityThresholds: { excellent: 90, good: 70, fair: 60, poor: 40, critical: 0, passing: 70 } } cachedStandards = defaultStandards setStandards(defaultStandards) } finally { setLoading(false) } }, []) useEffect(() => { // Only fetch if no cached standards if (!cachedStandards) { fetchStandards() } // WebSocket disabled to prevent duplicate connections - using main App.tsx WebSocket instead // Quality validation works fine without WebSocket for now return // Temporarily disable WebSocket in useQualityStandards to prevent duplicate connections // Set up shared WebSocket connection if (false && !wsConnection || (wsConnection?.readyState !== WebSocket.OPEN && wsConnection?.readyState !== WebSocket.CONNECTING)) { try { // Only create new connection if not already connecting if (!wsConnection || wsConnection.readyState === WebSocket.CLOSED) { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const wsUrl = `${protocol}//${window.location.hostname}:3001` wsConnection = new WebSocket(wsUrl) wsConnection.onmessage = (event) => { try { const data = JSON.parse(event.data) if (data.type === 'standards-update') { // Clear cache and notify all listeners cachedStandards = null wsListeners.forEach(listener => listener()) } } catch (error) { console.error('Error parsing WebSocket message:', error) } } wsConnection.onerror = (error) => { // Only log error if it's not a connection refused error during page load if (wsConnection && wsConnection.readyState !== WebSocket.CLOSED) { console.error('WebSocket error in quality standards:', error) } } wsConnection.onclose = () => { // Mark connection as null only if it's truly closed if (wsConnection?.readyState === WebSocket.CLOSED) { wsConnection = null } } } } catch (error) { console.error('Failed to create WebSocket:', error) } } // Add listener for this component const updateListener = () => { fetchStandards() } wsListeners.add(updateListener) return () => { wsListeners.delete(updateListener) // Only close WebSocket if no more listeners if (wsListeners.size === 0 && wsConnection) { wsConnection.close() wsConnection = null } } }, [fetchStandards]) // Validate memory quality const validateMemory = useCallback((memory: Memory): QualityValidation => { if (!standards) { return { score: 0, level: 'critical', issues: ['Standards not loaded'], suggestions: [], meetsStandards: false } } const issues: string[] = [] const suggestions: string[] = [] let score = 100 // Extract title from content const titleMatch = memory.content.match(/^#+\s+(.+)$/m) || memory.content.match(/^(.+?)[\r\n]/) const title = titleMatch ? titleMatch[1].trim() : '' // Title validation if (title.length < standards.titleMinLength) { issues.push(`Title too short (${title.length}/${standards.titleMinLength} chars)`) suggestions.push('Expand title with more specific details') score -= 20 } if (title.length > standards.titleMaxLength) { issues.push(`Title too long (${title.length}/${standards.titleMaxLength} chars)`) suggestions.push('Shorten title to essential information') score -= 10 } // Check forbidden patterns standards.forbiddenPatterns.forEach(pattern => { const regex = new RegExp(pattern, 'i') if (regex.test(title)) { issues.push(`Title contains forbidden pattern: ${pattern}`) suggestions.push('Remove generic terms and be more specific') score -= 15 } }) // Check weak words const titleWords = title.toLowerCase().split(/\s+/) const weakWordsFound = standards.weakWords.filter(word => titleWords.includes(word.toLowerCase()) ) if (weakWordsFound.length > 0) { issues.push(`Title contains weak words: ${weakWordsFound.join(', ')}`) suggestions.push('Replace with stronger action words') score -= 10 * weakWordsFound.length } // Check for strong action words const hasStrongAction = standards.strongActions.some(action => title.toLowerCase().includes(action.toLowerCase()) ) if (!hasStrongAction) { issues.push('Title lacks strong action word') suggestions.push(`Start with action words like: ${standards.strongActions.slice(0, 3).join(', ')}`) score -= 15 } // Description validation (content without title) const description = memory.content.replace(/^#+\s+.+$/m, '').trim() if (description.length < standards.descriptionMinLength) { issues.push(`Description too short (${description.length}/${standards.descriptionMinLength} chars)`) suggestions.push('Add more technical details and context') score -= 20 } // Ensure score is within bounds score = Math.max(0, Math.min(100, score)) // Determine quality level let level: QualityValidation['level'] = 'critical' if (score >= standards.qualityThresholds.excellent) level = 'excellent' else if (score >= standards.qualityThresholds.good) level = 'good' else if (score >= standards.qualityThresholds.fair) level = 'fair' else if (score >= standards.qualityThresholds.poor) level = 'poor' return { score, level, issues, suggestions, meetsStandards: score >= standards.qualityThresholds.passing } }, [standards]) // Generate improved title const suggestImprovedTitle = useCallback((currentTitle: string): string => { if (!standards) return currentTitle let improved = currentTitle // Remove forbidden patterns standards.forbiddenPatterns.forEach(pattern => { const regex = new RegExp(pattern, 'gi') improved = improved.replace(regex, '') }) // Remove weak words standards.weakWords.forEach(word => { const regex = new RegExp(`\\b${word}\\b`, 'gi') improved = improved.replace(regex, '') }) // Clean up extra spaces improved = improved.replace(/\s+/g, ' ').trim() // Add action word if missing const hasAction = standards.strongActions.some(action => improved.toLowerCase().includes(action.toLowerCase()) ) if (!hasAction && standards.strongActions.length > 0) { improved = `${standards.strongActions[0]} ${improved}` } // Ensure proper length if (improved.length > standards.titleMaxLength) { improved = improved.substring(0, standards.titleMaxLength - 3) + '...' } return improved }, [standards]) return { standards, loading, error, validateMemory, suggestImprovedTitle, refetch: fetchStandards } }