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