use gloo_net::http::Request;
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskGraphData {
pub nodes: Vec<TaskNode>,
pub edges: Vec<TaskEdge>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskNode {
pub id: String,
pub label: String,
pub phase: String,
pub status: String,
pub duration: Option<String>,
pub description: Option<String>,
pub priority: Option<String>,
pub difficulty: Option<String>,
pub crate_name: Option<String>,
pub issue: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskEdge {
pub source: String,
pub target: String,
#[serde(rename = "type")]
pub edge_type: String,
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = renderTaskGraph)]
fn render_task_graph(nodes: JsValue, edges: JsValue);
}
async fn fetch_task_graph() -> Result<TaskGraphData, String> {
let response = Request::get("/api/task-graph")
.send()
.await
.map_err(|e| format!("Network error: {}", e))?;
if !response.ok() {
return Err(format!("HTTP error: {}", response.status()));
}
let data = response
.json::<TaskGraphData>()
.await
.map_err(|e| format!("Parse error: {}", e))?;
Ok(data)
}
#[component]
pub fn TaskDependencyGraph() -> impl IntoView {
let graph_data = LocalResource::new(|| async move { fetch_task_graph().await });
Effect::new(move |_| {
if let Some(Ok(data)) = graph_data.get().as_ref().map(|r| r.as_ref()) {
web_sys::console::log_1(
&format!(
"Task graph data loaded: {} nodes, {} edges",
data.nodes.len(),
data.edges.len()
)
.into(),
);
if let Ok(nodes_js) = serde_wasm_bindgen::to_value(&data.nodes) {
if let Ok(edges_js) = serde_wasm_bindgen::to_value(&data.edges) {
web_sys::console::log_1(&"Calling renderTaskGraph()...".into());
let window = web_sys::window().expect("no global window exists");
let closure = wasm_bindgen::closure::Closure::once(move || {
render_task_graph(nodes_js, edges_js);
web_sys::console::log_1(&"renderTaskGraph() executed in next frame".into());
});
window
.request_animation_frame(closure.as_ref().unchecked_ref())
.expect("should register `requestAnimationFrame`");
closure.forget();
} else {
web_sys::console::error_1(&"Failed to convert edges to JsValue".into());
}
} else {
web_sys::console::error_1(&"Failed to convert nodes to JsValue".into());
}
} else {
web_sys::console::log_1(&"Task graph data not ready or errored".into());
}
});
view! {
<div class="task-graph-container">
<div class="page-header">
<h2>"Task Dependency Graph"</h2>
<p class="subtitle">"Visualize task dependencies and execution order"</p>
</div>
<Suspense fallback=move || view! { <div class="loading">"Loading task graph..."</div> }>
{move || match graph_data.get().as_ref().map(|r| r.as_ref()) {
Some(Ok(data)) => {
let node_count = data.nodes.len();
let edge_count = data.edges.len();
view! {
<div class="graph-content">
<div class="graph-stats">
<div class="stat-item">
<span class="stat-label">"Tasks: "</span>
<span class="stat-value">{node_count}</span>
</div>
<div class="stat-item">
<span class="stat-label">"Dependencies: "</span>
<span class="stat-value">{edge_count}</span>
</div>
</div>
<div class="graph-legend">
<h3>"Legend"</h3>
<div class="legend-items">
<div class="legend-item">
<div class="legend-color" style="background: #4CAF50;"></div>
<span>"Complete"</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #FFC107;"></div>
<span>"In Progress"</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #9E9E9E;"></div>
<span>"Future"</span>
</div>
</div>
</div>
<div id="d3-graph" style="width: 100%; height: 600px; border: 1px solid #333; background: #1a1a1a; border-radius: 8px;"></div>
<div id="task-tooltip" class="task-tooltip hidden">
<div class="tooltip-header">
<h3 id="tooltip-title"></h3>
<button id="tooltip-close" class="tooltip-close-btn">"×"</button>
</div>
<div id="tooltip-content" class="tooltip-content"></div>
</div>
<div class="graph-instructions">
<p>"💡 Tip: Drag nodes to rearrange. Zoom with mouse wheel. Pan by dragging background."</p>
</div>
</div>
}.into_any()
},
Some(Err(e)) => {
let err = e.clone();
view! {
<div class="error">
<h3>"Failed to load task graph"</h3>
<p>{err}</p>
</div>
}.into_any()
},
None => {
view! { <div class="loading">"Loading task graph..."</div> }.into_any()
}
}}
</Suspense>
</div>
}
}