Added citation and quality metrics to the content editor.

This commit is contained in:
ajaysi
2025-09-03 09:40:05 +05:30
parent 10b50f9732
commit 5efee4235d
35 changed files with 6987 additions and 1123 deletions

View File

@@ -5,12 +5,74 @@ export function escapeHtml(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// Format draft content with proper LinkedIn styling
export function formatDraftContent(content: string): string {
// Format draft content with proper LinkedIn styling and inline citations
export function formatDraftContent(content: string, citations?: any[], researchSources?: any[]): string {
if (!content) return '';
let formatted = escapeHtml(content);
// Insert inline citations if available
if (citations && citations.length > 0 && researchSources && researchSources.length > 0) {
console.log('🔍 [formatDraftContent] Processing citations:', {
citationsCount: citations.length,
researchSourcesCount: researchSources.length,
citations: citations,
contentLength: content.length
});
// Create a map of citation references to source numbers
const citationMap = new Map();
citations.forEach((citation, index) => {
if (citation.reference && citation.reference.startsWith('Source ')) {
const sourceNum = citation.reference.replace('Source ', '');
citationMap.set(citation.reference, sourceNum);
}
});
console.log('🔍 [formatDraftContent] Citation map created:', citationMap);
// Since citation references don't exist in the content text,
// we need to insert citations strategically throughout the content
const citationEntries = Array.from(citationMap.entries());
const totalCitations = citationEntries.length;
if (totalCitations > 0) {
// Split content into sentences for strategic citation placement
const sentences = formatted.split(/[.!?]+/).filter(s => s.trim().length > 0);
const sentencesWithCitations: string[] = [];
citationEntries.forEach(([reference, sourceNum], index) => {
// Distribute citations across sentences
const targetSentenceIndex = Math.floor((index / totalCitations) * sentences.length);
const targetSentence = sentences[targetSentenceIndex] || sentences[sentences.length - 1];
// Add citation to the end of the target sentence using a superscript marker
const citeHtml = ` <sup class="liw-cite" data-source-index="${sourceNum}">[${sourceNum}]</sup>`;
const sentenceWithCitation = targetSentence.trim() + citeHtml;
sentencesWithCitations[targetSentenceIndex] = sentenceWithCitation;
console.log(`✅ [formatDraftContent] Added citation [${sourceNum}] to sentence ${targetSentenceIndex + 1}`);
});
// Reconstruct content with citations
formatted = sentences.map((sentence, index) => {
return sentencesWithCitations[index] || sentence;
}).join('. ') + '.';
console.log(`✅ [formatDraftContent] Inserted ${totalCitations} citations strategically throughout content`);
// Debug: Show sample of content with citations
const sampleContent = formatted.substring(0, 500) + (formatted.length > 500 ? '...' : '');
console.log('🔍 [formatDraftContent] Sample content with citations:', sampleContent);
// Debug: Count citation markers in final content
const citationMarkers = (formatted.match(/\[\d+\]/g) || []).length;
console.log(`🔍 [formatDraftContent] Found ${citationMarkers} citation markers in final content`);
}
console.log('🔍 [formatDraftContent] Final formatted content length:', formatted.length);
}
// Format hashtags
formatted = formatted.replace(/#(\w+)/g, '<span style="color: #0a66c2; font-weight: 600;">#$1</span>');

View File

@@ -0,0 +1,307 @@
/**
* Enhanced persistence utility for CopilotKit integration
* Uses localStorage and CopilotKit hooks for better state management
*/
import { useCopilotContext } from '@copilotkit/react-core';
// Storage keys for different types of data
export const STORAGE_KEYS = {
CHAT_HISTORY: 'alwrity-copilot-chat-history',
USER_PREFERENCES: 'alwrity-copilot-user-preferences',
CONVERSATION_CONTEXT: 'alwrity-copilot-conversation-context',
DRAFT_CONTENT: 'alwrity-copilot-draft-content',
LAST_SESSION: 'alwrity-copilot-last-session'
};
// Chat message interface
export interface ChatMessage {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: number;
metadata?: {
action?: string;
result?: any;
context?: string;
};
}
// User preferences interface
export interface UserPreferences {
tone: string;
industry: string;
target_audience: string;
content_goals: string[];
writing_style: string;
hashtag_preferences: boolean;
cta_preferences: boolean;
last_used_actions: string[];
favorite_topics: string[];
last_updated: number;
}
// Conversation context interface
export interface ConversationContext {
currentTopic: string;
industry: string;
tone: string;
targetAudience: string;
keyPoints: string[];
lastUpdated: number;
}
// Main persistence manager class
export class CopilotPersistenceManager {
private static instance: CopilotPersistenceManager;
private constructor() {}
public static getInstance(): CopilotPersistenceManager {
if (!CopilotPersistenceManager.instance) {
CopilotPersistenceManager.instance = new CopilotPersistenceManager();
}
return CopilotPersistenceManager.instance;
}
// Chat history persistence
public saveChatHistory(messages: ChatMessage[]): void {
try {
// Keep only last 100 messages to prevent excessive storage
const trimmedMessages = messages.slice(-100);
localStorage.setItem(STORAGE_KEYS.CHAT_HISTORY, JSON.stringify(trimmedMessages));
console.log(`💾 Saved ${trimmedMessages.length} chat messages`);
} catch (error) {
console.error('❌ Failed to save chat history:', error);
}
}
public loadChatHistory(): ChatMessage[] {
try {
const stored = localStorage.getItem(STORAGE_KEYS.CHAT_HISTORY);
if (!stored) return [];
const messages = JSON.parse(stored);
console.log(`📖 Loaded ${messages.length} chat messages`);
return messages;
} catch (error) {
console.error('❌ Failed to load chat history:', error);
return [];
}
}
public addChatMessage(message: ChatMessage): void {
try {
const existing = this.loadChatHistory();
existing.push(message);
this.saveChatHistory(existing);
} catch (error) {
console.error('❌ Failed to add chat message:', error);
}
}
// User preferences persistence
public saveUserPreferences(preferences: Partial<UserPreferences>): void {
try {
const existing = this.loadUserPreferences();
const updated = { ...existing, ...preferences, last_updated: Date.now() };
localStorage.setItem(STORAGE_KEYS.USER_PREFERENCES, JSON.stringify(updated));
console.log('💾 Saved user preferences');
} catch (error) {
console.error('❌ Failed to save user preferences:', error);
}
}
public loadUserPreferences(): UserPreferences {
try {
const stored = localStorage.getItem(STORAGE_KEYS.USER_PREFERENCES);
if (!stored) {
return {
tone: 'Professional',
industry: 'Technology',
target_audience: 'Professionals',
content_goals: ['Engagement', 'Thought Leadership'],
writing_style: 'Clear and Concise',
hashtag_preferences: true,
cta_preferences: true,
last_used_actions: [],
favorite_topics: [],
last_updated: Date.now()
};
}
const preferences = JSON.parse(stored);
console.log('📖 Loaded user preferences');
return preferences;
} catch (error) {
console.error('❌ Failed to load user preferences:', error);
// Return default preferences instead of recursive call
return {
tone: 'Professional',
industry: 'Technology',
target_audience: 'Professionals',
content_goals: ['Engagement', 'Thought Leadership'],
writing_style: 'Clear and Concise',
hashtag_preferences: true,
cta_preferences: true,
last_used_actions: [],
favorite_topics: [],
last_updated: Date.now()
};
}
}
// Conversation context persistence
public saveConversationContext(context: Partial<ConversationContext>): void {
try {
const existing = this.loadConversationContext();
const updated = { ...existing, ...context, lastUpdated: Date.now() };
localStorage.setItem(STORAGE_KEYS.CONVERSATION_CONTEXT, JSON.stringify(updated));
console.log('💾 Saved conversation context');
} catch (error) {
console.error('❌ Failed to save conversation context:', error);
}
}
public loadConversationContext(): ConversationContext {
try {
const stored = localStorage.getItem(STORAGE_KEYS.CONVERSATION_CONTEXT);
if (!stored) {
return {
currentTopic: '',
industry: 'Technology',
tone: 'Professional',
targetAudience: 'Professionals',
keyPoints: [],
lastUpdated: Date.now()
};
}
const context = JSON.parse(stored);
console.log('📖 Loaded conversation context');
return context;
} catch (error) {
console.error('❌ Failed to load conversation context:', error);
// Return default context instead of recursive call
return {
currentTopic: '',
industry: 'Technology',
tone: 'Professional',
targetAudience: 'Professionals',
keyPoints: [],
lastUpdated: Date.now()
};
}
}
// Draft content persistence
public saveDraftContent(draft: string): void {
try {
localStorage.setItem(STORAGE_KEYS.DRAFT_CONTENT, draft);
console.log('💾 Saved draft content');
} catch (error) {
console.error('❌ Failed to save draft content:', error);
}
}
public loadDraftContent(): string {
try {
const stored = localStorage.getItem(STORAGE_KEYS.DRAFT_CONTENT);
if (stored) {
console.log('📖 Loaded draft content');
return stored;
}
return '';
} catch (error) {
console.error('❌ Failed to load draft content:', error);
return '';
}
}
// Session management
public saveLastSession(): void {
try {
const sessionData = {
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent
};
localStorage.setItem(STORAGE_KEYS.LAST_SESSION, JSON.stringify(sessionData));
console.log('💾 Saved session data');
} catch (error) {
console.error('❌ Failed to save session data:', error);
}
}
public loadLastSession(): any {
try {
const stored = localStorage.getItem(STORAGE_KEYS.LAST_SESSION);
if (stored) {
const session = JSON.parse(stored);
console.log('📖 Loaded session data');
return session;
}
return null;
} catch (error) {
console.error('❌ Failed to load session data:', error);
return null;
}
}
// Clear all persistence data
public clearAllData(): void {
try {
Object.values(STORAGE_KEYS).forEach(key => {
localStorage.removeItem(key);
});
console.log('🗑️ Cleared all persistence data');
} catch (error) {
console.error('❌ Failed to clear persistence data:', error);
}
}
// Get storage statistics
public getStorageStats(): any {
try {
const stats = {
chatHistory: this.loadChatHistory().length,
hasUserPreferences: !!localStorage.getItem(STORAGE_KEYS.USER_PREFERENCES),
hasConversationContext: !!localStorage.getItem(STORAGE_KEYS.CONVERSATION_CONTEXT),
hasDraftContent: !!localStorage.getItem(STORAGE_KEYS.DRAFT_CONTENT),
hasLastSession: !!localStorage.getItem(STORAGE_KEYS.LAST_SESSION),
totalKeys: Object.keys(localStorage).filter(key => key.includes('alwrity-copilot')).length
};
console.log('📊 Storage statistics:', stats);
return stats;
} catch (error) {
console.error('❌ Failed to get storage stats:', error);
return {};
}
}
}
// Hook for using persistence in React components
export const useCopilotPersistence = () => {
const copilotContext = useCopilotContext();
const persistenceManager = CopilotPersistenceManager.getInstance();
return {
persistenceManager,
copilotContext,
// Convenience methods
saveChatHistory: persistenceManager.saveChatHistory.bind(persistenceManager),
loadChatHistory: persistenceManager.loadChatHistory.bind(persistenceManager),
addChatMessage: persistenceManager.addChatMessage.bind(persistenceManager),
saveUserPreferences: persistenceManager.saveUserPreferences.bind(persistenceManager),
loadUserPreferences: persistenceManager.loadUserPreferences.bind(persistenceManager),
saveConversationContext: persistenceManager.saveConversationContext.bind(persistenceManager),
loadConversationContext: persistenceManager.loadConversationContext.bind(persistenceManager),
saveDraftContent: persistenceManager.saveDraftContent.bind(persistenceManager),
loadDraftContent: persistenceManager.loadDraftContent.bind(persistenceManager),
saveLastSession: persistenceManager.saveLastSession.bind(persistenceManager),
loadLastSession: persistenceManager.loadLastSession.bind(persistenceManager),
clearAllData: persistenceManager.clearAllData.bind(persistenceManager),
getStorageStats: persistenceManager.getStorageStats.bind(persistenceManager)
};
};

View File

@@ -23,7 +23,6 @@ export const VALID_TONES = [
] as const;
export const VALID_SEARCH_ENGINES = [
'metaphor',
'google',
'tavily'
] as const;
@@ -158,8 +157,12 @@ export function mapIndustry(industry: string | undefined): string {
}
export function mapSearchEngine(engine: string | undefined): SearchEngine {
// Force Google for now until METAPHOR issue is resolved
return SearchEngine.GOOGLE;
/* Original logic - commented out temporarily
const eng = normalizeEnum(engine);
if (!eng) return SearchEngine.METAPHOR;
if (!eng) return SearchEngine.GOOGLE;
const exact = VALID_SEARCH_ENGINES.find(v => v.toLowerCase() === eng);
if (exact) return exact as SearchEngine;
@@ -167,7 +170,8 @@ export function mapSearchEngine(engine: string | undefined): SearchEngine {
if (eng.includes('google')) return SearchEngine.GOOGLE;
if (eng.includes('tavily')) return SearchEngine.TAVILY;
return SearchEngine.METAPHOR;
return SearchEngine.GOOGLE;
*/
}
export function mapResponseType(responseType: string | undefined): string {

View File

@@ -0,0 +1,88 @@
/**
* Utility to test and debug CopilotKit persistence
*/
export const testPersistence = () => {
console.log('🧪 Testing CopilotKit persistence...');
// Check localStorage for persisted data
const chatData = localStorage.getItem('alwrity-copilot-chat');
const prefsData = localStorage.getItem('alwrity-copilot-preferences');
const contextData = localStorage.getItem('alwrity-copilot-context');
console.log('📊 Persistence Test Results:', {
chat: {
exists: !!chatData,
length: chatData ? JSON.parse(chatData).length : 0,
sample: chatData ? JSON.parse(chatData).slice(0, 2) : null
},
preferences: {
exists: !!prefsData,
data: prefsData ? JSON.parse(prefsData) : null
},
context: {
exists: !!contextData,
data: contextData ? JSON.parse(contextData) : null
}
});
// Check for any other CopilotKit related data
const allKeys = Object.keys(localStorage);
const copilotKeys = allKeys.filter(key => key.includes('copilot') || key.includes('alwrity'));
console.log('🔍 All CopilotKit related localStorage keys:', copilotKeys);
return {
chat: !!chatData,
preferences: !!prefsData,
context: !!contextData,
allCopilotKeys: copilotKeys
};
};
export const clearPersistence = () => {
console.log('🗑️ Clearing CopilotKit persistence...');
localStorage.removeItem('alwrity-copilot-chat');
localStorage.removeItem('alwrity-copilot-preferences');
localStorage.removeItem('alwrity-copilot-context');
// Clear any other CopilotKit related data
const allKeys = Object.keys(localStorage);
const copilotKeys = allKeys.filter(key => key.includes('copilot') || key.includes('alwrity'));
copilotKeys.forEach(key => {
localStorage.removeItem(key);
console.log(`🗑️ Removed: ${key}`);
});
console.log('✅ Persistence cleared');
};
export const simulateChatMessage = () => {
console.log('💬 Simulating chat message for persistence test...');
const testMessage = {
role: 'user',
content: 'This is a test message to verify persistence',
timestamp: Date.now(),
id: `test-${Date.now()}`
};
// Try to store in the expected format
try {
const existingChat = localStorage.getItem('alwrity-copilot-chat');
const chatArray = existingChat ? JSON.parse(existingChat) : [];
chatArray.push(testMessage);
// Keep only last 10 messages for testing
const trimmedChat = chatArray.slice(-10);
localStorage.setItem('alwrity-copilot-chat', JSON.stringify(trimmedChat));
console.log('✅ Test message stored:', testMessage);
return true;
} catch (error) {
console.error('❌ Failed to store test message:', error);
return false;
}
};