UNPKG

lanonasis-memory

Version:

Memory as a Service integration - AI-powered memory management with semantic search (Compatible with CLI v3.0.6+)

565 lines (487 loc) 19.6 kB
// @ts-check (function () { const vscode = acquireVsCodeApi(); const rootElement = document.getElementById('root'); // State management let state = { authenticated: false, memories: [], loading: true, enhancedMode: false, cliVersion: null, cached: false, expandedGroups: new Set(['context', 'project', 'knowledge']), promptInput: '', refinedPrompt: '', refinementNotes: [], userName: rootElement?.dataset.userName || '', brandIcon: rootElement?.dataset.brandIcon || '' }; // Initialize document.addEventListener('DOMContentLoaded', () => { setupEventListeners(); render(); }); // Handle messages from extension window.addEventListener('message', event => { const message = event.data; switch (message.type) { case 'updateState': state = { ...state, ...message.state }; render(); break; case 'searchResults': displaySearchResults(message.results, message.query); break; case 'error': showError(message.message); break; } }); function setupEventListeners() { // Search const searchInput = document.getElementById('search-input'); if (searchInput) { searchInput.addEventListener('input', debounce((e) => { const query = e.target.value.trim(); if (query.length > 2) { vscode.postMessage({ type: 'searchMemories', query: query }); } }, 300)); } } function render() { const root = document.getElementById('root'); if (!root) return; if (state.loading) { root.innerHTML = getLoadingHTML(); return; } if (!state.authenticated) { root.innerHTML = getAuthHTML(); attachAuthListeners(); return; } root.innerHTML = getMainHTML(); attachMainListeners(); } function getLoadingHTML() { return ` <div class="loading-state"> <div class="spinner"></div> <p>Loading Lanonasis Memory...</p> </div> `; } function getAuthHTML() { return ` <div class="auth-state"> <div class="auth-icon">🧭</div> <h3>Welcome to Lanonasis Memory</h3> <p>Choose how you'd like to connect. You can switch between OAuth and API key authentication at any time.</p> <div class="auth-actions"> <button class="btn" id="oauth-btn"> <span class="btn-icon">🌐 Continue in Browser</span> </button> <button class="btn" id="api-key-btn"> <span class="btn-icon">🔑 Enter API Key</span> </button> </div> <div class="auth-actions"> <button class="btn btn-secondary" id="command-palette-btn"> <span class="btn-icon">⌨️ Command Palette</span> </button> <button class="btn btn-secondary" id="get-key-btn"> <span class="btn-icon">📬 Get API Key</span> </button> <button class="btn btn-secondary" id="settings-btn"> <span class="btn-icon">⚙️ Settings</span> </button> </div> <p class="auth-hint">If the buttons fail, run <code>Lanonasis: Authenticate</code> or <code>Lanonasis: Configure Authentication</code> from the command palette.</p> </div> `; } function getMainHTML() { const hasMemories = state.memories && state.memories.length > 0; return ` <div class="sidebar-header"> <div class="header-title"> ${state.brandIcon ? `<img class="brand-icon" src="${state.brandIcon}" alt="Lanonasis logo">` : '<div class="brand-placeholder">LN</div>'} <div class="header-text"> <h2>Lanonasis Memory</h2> <div class="welcome-line">Welcome, ${escapeHtml(state.userName || 'creator')}</div> </div> <span class="status-badge"> <span class="status-dot"></span> Connected </span> </div> ${state.enhancedMode ? getEnhancedBannerHTML() : ''} <div class="search-container"> <span class="search-icon">🔍</span> <input type="text" class="search-box" id="search-input" placeholder="Search memories..." /> </div> <div class="action-buttons"> <button class="btn" id="create-memory-btn"> <span class="btn-icon">➕ Create</span> </button> <button class="btn btn-secondary" id="refresh-btn"> <span class="btn-icon">🔄 Refresh</span> </button> </div> ${state.cached ? '<div class="cache-indicator">📦 Cached data (click refresh for latest)</div>' : ''} ${getPromptRefinerHTML()} </div> ${hasMemories ? getMemoriesHTML() : getEmptyStateHTML()} `; } function getEnhancedBannerHTML() { return ` <div class="enhanced-banner"> <div class="enhanced-banner-header"> 🚀 Enhanced Mode Active </div> <div class="enhanced-banner-text"> CLI ${state.cliVersion || 'v3.0+'} detected - Performance optimized </div> </div> `; } function getPromptRefinerHTML() { const hasOutput = (state.refinedPrompt || '').trim().length > 0; const notes = state.refinementNotes || []; return ` <div class="card"> <div class="card-header"> <div> <div class="label">Prompt Refiner</div> <div class="subdued">Tightens prompts using REPL-style checklist</div> </div> <span class="pill">Beta</span> </div> <div class="field"> <label for="prompt-input">Raw prompt</label> <textarea id="prompt-input" class="textarea" rows="4" placeholder="Paste your prompt here...">${escapeHtml(state.promptInput || '')}</textarea> </div> <div class="refiner-actions"> <button class="btn" id="refine-btn"> <span class="btn-icon">⚡ Refine</span> </button> <button class="btn btn-secondary" id="copy-refined-btn" ${hasOutput ? '' : 'disabled'}> <span class="btn-icon">📋 Copy refined</span> </button> </div> <div class="refined-output ${hasOutput ? '' : 'muted'}" id="refined-output"> ${hasOutput ? escapeHtml(state.refinedPrompt) : 'Refined prompt will appear here'} </div> ${notes.length ? ` <div class="refiner-notes"> ${notes.map(note => `<div class="note-item">• ${escapeHtml(note)}</div>`).join('')} </div> ` : ''} </div> `; } function getMemoriesHTML() { const groupedMemories = groupMemoriesByType(state.memories); const types = Object.keys(groupedMemories); if (types.length === 0) { return getEmptyStateHTML(); } return ` <div class="memories-container"> <div class="section-header"> <span>Your Memories</span> <span>${state.memories.length} total</span> </div> ${types.map(type => getMemoryGroupHTML(type, groupedMemories[type])).join('')} </div> `; } function getMemoryGroupHTML(type, memories) { const isExpanded = state.expandedGroups.has(type); const icon = getTypeIcon(type); return ` <div class="memory-group"> <div class="memory-type-header" data-type="${type}"> <span class="memory-type-icon">${icon}</span> <span>${capitalizeFirst(type)}</span> <span class="memory-count">${memories.length}</span> </div> ${isExpanded ? ` <ul class="memory-list"> ${memories.map(memory => getMemoryItemHTML(memory)).join('')} </ul> ` : ''} </div> `; } function getMemoryItemHTML(memory) { const date = new Date(memory.created_at).toLocaleDateString(); const preview = (memory.content || '').substring(0, 80); return ` <li class="memory-item" data-id="${memory.id}"> <div class="memory-title">${escapeHtml(memory.title)}</div> <div class="memory-meta"> <span>📅 ${date}</span> ${memory.tags && memory.tags.length > 0 ? `<span>🏷️ ${memory.tags.length}</span>` : ''} </div> </li> `; } function getEmptyStateHTML() { return ` <div class="empty-state"> <div class="empty-icon">📝</div> <h3>No Memories Yet</h3> <p>Get started by creating your first memory from selected text or a file.</p> <button class="btn" id="create-first-memory-btn"> <span class="btn-icon">✨ Create Your First Memory</span> </button> <p class="mt-2" style="font-size: 12px; opacity: 0.7;"> Tip: Select text in any file and press Cmd+Shift+Alt+M </p> </div> `; } function attachAuthListeners() { const oauthBtn = document.getElementById('oauth-btn'); const apiKeyBtn = document.getElementById('api-key-btn'); const commandPaletteBtn = document.getElementById('command-palette-btn'); const getKeyBtn = document.getElementById('get-key-btn'); const settingsBtn = document.getElementById('settings-btn'); oauthBtn?.addEventListener('click', () => { vscode.postMessage({ type: 'authenticate', mode: 'oauth' }); }); apiKeyBtn?.addEventListener('click', () => { vscode.postMessage({ type: 'authenticate', mode: 'apikey' }); }); commandPaletteBtn?.addEventListener('click', () => { vscode.postMessage({ type: 'openCommandPalette' }); }); getKeyBtn?.addEventListener('click', () => { vscode.postMessage({ type: 'getApiKey' }); }); settingsBtn?.addEventListener('click', () => { vscode.postMessage({ type: 'showSettings' }); }); } function attachMainListeners() { // Create memory button const createBtn = document.getElementById('create-memory-btn'); const createFirstBtn = document.getElementById('create-first-memory-btn'); const refreshBtn = document.getElementById('refresh-btn'); const promptInput = document.getElementById('prompt-input'); const refineBtn = document.getElementById('refine-btn'); const copyRefinedBtn = document.getElementById('copy-refined-btn'); createBtn?.addEventListener('click', () => { vscode.postMessage({ type: 'createMemory' }); }); createFirstBtn?.addEventListener('click', () => { vscode.postMessage({ type: 'createMemory' }); }); refreshBtn?.addEventListener('click', () => { vscode.postMessage({ type: 'refresh' }); }); promptInput?.addEventListener('input', (e) => { state.promptInput = e.target.value; }); refineBtn?.addEventListener('click', () => { const rawPrompt = (state.promptInput || '').trim(); if (!rawPrompt) { showInfo('Add a prompt to refine'); return; } const refined = refinePrompt(rawPrompt); state.refinedPrompt = refined.prompt; state.refinementNotes = refined.notes; render(); }); copyRefinedBtn?.addEventListener('click', async () => { const text = (state.refinedPrompt || '').trim(); if (!text) return; try { await navigator.clipboard.writeText(text); showInfo('Refined prompt copied to clipboard'); } catch (error) { showError('Unable to copy to clipboard'); } }); // Memory type headers (toggle expand/collapse) document.querySelectorAll('.memory-type-header').forEach(header => { header.addEventListener('click', (e) => { const type = e.currentTarget.getAttribute('data-type'); if (type) { toggleGroupExpansion(type); } }); }); // Memory items document.querySelectorAll('.memory-item').forEach(item => { item.addEventListener('click', (e) => { const memoryId = e.currentTarget.getAttribute('data-id'); const memory = state.memories.find(m => m.id === memoryId); if (memory) { vscode.postMessage({ type: 'openMemory', memory: memory }); } }); }); } function toggleGroupExpansion(type) { if (state.expandedGroups.has(type)) { state.expandedGroups.delete(type); } else { state.expandedGroups.add(type); } render(); } function displaySearchResults(results, query) { if (results.length === 0) { showInfo(`No results found for "${query}"`); return; } // Update state with search results state.memories = results; render(); } function showError(message) { const root = document.getElementById('root'); const errorDiv = document.createElement('div'); errorDiv.className = 'error-message'; errorDiv.textContent = message; root?.prepend(errorDiv); setTimeout(() => errorDiv.remove(), 5000); } function showInfo(message) { const root = document.getElementById('root'); const infoDiv = document.createElement('div'); infoDiv.className = 'info-message'; infoDiv.textContent = message; root?.prepend(infoDiv); setTimeout(() => infoDiv.remove(), 3000); } // Utility functions function groupMemoriesByType(memories) { const grouped = {}; memories.forEach(memory => { const type = memory.memory_type || 'context'; if (!grouped[type]) { grouped[type] = []; } grouped[type].push(memory); }); return grouped; } function getTypeIcon(type) { const icons = { 'context': '💡', 'project': '📁', 'knowledge': '📚', 'reference': '🔗', 'personal': '👤', 'workflow': '⚙️', 'conversation': '💬' }; return icons[type] || '📄'; } function capitalizeFirst(str) { return str.charAt(0).toUpperCase() + str.slice(1); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function refinePrompt(rawPrompt) { const normalized = collapseWhitespace(rawPrompt); const lines = normalized.split('\n').map(l => l.trim()).filter(Boolean); const roleLine = lines.find(l => /act as|you are|role:/i.test(l)); const objectiveLine = lines.find(l => l !== roleLine) || normalized; const context = []; const constraints = []; const outputs = []; lines.forEach(line => { if (line === roleLine || line === objectiveLine) return; const lower = line.toLowerCase(); const cleaned = stripBullet(line); if (/(output|format|respond|return|deliver)/.test(lower)) { outputs.push(cleaned); return; } if (/(avoid|do not|never|without|limit|must|should)/.test(lower)) { constraints.push(cleaned); return; } context.push(cleaned); }); const result = []; const notes = []; if (roleLine) { result.push(`Role: ${stripBullet(roleLine)}`); notes.push('Kept explicit role/context from input'); } result.push(`Goal: ${stripBullet(objectiveLine)}`); notes.push('Elevated primary goal to the top'); if (context.length) { result.push('Context:'); context.forEach(item => result.push(`- ${item}`)); } if (constraints.length) { result.push('Constraints:'); constraints.forEach(item => result.push(`- ${item}`)); notes.push('Preserved guardrails and boundaries'); } if (outputs.length) { result.push('Output:'); outputs.forEach(item => result.push(`- ${item}`)); } else { result.push('Output:'); result.push('- Provide the final answer only. No extra commentary.'); } result.push('Check-before-answer:'); result.push('- Ask for any missing detail before drafting the answer.'); result.push('- Keep responses concise and ordered.'); notes.push(`Normalized ${lines.length} input line(s) and removed filler whitespace`); return { prompt: result.join('\n').trim(), notes }; } function stripBullet(text) { return text.replace(/^[-*•\d.\s]+/, '').trim(); } function collapseWhitespace(text) { return text .replace(/\r\n/g, '\n') .replace(/\n{3,}/g, '\n\n') .replace(/[ \t]+/g, ' ') .trim(); } function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } })();