138 lines
5.1 KiB
Rust
138 lines
5.1 KiB
Rust
|
|
// content_graph/graph_view.rs
|
||
|
|
//
|
||
|
|
// Full-page interactive knowledge graph via Cytoscape.js.
|
||
|
|
// Follows the SSR+WASM delegation pattern used across the codebase.
|
||
|
|
//
|
||
|
|
// SSR: renders container div + embedded JSON data script tag.
|
||
|
|
// WASM: Effect::new calls window.ContentGraph.render() after mount.
|
||
|
|
|
||
|
|
use leptos::prelude::*;
|
||
|
|
|
||
|
|
use super::GRAPH_JSON;
|
||
|
|
|
||
|
|
// ── Entry point (delegating component) ───────────────────────────────────────
|
||
|
|
|
||
|
|
/// Full-page knowledge graph view.
|
||
|
|
///
|
||
|
|
/// `node_id`: when provided, the graph centers on that node (ego view).
|
||
|
|
#[component]
|
||
|
|
pub fn GraphView(
|
||
|
|
#[prop(optional)] node_id: Option<String>,
|
||
|
|
#[prop(default = String::from("light"))] theme: String,
|
||
|
|
) -> impl IntoView {
|
||
|
|
#[cfg(not(target_arch = "wasm32"))]
|
||
|
|
return view! { <GraphViewInner node_id=node_id theme=theme /> };
|
||
|
|
|
||
|
|
#[cfg(target_arch = "wasm32")]
|
||
|
|
return view! { <GraphViewClient node_id=node_id theme=theme /> };
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Inner (SSR) ───────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
#[component]
|
||
|
|
fn GraphViewInner(
|
||
|
|
node_id: Option<String>,
|
||
|
|
theme: String,
|
||
|
|
) -> impl IntoView {
|
||
|
|
let focus = node_id.unwrap_or_default();
|
||
|
|
let graph_json_safe = GRAPH_JSON.replace("</", "\\/");
|
||
|
|
|
||
|
|
view! {
|
||
|
|
<div class="content-graph-page w-full">
|
||
|
|
// Embedded graph data — read by the JS shim
|
||
|
|
<script
|
||
|
|
type="application/json"
|
||
|
|
id="content-graph-data"
|
||
|
|
inner_html=graph_json_safe
|
||
|
|
/>
|
||
|
|
// Cytoscape container
|
||
|
|
<div
|
||
|
|
id="content-graph-cyto"
|
||
|
|
data-focus=focus
|
||
|
|
data-theme=theme
|
||
|
|
class="w-full h-[600px] ds-bg-page ds-border ds-rounded-lg overflow-hidden"
|
||
|
|
aria-label="Knowledge graph visualisation"
|
||
|
|
role="img"
|
||
|
|
>
|
||
|
|
<noscript>
|
||
|
|
<p class="p-4 ds-caption text-center">
|
||
|
|
"Interactive graph requires JavaScript."
|
||
|
|
</p>
|
||
|
|
</noscript>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Client (WASM) ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
#[component]
|
||
|
|
fn GraphViewClient(
|
||
|
|
node_id: Option<String>,
|
||
|
|
theme: String,
|
||
|
|
) -> impl IntoView {
|
||
|
|
let focus = node_id.unwrap_or_default();
|
||
|
|
let focus_clone = focus.clone();
|
||
|
|
let theme_clone = theme.clone();
|
||
|
|
|
||
|
|
// Initialise Cytoscape after the component mounts in the browser
|
||
|
|
Effect::new(move |_| {
|
||
|
|
let focus = focus_clone.clone();
|
||
|
|
let theme = theme_clone.clone();
|
||
|
|
call_cytoscape_render("content-graph-cyto", &focus, &theme);
|
||
|
|
});
|
||
|
|
|
||
|
|
// On unmount, destroy the Cytoscape instance
|
||
|
|
on_cleanup(move || {
|
||
|
|
call_cytoscape_destroy("content-graph-cyto");
|
||
|
|
});
|
||
|
|
|
||
|
|
view! {
|
||
|
|
<div class="content-graph-page w-full">
|
||
|
|
<div
|
||
|
|
id="content-graph-cyto"
|
||
|
|
data-focus=focus
|
||
|
|
data-theme=theme
|
||
|
|
class="w-full h-[600px] ds-bg-page ds-border ds-rounded-lg overflow-hidden"
|
||
|
|
aria-label="Knowledge graph visualisation"
|
||
|
|
role="img"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── JS interop ────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
#[cfg(target_arch = "wasm32")]
|
||
|
|
fn call_cytoscape_render(container_id: &str, focus: &str, theme: &str) {
|
||
|
|
use wasm_bindgen::JsValue;
|
||
|
|
|
||
|
|
let Some(window) = web_sys::window() else { return };
|
||
|
|
let Ok(content_graph) = js_sys::Reflect::get(&window, &JsValue::from_str("ContentGraph")) else { return };
|
||
|
|
let Ok(render_fn) = js_sys::Reflect::get(&content_graph, &JsValue::from_str("render")) else { return };
|
||
|
|
let render_fn: js_sys::Function = render_fn.dyn_into().unwrap_or_else(|_| js_sys::Function::new_no_args(""));
|
||
|
|
let _ = render_fn.call3(
|
||
|
|
&content_graph,
|
||
|
|
&JsValue::from_str(container_id),
|
||
|
|
&JsValue::from_str(focus),
|
||
|
|
&JsValue::from_str(theme),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(not(target_arch = "wasm32"))]
|
||
|
|
fn call_cytoscape_render(_container_id: &str, _focus: &str, _theme: &str) {}
|
||
|
|
|
||
|
|
#[cfg(target_arch = "wasm32")]
|
||
|
|
fn call_cytoscape_destroy(container_id: &str) {
|
||
|
|
use wasm_bindgen::JsValue;
|
||
|
|
|
||
|
|
let Some(window) = web_sys::window() else { return };
|
||
|
|
let Ok(content_graph) = js_sys::Reflect::get(&window, &JsValue::from_str("ContentGraph")) else { return };
|
||
|
|
let Ok(destroy_fn) = js_sys::Reflect::get(&content_graph, &JsValue::from_str("destroy")) else { return };
|
||
|
|
let destroy_fn: js_sys::Function = destroy_fn.dyn_into().unwrap_or_else(|_| js_sys::Function::new_no_args(""));
|
||
|
|
let _ = destroy_fn.call1(&content_graph, &JsValue::from_str(container_id));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(not(target_arch = "wasm32"))]
|
||
|
|
fn call_cytoscape_destroy(_container_id: &str) {}
|