From 3de990cf7deec44f9c596778530b981074506555 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 29 May 2026 22:58:22 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20header=20graph=20repens=C3=A9=20+=20fil?= =?UTF-8?q?tre=20tag=20+=20Ctrl+survol=20aper=C3=A7u=20contenu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/index.html | 22 ++++++++----- frontend/js/graph.js | 78 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 13 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 7e5cf5e..01532f0 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -700,14 +700,19 @@
-
-
Vue Graphique
-
- - - - - +
+
+ + + + + + + + + + Ctrl+survol = aperçu + @@ -727,6 +732,7 @@
+
Vue Graphique
diff --git a/frontend/js/graph.js b/frontend/js/graph.js index 01bdd4d..75eef68 100644 --- a/frontend/js/graph.js +++ b/frontend/js/graph.js @@ -50,8 +50,12 @@ export const GraphViewManager = { _scope: 'directory', _depth: 1, _searchTerm: '', + _tagFilter: '', _hoveredNode: null, _tooltipEl: null, + _previewCache: {}, + _previewPending: null, + _ctrlHoverNode: null, async open(vault, path, type) { this._vault = vault; @@ -85,7 +89,7 @@ export const GraphViewManager = { _cacheKey: '', _getCacheKey() { - return `${this._vault}|${this._path}|${this._depth}|${this._scope}|${this._searchTerm}`; + return `${this._vault}|${this._path}|${this._depth}|${this._scope}|${this._searchTerm}|${this._tagFilter}`; }, async _fetchAndRender() { @@ -111,7 +115,7 @@ export const GraphViewManager = { if (this._path) params.set('path', this._path); params.set('depth', String(this._depth)); params.set('scope', this._scope); - if (this._searchTerm) params.set('tag', this._searchTerm); + if (this._tagFilter) params.set('tag', this._tagFilter); try { const data = await api( @@ -167,6 +171,11 @@ export const GraphViewManager = { } }, + setTagFilter(tag) { + this._tagFilter = tag; + this.reload(); + }, + close() { const modal = document.getElementById('graph-modal'); if (modal) modal.classList.remove('active'); @@ -511,8 +520,9 @@ export const GraphViewManager = { return null; }, - _showTooltip(node, screenX, screenY) { + _showTooltip(node, screenX, screenY, isPreview) { if (!this._tooltipEl) return; + if (isPreview) return; // Preview tooltip handled by _showPreviewTooltip const tags = (node.tags || []).slice(0, 5).join(', '); const inc = node.incoming_count || 0; const out = node.outgoing_count || 0; @@ -521,8 +531,10 @@ export const GraphViewManager = { ${node.type === 'file' ? `
${node.path}` : ''} ${tags ? `
🏷️ ${tags}` : ''} ${inc + out > 0 ? `
🔗 ${out} sortants · ${inc} entrants` : ''} +
Ctrl+survol pour aperçu `; this._tooltipEl.style.display = 'block'; + this._tooltipEl.style.maxWidth = '250px'; this._tooltipEl.style.left = (screenX + 15) + 'px'; this._tooltipEl.style.top = (screenY - 10) + 'px'; }, @@ -609,17 +621,63 @@ export const GraphViewManager = { if (hit) { this._canvas.style.cursor = 'pointer'; this._canvas.title = hit.node.type === 'file' - ? `📄 ${hit.node.name} (cliquer pour ouvrir)` + ? `📄 ${hit.node.name} (cliquer pour ouvrir${e.ctrlKey ? ', Ctrl+survol = aperçu' : ''})` : `📁 ${hit.node.name} (cliquer pour explorer)`; - this._showTooltip(hit.node, e.clientX, e.clientY); + + // Ctrl+hover: fetch content preview for file nodes + if (e.ctrlKey && hit.node.type === 'file') { + if (this._ctrlHoverNode !== hit.node) { + this._ctrlHoverNode = hit.node; + this._fetchPreview(hit.node, e.clientX, e.clientY); + } + this._showTooltip(hit.node, e.clientX, e.clientY, true); + } else { + this._ctrlHoverNode = null; + this._showTooltip(hit.node, e.clientX, e.clientY, false); + } } else { this._canvas.style.cursor = 'grab'; this._canvas.title = ''; + this._ctrlHoverNode = null; this._hideTooltip(); } } }, + async _fetchPreview(node, screenX, screenY) { + const cacheKey = `${this._vault}:${node.path}`; + if (this._previewCache[cacheKey] !== undefined) { + this._showPreviewTooltip(node, this._previewCache[cacheKey], screenX, screenY); + return; + } + // Show loading indicator + this._showPreviewTooltip(node, 'Chargement...', screenX, screenY); + try { + const data = await api(`/api/file/${encodeURIComponent(this._vault)}?path=${encodeURIComponent(node.path)}`); + const content = (data.content || '').substring(0, 400).replace(/\\n/g, ' '); + this._previewCache[cacheKey] = content; + this._showPreviewTooltip(node, content, screenX, screenY); + } catch { + this._previewCache[cacheKey] = '(impossible de charger le contenu)'; + this._showPreviewTooltip(node, '(impossible de charger le contenu)', screenX, screenY); + } + }, + + _showPreviewTooltip(node, preview, screenX, screenY) { + if (!this._tooltipEl || this._ctrlHoverNode !== node) return; + const tags = (node.tags || []).slice(0, 3).join(', '); + this._tooltipEl.innerHTML = ` + ${node.name} + ${node.path} + ${tags ? `🏷️ ${tags}` : ''} +
${preview}
+ `; + this._tooltipEl.style.display = 'block'; + this._tooltipEl.style.maxWidth = '350px'; + this._tooltipEl.style.left = Math.min(screenX + 15, window.innerWidth - 370) + 'px'; + this._tooltipEl.style.top = Math.min(screenY - 10, window.innerHeight - 180) + 'px'; + }, + _onMouseUp(e) { if (this._dragging && this._dragNode) { const node = this._dragNode.node; @@ -698,6 +756,16 @@ export function initGraphView() { }); } + // Tag filter input + const tagFilterInput = document.getElementById('graph-tag-filter'); + if (tagFilterInput) { + let tagDebounce; + tagFilterInput.addEventListener('input', () => { + clearTimeout(tagDebounce); + tagDebounce = setTimeout(() => gm.setTagFilter(tagFilterInput.value.trim()), 400); + }); + } + // Export PNG const exportBtn = document.getElementById('graph-export'); if (exportBtn) exportBtn.addEventListener('click', () => gm.exportPNG());