UNPKG

claritykit-svelte

Version:

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

548 lines (547 loc) 22.5 kB
import { Extension } from '@tiptap/core'; import { Plugin, PluginKey } from '@tiptap/pm/state'; import { Decoration, DecorationSet } from '@tiptap/pm/view'; import { createAIAnalysisService } from '../services/AIAnalysisService'; import { SuggestionRenderer } from '../components/SuggestionRenderer'; const PersonalAssistantExtensionKey = new PluginKey('personalAssistant'); export const PersonalAssistantExtension = Extension.create({ name: 'personalAssistant', addOptions() { return { enabled: true, serviceProvider: 'mcp', enabledSuggestionTypes: ['grammar', 'tone', 'clarity', 'facts'], analysisDelay: 300, maxSuggestions: 10, privacyMode: 'private', dataRetentionDays: 30, allowTelemetry: false, showInlineAnnotations: true, showSuggestionPopover: true, animationsEnabled: true, enableKnowledgeIntegration: true, writingStyle: 'casual', tonePreference: ['helpful', 'clear'], }; }, addStorage() { return { isEnabled: this.options.enabled, analysisService: null, suggestionRenderer: null, currentSuggestions: [], analysisTimeout: null, isAnalyzing: false, lastAnalysisTime: 0, userPreferences: {}, }; }, onCreate() { // Initialize AI analysis service this.storage.analysisService = createAIAnalysisService({ provider: this.options.serviceProvider, apiKey: this.options.apiKey, privacyMode: this.options.privacyMode, enabledTypes: this.options.enabledSuggestionTypes, writingStyle: this.options.writingStyle, customInstructions: this.options.customInstructions, onError: this.options.onError, // Required properties for AIAssistantConfig maxSuggestions: this.options.maxSuggestions, analysisDelay: this.options.analysisDelay, dataRetentionDays: this.options.dataRetentionDays, allowTelemetry: this.options.allowTelemetry, encryptionEnabled: false, cachingEnabled: true, offlineMode: false, streamingEnabled: true, tonePreference: this.options.tonePreference, knowledgeGraphEnabled: this.options.enableKnowledgeIntegration, chatContextEnabled: true, collaborativeAnalysis: false, }); // Initialize suggestion renderer this.storage.suggestionRenderer = new SuggestionRenderer({ showInlineAnnotations: this.options.showInlineAnnotations, showPopover: this.options.showSuggestionPopover, animationsEnabled: this.options.animationsEnabled, maxPopoverWidth: 300, popoverDelay: 500, highlightColors: { grammar: '#ef4444', tone: '#f59e0b', clarity: '#06b6d4', facts: '#8b5cf6', style: '#10b981', conciseness: '#f97316', sensitivity: '#ec4899', knowledge: '#6366f1', completion: '#84cc16', }, onAccept: (suggestion) => { this.editor.commands.acceptSuggestion(suggestion.id); }, onDismiss: (suggestionId) => { this.editor.commands.dismissSuggestion(suggestionId); }, }); console.log('Personal AI Assistant initialized', { provider: this.options.serviceProvider, privacyMode: this.options.privacyMode, enabledTypes: this.options.enabledSuggestionTypes, }); }, onDestroy() { // Cleanup resources this.storage.analysisService?.destroy(); this.storage.suggestionRenderer?.destroy(); }, addCommands() { return { toggleAIAnalysis: () => ({ editor, state, dispatch }) => { this.storage.isEnabled = !this.storage.isEnabled; if (!this.storage.isEnabled) { // Clear all suggestions when disabled this.storage.currentSuggestions = []; // Update decorations to remove highlights if (dispatch) { const plugin = PersonalAssistantExtensionKey.get(state); if (plugin) { dispatch(state.tr.setMeta(PersonalAssistantExtensionKey, { type: 'clearSuggestions' })); } } } return true; }, acceptSuggestion: (suggestionId) => ({ editor, state, dispatch }) => { const suggestion = this.storage.currentSuggestions .find(s => s.id === suggestionId); if (!suggestion || !dispatch) return false; // Apply the suggestion const tr = state.tr; if (suggestion.replacement) { tr.replaceWith(suggestion.range.from, suggestion.range.to, state.schema.text(suggestion.replacement)); } // Remove the suggestion from current list this.storage.currentSuggestions = this.storage.currentSuggestions .filter(s => s.id !== suggestionId); // Update decorations tr.setMeta(PersonalAssistantExtensionKey, { type: 'acceptSuggestion', suggestionId, }); dispatch(tr); // Call callback this.options.onSuggestionAccepted?.(suggestion); return true; }, dismissSuggestion: (suggestionId) => ({ state, dispatch }) => { // Remove suggestion from current list this.storage.currentSuggestions = this.storage.currentSuggestions .filter(s => s.id !== suggestionId); // Update decorations if (dispatch) { dispatch(state.tr.setMeta(PersonalAssistantExtensionKey, { type: 'dismissSuggestion', suggestionId, })); } // Call callback this.options.onSuggestionDismissed?.(suggestionId); return true; }, analyzeContent: () => ({ editor }) => { if (!this.storage.isEnabled || this.storage.isAnalyzing) { return false; } this.triggerAnalysis(); return true; }, insertCompletion: (completion) => ({ editor, state, dispatch }) => { if (!dispatch) return false; const { from } = state.selection; const tr = state.tr.insertText(completion, from); dispatch(tr); return true; }, }; }, addProseMirrorPlugins() { return [ new Plugin({ key: PersonalAssistantExtensionKey, state: { init: () => { return { decorations: DecorationSet.empty, suggestions: [], }; }, apply: (tr, oldState) => { let { decorations, suggestions } = oldState; // Map decorations through the transaction decorations = decorations.map(tr.mapping, tr.doc); // Handle meta actions const meta = tr.getMeta(PersonalAssistantExtensionKey); if (meta) { switch (meta.type) { case 'updateSuggestions': suggestions = meta.suggestions; decorations = this.createDecorations(tr.doc, suggestions); break; case 'clearSuggestions': suggestions = []; decorations = DecorationSet.empty; break; case 'acceptSuggestion': case 'dismissSuggestion': suggestions = suggestions.filter(s => s.id !== meta.suggestionId); decorations = this.createDecorations(tr.doc, suggestions); break; } } return { decorations, suggestions }; }, }, props: { decorations: (state) => { const pluginState = PersonalAssistantExtensionKey.getState(state); return pluginState?.decorations; }, handleDOMEvents: { // Debounced analysis on content changes input: (view) => { if (this.storage.isEnabled) { this.debounceAnalysis(); } return false; }, }, }, }), ]; }, /** * Create decorations for AI suggestions */ createDecorations(doc, suggestions) { const decorations = []; suggestions.forEach(suggestion => { if (!suggestion.range) return; const decoration = Decoration.inline(suggestion.range.from, suggestion.range.to, { class: `ai-suggestion ai-suggestion--${suggestion.type}`, 'data-suggestion-id': suggestion.id, 'data-suggestion-type': suggestion.type, title: suggestion.message, }); decorations.push(decoration); }); return DecorationSet.create(doc, decorations); }, /** * Debounced analysis trigger */ debounceAnalysis() { if (this.storage.analysisTimeout) { clearTimeout(this.storage.analysisTimeout); } this.storage.analysisTimeout = setTimeout(() => { this.triggerAnalysis(); }, this.options.analysisDelay); }, /** * Trigger AI analysis of current content */ async triggerAnalysis() { if (!this.storage.isEnabled || !this.storage.analysisService || this.storage.isAnalyzing) { return; } const content = this.editor.getText(); if (!content.trim()) { return; } this.storage.isAnalyzing = true; this.storage.lastAnalysisTime = Date.now(); try { const context = this.getAnalysisContext(); const results = await this.storage.analysisService.analyzeText(content, context); // Filter and limit suggestions const filteredResults = results .filter(result => this.options.enabledSuggestionTypes.includes(result.type)) .slice(0, this.options.maxSuggestions); this.storage.currentSuggestions = filteredResults; // Update decorations const tr = this.editor.state.tr.setMeta(PersonalAssistantExtensionKey, { type: 'updateSuggestions', suggestions: filteredResults, }); this.editor.view.dispatch(tr); // Call callback this.options.onAnalysisComplete?.(filteredResults); } catch (error) { console.error('AI analysis failed:', error); this.options.onError?.(error); } finally { this.storage.isAnalyzing = false; } }, /** * Get context for AI analysis */ getAnalysisContext() { // Get context from ChatThread history if available const chatContext = this.getChatThreadContext(); // Get knowledge graph context const knowledgeContext = this.getKnowledgeContext(); // Get user preferences const userContext = this.getUserContext(); return { ...chatContext, ...knowledgeContext, ...userContext, writingStyle: this.options.writingStyle, tonePreference: this.options.tonePreference, customInstructions: this.options.customInstructions, }; }, /** * Get ChatThread context for contextual analysis */ getChatThreadContext() { try { // Try to get ChatThread context from the editor's context const editorElement = this.editor.view.dom.closest('[data-chat-thread]'); if (!editorElement) { return { conversationType: 'document' }; } // Extract thread information from data attributes or context const threadId = editorElement.getAttribute('data-thread-id'); const channelId = editorElement.getAttribute('data-channel-id'); const userId = editorElement.getAttribute('data-user-id'); // Get recent messages from the thread (if available) const messageElements = editorElement.querySelectorAll('[data-message]'); const recentMessages = Array.from(messageElements) .slice(-3) // Last 3 messages .map(el => ({ role: el.getAttribute('data-message-role') || 'user', content: el.textContent?.slice(0, 200) || '', // First 200 chars timestamp: new Date(el.getAttribute('data-message-time') || Date.now()) })); return { conversationType: 'chat', threadId, channelId, userId, recentMessages, messageCount: messageElements.length, isCollaborative: editorElement.hasAttribute('data-collaborative') }; } catch (error) { console.warn('Failed to get ChatThread context:', error); return { conversationType: 'document' }; } }, /** * Get knowledge graph context for concept suggestions */ getKnowledgeContext() { if (!this.options.enableKnowledgeIntegration) { return {}; } try { // Get semantic tags from the document const semanticTags = this.extractSemanticTags(); // Get linked concepts from block links const linkedConcepts = this.extractLinkedConcepts(); // Get related entities from knowledge graph const relatedEntities = this.getRelatedEntities(semanticTags, linkedConcepts); return { semanticTags, linkedConcepts, relatedEntities, knowledgeGraphAvailable: true, entityCount: relatedEntities.length }; } catch (error) { console.warn('Failed to get knowledge context:', error); return { knowledgeGraphAvailable: false }; } }, /** * Get user context for personalized suggestions */ getUserContext() { try { // Get user preferences from configuration manager const configManager = this.getConfigManager(); const personalizationData = configManager?.getPersonalizationData(); // Get writing patterns from current session const currentText = this.editor.getText(); const writingPatterns = this.analyzeWritingPatterns(currentText); // Get user's interaction history with suggestions const interactionHistory = this.getSuggestionHistory(); return { writingStyle: this.options.writingStyle, languageLevel: personalizationData?.languageLevel || 'intermediate', domainExpertise: personalizationData?.domainExpertise || [], writingPatterns, preferredSuggestionTypes: personalizationData?.preferredSuggestions || [], dismissedRules: personalizationData?.dismissedRules || [], acceptanceRates: personalizationData?.acceptanceRates || {}, avgResponseTime: personalizationData?.avgResponseTime || 0, interactionHistory: interactionHistory.slice(-10), // Last 10 interactions sessionStartTime: Date.now() }; } catch (error) { console.warn('Failed to get user context:', error); return { writingStyle: this.options.writingStyle, languageLevel: 'intermediate' }; } }, /** * Extract semantic tags from the document */ extractSemanticTags() { const tags = []; // Find semantic tag marks in the document this.editor.state.doc.descendants((node) => { if (node.marks) { node.marks.forEach(mark => { if (mark.type.name === 'semanticTag') { const tagData = mark.attrs; if (tagData.conceptId) { tags.push(tagData.conceptId); } } }); } return true; }); return [...new Set(tags)]; // Remove duplicates }, /** * Extract linked concepts from block links */ extractLinkedConcepts() { const concepts = []; // Find block link nodes in the document this.editor.state.doc.descendants((node) => { if (node.type.name === 'blockLink') { const linkData = node.attrs; if (linkData.blockId && linkData.title) { concepts.push({ id: linkData.blockId, type: linkData.blockType || 'block', title: linkData.title }); } } return true; }); return concepts; }, /** * Get related entities from knowledge graph */ getRelatedEntities(semanticTags, linkedConcepts) { // This would integrate with the MaterialViewTable or knowledge graph API // For now, return mock related entities based on tags and concepts const mockEntities = [ ...semanticTags.map(tag => ({ id: `concept-${tag}`, label: tag.replace(/[_-]/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), type: 'concept', relevance: 0.8 })), ...linkedConcepts.map(concept => ({ id: concept.id, label: concept.title, type: concept.type, relevance: 0.9 })) ]; // Sort by relevance and return top 10 return mockEntities .sort((a, b) => b.relevance - a.relevance) .slice(0, 10); }, /** * Analyze current writing patterns */ analyzeWritingPatterns(text) { const patterns = []; if (!text) return patterns; // Analyze sentence length const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0); const avgSentenceLength = sentences.reduce((sum, s) => sum + s.length, 0) / sentences.length; if (avgSentenceLength > 100) { patterns.push({ pattern: 'long_sentences', strength: Math.min(avgSentenceLength / 100, 1) }); } // Analyze paragraph structure const paragraphs = text.split(/\n\s*\n/).filter(p => p.trim().length > 0); const avgParagraphLength = paragraphs.reduce((sum, p) => sum + p.length, 0) / paragraphs.length; if (avgParagraphLength > 500) { patterns.push({ pattern: 'long_paragraphs', strength: Math.min(avgParagraphLength / 500, 1) }); } // Analyze vocabulary complexity const words = text.toLowerCase().match(/\b\w+\b/g) || []; const uniqueWords = new Set(words); const vocabularyRatio = uniqueWords.size / words.length; if (vocabularyRatio > 0.7) { patterns.push({ pattern: 'complex_vocabulary', strength: vocabularyRatio }); } // Analyze tone indicators const formalWords = ['therefore', 'however', 'furthermore', 'consequently', 'moreover']; const casualWords = ['like', 'really', 'pretty', 'kind of', 'sort of']; const formalCount = formalWords.reduce((count, word) => count + (text.toLowerCase().match(new RegExp(`\\b${word}\\b`, 'g'))?.length || 0), 0); const casualCount = casualWords.reduce((count, word) => count + (text.toLowerCase().match(new RegExp(`\\b${word}\\b`, 'g'))?.length || 0), 0); if (formalCount > casualCount) { patterns.push({ pattern: 'formal_tone', strength: formalCount / (formalCount + casualCount) }); } else if (casualCount > formalCount) { patterns.push({ pattern: 'casual_tone', strength: casualCount / (formalCount + casualCount) }); } return patterns; }, /** * Get suggestion interaction history */ getSuggestionHistory() { // This would typically be stored in local storage or user preferences try { const history = localStorage.getItem('ai-assistant-suggestion-history'); return history ? JSON.parse(history) : []; } catch (error) { return []; } }, /** * Get configuration manager instance */ getConfigManager() { // Try to get config manager from global context or import try { // This would be properly imported in a real implementation return window.aiAssistantConfigManager; } catch (error) { return null; } } });