tawasuyu

tawasuyu/brahman

Rust 202TOML 61JSON 7HTML 12471 Definiciones

gioser-web: revert Cytoscape, back to SVG gioser-graph-web

- Remove cytoscape-graph.js (wasn't working with hidden deck)
- Restore gioser-graph-web Rust SVG widget with gioser-web dep
- Rebuild WASM (514K)

7f7ba1fef93dc16c1596b26866abeceb3dc482f0 · por Sergio · (hace 5 meses) · padre 24218447e · ver el árbol en este commit

Delta semántico · 22

crates/apps/gioser-web/cytoscape-graph.js

crates/apps/gioser-web/pkg/gioser_web.d.ts

crates/apps/gioser-web/src/lib.rs

9 archivos · +217 −290

diff --git a/Cargo.lock b/Cargo.lock
index 512a4dd..bec4a72 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4776,6 +4776,7 @@ dependencies = [
  "barra-web",
  "fana-md-reader-web",
  "gioser-canvas-web",
+ "gioser-graph-web",
  "js-sys",
  "revista-web",
  "wasm-bindgen",
diff --git a/crates/apps/gioser-web/Cargo.toml b/crates/apps/gioser-web/Cargo.toml
index c9d0cf6..d537ae2 100644
--- a/crates/apps/gioser-web/Cargo.toml
+++ b/crates/apps/gioser-web/Cargo.toml
@@ -14,6 +14,7 @@ gioser-canvas-web = { path = "../../modules/gioser/gioser-canvas-web" }
 fana-md-reader-web = { path = "../../modules/fana/fana-md-reader-web" }
 revista-web = { path = "../../modules/revista/revista-web" }
 barra-web = { path = "../../modules/barra/barra-web" }
+gioser-graph-web = { path = "../../modules/gioser/gioser-graph-web" }
 wasm-bindgen.workspace = true
 wasm-bindgen-futures.workspace = true
 js-sys.workspace = true
diff --git a/crates/apps/gioser-web/cytoscape-graph.js b/crates/apps/gioser-web/cytoscape-graph.js
deleted file mode 100644
index 9df419e..0000000
--- a/crates/apps/gioser-web/cytoscape-graph.js
+++ /dev/null
@@ -1,226 +0,0 @@
-/**
- * gioser-graph.js — Grafo semántico con Cytoscape.js
- *
- * Detecta automáticamente elementos <gioser-graph> agregados al DOM
- * incluso los creados dinámicamente por el WASM.
- *
- * Espera a que el contenedor tenga tamaño real (>0) antes de montar
- * Cytoscape, porque el deck arranca oculto (transform:scale(0)).
- *
- * Efecto "wineandcheesemap": clic en nodo → centra + desvanece resto.
- * Doble clic → callback de navegación (window.__gioserGraphNavigate).
- */
-
-(function () {
-  'use strict';
-
-  var COLORS = {
-    logos:  '#d0dbff', aire:   '#d0dbff',
-    nomos:  '#f59056', fuego:  '#f59056',
-    kay:    '#d49873', tierra: '#d49873',
-    uku:    '#6cd0f3', agua:   '#6cd0f3',
-  };
-  function caminoColor(c) { return COLORS[c] || '#888'; }
-
-  function initGraph(container) {
-    var apiUrl = container.getAttribute('data-api-url') || 'https://api.gioser.net';
-    var onNavigate = window.__gioserGraphNavigate || null;
-
-    if (typeof cytoscape === 'undefined') {
-      var chk = setInterval(function () {
-        if (typeof cytoscape !== 'undefined') { clearInterval(chk); initGraph(container); }
-      }, 100);
-      return;
-    }
-
-    // Esperar a que el contenedor tenga ancho real (deck oculto → 0)
-    var wait = setInterval(function () {
-      if (container.offsetWidth > 0 || container.offsetHeight > 0) {
-        clearInterval(wait);
-        doInit(container, apiUrl, onNavigate);
-      }
-    }, 50);
-
-    // Timeout: si después de 10s sigue en 0, forzar con tamaño mínimo
-    setTimeout(function () {
-      if (container.offsetWidth === 0 || container.offsetHeight === 0) {
-        clearInterval(wait);
-        container.style.minWidth = '200px';
-        container.style.minHeight = '180px';
-        doInit(container, apiUrl, onNavigate);
-      }
-    }, 10000);
-  }
-
-  function doInit(container, apiUrl, onNavigate) {
-    fetch(apiUrl + '/graph?limit=500')
-      .then(function (r) { return r.json(); })
-      .then(function (data) {
-        if (!data.nodes || data.nodes.length === 0) {
-          container.innerHTML = '';
-          return;
-        }
-
-        var elements = [];
-        for (var i = 0; i < data.nodes.length; i++) {
-          var d = data.nodes[i].data;
-          if (!d.doc_id) continue;
-          var color = caminoColor(d.camino);
-          elements.push({ group: 'nodes', data: {
-            id: d.id, doc_id: d.doc_id,
-            label: d.name.length > 24 ? d.name.slice(0, 22) + '\u2026' : d.name,
-            camino: d.camino.toUpperCase(), color: color,
-          }});
-        }
-
-        var nodeIds = {};
-        for (var i = 0; i < elements.length; i++) nodeIds[elements[i].data.id] = true;
-
-        for (var i = 0; i < data.edges.length; i++) {
-          var ed = data.edges[i].data;
-          if (!nodeIds[ed.source] || !nodeIds[ed.target]) continue;
-          elements.push({ group: 'edges', data: {
-            id: ed.id, source: ed.source, target: ed.target,
-            weight: ed.weight || 0.7,
-          }});
-        }
-
-        var tipMap = {};
-        for (var i = 0; i < data.nodes.length; i++) {
-          var d = data.nodes[i].data;
-          if (d.doc_id) tipMap[d.id] = d;
-        }
-
-        var cy = cytoscape({
-          container: container,
-          elements: elements,
-          style: [
-            { selector: 'edge', style: {
-              'width': 'mapData(weight, 0.5, 1.0, 0.5, 4.5)',
-              'line-color': 'rgba(255,255,255,0.16)',
-              'curve-style': 'haystack', 'haystack-radius': 0,
-              'opacity': 0.6,
-            }},
-            { selector: 'node', style: {
-              'shape': 'round-rectangle',
-              'width': 130, 'height': 34,
-              'background-color': 'data(color)', 'background-opacity': 0.18,
-              'border-color': 'data(color)', 'border-width': 1.8, 'border-opacity': 0.55,
-              'color': 'rgba(232,234,245,0.88)',
-              'font-size': 11, 'font-family': 'Inter, system-ui, sans-serif',
-              'font-weight': 500,
-              'text-valign': 'center', 'text-halign': 'center',
-              'label': 'data(label)', 'min-zoomed-font-size': 8,
-              'transition-property': 'background-opacity, border-opacity, border-width',
-              'transition-duration': 180,
-            }},
-          ],
-          layout: { name: 'preset', fit: false },
-        });
-
-        cy.layout({
-          name: 'cose',
-          animate: 'end',
-          animationEasing: 'ease-out',
-          animationDuration: 600,
-          idealEdgeLength: 150,
-          nodeRepulsion: 7000,
-          gravity: 0.2,
-          numIter: 800,
-          fit: true,
-          padding: 25,
-        }).run();
-
-        // Tooltip
-        var tooltipEl = document.createElement('div');
-        tooltipEl.className = 'cy-tooltip';
-        tooltipEl.style.cssText =
-          'position:absolute;z-index:10;pointer-events:none;' +
-          'background:rgba(6,5,13,0.90);color:#e8eaf5;' +
-          'padding:6px 10px;border-radius:8px;font-size:11px;' +
-          'font-family:Inter,sans-serif;line-height:1.4;' +
-          'border:1px solid rgba(255,255,255,0.10);' +
-          'backdrop-filter:blur(8px);max-width:220px;' +
-          'opacity:0;transition:opacity 180ms ease;';
-        container.style.position = 'relative';
-        container.appendChild(tooltipEl);
-
-        cy.on('mouseover', 'node', function (ev) {
-          var n = ev.target;
-          n.style({ 'background-opacity': 0.45, 'border-opacity': 0.9, 'border-width': 2.2 });
-          var t = tipMap[n.id()];
-          if (t && t.preview) {
-            tooltipEl.textContent = t.preview.slice(0, 130);
-            tooltipEl.style.opacity = '1';
-          }
-        });
-
-        cy.on('mouseout', 'node', function (ev) {
-          ev.target.style({ 'background-opacity': 0.18, 'border-opacity': 0.55, 'border-width': 1.8 });
-          tooltipEl.style.opacity = '0';
-        });
-
-        cy.on('mousemove', function (ev) {
-          if (tooltipEl.style.opacity === '1') {
-            var p = ev.renderedPosition || { x: 0, y: 0 };
-            tooltipEl.style.left = (p.x + 14) + 'px';
-            tooltipEl.style.top = (p.y - 10) + 'px';
-          }
-        });
-
-        cy.on('click', 'node', function (ev) {
-          var node = ev.target;
-          cy.nodes().not(node).not(node.neighborhood()).forEach(function (n) {
-            n.style({ 'opacity': 0.12 });
-          });
-          cy.edges().forEach(function (e) { e.style({ 'opacity': 0.06 }); });
-          node.neighborhood().nodes().forEach(function (n) { n.style({ 'opacity': 1 }); });
-          node.style({ 'opacity': 1, 'background-opacity': 0.40, 'border-opacity': 1 });
-          node.connectedEdges().forEach(function (e) { e.style({ 'opacity': 0.7 }); });
-          cy.animate({ center: { eles: node }, zoom: 2.5, duration: 350 });
-        });
-
-        cy.on('dblclick', 'node', function (ev) {
-          var docId = ev.target.data('doc_id');
-          if (onNavigate && docId) onNavigate(docId);
-        });
-
-        cy.on('click', function (ev) {
-          if (ev.target === cy) {
-            cy.nodes().forEach(function (n) {
-              n.style({ 'opacity': 1, 'background-opacity': 0.18, 'border-opacity': 0.55, 'border-width': 1.8 });
-            });
-            cy.edges().forEach(function (e) { e.style({ 'opacity': 0.6 }); });
-            cy.animate({ zoom: 1, pan: { x: 0, y: 0 }, duration: 300 });
-          }
-        });
-
-        var ro = new ResizeObserver(function () { cy.resize().fit(25); });
-        ro.observe(container);
-      })
-      .catch(function (err) {
-        console.warn('gioser-graph: error:', err);
-        container.innerHTML =
-          '<div style="padding:1rem;text-align:center;color:rgba(232,234,245,0.30);' +
-          'font-size:0.8rem;font-family:Inter,sans-serif;">' +
-          '· grafo no disponible ·</div>';
-      });
-  }
-
-  // MutationObserver
-  var observer = new MutationObserver(function (mutations) {
-    for (var m = 0; m < mutations.length; m++) {
-      var added = mutations[m].addedNodes;
-      for (var i = 0; i < added.length; i++) {
-        var el = added[i];
-        if (el.tagName && el.tagName.toLowerCase() === 'gioser-graph') { initGraph(el); }
-        var gs = el.querySelectorAll ? el.querySelectorAll('gioser-graph') : [];
-        for (var j = 0; j < gs.length; j++) { initGraph(gs[j]); }
-      }
-    }
-  });
-  observer.observe(document.documentElement, { childList: true, subtree: true });
-
-  var existing = document.querySelectorAll('gioser-graph');
-  for (var i = 0; i < existing.length; i++) { initGraph(existing[i]); }
-})();
diff --git a/crates/apps/gioser-web/index.html b/crates/apps/gioser-web/index.html
index f06dcff..9254583 100644
--- a/crates/apps/gioser-web/index.html
+++ b/crates/apps/gioser-web/index.html
@@ -99,9 +99,6 @@
     </a>
   </nav>
 
-  <!-- Cytoscape.js para el grafo semántico -->
-  <script src="https://cdn.jsdelivr.net/npm/cytoscape@3.30.4/dist/cytoscape.min.js"></script>
-
   <script type="module">
     import init from "./pkg/gioser_web.js";
     init().catch(err => {
@@ -111,8 +108,5 @@
         String(err) + '</pre>');
     });
   </script>
-
-  <!-- Grafo semántico (Cytoscape) — se auto-inicializa en <gioser-graph> -->
-  <script src="./cytoscape-graph.js" defer></script>
 </body>
 </html>
diff --git a/crates/apps/gioser-web/pkg/gioser_web.d.ts b/crates/apps/gioser-web/pkg/gioser_web.d.ts
index 7405bc0..d5a7121 100644
--- a/crates/apps/gioser-web/pkg/gioser_web.d.ts
+++ b/crates/apps/gioser-web/pkg/gioser_web.d.ts
@@ -8,13 +8,14 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
 export interface InitOutput {
     readonly memory: WebAssembly.Memory;
     readonly boot: () => void;
-    readonly __wasm_bindgen_func_elem_175: (a: number, b: number, c: number) => void;
-    readonly __wasm_bindgen_func_elem_1203: (a: number, b: number, c: number, d: number) => void;
-    readonly __wasm_bindgen_func_elem_174: (a: number, b: number, c: number) => void;
-    readonly __wasm_bindgen_func_elem_174_3: (a: number, b: number, c: number) => void;
-    readonly __wasm_bindgen_func_elem_442: (a: number, b: number, c: number) => void;
-    readonly __wasm_bindgen_func_elem_219: (a: number, b: number, c: number) => void;
-    readonly __wasm_bindgen_func_elem_220: (a: number, b: number) => void;
+    readonly __wasm_bindgen_func_elem_218: (a: number, b: number, c: number) => void;
+    readonly __wasm_bindgen_func_elem_1398: (a: number, b: number, c: number, d: number) => void;
+    readonly __wasm_bindgen_func_elem_217: (a: number, b: number, c: number) => void;
+    readonly __wasm_bindgen_func_elem_217_3: (a: number, b: number, c: number) => void;
+    readonly __wasm_bindgen_func_elem_494: (a: number, b: number, c: number) => void;
+    readonly __wasm_bindgen_func_elem_593: (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_288: (a: number, b: number) => void;
     readonly __wbindgen_export: (a: number, b: number) => number;
     readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
     readonly __wbindgen_export3: (a: number) => void;
diff --git a/crates/apps/gioser-web/pkg/gioser_web.js b/crates/apps/gioser-web/pkg/gioser_web.js
index a257310..5a39956 100644
--- a/crates/apps/gioser-web/pkg/gioser_web.js
+++ b/crates/apps/gioser-web/pkg/gioser_web.js
@@ -11,6 +11,13 @@ function __wbg_get_imports() {
             const ret = typeof(v) === 'boolean' ? v : undefined;
             return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0;
         },
+        __wbg___wbindgen_debug_string_edece8177ad01481: function(arg0, arg1) {
+            const ret = debugString(getObject(arg1));
+            const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
+            const len1 = WASM_VECTOR_LEN;
+            getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
+            getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
+        },
         __wbg___wbindgen_is_function_5cd60d5cf78b4eef: function(arg0) {
             const ret = typeof(getObject(arg0)) === 'function';
             return ret;
@@ -118,6 +125,10 @@ function __wbg_get_imports() {
             const ret = getObject(arg0).createBuffer();
             return isLikeNone(ret) ? 0 : addHeapObject(ret);
         },
+        __wbg_createElementNS_10d5e4db26ea11c7: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
+            const ret = getObject(arg0).createElementNS(arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
+            return addHeapObject(ret);
+        }, arguments); },
         __wbg_createElement_d10771800cfb6e7e: function() { return handleError(function (arg0, arg1, arg2) {
             const ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2));
             return addHeapObject(ret);
@@ -286,6 +297,56 @@ function __wbg_get_imports() {
             const ret = result;
             return ret;
         },
+        __wbg_instanceof_SvgElement_46537942d3e1376d: function(arg0) {
+            let result;
+            try {
+                result = getObject(arg0) instanceof SVGElement;
+            } catch (_) {
+                result = false;
+            }
+            const ret = result;
+            return ret;
+        },
+        __wbg_instanceof_SvgLineElement_d8200164030c234d: function(arg0) {
+            let result;
+            try {
+                result = getObject(arg0) instanceof SVGLineElement;
+            } catch (_) {
+                result = false;
+            }
+            const ret = result;
+            return ret;
+        },
+        __wbg_instanceof_SvgRectElement_f5a06e74af743100: function(arg0) {
+            let result;
+            try {
+                result = getObject(arg0) instanceof SVGRectElement;
+            } catch (_) {
+                result = false;
+            }
+            const ret = result;
+            return ret;
+        },
+        __wbg_instanceof_SvgTextElement_06345cd3cc71c951: function(arg0) {
+            let result;
+            try {
+                result = getObject(arg0) instanceof SVGTextElement;
+            } catch (_) {
+                result = false;
+            }
+            const ret = result;
+            return ret;
+        },
+        __wbg_instanceof_SvgsvgElement_767ee4029321b850: function(arg0) {
+            let result;
+            try {
+                result = getObject(arg0) instanceof SVGSVGElement;
+            } catch (_) {
+                result = false;
+            }
+            const ret = result;
+            return ret;
+        },
         __wbg_instanceof_WebGl2RenderingContext_419098f7bf88e87e: function(arg0) {
             let result;
             try {
@@ -400,6 +461,9 @@ function __wbg_get_imports() {
         __wbg_set_passive_2233318ae781e44d: function(arg0, arg1) {
             getObject(arg0).passive = arg1 !== 0;
         },
+        __wbg_set_textContent_9c5d65d703443b6d: function(arg0, arg1, arg2) {
+            getObject(arg0).textContent = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2);
+        },
         __wbg_set_width_25112eb6bf1148df: function(arg0, arg1) {
             getObject(arg0).width = arg1 >>> 0;
         },
@@ -433,6 +497,10 @@ function __wbg_get_imports() {
             const ret = getObject(arg0).style;
             return addHeapObject(ret);
         },
+        __wbg_style_e34c1197b6088769: function(arg0) {
+            const ret = getObject(arg0).style;
+            return addHeapObject(ret);
+        },
         __wbg_target_84e05e84ffc12989: function(arg0) {
             const ret = getObject(arg0).target;
             return isLikeNone(ret) ? 0 : addHeapObject(ret);
@@ -492,46 +560,51 @@ function __wbg_get_imports() {
             return ret;
         },
         __wbindgen_cast_0000000000000001: function(arg0, arg1) {
-            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 128, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_1203);
+            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 176, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_1398);
             return addHeapObject(ret);
         },
         __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`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_175);
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_218);
             return addHeapObject(ret);
         },
         __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`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_174);
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_217);
             return addHeapObject(ret);
         },
         __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`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_174_3);
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_217_3);
             return addHeapObject(ret);
         },
         __wbindgen_cast_0000000000000005: function(arg0, arg1) {
-            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("MouseEvent")], shim_idx: 122, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_442);
+            // 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_494);
             return addHeapObject(ret);
         },
         __wbindgen_cast_0000000000000006: function(arg0, arg1) {
-            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PointerEvent")], shim_idx: 51, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_219);
+            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("MouseEvent")], shim_idx: 170, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_593);
             return addHeapObject(ret);
         },
         __wbindgen_cast_0000000000000007: function(arg0, arg1) {
-            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 53, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_220);
+            // 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);
             return addHeapObject(ret);
         },
         __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`.
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_288);
+            return addHeapObject(ret);
+        },
+        __wbindgen_cast_0000000000000009: function(arg0, arg1) {
             // Cast intrinsic for `Ref(Slice(F32)) -> NamedExternref("Float32Array")`.
             const ret = getArrayF32FromWasm0(arg0, arg1);
             return addHeapObject(ret);
         },
