PageSourceSearch

https://dynamic-salmiakki-095693.netlify.app/conversation-context.js

js dynamic-salmiakki-095693.netlify.app collected 2026-10-03 10:40:30 UTC 19,826 bytes, 547 lines download raw bytes

1/**
2 * Conversation Context Manager
3 * Handles session tracking, entity memory, and contextual continuity
4 */
5
6class ConversationContext {
7    constructor() {
8        this.sessionId = this.generateSessionId();
9        this.conversationHistory = [];
10        this.entityMemory = new Map(); // Track mentioned entities
11        this.topicMemory = new Set(); // Track conversation topics
12        this.maxHistoryLength = 20; // Keep last 20 messages
13        this.maxContextLength = 8; // Include last 8 relevant messages in context
14        
15        // Load existing session if available
16        this.loadSession();
17        
18        console.log(`[Context] Initialized session: ${this.sessionId}`);
19    }
20    
21    /**
22     * Generate unique session ID
23     */
24    generateSessionId() {
25        return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
26    }
27    
28    /**
29     * Add message to conversation history with entity extraction
30     */
31    addMessage(role, content, metadata = {}) {
32        const message = {
33            role,
34            content,
35            timestamp: Date.now(),
36            entities: this.extractEntities(content),
37            topics: this.extractTopics(content),
38            metadata
39        };
40        
41        this.conversationHistory.push(message);
42        
43        // Update entity and topic memory
44        this.updateEntityMemory(message.entities, message);
45        this.updateTopicMemory(message.topics);
46        
47        // Trim history if too long
48        if (this.conversationHistory.length > this.maxHistoryLength) {
49            this.conversationHistory = this.conversationHistory.slice(-this.maxHistoryLength);
50        }
51        
52        // Save to storage
53        this.saveSession();
54        
55        console.log(`[Context] Added ${role} message with ${message.entities.length} entities`);
56        return message;
57    }
58    
59    /**
60     * Extract entities from text (people, places, organizations)
61     */
62    extractEntities(text) {
63        // Handle undefined/null text
64        if (!text || typeof text !== 'string') {
65            return [];
66        }
67        
68        const entities = [];
69        
70        // Common patterns for entity extraction
71        const patterns = {
72            // People: Capitalized names (2+ words)
73            people: /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b/g,
74            // Places: Common location indicators
75            places: /\b(?:in|at|from|to)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\b/g,
76            // Organizations/Movies: Quoted or capitalized multi-word entities
77            organizations: /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,3})\b/g
78        };
79        
80        // Extract people names
81        let match;
82        while ((match = patterns.people.exec(text)) !== null) {
83            const entity = match[1];
84            if (this.isLikelyPersonName(entity)) {
85                entities.push({
86                    text: entity,
87                    type: 'person',
88                    confidence: 0.8
89                });
90            }
91        }
92        
93        // Extract places
94        patterns.places.lastIndex = 0;
95        while ((match = patterns.places.exec(text)) !== null) {
96            entities.push({
97                text: match[1],
98                type: 'place',
99                confidence: 0.6
100            });
101        }
102        
103        // Remove duplicates
104        const uniqueEntities = entities.filter((entity, index, self) => 
105            index === self.findIndex(e => e.text.toLowerCase() === entity.text.toLowerCase())
106        );
107        
108        return uniqueEntities;
109    }
110    
111    /**
112     * Check if text is likely a person name
113     */
114    isLikelyPersonName(text) {
115        const commonWords = ['The', 'And', 'Or', 'But', 'For', 'With', 'About', 'Mission', 'Top', 'Gun'];
116        const words = text.split(' ');
117        
118        // Skip if contains common non-name words
119        if (words.some(word => commonWords.includes(word))) {
120            return false;
121        }
122        
123        // Must be 2-4 words
124        return words.length >= 2 && words.length <= 4;
125    }
126    
127    /**
128     * Extract topics from text
129     */
130    extractTopics(text) {
131        // Handle undefined/null text
132        if (!text || typeof text !== 'string') {
133            return [];
134        }
135        
136        const topics = [];
137        const topicKeywords = {
138            'movies': ['film', 'movie', 'cinema', 'actor', 'actress', 'director', 'role'],
139            'marriage': ['marriage', 'married', 'wife', 'husband', 'divorce', 'relationship'],
140            'career': ['career', 'work', 'job', 'profession', 'achievement', 'award'],
141            'family': ['family', 'children', 'kids', 'parents', 'father', 'mother'],
142            'biography': ['born', 'birth', 'life', 'biography', 'background', 'early']
143        };
144        
145        const lowerText = text.toLowerCase();
146        
147        for (const [topic, keywords] of Object.entries(topicKeywords)) {
148            if (keywords.some(keyword => lowerText.includes(keyword))) {
149                topics.push(topic);
150            }
151        }
152        
153        return topics;
154    }
155    
156    /**
157     * Update entity memory with new entities
158     */
159    updateEntityMemory(entities, message) {
160        entities.forEach(entity => {
161            const key = entity.text.toLowerCase();
162            
163            if (!this.entityMemory.has(key)) {
164                this.entityMemory.set(key, {
165                    text: entity.text,
166                    type: entity.type,
167                    firstMention: message.timestamp,
168                    lastMention: message.timestamp,
169                    mentionCount: 1,
170                    contexts: [message.content.substring(0, 100)]
171                });
172            } else {
173                const existing = this.entityMemory.get(key);
174                existing.lastMention = message.timestamp;
175                existing.mentionCount++;
176                existing.contexts.push(message.content.substring(0, 100));
177                
178                // Keep only last 3 contexts
179                if (existing.contexts.length > 3) {
180                    existing.contexts = existing.contexts.slice(-3);
181                }
182            }
183        });
184    }
185    
186    /**
187     * Update topic memory
188     */
189    updateTopicMemory(topics) {
190        topics.forEach(topic => this.topicMemory.add(topic));
191    }
192    
193    /**
194     * Get contextual information for a new query
195     */
196    getContextForQuery(query) {
197        const queryEntities = this.extractEntities(query);
198        const queryTopics = this.extractTopics(query);
199        
200        // Find relevant previous messages
201        const relevantMessages = this.findRelevantMessages(queryEntities, queryTopics);
202        
203        // Build context summary
204        const context = {
205            sessionId: this.sessionId,
206            relevantHistory: relevantMessages,
207            mentionedEntities: this.getRelevantEntities(queryEntities),
208            activeTopics: Array.from(this.topicMemory),
209            conversationLength: this.conversationHistory.length
210        };
211        
212        console.log(`[Context] Generated context with ${relevantMessages.length} relevant messages`);
213        return context;
214    }
215    
216    /**
217     * Find messages relevant to current query
218     */
219    findRelevantMessages(queryEntities, queryTopics) {
220        const relevantMessages = [];
221        const entityTexts = queryEntities.map(e => e.text.toLowerCase());
222        
223        // Score messages by relevance
224        const scoredMessages = this.conversationHistory.map(message => {
225            let score = 0;
226            
227            // Recent messages get higher scores
228            const age = Date.now() - message.timestamp;
229            const recencyScore = Math.max(0, 1 - (age / (30 * 60 * 1000))); // 30 minutes
230            score += recencyScore * 0.3;
231            
232            // Entity overlap
233            const messageEntityTexts = message.entities.map(e => e.text.toLowerCase());
234            const entityOverlap = entityTexts.filter(e => messageEntityTexts.includes(e)).length;
235            score += entityOverlap * 0.5;
236            
237            // Topic overlap
238            const topicOverlap = queryTopics.filter(t => message.topics.includes(t)).length;
239            score += topicOverlap * 0.2;
240            
241            return { message, score };
242        });
243        
244        // Sort by score and take top messages
245        return scoredMessages
246            .sort((a, b) => b.score - a.score)
247            .slice(0, this.maxContextLength)
248            .map(item => item.message);
249    }
250    
251    /**
252     * Get relevant entities for context
253     */
254    getRelevantEntities(queryEntities) {
255        const relevant = [];
256        const queryTexts = queryEntities.map(e => e.text.toLowerCase());
257        
258        for (const [key, entity] of this.entityMemory.entries()) {
259            // Include if mentioned in query or recently mentioned
260            const recentlyMentioned = (Date.now() - entity.lastMention) < (10 * 60 * 1000); // 10 minutes
261            const mentionedInQuery = queryTexts.includes(key);
262            
263            if (mentionedInQuery || recentlyMentioned) {
264                relevant.push(entity);
265            }
266        }
267        
268        return relevant;
269    }
270    
271    /**
272     * Resolve references in query (e.g., "his wife", "that movie")
273     */
274    resolveReferences(query) {
275        let resolvedQuery = query;
276        
277        // Only resolve references if we have recent conversation context (within last 2 messages)
278        if (this.conversationHistory.length < 2) {
279            console.log('[Context] No recent context for reference resolution');
280            return resolvedQuery;
281        }
282        
283        // Get the most recent user message to check relevance
284        const recentMessages = this.conversationHistory.slice(-3);
285        const lastUserMessage = recentMessages.reverse().find(msg => msg.role === 'user');
286        
287        if (!lastUserMessage || Date.now() - lastUserMessage.timestamp > 300000) { // 5 minutes
288            console.log('[Context] Last user message too old for reference resolution');
289            return resolvedQuery;
290        }
291        
292        // Simple reference resolution patterns with stricter validation
293        const references = {
294            'his wife': this.findRecentEntity('person', 'wife'),
295            'his wives': this.findRecentEntity('person', 'wife'),
296            'her husband': this.findRecentEntity('person', 'husband'),
297            'that movie': this.findRecentEntity('movie'),
298            'the film': this.findRecentEntity('movie'),
299            'that person': this.findRecentEntity('person')
300        };
301        
302        for (const [reference, entity] of Object.entries(references)) {
303            if (entity && resolvedQuery.toLowerCase().includes(reference)) {
304                // Additional validation: ensure the entity was mentioned recently
305                const timeSinceLastMention = Date.now() - entity.lastMention;
306                if (timeSinceLastMention > 300000) { // 5 minutes
307                    console.log(`[Context] Entity "${entity.text}" too old for resolution (${timeSinceLastMention}ms ago)`);
308                    continue;
309                }
310                
311                // Check if the entity is contextually relevant to re
311cent conversation
312                const isRelevant = this.isEntityRelevantToRecentContext(entity, recentMessages);
313                if (!isRelevant) {
314                    console.log(`[Context] Entity "${entity.text}" not relevant to recent context`);
315                    continue;
316                }
317                
318                resolvedQuery = resolvedQuery.replace(
319                    new RegExp(reference, 'gi'), 
320                    entity.text
321                );
322                console.log(`[Context] Resolved "${reference}" to "${entity.text}"`);
323            }
324        }
325        
326        return resolvedQuery;
327    }
328    
329    /**
330     * Find recent entity of specific type
331     */
332    findRecentEntity(type, context = null) {
333        const candidates = Array.from(this.entityMemory.values())
334            .filter(entity => {
335                // Basic type filter
336                if (entity.type !== type) return false;
337                
338                // Time filter: only consider entities mentioned in last 5 minutes
339                const timeSinceLastMention = Date.now() - entity.lastMention;
340                if (timeSinceLastMention > 300000) return false;
341                
342                // Frequency filter: entity should have been mentioned at least once recently
343                if (entity.mentionCount < 1) return false;
344                
345                return true;
346            })
347            .sort((a, b) => b.lastMention - a.lastMention);
348        
349        if (context) {
350            // Look for entity mentioned in context with additional validation
351            const contextMatch = candidates.find(entity => {
352                const hasContextMatch = entity.contexts.some(ctx => 
353                    ctx.toLowerCase().includes(context.toLowerCase())
354                );
355                
356                // Additional check: ensure the context is recent
357                const recentContexts = entity.contexts.slice(-2); // Last 2 contexts
358                const hasRecentContextMatch = recentContexts.some(ctx => 
359                    ctx.toLowerCase().includes(context.toLowerCase())
360                );
361                
362                return hasContextMatch && hasRecentContextMatch;
363            });
364            
365            if (contextMatch) {
366                console.log(`[Context] Found contextual match for "${context}": ${contextMatch.text}`);
367                return contextMatch;
368            }
369        }
370        
371        // Return most recent candidate, but only if it's very recent (within 2 minutes)
372        const mostRecent = candidates[0];
373        if (mostRecent && (Date.now() - mostRecent.lastMention) < 120000) {
374            console.log(`[Context] Found recent entity: ${mostRecent.text}`);
375            return mostRecent;
376        }
377        
378        console.log(`[Context] No suitable recent entity found for type "${type}" with context "${context}"`);
379        return null;
380    }
381    
382    /**
383     * Check if entity is relevant to recent conversation context
384     */
385    isEntityRelevantToRecentContext(entity, recentMessages) {
386        // Check if entity was mentioned in recent messages
387        const recentMentions = recentMessages.filter(msg => 
388            msg.entities.some(e => e.text.toLowerCase() === entity.text.toLowerCase())
389        );
390        
391        return recentMentions.length > 0;
392    }
393    
394    /**
395     * Save session to localStorage
396     */
397    saveSession() {
398        try {
399            const sessionData = {
400                sessionId: this.sessionId,
401                conversationHistory: this.conversationHistory,
402                entityMemory: Array.from(this.entityMemory.entries()),
403                topicMemory: Array.from(this.topicMemory),
404                timestamp: Date.now()
405            };
406            
407            localStorage.setItem(`wikiLive_session_${this.sessionId}`, JSON.stringify(sessionData));
408        } catch (error) {
409            console.warn('[Context] Failed to save session:', error);
410        }
411    }
412    
413    /**
414     * Load session from localStorage
415     */
416    loadSession() {
417        try {
418            // Try to load the most recent session
419            const sessions = this.getAllSessions();
420            if (sessions.length > 0) {
421                const latestSession = sessions[0];
422                this.sessionId = latestSession.sessionId;
423                this.conversationHistory = latestSession.conversationHistory || [];
424                this.entityMemory = new Map(latestSession.entityMemory || []);
425                this.topicMemory = new Set(latestSession.topicMemory || []);
426                
427                console.log(`[Context] Loaded session with ${this.conversationHistory.length} messages`);
428            }
429        } catch (error) {
430            console.warn('[Context] Failed to load session:', error);
431        }
432    }
433    
434    /**
435     * Get all sessions sorted by timestamp
436     */
437    getAllSessions() {
438        const sessions = [];
439        
440        for (let i = 0; i < localStorage.length; i++) {
441            const key = localStorage.key(i);
442            if (key && key.startsWith('wikiLive_session_')) {
443                try {
444                    const sessionData = JSON.parse(localStorage.getItem(key));
445                    sessions.push(sessionData);
446                } catch (error) {
447                    console.warn('[Context] Failed to parse session:', key);
448                }
449            }
450        }
451        
452        return sessions.sort((a, b) => b.timestamp - a.timestamp);
453    }
454    
455    /**
456     * Clear old sessions (keep last 5)
457     */
458    cleanupOldSessions() {
459        const sessions = this.getAllSessions();
460        
461        // Remove sessions older than 7 days or beyond the limit
462        const cutoffTime = Date.now() - (7 * 24 * 60 * 60 * 1000); // 7 days
463        const sessionsToRemove = sessions.slice(5).concat(
464            sessions.filter(s => s.timestamp < cutoffTime)
465        );
466        
467        sessionsToRemove.forEach(session => {
468            localStorage.removeItem(`wikiLive_session_${session.sessionId}`);
469        });
470        
471        if (sessionsToRemove.length > 0) {
472            console.log(`[Context] Cleaned up ${sessionsToRemove.length} old sessions`);
473        }
474    }
475    
476    /**
477     * Start new conversation session
478     */
479    startNewSession() {
480        this.sessionId = this.generateSessionId();
481        this.conversationHistory = [];
482        this.entityMemory = new Map();
483        this.topicMemory = new Set();
484        
485        console.log(`[Context] Started new session: ${this.sessionId}`);
486    }
487    
488    /**
489     * Get conversation summary for display
490     */
491    getConversationSummary() {
492        const entities = Array.from(this.entityMemory.values())
493            .sort((a, b) => b.mentionCount - a.mentionCount)
494            .slice(0, 5);
495        
496        return {
497            sessionId: this.sessionId,
498            messageCount: this.conversationHistory.length,
499            topEntities: entities,
500            activeTopics: Array.from(this.topicMemory),
501            sessionAge: this.conversationHistory.length > 0 ? 
502                Date.now() - this.conversationHistory[0].timestamp : 0
503        };
504    }
505    
506    /**
507     * Clear stale context data to prevent carryover between sessions
508     */
509    clearStaleContext() {
510        const now = Date.now();
511        const staleThreshold = 600000; // 10 minutes
512        
513        // Remove stale entities
514        const staleEntities = [];
515        for (const [key, entity] of this.entityMemory.entries()) {
516            if (now - entity.lastMention > staleThreshold) {
517                staleEntities.push(key);
518            }
519        }
520        
521        staleEntities.forEach(key => {
522            console.log(`[Context] Removing stale entity: ${key}`);
523            this.entityMemory.delete(key);
524        });
525        
526        // Clear old conversation history (keep only last 10 messages)
527        if (this.conversationHistory.length > 10) {
528            this.conversationHistory = this.conversationHistory.slice(-10);
529            console.log('[Context] Trimmed conversation history to last 10 messages');
530        }
531        
532        // Clear old topics
533        this.topicMemory.clear();
534        console.log('[Context] Cleared topic memory');
535        
536        // Save cleaned session
537        this.saveSession();
538    }
539}
540
541// Global instance
542window.conversationContext = new ConversationContext();
543
544// Export for use in other modules
545if (typeof module !== 'undefined' && module.exports) {
546    module.exports = ConversationContext;
547}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.