File size: 6,525 Bytes
7c43635
 
 
028a336
 
27e273f
7c43635
 
 
028a336
7c43635
 
 
bf5eed8
028a336
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c43635
028a336
7c43635
 
 
 
 
 
 
 
 
 
 
 
 
 
25fcb2c
 
7a5d3f8
25fcb2c
7c43635
 
 
27e273f
 
 
 
7c43635
 
 
27e273f
 
 
7c43635
27e273f
7c43635
 
27e273f
 
 
25fcb2c
7c43635
27e273f
 
 
 
 
 
 
 
 
 
 
 
 
 
7c43635
27e273f
 
 
 
89c005a
25fcb2c
 
 
27e273f
 
 
 
 
 
 
 
 
 
 
 
 
25fcb2c
 
27e273f
 
 
 
 
 
 
 
 
 
25fcb2c
27e273f
 
 
 
 
89c005a
27e273f
25fcb2c
7c43635
028a336
 
 
 
7c43635
 
9912372
7c43635
 
 
 
 
 
 
27e273f
028a336
 
 
7c43635
7e08bc6
028a336
7e08bc6
028a336
7c43635
25fcb2c
 
7c43635
 
 
 
 
9912372
7c43635
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import gradio as gr
import json
from graphviz import Digraph
import os
from tempfile import NamedTemporaryFile
from sample_data import COMPLEX_SAMPLE_JSON # El JSON de ejemplo se mantiene sin cambios

def generate_concept_map(json_input: str) -> str:
    """
    Generate concept map from JSON and return as image file
    
    Args:
        json_input (str): JSON describing the concept map structure.
            
            REQUIRED FORMAT EXAMPLE:
            {
                "central_node": "AI",
                "nodes": [
                    {
                        "id": "ml",
                        "label": "Machine Learning",
                        "relationship": "subcategory",
                        "subnodes": [
                            {
                                "id": "dl",
                                "label": "Deep Learning",
                                "relationship": "type",
                                "subnodes": [
                                    {
                                        "id": "cnn",
                                        "label": "CNN",
                                        "relationship": "architecture"
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
    
    Returns:
        str: Path to generated PNG image file
    """
    try:
        if not json_input.strip():
            return "Error: Empty input"
            
        data = json.loads(json_input)
        
        if 'central_node' not in data or 'nodes' not in data:
            raise ValueError("Missing required fields: central_node or nodes")

        dot = Digraph(
            name='ConceptMap',
            format='png',
            graph_attr={
                'rankdir': 'TB', # Top-to-Bottom
                'splines': 'ortho', # Straight lines
                'bgcolor': 'white', # Fondo blanco
                'pad': '0.5' # ¡Este es el margen! 0.5 pulgadas
            }
        )
        
        # Base color for the central node
        base_color = '#19191a' # Casi negro

        # Central node (now a rounded box)
        dot.node(
            'central',
            data['central_node'],
            shape='box', # Ahora es un rectángulo
            style='filled,rounded', # Redondeado
            fillcolor=base_color, # Color base
            fontcolor='white',
            fontsize='16' # Un poco más grande para el título
        )
        
        # Helper function to recursively add nodes and edges
        def add_nodes_and_edges(parent_id, nodes_list, current_depth=0):
            # Calculate color for current depth, making it lighter
            lightening_factor = 0.12 # How much lighter each level gets
            
            # Convert base_color hex to RGB
            base_r = int(base_color[1:3], 16)
            base_g = int(base_color[3:5], 16)
            base_b = int(base_color[5:7], 16)

            # Calculate current node color
            current_r = base_r + int((255 - base_r) * current_depth * lightening_factor)
            current_g = base_g + int((255 - base_g) * current_depth * lightening_factor)
            current_b = base_b + int((255 - base_b) * current_depth * lightening_factor)

            # Clamp values to 255
            current_r = min(255, current_r)
            current_g = min(255, current_g)
            current_b = min(255, current_b)
            
            node_fill_color = f'#{current_r:02x}{current_g:02x}{current_b:02x}'

            # Font color: white for dark nodes, black for very light nodes
            font_color = 'white' if current_depth * lightening_factor < 0.6 else 'black'
            
            # Edge colors can remain constant or change. Let's make them slightly visible.
            edge_color = '#4a4a4a' # Un gris oscuro para las líneas
            font_size = max(9, 14 - (current_depth * 2)) # Adjust font size based on depth
            edge_font_size = max(7, 10 - (current_depth * 1))

            for node in nodes_list:
                node_id = node.get('id')
                label = node.get('label')
                relationship = node.get('relationship')
                
                if not all([node_id, label, relationship]):
                    raise ValueError(f"Invalid node: {node}")
                    
                dot.node(
                    node_id,
                    label,
                    shape='box', # Rectángulo
                    style='filled,rounded', # Redondeado
                    fillcolor=node_fill_color, 
                    fontcolor=font_color,
                    fontsize=str(font_size) 
                )
                
                dot.edge(
                    parent_id,
                    node_id,
                    label=relationship,
                    color=edge_color,
                    fontcolor=edge_color, # Color de la fuente de la arista también gris
                    fontsize=str(edge_font_size) 
                )
                
                if 'subnodes' in node:
                    add_nodes_and_edges(node_id, node['subnodes'], current_depth + 1)

        # Start processing from the top-level nodes connected to the central node
        add_nodes_and_edges('central', data.get('nodes', []), current_depth=1) # Initial depth is 1 for nodes under central

        # Save to temporary file
        with NamedTemporaryFile(delete=False, suffix='.png') as tmp:
            dot.render(tmp.name, format='png', cleanup=True)
            return tmp.name + '.png'

    except json.JSONDecodeError:
        return "Error: Invalid JSON format"
    except Exception as e:
        return f"Error: {str(e)}"

if __name__ == "__main__":
    demo = gr.Interface(
        fn=generate_concept_map,
        inputs=gr.Textbox(
            value=COMPLEX_SAMPLE_JSON, 
            placeholder="Paste JSON following the documented format",
            label="Structured JSON Input",
            lines=25
        ),
        outputs=gr.Image(
            label="Generated Concept Map",
            type="filepath",
            show_download_button=True
        ),
        title="AI Concept Map (Custom Style)",
        description="Generates an AI concept map with custom rounded boxes, color gradient, and white background."
    )
    
    demo.launch(
        mcp_server=True,
        share=False,
        server_port=7860,
        server_name="0.0.0.0"
    )