import gleam/list import gleam/float import gleam/int pub type Node { Node( card_id: String, label: String, mastery_level: Float, brightness: Float, x: Float, y: Float, connections: List(String), ) } pub type ConstellationMap { ConstellationMap( nodes: List(Node), edges: List(#(String, String, Float)), total_mastery: Float, focus_areas: List(#(String, Float)), ) } pub fn create_node( card_id: String, label: String, mastery: Float, ) -> Node { Node( card_id: card_id, label: label, mastery_level: mastery, brightness: mastery /. 100.0, x: 0.0, y: 0.0, connections: [], ) } pub fn connect_nodes( source: Node, target: Node, strength: Float, ) -> #(Node, Node, Float) { let updated_source = Node( ..source, connections: [target.card_id, ..source.connections], ) #(updated_source, target, strength) } pub fn calculate_brightness(mastery_level: Float) -> Float { // Scale brightness based on mastery mastery_level /. 100.0 } pub fn build_constellation( cards: List(#(String, String, Float)), ) -> ConstellationMap { let nodes = list.map(cards, fn(card) { let #(id, label, mastery) = card create_node(id, label, mastery) }) let total_mastery = case list.length(nodes) { 0 -> 0.0 n -> list.fold(nodes, 0.0, fn(acc, node) { acc +. node.mastery_level }) /. int.to_float(n) } let edges = build_relationships(nodes) ConstellationMap( nodes: position_nodes(nodes), edges: edges, total_mastery: total_mastery, focus_areas: find_focus_areas(nodes), ) } fn build_relationships(nodes: List(Node)) -> List(#(String, String, Float)) { // Create connections between related topics case nodes { [] -> [] [_] -> [] [a, b, ..rest] -> { let connection = #(a.card_id, b.card_id, 0.5) [connection, ..build_relationships([b, ..rest])] } } } fn position_nodes(nodes: List(Node)) -> List(Node) { position_nodes_grid(nodes, 0, 0) } fn position_nodes_grid(nodes: List(Node), row: Int, col: Int) -> List(Node) { case nodes { [] -> [] [node, ..rest] -> { let x = 120.0 +. int.to_float(col) *. 180.0 let y = 140.0 +. int.to_float(row) *. 160.0 let placed = Node(..node, x: x, y: y) case col >= 4 { True -> [placed, ..position_nodes_grid(rest, row + 1, 0)] False -> [placed, ..position_nodes_grid(rest, row, col + 1)] } } } } fn find_focus_areas(nodes: List(Node)) -> List(#(String, Float)) { nodes |> list.filter(fn(node) { node.mastery_level <. 70.0 }) |> list.map(fn(node) { #(node.label, node.mastery_level) }) |> list.sort(fn(a, b) { float.compare(a.1, b.1) }) } pub fn generate_svg_constellation(map: ConstellationMap) -> String { let svg_header = "" let edges_svg = list.map(map.edges, fn(edge) { let #(from_id, to_id, strength) = edge let from_node = list.find(map.nodes, fn(n) { n.card_id == from_id }) let to_node = list.find(map.nodes, fn(n) { n.card_id == to_id }) case from_node, to_node { Ok(f), Ok(t) -> { let opacity = 0.2 +. strength *. 0.3 "" } _, _ -> "" } }) |> list.fold("", fn(acc, svg) { acc <> svg }) let nodes_svg = list.map(map.nodes, fn(node) { let glow_radius = node.brightness *. 50.0 "" <> node.label <> "" }) |> list.fold("", fn(acc, svg) { acc <> svg }) svg_header <> edges_svg <> nodes_svg <> "" } pub fn calculate_heatmap_analytics(map: ConstellationMap) -> String { let weak_areas = list.length(map.focus_areas) let strong_areas = list.length(map.nodes) - weak_areas "Total Nodes: " <> int.to_string(list.length(map.nodes)) <> " | Strong: " <> int.to_string(strong_areas) <> " | Needs Work: " <> int.to_string(weak_areas) <> " | Overall Mastery: " <> float.to_string(map.total_mastery) <> "%" } pub fn identify_knowledge_gaps(map: ConstellationMap) -> List(String) { list.map(map.focus_areas, fn(area) { let #(label, mastery) = area "📍 " <> label <> " (" <> float.to_string(mastery) <> "% mastery)" }) } pub fn recommend_next_topics( map: ConstellationMap, ) -> List(String) { let poorly_mastered = list.filter(map.nodes, fn(node) { node.mastery_level <. 50.0 }) list.map(poorly_mastered, fn(node) { node.label }) }