gioser-graph: fix edge UUID mapping, index 4 docs, unify page bg

- Fix: map positions by node.id (UUID) not doc_id — edges now draw
- Index the 4 docs/ files into Qdrant (15 fragments via index-gioser-docs.py)
- Page background: single smooth radial-gradient per element (no color
  divisions), animated 'page-breathe' — opacity pulses 0.35↔0.80
- Graph CSS: 'graph-breathe' 5s opacity animation (feels alive)
This commit is contained in:
Sergio
2026-05-23 15:40:42 +00:00
parent b5032de1e3
commit fa2bedf851
6 changed files with 105 additions and 191 deletions
+8 -8
View File
@@ -8,14 +8,14 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
export interface InitOutput { export interface InitOutput {
readonly memory: WebAssembly.Memory; readonly memory: WebAssembly.Memory;
readonly boot: () => void; readonly boot: () => void;
readonly __wasm_bindgen_func_elem_219: (a: number, b: number, c: number) => void; readonly __wasm_bindgen_func_elem_216: (a: number, b: number, c: number) => void;
readonly __wasm_bindgen_func_elem_1408: (a: number, b: number, c: number, d: number) => void; readonly __wasm_bindgen_func_elem_1404: (a: number, b: number, c: number, d: number) => void;
readonly __wasm_bindgen_func_elem_218: (a: number, b: number, c: number) => void; readonly __wasm_bindgen_func_elem_215: (a: number, b: number, c: number) => void;
readonly __wasm_bindgen_func_elem_218_3: (a: number, b: number, c: number) => void; readonly __wasm_bindgen_func_elem_215_3: (a: number, b: number, c: number) => void;
readonly __wasm_bindgen_func_elem_496: (a: number, b: number, c: number) => void; readonly __wasm_bindgen_func_elem_492: (a: number, b: number, c: number) => void;
readonly __wasm_bindgen_func_elem_602: (a: number, b: number, c: number) => void; readonly __wasm_bindgen_func_elem_598: (a: number, b: number, c: number) => void;
readonly __wasm_bindgen_func_elem_289: (a: number, b: number, c: number) => void; readonly __wasm_bindgen_func_elem_285: (a: number, b: number, c: number) => void;
readonly __wasm_bindgen_func_elem_288: (a: number, b: number) => void; readonly __wasm_bindgen_func_elem_284: (a: number, b: number) => void;
readonly __wbindgen_export: (a: number, b: number) => number; readonly __wbindgen_export: (a: number, b: number) => number;
readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_export3: (a: number) => void; readonly __wbindgen_export3: (a: number) => void;
+24 -24
View File
@@ -571,42 +571,42 @@ function __wbg_get_imports() {
}, },
__wbindgen_cast_0000000000000001: function(arg0, arg1) { __wbindgen_cast_0000000000000001: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 179, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 179, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_1408); const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_1404);
return addHeapObject(ret); return addHeapObject(ret);
}, },
__wbindgen_cast_0000000000000002: function(arg0, arg1) { __wbindgen_cast_0000000000000002: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [F64], shim_idx: 2, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [F64], shim_idx: 2, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_219); const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_216);
return addHeapObject(ret); return addHeapObject(ret);
}, },
__wbindgen_cast_0000000000000003: function(arg0, arg1) { __wbindgen_cast_0000000000000003: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 6, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 6, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_218); const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_215);
return addHeapObject(ret); return addHeapObject(ret);
}, },
__wbindgen_cast_0000000000000004: function(arg0, arg1) { __wbindgen_cast_0000000000000004: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 6, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 6, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_218_3); const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_215_3);
return addHeapObject(ret); return addHeapObject(ret);
}, },
__wbindgen_cast_0000000000000005: function(arg0, arg1) { __wbindgen_cast_0000000000000005: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("MouseEvent")], shim_idx: 137, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("MouseEvent")], shim_idx: 137, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_496); const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_492);
return addHeapObject(ret); return addHeapObject(ret);
}, },
__wbindgen_cast_0000000000000006: function(arg0, arg1) { __wbindgen_cast_0000000000000006: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("MouseEvent")], shim_idx: 173, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("MouseEvent")], shim_idx: 173, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_602); const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_598);
return addHeapObject(ret); return addHeapObject(ret);
}, },
__wbindgen_cast_0000000000000007: function(arg0, arg1) { __wbindgen_cast_0000000000000007: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PointerEvent")], shim_idx: 67, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PointerEvent")], shim_idx: 67, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_289); const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_285);
return addHeapObject(ret); return addHeapObject(ret);
}, },
__wbindgen_cast_0000000000000008: function(arg0, arg1) { __wbindgen_cast_0000000000000008: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 69, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 69, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_288); const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_284);
return addHeapObject(ret); return addHeapObject(ret);
}, },
__wbindgen_cast_0000000000000009: function(arg0, arg1) { __wbindgen_cast_0000000000000009: function(arg0, arg1) {
@@ -633,34 +633,34 @@ function __wbg_get_imports() {
}; };
} }
function __wasm_bindgen_func_elem_288(arg0, arg1) { function __wasm_bindgen_func_elem_284(arg0, arg1) {
wasm.__wasm_bindgen_func_elem_288(arg0, arg1); wasm.__wasm_bindgen_func_elem_284(arg0, arg1);
} }
function __wasm_bindgen_func_elem_218(arg0, arg1, arg2) { function __wasm_bindgen_func_elem_215(arg0, arg1, arg2) {
wasm.__wasm_bindgen_func_elem_218(arg0, arg1, addHeapObject(arg2)); wasm.__wasm_bindgen_func_elem_215(arg0, arg1, addHeapObject(arg2));
} }
function __wasm_bindgen_func_elem_218_3(arg0, arg1, arg2) { function __wasm_bindgen_func_elem_215_3(arg0, arg1, arg2) {
wasm.__wasm_bindgen_func_elem_218_3(arg0, arg1, addHeapObject(arg2)); wasm.__wasm_bindgen_func_elem_215_3(arg0, arg1, addHeapObject(arg2));
} }
function __wasm_bindgen_func_elem_496(arg0, arg1, arg2) { function __wasm_bindgen_func_elem_492(arg0, arg1, arg2) {
wasm.__wasm_bindgen_func_elem_496(arg0, arg1, addHeapObject(arg2)); wasm.__wasm_bindgen_func_elem_492(arg0, arg1, addHeapObject(arg2));
} }
function __wasm_bindgen_func_elem_602(arg0, arg1, arg2) { function __wasm_bindgen_func_elem_598(arg0, arg1, arg2) {
wasm.__wasm_bindgen_func_elem_602(arg0, arg1, addHeapObject(arg2)); wasm.__wasm_bindgen_func_elem_598(arg0, arg1, addHeapObject(arg2));
} }
function __wasm_bindgen_func_elem_289(arg0, arg1, arg2) { function __wasm_bindgen_func_elem_285(arg0, arg1, arg2) {
wasm.__wasm_bindgen_func_elem_289(arg0, arg1, addHeapObject(arg2)); wasm.__wasm_bindgen_func_elem_285(arg0, arg1, addHeapObject(arg2));
} }
function __wasm_bindgen_func_elem_1408(arg0, arg1, arg2) { function __wasm_bindgen_func_elem_1404(arg0, arg1, arg2) {
try { try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.__wasm_bindgen_func_elem_1408(retptr, arg0, arg1, addHeapObject(arg2)); wasm.__wasm_bindgen_func_elem_1404(retptr, arg0, arg1, addHeapObject(arg2));
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
if (r1) { if (r1) {
@@ -671,8 +671,8 @@ function __wasm_bindgen_func_elem_1408(arg0, arg1, arg2) {
} }
} }
function __wasm_bindgen_func_elem_219(arg0, arg1, arg2) { function __wasm_bindgen_func_elem_216(arg0, arg1, arg2) {
wasm.__wasm_bindgen_func_elem_219(arg0, arg1, arg2); wasm.__wasm_bindgen_func_elem_216(arg0, arg1, arg2);
} }
function addHeapObject(obj) { function addHeapObject(obj) {
Binary file not shown.
+8 -8
View File
@@ -2,14 +2,14 @@
/* eslint-disable */ /* eslint-disable */
export const memory: WebAssembly.Memory; export const memory: WebAssembly.Memory;
export const boot: () => void; export const boot: () => void;
export const __wasm_bindgen_func_elem_219: (a: number, b: number, c: number) => void; export const __wasm_bindgen_func_elem_216: (a: number, b: number, c: number) => void;
export const __wasm_bindgen_func_elem_1408: (a: number, b: number, c: number, d: number) => void; export const __wasm_bindgen_func_elem_1404: (a: number, b: number, c: number, d: number) => void;
export const __wasm_bindgen_func_elem_218: (a: number, b: number, c: number) => void; export const __wasm_bindgen_func_elem_215: (a: number, b: number, c: number) => void;
export const __wasm_bindgen_func_elem_218_3: (a: number, b: number, c: number) => void; export const __wasm_bindgen_func_elem_215_3: (a: number, b: number, c: number) => void;
export const __wasm_bindgen_func_elem_496: (a: number, b: number, c: number) => void; export const __wasm_bindgen_func_elem_492: (a: number, b: number, c: number) => void;
export const __wasm_bindgen_func_elem_602: (a: number, b: number, c: number) => void; export const __wasm_bindgen_func_elem_598: (a: number, b: number, c: number) => void;
export const __wasm_bindgen_func_elem_289: (a: number, b: number, c: number) => void; export const __wasm_bindgen_func_elem_285: (a: number, b: number, c: number) => void;
export const __wasm_bindgen_func_elem_288: (a: number, b: number) => void; export const __wasm_bindgen_func_elem_284: (a: number, b: number) => void;
export const __wbindgen_export: (a: number, b: number) => number; export const __wbindgen_export: (a: number, b: number) => number;
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number; export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_export3: (a: number) => void; export const __wbindgen_export3: (a: number) => void;
+12 -34
View File
@@ -231,49 +231,27 @@ body.deck-active-tierra .deck { --deck-glow: rgba(212, 152, 115, 0.24); }
inset: 0; inset: 0;
pointer-events: none; pointer-events: none;
z-index: 0; z-index: 0;
transition: opacity 2s ease;
} }
.deck-page[data-element="aire"] .page-ambience { .deck-page[data-element="aire"] .page-ambience {
background: background: radial-gradient(circle at 50% 60%, rgba(208,219,255,0.18), transparent 70%);
radial-gradient(circle at 18% 22%, rgba(208, 219, 255, 0.20), transparent 38%), animation: page-breathe 8s ease-in-out infinite alternate;
radial-gradient(circle at 78% 68%, rgba(208, 219, 255, 0.14), transparent 40%),
radial-gradient(circle at 45% 90%, rgba(180, 200, 255, 0.10), transparent 45%);
animation: aire-drift 45s ease-in-out infinite alternate;
} }
.deck-page[data-element="fuego"] .page-ambience { .deck-page[data-element="fuego"] .page-ambience {
background: background: radial-gradient(circle at 50% 80%, rgba(245,144,86,0.22), transparent 70%);
radial-gradient(circle at 50% 100%, rgba(245, 144, 86, 0.35), transparent 55%), animation: page-breathe 6s ease-in-out infinite alternate;
radial-gradient(circle at 25% 80%, rgba(255, 90, 40, 0.18), transparent 35%),
radial-gradient(circle at 80% 85%, rgba(255, 140, 60, 0.18), transparent 35%);
animation: fuego-flicker 12s ease-in-out infinite;
} }
.deck-page[data-element="agua"] .page-ambience { .deck-page[data-element="agua"] .page-ambience {
background: background: radial-gradient(circle at 50% 80%, rgba(108,208,243,0.18), transparent 70%);
radial-gradient(ellipse at 50% 95%, rgba(60, 160, 230, 0.30), transparent 60%), animation: page-breathe 10s ease-in-out infinite alternate;
radial-gradient(ellipse at 20% 70%, rgba(108, 208, 243, 0.15), transparent 50%),
radial-gradient(ellipse at 80% 75%, rgba(108, 208, 243, 0.12), transparent 50%);
animation: agua-tide 30s ease-in-out infinite alternate;
} }
.deck-page[data-element="tierra"] .page-ambience { .deck-page[data-element="tierra"] .page-ambience {
background: background: radial-gradient(circle at 50% 85%, rgba(140,100,60,0.22), transparent 70%);
radial-gradient(ellipse at 50% 100%, rgba(120, 80, 40, 0.40), transparent 60%), animation: page-breathe 7s ease-in-out infinite alternate;
radial-gradient(ellipse at 22% 88%, rgba(180, 130, 80, 0.20), transparent 45%),
radial-gradient(ellipse at 78% 88%, rgba(150, 100, 60, 0.22), transparent 45%);
} }
/* Animaciones sutiles — solo opacidad, sin desplazamiento visible. @keyframes page-breathe {
El movimiento (transform) del fondo causaba un "respiro" molesto from { opacity: 0.35; }
al abrir el deck. */ to { opacity: 0.80; }
@keyframes aire-drift {
from { opacity: 0.60; }
to { opacity: 0.90; }
}
@keyframes fuego-flicker {
0%, 100% { opacity: 0.65; }
35% { opacity: 0.90; }
60% { opacity: 0.75; }
}
@keyframes agua-tide {
from { opacity: 0.55; }
to { opacity: 0.85; }
} }
/* Head + controls */ /* Head + controls */
+40 -104
View File
@@ -1,11 +1,11 @@
//! `gioser-graph-web` — widget de grafo semántico SVG inline. //! `gioser-graph-web` — grafo semántico SVG inline.
//! //!
//! Layout grid: 3 columnas, filas según la cantidad de nodos. //! Layout grid: 3 columnas, filas según cantidad de nodos.
//! Los nodos son rectángulos redondeados con texto + subtexto (camino). //! Nodos: rectángulos redondeados 170×44px con texto + subtexto (camino).
//! Aristas se dibujan entre todos los pares con líneas semitransparentes: //! Aristas: opacidad/brillo según weight (más peso = más blanca y opaca).
//! mientras mayor el weight, más opaca y brillante la línea. //! Respiración CSS suave en el SVG (opacity oscila perpetua).
//! Animación CSS de respiración suave en el SVG.
//! Hover: glow + opacidad. //! Hover: glow + opacidad.
//! Las aristas conectan por ID (UUID), no por doc_id.
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
@@ -43,8 +43,6 @@ struct NodeData {
name: String, name: String,
camino: String, camino: String,
doc_id: Option<String>, doc_id: Option<String>,
chunk: Option<u32>,
tags: Option<Vec<String>>,
#[allow(dead_code)] #[allow(dead_code)]
preview: Option<String>, preview: Option<String>,
} }
@@ -76,8 +74,8 @@ type NavCallback = Rc<RefCell<Option<Box<dyn FnMut(String)>>>>;
const CANVAS_W: f64 = 800.0; const CANVAS_W: f64 = 800.0;
const CANVAS_H: f64 = 420.0; const CANVAS_H: f64 = 420.0;
const NODE_W: f64 = 170.0; // más grandes const NODE_W: f64 = 170.0;
const NODE_H: f64 = 44.0; // más grandes const NODE_H: f64 = 44.0;
const COLS: usize = 3; const COLS: usize = 3;
const CAMINO_COLORS: &[(&str, &str)] = &[ const CAMINO_COLORS: &[(&str, &str)] = &[
@@ -94,26 +92,6 @@ fn camino_color(camino: &str) -> &str {
"#888888" "#888888"
} }
fn weight_alpha(w: Option<f64>) -> f64 {
// weight 0.5 → 0.40, weight 1.0 → 0.85
match w {
Some(v) => 0.40 + (v - 0.5).max(0.0) * 0.9,
None => 0.35,
}
.clamp(0.15, 0.95)
}
fn weight_stroke_color(w: Option<f64>, base: &str) -> String {
let alpha = weight_alpha(w);
// Extraer color base, añadir alpha
// Asumimos formato #rrggbb
if base.len() >= 7 && alpha < 1.0 {
format!("{}", base) // se sobreescribe con stroke-opacity
} else {
base.to_string()
}
}
pub struct GraphWidget { pub struct GraphWidget {
container: HtmlElement, container: HtmlElement,
api_url: String, api_url: String,
@@ -147,7 +125,6 @@ impl GraphWidget {
pub async fn load(&mut self) -> Result<(), JsValue> { pub async fn load(&mut self) -> Result<(), JsValue> {
let url = format!("{}/graph?limit=500", self.api_url); let url = format!("{}/graph?limit=500", self.api_url);
let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?; let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
let resp_value = JsFuture::from(window.fetch_with_str(&url)).await?; let resp_value = JsFuture::from(window.fetch_with_str(&url)).await?;
let resp: Response = resp_value.dyn_into()?; let resp: Response = resp_value.dyn_into()?;
if !resp.ok() { if !resp.ok() {
@@ -159,17 +136,11 @@ impl GraphWidget {
let graph: GraphResponse = let graph: GraphResponse =
serde_json::from_str(&body).map_err(|e| JsValue::from_str(&format!("JSON: {e}")))?; serde_json::from_str(&body).map_err(|e| JsValue::from_str(&format!("JSON: {e}")))?;
let nodes: Vec<NodeData> = graph let nodes: Vec<NodeData> = graph.nodes.into_iter().map(|n| n.data).collect();
.nodes
.into_iter()
.map(|n| n.data)
.filter(|n| n.doc_id.is_some())
.collect();
let edges: Vec<EdgeData> = graph.edges.into_iter().map(|e| e.data).collect(); let edges: Vec<EdgeData> = graph.edges.into_iter().map(|e| e.data).collect();
self.nodes = nodes; self.nodes = nodes;
self.edges = edges; self.edges = edges;
self.render(); self.render();
Ok(()) Ok(())
} }
@@ -192,7 +163,6 @@ impl GraphWidget {
.unwrap(); .unwrap();
svg.set_attribute("viewBox", &format!("0 0 {} {}", CANVAS_W as u32, CANVAS_H as u32)).ok(); svg.set_attribute("viewBox", &format!("0 0 {} {}", CANVAS_W as u32, CANVAS_H as u32)).ok();
svg.set_attribute("width", "100%").ok(); svg.set_attribute("width", "100%").ok();
svg.set_attribute("height", &format!("{}px", CANVAS_H as u32)).ok();
svg.set_attribute("preserveAspectRatio", "xMidYMid meet").ok(); svg.set_attribute("preserveAspectRatio", "xMidYMid meet").ok();
svg.style().set_property("display", "block").ok(); svg.style().set_property("display", "block").ok();
svg.style().set_property("margin", "1.5rem auto 0").ok(); svg.style().set_property("margin", "1.5rem auto 0").ok();
@@ -202,33 +172,20 @@ impl GraphWidget {
svg.style().set_property("border-radius", "12px").ok(); svg.style().set_property("border-radius", "12px").ok();
svg.style().set_property("border", "1px solid rgba(216,168,93,0.15)").ok(); svg.style().set_property("border", "1px solid rgba(216,168,93,0.15)").ok();
// Estilo para animación de respiración en el SVG // Estilo inline en SVG: respiración y transiciones
// Se añade un <style> dentro del SVG
let style_el = self.document.create_element_ns(Some(ns), "style").unwrap(); let style_el = self.document.create_element_ns(Some(ns), "style").unwrap();
style_el.set_text_content(Some( style_el.set_text_content(Some(
"@keyframes graph-breathe {\ "@keyframes graph-breathe {\
0%, 100% { opacity: 1; }\ 0%, 100% { opacity: 1; }\
50% { opacity: 0.94; }\ 50% { opacity: 0.92; }\
}\ }\
.gb-svg {\ .gb-svg { animation: graph-breathe 5s ease-in-out infinite; }\
animation: graph-breathe 4.2s ease-in-out infinite;\ .gb-node { transition: filter 250ms ease, opacity 200ms ease; }\
}\ .gb-node:hover { filter: drop-shadow(0 0 14px rgba(255,255,255,0.2)); }\
.gb-node {\ .gb-line { transition: opacity 400ms ease; }",
transition: filter 250ms ease, opacity 200ms ease;\
}\
.gb-node:hover {\
filter: drop-shadow(0 0 14px rgba(255,255,255,0.2));\
}\
.gb-line {\
transition: opacity 300ms ease;\
}\
.gb-line:hover {\
opacity: 0.9 !important;\
}",
)); ));
svg.append_child(&style_el).ok(); svg.append_child(&style_el).ok();
// Grupo con animación de respiración
let breathe_group: web_sys::SvgElement = self let breathe_group: web_sys::SvgElement = self
.document .document
.create_element_ns(Some(ns), "g") .create_element_ns(Some(ns), "g")
@@ -237,20 +194,18 @@ impl GraphWidget {
.unwrap(); .unwrap();
breathe_group.set_attribute("class", "gb-svg").ok(); breathe_group.set_attribute("class", "gb-svg").ok();
// Mapa: doc_id → (x, y) // Mapa: node.id → (x, y) — usamos UUID, no doc_id
let pos_map: std::collections::HashMap<&str, (f64, f64)> = positions let pos_map: std::collections::HashMap<&str, (f64, f64)> = positions
.iter() .iter()
.map(|(id, p)| (id.as_str(), *p)) .map(|(id, p)| (id.as_str(), *p))
.collect(); .collect();
// Calcular max weight para normalizar
let max_w = self.edges.iter() let max_w = self.edges.iter()
.filter_map(|e| e.weight) .filter_map(|e| e.weight)
.fold(0.0_f64, f64::max) .fold(0.0_f64, f64::max)
.max(0.5); .max(0.5);
// ── Aristas ── // ── Aristas ──
// Agrupar por par no dirigido (source, target) para no duplicar
let mut drawn = std::collections::HashSet::new(); let mut drawn = std::collections::HashSet::new();
for edge in &self.edges { for edge in &self.edges {
let key = if edge.source < edge.target { let key = if edge.source < edge.target {
@@ -260,29 +215,23 @@ impl GraphWidget {
}; };
if !drawn.insert(key) { continue; } if !drawn.insert(key) { continue; }
let src_pos = pos_map.get(edge.source.as_str()); let Some((x1, y1)) = pos_map.get(edge.source.as_str()) else { continue; };
let tgt_pos = pos_map.get(edge.target.as_str()); let Some((x2, y2)) = pos_map.get(edge.target.as_str()) else { continue; };
if let (Some((x1, y1)), Some((x2, y2))) = (src_pos, tgt_pos) {
let w = edge.weight.unwrap_or(0.7);
let norm_w = (w / max_w).clamp(0.0, 1.0);
let alpha = 0.15 + norm_w * 0.70;
let sw = 1.0 + norm_w * 4.0;
let r = (255.0 - (1.0 - norm_w) * 80.0) as u32;
let g = (255.0 - (1.0 - norm_w) * 60.0) as u32;
let b = (255.0 - (1.0 - norm_w) * 40.0) as u32;
let line: SvgLineElement = self let line: SvgLineElement = self
.document .document
.create_element_ns(Some(ns), "line") .create_element_ns(Some(ns), "line")
.unwrap() .unwrap()
.dyn_into() .dyn_into()
.unwrap(); .unwrap();
// Calcular opacidad basada en weight
let w = edge.weight.unwrap_or(0.7);
let norm_w = (w / max_w).clamp(0.0, 1.0);
let alpha = 0.15 + norm_w * 0.70; // 0.15 a 0.85
// Calcular grosor: 1 a 5 según weight normalizado
let sw = 1.0 + norm_w * 4.0;
// Color: más brillante = más blanco
let r = (255.0 - (1.0 - norm_w) * 80.0) as u32;
let g = (255.0 - (1.0 - norm_w) * 60.0) as u32;
let b = (255.0 - (1.0 - norm_w) * 40.0) as u32;
line.set_attribute("x1", &format!("{:.1}", x1)).ok(); line.set_attribute("x1", &format!("{:.1}", x1)).ok();
line.set_attribute("y1", &format!("{:.1}", y1)).ok(); line.set_attribute("y1", &format!("{:.1}", y1)).ok();
line.set_attribute("x2", &format!("{:.1}", x2)).ok(); line.set_attribute("x2", &format!("{:.1}", x2)).ok();
@@ -291,10 +240,8 @@ impl GraphWidget {
line.set_attribute("stroke-width", &format!("{:.1}", sw)).ok(); line.set_attribute("stroke-width", &format!("{:.1}", sw)).ok();
line.set_attribute("stroke-opacity", &format!("{:.2}", alpha)).ok(); line.set_attribute("stroke-opacity", &format!("{:.2}", alpha)).ok();
line.set_attribute("class", "gb-line").ok(); line.set_attribute("class", "gb-line").ok();
breathe_group.append_child(&line).ok(); breathe_group.append_child(&line).ok();
} }
}
// ── Nodos ── // ── Nodos ──
let on_nav = self.on_navigate.clone(); let on_nav = self.on_navigate.clone();
@@ -321,7 +268,6 @@ impl GraphWidget {
let rx = cx - NODE_W / 2.0; let rx = cx - NODE_W / 2.0;
let ry = cy - NODE_H / 2.0; let ry = cy - NODE_H / 2.0;
// Sombra suave de fondo (círculo/glow detrás del rect)
let glow: SvgCircleElement = self let glow: SvgCircleElement = self
.document .document
.create_element_ns(Some(ns), "circle") .create_element_ns(Some(ns), "circle")
@@ -330,9 +276,9 @@ impl GraphWidget {
.unwrap(); .unwrap();
glow.set_attribute("cx", &format!("{:.1}", cx)).ok(); glow.set_attribute("cx", &format!("{:.1}", cx)).ok();
glow.set_attribute("cy", &format!("{:.1}", cy)).ok(); glow.set_attribute("cy", &format!("{:.1}", cy)).ok();
glow.set_attribute("r", "30").ok(); glow.set_attribute("r", "32").ok();
glow.set_attribute("fill", &color).ok(); glow.set_attribute("fill", &color).ok();
glow.set_attribute("fill-opacity", "0.06").ok(); glow.set_attribute("fill-opacity", "0.05").ok();
g.append_child(&glow).ok(); g.append_child(&glow).ok();
let rect: SvgRectElement = self let rect: SvgRectElement = self
@@ -390,15 +336,14 @@ impl GraphWidget {
g.append_child(&text).ok(); g.append_child(&text).ok();
g.append_child(&sub).ok(); g.append_child(&sub).ok();
// Hover glow // Hover
let rect_clone = rect.clone(); let rect_clone = rect.clone();
let color_c = color.clone(); let color_c = color.clone();
let glow_clone = glow.clone(); let glow_clone = glow.clone();
let enter = Closure::<dyn FnMut(MouseEvent)>::new(move |_| { let enter = Closure::<dyn FnMut(MouseEvent)>::new(move |_| {
rect_clone.set_attribute("fill-opacity", "0.55").ok(); rect_clone.set_attribute("fill-opacity", "0.55").ok();
rect_clone.set_attribute("stroke-opacity", "1").ok(); rect_clone.set_attribute("stroke-opacity", "1").ok();
rect_clone rect_clone.style()
.style()
.set_property("filter", &format!("drop-shadow(0 0 12px {})", color_c)) .set_property("filter", &format!("drop-shadow(0 0 12px {})", color_c))
.ok(); .ok();
glow_clone.set_attribute("fill-opacity", "0.20").ok(); glow_clone.set_attribute("fill-opacity", "0.20").ok();
@@ -412,7 +357,7 @@ impl GraphWidget {
rect_clone2.set_attribute("fill-opacity", "0.28").ok(); rect_clone2.set_attribute("fill-opacity", "0.28").ok();
rect_clone2.set_attribute("stroke-opacity", "0.7").ok(); rect_clone2.set_attribute("stroke-opacity", "0.7").ok();
rect_clone2.style().set_property("filter", "none").ok(); rect_clone2.style().set_property("filter", "none").ok();
glow_clone2.set_attribute("fill-opacity", "0.06").ok(); glow_clone2.set_attribute("fill-opacity", "0.05").ok();
}); });
g.add_event_listener_with_callback("mouseleave", leave.as_ref().unchecked_ref()).ok(); g.add_event_listener_with_callback("mouseleave", leave.as_ref().unchecked_ref()).ok();
leave.forget(); leave.forget();
@@ -434,41 +379,32 @@ impl GraphWidget {
} }
} }
// ─── Layout grid: 3 columnas, filas según número de nodos ────────── // ─── Layout grid: 3 columnas ─────────────────────────────────────
fn grid_layout(nodes: &[NodeData], w: f64, h: f64) -> Vec<(String, (f64, f64))> { fn grid_layout(nodes: &[NodeData], w: f64, h: f64) -> Vec<(String, (f64, f64))> {
let n = nodes.len(); let n = nodes.len();
if n == 0 { return vec![]; } if n == 0 { return vec![]; }
let rows = (n + COLS - 1) / COLS; // ceiling div let rows = (n + COLS - 1) / COLS;
let actual_rows = rows.max(3); // mínimo 3 filas visuales let actual_rows = rows.max(3);
let margin_x = NODE_W / 2.0 + 20.0; let margin_x = NODE_W / 2.0 + 20.0;
let margin_y = NODE_H / 2.0 + 16.0; let margin_y = NODE_H / 2.0 + 20.0;
let usable_w = w - margin_x * 2.0; let usable_w = w - margin_x * 2.0;
let usable_h = h - margin_y * 2.0; let usable_h = h - margin_y * 2.0;
let col_gap = usable_w / (COLS as f64); let col_gap = usable_w / (COLS as f64);
let row_gap = usable_h / (actual_rows as f64); let row_gap = usable_h / (actual_rows as f64);
let mut positions = Vec::with_capacity(n); let mut out = Vec::with_capacity(n);
for (i, node) in nodes.iter().enumerate() { for (i, node) in nodes.iter().enumerate() {
let col = i % COLS; let col = i % COLS;
let row = i / COLS; let row = i / COLS;
// Centrar si en última fila hay menos de 3
let offset_x = if row == rows - 1 && n % COLS != 0 { let offset_x = if row == rows - 1 && n % COLS != 0 {
// centrar fila
let remaining = n - row * COLS; let remaining = n - row * COLS;
(usable_w - remaining as f64 * col_gap) / 2.0 (usable_w - remaining as f64 * col_gap) / 2.0
} else { } else { 0.0 };
0.0
};
let x = margin_x + offset_x + col as f64 * col_gap + col_gap / 2.0; let x = margin_x + offset_x + col as f64 * col_gap + col_gap / 2.0;
let y = margin_y + row as f64 * row_gap + row_gap / 2.0; let y = margin_y + row as f64 * row_gap + row_gap / 2.0;
// Desplazar cada fila un tercio para dar sensación de expansión vertical out.push((node.id.clone(), (x, y)));
let y_adj = y; // ya está bien con el grid
positions.push((node.doc_id.clone().unwrap_or_default(), (x, y_adj)));
} }
out
positions
} }