-        __wbindgen_cast_0000000000000009: function(arg0, arg1) {
+        __wbindgen_cast_000000000000000a: function(arg0, arg1) {
             // Cast intrinsic for `Ref(String) -> Externref`.
             const ret = getStringFromWasm0(arg0, arg1);
             return addHeapObject(ret);
@@ -550,30 +623,34 @@ function __wbg_get_imports() {
     };
 }
 
-function __wasm_bindgen_func_elem_220(arg0, arg1) {
-    wasm.__wasm_bindgen_func_elem_220(arg0, arg1);
+function __wasm_bindgen_func_elem_288(arg0, arg1) {
+    wasm.__wasm_bindgen_func_elem_288(arg0, arg1);
+}
+
+function __wasm_bindgen_func_elem_217(arg0, arg1, arg2) {
+    wasm.__wasm_bindgen_func_elem_217(arg0, arg1, addHeapObject(arg2));
 }
 
-function __wasm_bindgen_func_elem_174(arg0, arg1, arg2) {
-    wasm.__wasm_bindgen_func_elem_174(arg0, arg1, addHeapObject(arg2));
+function __wasm_bindgen_func_elem_217_3(arg0, arg1, arg2) {
+    wasm.__wasm_bindgen_func_elem_217_3(arg0, arg1, addHeapObject(arg2));
 }
 
-function __wasm_bindgen_func_elem_174_3(arg0, arg1, arg2) {
-    wasm.__wasm_bindgen_func_elem_174_3(arg0, arg1, addHeapObject(arg2));
+function __wasm_bindgen_func_elem_494(arg0, arg1, arg2) {
+    wasm.__wasm_bindgen_func_elem_494(arg0, arg1, addHeapObject(arg2));
 }
 
-function __wasm_bindgen_func_elem_442(arg0, arg1, arg2) {
-    wasm.__wasm_bindgen_func_elem_442(arg0, arg1, addHeapObject(arg2));
+function __wasm_bindgen_func_elem_593(arg0, arg1, arg2) {
+    wasm.__wasm_bindgen_func_elem_593(arg0, arg1, addHeapObject(arg2));
 }
 
-function __wasm_bindgen_func_elem_219(arg0, arg1, arg2) {
-    wasm.__wasm_bindgen_func_elem_219(arg0, arg1, addHeapObject(arg2));
+function __wasm_bindgen_func_elem_289(arg0, arg1, arg2) {
+    wasm.__wasm_bindgen_func_elem_289(arg0, arg1, addHeapObject(arg2));
 }
 
-function __wasm_bindgen_func_elem_1203(arg0, arg1, arg2) {
+function __wasm_bindgen_func_elem_1398(arg0, arg1, arg2) {
     try {
         const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
-        wasm.__wasm_bindgen_func_elem_1203(retptr, arg0, arg1, addHeapObject(arg2));
+        wasm.__wasm_bindgen_func_elem_1398(retptr, arg0, arg1, addHeapObject(arg2));
         var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
         var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
         if (r1) {
@@ -584,8 +661,8 @@ function __wasm_bindgen_func_elem_1203(arg0, arg1, arg2) {
     }
 }
 
-function __wasm_bindgen_func_elem_175(arg0, arg1, arg2) {
-    wasm.__wasm_bindgen_func_elem_175(arg0, arg1, arg2);
+function __wasm_bindgen_func_elem_218(arg0, arg1, arg2) {
+    wasm.__wasm_bindgen_func_elem_218(arg0, arg1, arg2);
 }
 
 function addHeapObject(obj) {
@@ -601,6 +678,71 @@ const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined')
     ? { register: () => {}, unregister: () => {} }
     : new FinalizationRegistry(state => wasm.__wbindgen_export4(state.a, state.b));
 
+function debugString(val) {
+    // primitive types
+    const type = typeof val;
+    if (type == 'number' || type == 'boolean' || val == null) {
+        return  `${val}`;
+    }
+    if (type == 'string') {
+        return `"${val}"`;
+    }
+    if (type == 'symbol') {
+        const description = val.description;
+        if (description == null) {
+            return 'Symbol';
+        } else {
+            return `Symbol(${description})`;
+        }
+    }
+    if (type == 'function') {
+        const name = val.name;
+        if (typeof name == 'string' && name.length > 0) {
+            return `Function(${name})`;
+        } else {
+            return 'Function';
+        }
+    }
+    // objects
+    if (Array.isArray(val)) {
+        const length = val.length;
+        let debug = '[';
+        if (length > 0) {
+            debug += debugString(val[0]);
+        }
+        for(let i = 1; i < length; i++) {
+            debug += ', ' + debugString(val[i]);
+        }
+        debug += ']';
+        return debug;
+    }
+    // Test for built-in
+    const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
+    let className;
+    if (builtInMatches && builtInMatches.length > 1) {
+        className = builtInMatches[1];
+    } else {
+        // Failed to match the standard '[object ClassName]'
+        return toString.call(val);
+    }
+    if (className == 'Object') {
+        // we're a user defined class or Object
+        // JSON.stringify avoids problems with cycles, and is generally much
+        // easier than looping through ownProperties of `val`.
+        try {
+            return 'Object(' + JSON.stringify(val) + ')';
+        } catch (_) {
+            return 'Object';
+        }
+    }
+    // errors
+    if (val instanceof Error) {
+        return `${val.name}: ${val.message}\n${val.stack}`;
+    }
+    // TODO we could test for more things here, like `Set`s and `Map`s.
+    return className;
+}
+
 function dropObject(idx) {
     if (idx < 1028) return;
     heap[idx] = heap_next;
diff --git a/crates/apps/gioser-web/pkg/gioser_web_bg.wasm b/crates/apps/gioser-web/pkg/gioser_web_bg.wasm
index 2022d2c..49ad85b 100644
Binary files a/crates/apps/gioser-web/pkg/gioser_web_bg.wasm and b/crates/apps/gioser-web/pkg/gioser_web_bg.wasm differ
diff --git a/crates/apps/gioser-web/pkg/gioser_web_bg.wasm.d.ts b/crates/apps/gioser-web/pkg/gioser_web_bg.wasm.d.ts
index 6344d75..af910e6 100644
--- a/crates/apps/gioser-web/pkg/gioser_web_bg.wasm.d.ts
+++ b/crates/apps/gioser-web/pkg/gioser_web_bg.wasm.d.ts
@@ -2,13 +2,14 @@
 /* eslint-disable */
 export const memory: WebAssembly.Memory;
 export const boot: () => void;
-export const __wasm_bindgen_func_elem_175: (a: number, b: number, c: number) => void;
-export const __wasm_bindgen_func_elem_1203: (a: number, b: number, c: number, d: number) => void;
-export const __wasm_bindgen_func_elem_174: (a: number, b: number, c: number) => void;
-export const __wasm_bindgen_func_elem_174_3: (a: number, b: number, c: number) => void;
-export const __wasm_bindgen_func_elem_442: (a: number, b: number, c: number) => void;
-export const __wasm_bindgen_func_elem_219: (a: number, b: number, c: number) => void;
-export const __wasm_bindgen_func_elem_220: (a: number, b: number) => void;
+export const __wasm_bindgen_func_elem_218: (a: number, b: number, c: number) => void;
+export const __wasm_bindgen_func_elem_1398: (a: number, b: number, c: number, d: number) => void;
+export const __wasm_bindgen_func_elem_217: (a: number, b: number, c: number) => void;
+export const __wasm_bindgen_func_elem_217_3: (a: number, b: number, c: number) => void;
+export const __wasm_bindgen_func_elem_494: (a: number, b: number, c: number) => void;
+export const __wasm_bindgen_func_elem_593: (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_288: (a: number, b: number) => void;
 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_export3: (a: number) => void;
diff --git a/crates/apps/gioser-web/src/lib.rs b/crates/apps/gioser-web/src/lib.rs
index 678431a..e9651f2 100644
--- a/crates/apps/gioser-web/src/lib.rs
+++ b/crates/apps/gioser-web/src/lib.rs
@@ -21,6 +21,7 @@ use std::rc::Rc;
 
 use barra_web::{Task, TaskList};
 use gioser_canvas_web::{tips, Renderer};
+use gioser_graph_web::GraphWidget;
 use fana_md_reader_web::Reader;
 use revista_web::Deck;
 use wasm_bindgen::prelude::*;
@@ -281,34 +282,46 @@ impl AppState {
             if let Err(e) = reader.open_url(&url_owned, &element_owned).await {
                 web_sys::console::warn_1(&e);
             }
-            // Montar contenedor del grafo (Cytoscape.js) debajo del md
+            // Después de cargar el md, montar el grafo debajo
             let graph_container_id = format!("graph-{}-container", element_owned);
+            // Si ya existe, no lo duplicamos
             if document_clone.get_element_by_id(&graph_container_id).is_some() {
                 return;
             }
+            // Crear contenedor debajo del content
             let wrapper: HtmlElement = document_clone
-                .create_element("gioser-graph")
+                .create_element("div")
                 .ok()
                 .and_then(|e| e.dyn_into::<HtmlElement>().ok())
-                .unwrap_or_else(|| {
-                    // fallback: div normal
-                    let d: HtmlElement = document_clone
-                        .create_element("div")
-                        .ok()
-                        .and_then(|e| e.dyn_into().ok())
-                        .unwrap();
-                    d
-                });
+                .unwrap();
             wrapper.set_id(&graph_container_id);
-            wrapper.set_attribute("data-api-url", "https://api.gioser.net").ok();
-            wrapper.style().set_property("margin-top", "1.5rem").ok();
+            wrapper.style().set_property("margin-top", "1rem").ok();
             wrapper.style().set_property("padding-top", "1rem").ok();
-            wrapper.style().set_property("border-top", "1px solid rgba(255,255,255,0.06)").ok();
-            wrapper.style().set_property("min-height", "220px").ok();
+            wrapper.style().set_property("border-top", "1px solid rgba(255,255,255,0.08)").ok();
+            // Label
+            let label: HtmlElement = document_clone
+                .create_element("div")
+                .ok()
+                .and_then(|e| e.dyn_into::<HtmlElement>().ok())
+                .unwrap();
+            label.set_inner_html(
+                "<span style=\"font-family: Inter, sans-serif; font-size: 0.75rem; \
+                 letter-spacing: 0.3em; text-transform: uppercase; color: rgba(232,234,245,0.45);\">
+                 · grafo semántico ·
+                 </span>"
+            );
+            wrapper.append_child(&label).ok();
             content_clone.append_child(&wrapper).ok();
-            // El script cytoscape-graph.js usa MutationObserver
-            // para detectar <gioser-graph> dinámicos. No hace falta
-            // disparar nada acá.
+            // Cargar el grafo
+            let mut graph = GraphWidget::new(
+                wrapper,
+                "https://api.gioser.net",
+                None, // callback simplificado por ahora
+            );
+            if let Err(e) = graph.load().await {
+                web_sys::console::warn_1(&format!("grafo: error al cargar: {:?}", e).into());
+                return;
+            }
         });
     }