| 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 { |
| |
| 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)) { |
| |
| 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 = |
| "<svg width='1000' height='1000' class='constellation-map'>" |
|
|
| 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 |
| "<line x1='" <> |
| float.to_string(f.x) <> |
| "' y1='" <> |
| float.to_string(f.y) <> |
| "' x2='" <> |
| float.to_string(t.x) <> |
| "' y2='" <> |
| float.to_string(t.y) <> |
| "' stroke='#bc13fe' stroke-width='2' opacity='" <> |
| float.to_string(opacity) <> |
| "' />" |
| } |
| _, _ -> "" |
| } |
| }) |
| |> list.fold("", fn(acc, svg) { acc <> svg }) |
|
|
| let nodes_svg = |
| list.map(map.nodes, fn(node) { |
| let glow_radius = node.brightness *. 50.0 |
| "<g><circle cx='" <> |
| float.to_string(node.x) <> |
| "' cy='" <> |
| float.to_string(node.y) <> |
| "' r='" <> |
| float.to_string(glow_radius) <> |
| "' fill='#00f2ff' opacity='" <> |
| float.to_string(node.brightness *. 0.3) <> |
| "' filter='url(#glow)' /><circle cx='" <> |
| float.to_string(node.x) <> |
| "' cy='" <> |
| float.to_string(node.y) <> |
| "' r='20' fill='#00f2ff' /><text x='" <> |
| float.to_string(node.x) <> |
| "' y='" <> |
| float.to_string(node.y +. 5.0) <> |
| "' font-size='12' text-anchor='middle' fill='white'>" <> |
| node.label <> |
| "</text></g>" |
| }) |
| |> list.fold("", fn(acc, svg) { acc <> svg }) |
|
|
| svg_header <> edges_svg <> nodes_svg <> "</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 |
| }) |
| } |
|
|
|
|