/** * FeatureLens — Visual Feedback & AI Task Staging Overlay * Zero-dependency Web Component with Shadow DOM Isolation */ class FeatureLensElement extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.isOpen = false; this.isInspectMode = false; this.notes = []; this.currentTarget = null; this.hoverOverlay = null; this.raycaster = null; this.mouse = null; } connectedCallback() { this.render(); this.initInspector(); this.loadNotes(); this.setupGlobalEvents(); } setupGlobalEvents() { // Listen for custom open event window.addEventListener('featurelens:toggle', () => this.toggleDrawer()); } async loadNotes() { try { const res = await fetch('/api/notes'); if (res.ok) { this.notes = await res.json(); this.updateBadge(); this.renderNotesList(); } } catch (err) { console.warn('[FeatureLens] Could not fetch notes from backend, using memory cache:', err); } } updateBadge() { const badge = this.shadowRoot.querySelector('#fl-badge'); if (badge) { const openCount = this.notes.filter(n => n.status === 'open').length; badge.textContent = openCount; badge.style.display = openCount > 0 ? 'inline-block' : 'none'; } } toggleDrawer(forceState) { this.isOpen = typeof forceState === 'boolean' ? forceState : !this.isOpen; const drawer = this.shadowRoot.querySelector('#fl-drawer'); const activator = this.shadowRoot.querySelector('#fl-activator'); if (drawer) { if (this.isOpen) { drawer.classList.add('open'); activator.classList.add('active'); this.loadNotes(); } else { drawer.classList.remove('open'); activator.classList.remove('active'); this.stopInspectMode(); } } } // ------------------------------------------------------------- // Visual Inspector (2D DOM & 3D Three.js Raycaster) // ------------------------------------------------------------- initInspector() { this.hoverOverlay = document.createElement('div'); this.hoverOverlay.id = 'fl-hover-highlight'; this.hoverOverlay.style.cssText = ` position: absolute; pointer-events: none; border: 2px solid #06b6d4; background: rgba(6, 182, 212, 0.15); z-index: 999998; display: none; transition: all 0.05s ease-out; border-radius: 4px; box-shadow: 0 0 12px rgba(6, 182, 212, 0.5); `; document.body.appendChild(this.hoverOverlay); if (window.THREE) { this.raycaster = new THREE.Raycaster(); this.mouse = new THREE.Vector2(); } } startInspectMode() { this.isInspectMode = true; this.toggleDrawer(false); const banner = document.createElement('div'); banner.id = 'fl-inspect-banner'; banner.style.cssText = ` position: fixed; top: 1rem; left: 50%; transform: translateX(-50%); background: #0f172a; border: 1px solid #06b6d4; color: #38bdf8; padding: 0.5rem 1rem; border-radius: 9999px; font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; font-weight: 700; box-shadow: 0 10px 25px rgba(0,0,0,0.8); z-index: 999999; display: flex; align-items: center; gap: 0.5rem; cursor: pointer; `; banner.innerHTML = `🎯 Click any 2D UI element or 3D CAD part to attach note [ESC to cancel]`; banner.onclick = () => this.stopInspectMode(); document.body.appendChild(banner); this._onMouseMove = this.handlePointerMove.bind(this); this._onClick = this.handlePointerClick.bind(this); this._onKeyDown = (e) => { if (e.key === 'Escape') this.stopInspectMode(); }; window.addEventListener('mousemove', this._onMouseMove, true); window.addEventListener('click', this._onClick, true); window.addEventListener('keydown', this._onKeyDown, true); } stopInspectMode() { this.isInspectMode = false; if (this.hoverOverlay) this.hoverOverlay.style.display = 'none'; const banner = document.getElementById('fl-inspect-banner'); if (banner) banner.remove(); window.removeEventListener('mousemove', this._onMouseMove, true); window.removeEventListener('click', this._onClick, true); window.removeEventListener('keydown', this._onKeyDown, true); } handlePointerMove(e) { if (!this.isInspectMode) return; // Check if hovering 3D canvas if (window.scene && window.camera && e.target.tagName === 'CANVAS' && this.raycaster) { const rect = e.target.getBoundingClientRect(); this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; this.raycaster.setFromCamera(this.mouse, window.camera); const intersects = this.raycaster.intersectObjects(window.scene.children, true); if (intersects.length > 0) { const hit = intersects[0]; let mesh = hit.object; let partId = mesh.userData?.partId || mesh.name || '3d-mesh'; // Highlight 3D part this.hoverOverlay.style.display = 'none'; return; } } // 2D Element highlight if (e.target && e.target !== this.hoverOverlay && !e.target.closest('feature-lens')) { const rect = e.target.getBoundingClientRect(); this.hoverOverlay.style.display = 'block'; this.hoverOverlay.style.top = `${rect.top + window.scrollY}px`; this.hoverOverlay.style.left = `${rect.left + window.scrollX}px`; this.hoverOverlay.style.width = `${rect.width}px`; this.hoverOverlay.style.height = `${rect.height}px`; } } handlePointerClick(e) { if (!this.isInspectMode) return; e.preventDefault(); e.stopPropagation(); let targetType = 'dom_element'; let targetSelector = ''; let partId = null; let coords3d = null; // 3D Intersect check if (window.scene && window.camera && e.target.tagName === 'CANVAS' && this.raycaster) { const rect = e.target.getBoundingClientRect(); this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; this.raycaster.setFromCamera(this.mouse, window.camera); const intersects = this.raycaster.intersectObjects(window.scene.children, true); if (intersects.length > 0) { const hit = intersects[0]; targetType = '3d_mesh'; partId = hit.object.userData?.partId || hit.object.name || 'chassis-part'; coords3d = { x: parseFloat(hit.point.x.toFixed(2)), y: parseFloat(hit.point.y.toFixed(2)), z: parseFloat(hit.point.z.toFixed(2)) }; } } if (targetType === 'dom_element') { targetSelector = this.computeSelectorPath(e.target); } this.currentTarget = { type: targetType, selector: targetSelector, partId: partId, coords3d: coords3d, route: window.location.pathname + window.location.hash }; this.stopInspectMode(); this.toggleDrawer(true); this.openCreateForm(); } computeSelectorPath(el) { if (!el || el === document.body || el === document.documentElement) return 'body'; if (el.id) return `#${el.id}`; if (el.className && typeof el.className === 'string') { const classes = el.className.trim().split(/\s+/).slice(0, 2).join('.'); if (classes) return `${el.tagName.toLowerCase()}.${classes}`; } return `${el.tagName.toLowerCase()}`; } // ------------------------------------------------------------- // AI Prompt Formatter // ------------------------------------------------------------- generateAiPrompt(note) { const suggestedFiles = note.suggested_files && note.suggested_files.length > 0 ? note.suggested_files.join(', ') : 'viewer/index.html, src/routes/notes.ts'; let targetDesc = `DOM Element: \`${note.target_selector || 'Global View'}\``; if (note.target_type === '3d_mesh') { targetDesc = `3D Mesh Part: \`${note.part_id}\` (Coords: X:${note.coordinates_3d?.x || 0}, Y:${note.coordinates_3d?.y || 0}, Z:${note.coordinates_3d?.z || 0})`; } return ` Feature Target: [${note.title}] Category: ${note.category?.toUpperCase()} | Priority: ${note.priority?.toUpperCase()} | Status: ${note.status?.toUpperCase()} Target Element: ${targetDesc} Route: ${note.route || window.location.pathname} Task / Improvement Specification: ${note.description} Technical Context & Candidate Source Files: - Location / Route: ${note.route} - Target Type: ${note.target_type} - Suggested Relevant Files: ${suggestedFiles} - Environment: 6U Tri-Mode Storage Array (Deno/Hono/Postgres + Three.js WebGL) `; } async copyAiPrompt(noteId) { const note = this.notes.find(n => n.id === noteId); if (!note) return; const prompt = this.generateAiPrompt(note); try { await navigator.clipboard.writeText(prompt); alert('✅ AI Task Prompt copied to clipboard! Paste it directly into your AI assistant.'); } catch (err) { console.warn('Clipboard write failed, showing text box:', err); prompt('Copy your AI task prompt:', prompt); } } // ------------------------------------------------------------- // Note CRUD Actions // ------------------------------------------------------------- openCreateForm() { const formContainer = this.shadowRoot.querySelector('#fl-form-container'); const listContainer = this.shadowRoot.querySelector('#fl-list-container'); if (formContainer && listContainer) { formContainer.style.display = 'block'; listContainer.style.display = 'none'; const targetLabel = this.shadowRoot.querySelector('#fl-target-label'); if (targetLabel) { if (this.currentTarget?.type === '3d_mesh') { targetLabel.innerHTML = `🎯 3D Mesh: ${this.currentTarget.partId} (X:${this.currentTarget.coords3d.x}, Y:${this.currentTarget.coords3d.y}, Z:${this.currentTarget.coords3d.z})`; } else if (this.currentTarget?.selector) { targetLabel.innerHTML = `🎯 2D Element: ${this.currentTarget.selector}`; } else { targetLabel.innerHTML = `🌐 Global App Scope`; } } } } closeCreateForm() { const formContainer = this.shadowRoot.querySelector('#fl-form-container'); const listContainer = this.shadowRoot.querySelector('#fl-list-container'); if (formContainer && listContainer) { formContainer.style.display = 'none'; listContainer.style.display = 'block'; this.currentTarget = null; } } async saveNote() { const titleInput = this.shadowRoot.querySelector('#fl-title-input'); const descInput = this.shadowRoot.querySelector('#fl-desc-input'); const catSelect = this.shadowRoot.querySelector('#fl-cat-select'); const prioSelect = this.shadowRoot.querySelector('#fl-prio-select'); if (!titleInput || !titleInput.value.trim()) { alert('Please enter a note title.'); return; } // Auto deduce candidate files let suggestedFiles = ['viewer/index.html']; if (this.currentTarget?.type === '3d_mesh') { suggestedFiles.push('viewer/index.html#threejs-cad', 'mechanical/'); } else if (this.currentTarget?.selector?.includes('bom')) { suggestedFiles.push('src/routes/bom.ts', 'src/db/bom.ts', 'viewer/bom.js'); } else if (this.currentTarget?.selector?.includes('traveler')) { suggestedFiles.push('src/routes/traveler.ts', 'src/db/traveler.ts', 'viewer/traveler.js'); } else if (this.currentTarget?.selector?.includes('hil') || this.currentTarget?.selector?.includes('diagnostics')) { suggestedFiles.push('src/routes/hil.ts', 'src/db/hil.ts', 'viewer/hil.js'); } const payload = { title: titleInput.value.trim(), description: descInput ? descInput.value.trim() : '', category: catSelect ? catSelect.value : 'feature', priority: prioSelect ? prioSelect.value : 'medium', status: 'open', route: window.location.pathname + window.location.hash, target_type: this.currentTarget?.type || 'global', target_selector: this.currentTarget?.selector || null, part_id: this.currentTarget?.partId || null, coordinates_3d: this.currentTarget?.coords3d || null, context_snapshot: { viewport: `${window.innerWidth}x${window.innerHeight}`, userAgent: navigator.userAgent, timestamp: new Date().toISOString() }, suggested_files: suggestedFiles }; try { const res = await fetch('/api/notes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (res.ok) { titleInput.value = ''; if (descInput) descInput.value = ''; this.closeCreateForm(); await this.loadNotes(); } } catch (err) { console.error('Error saving note:', err); } } async updateStatus(id, newStatus) { try { await fetch(`/api/notes/${id}/status`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: newStatus }) }); await this.loadNotes(); } catch (err) { console.error('Error updating status:', err); } } async deleteNote(id) { if (!confirm('Delete this feature note?')) return; try { await fetch(`/api/notes/${id}`, { method: 'DELETE' }); await this.loadNotes(); } catch (err) { console.error('Error deleting note:', err); } } // ------------------------------------------------------------- // Render Shadow DOM Template // ------------------------------------------------------------- render() { this.shadowRoot.innerHTML = `
🔍 FeatureLens
FeatureLens Studio (OSHW-AI)
Loading notes...
`; } renderNotesList() { const container = this.shadowRoot.querySelector('#fl-list-container'); if (!container) return; container.innerHTML = ''; if (this.notes.length === 0) { container.innerHTML = `
No active feature notes. Click Inspect & Pin to create one!
`; return; } this.notes.forEach(note => { const card = document.createElement('div'); card.className = 'fl-card'; let targetInfo = note.target_selector ? `${note.target_selector}` : 'Global Scope'; if (note.target_type === '3d_mesh') { targetInfo = `3D Part: ${note.part_id}`; } card.innerHTML = `
${note.category} ${note.status}
${note.title}
${targetInfo}
${note.description || 'No description provided.'}
`; container.appendChild(card); }); } } // Register Custom Web Component if (!customElements.get('feature-lens')) { customElements.define('feature-lens', FeatureLensElement); }