File size: 2,942 Bytes
8a3cb2b
a498dfd
f9a51ca
5d10601
f9a51ca
bea515c
a550b37
 
f9a51ca
 
 
bea515c
a550b37
f9a51ca
bea515c
 
f9a51ca
a550b37
f9a51ca
a550b37
bea515c
f9a51ca
bea515c
a550b37
bea515c
 
 
 
f9a51ca
bea515c
 
a550b37
 
 
 
 
 
bea515c
 
a550b37
bea515c
 
 
 
 
a550b37
 
bea515c
 
 
 
a550b37
 
 
 
 
 
 
 
 
 
 
bea515c
a550b37
 
 
 
bea515c
f9a51ca
 
 
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
# Author: Liam Grinstead
# SVG tree with tier and overlay labels

from lineage_tracker import get_ancestors, get_descendants

def _row_positions(n: int, width: int, y: int):
    if n <= 0:
        return []
    step = width // (n + 1)
    return [(step * (i + 1), y) for i in range(n)]

def render_lineage_tree(agent_id: str, overlay_color_hue: int = 260, max_depth: int = 6) -> str:
    width, height = 980, 480  # reduced height to avoid cutoff
    svg = [f"<svg width='{width}' height='{height}' xmlns='http://www.w3.org/2000/svg'>"]
    svg.append("<style>text{font-family:monospace}</style>")
    svg.append(f"<text x='12' y='24' font-size='16'>RFT Lineage: {agent_id}</text>")

    root_x, root_y = width // 2, 170

    # Ancestors
    ancestors = get_ancestors(agent_id)
    if ancestors:
        pos = _row_positions(len(ancestors), width, 80)
        for (x, y), aid in zip(pos, ancestors):
            svg.append(f"<circle cx='{x}' cy='{y}' r='14' fill='hsl({overlay_color_hue},60%,50%)'/>")
            svg.append(f"<text x='{x-28}' y='{y+32}' font-size='11'>{aid}</text>")
        x_last, y_last = pos[-1]
        svg.append(f"<line x1='{x_last}' y1='{y_last+14}' x2='{root_x}' y2='{root_y-20}' stroke='#555'/>")
    else:
        svg.append("<text x='12' y='80' font-size='12' fill='#888'>No ancestors</text>")

    # Root agent
    svg.append(
        f"<circle cx='{root_x}' cy='{root_y}' r='18' fill='#00b894'>"
        "<animate attributeName='r' values='18;22;18' dur='2s' repeatCount='indefinite'/>"
        "</circle>"
    )
    svg.append(f"<text x='{root_x-40}' y='{root_y+40}' font-size='12'>{agent_id}</text>")

    # Descendants
    levels = get_descendants(agent_id, depth=max_depth)
    y = 260
    gap = 90
    for d in range(1, max_depth + 1):
        nodes = levels.get(d, [])
        if not nodes:
            break
        pos = _row_positions(len(nodes), width, y)
        svg.append(f"<text x='12' y='{y-26}' font-size='12' fill='#888'>Gen {d}</text>")
        for (x, yy), item in zip(pos, nodes):
            hue = 40 + (15 * d)
            child_id = item.get("child_id", "?")
            tier = item.get("tier", "?")
            overlay = item.get("overlay", "?")
            svg.append(
                f"<circle cx='{x}' cy='{yy}' r='14' fill='hsl({hue},70%,55%)'>"
                "<animate attributeName='fill' "
                f"values='hsl({hue},70%,55%);hsl({hue},70%,65%);hsl({hue},70%,55%)' "
                "dur='2s' repeatCount='indefinite'/>"
                "</circle>"
            )
            label = f"{child_id} · {tier} · {overlay}"
            svg.append(f"<text x='{x-60}' y='{yy+32}' font-size='11'>{label}</text>")
            svg.append(
                f"<path d='M {root_x} {root_y+20} Q {(root_x+x)//2} {(root_y+yy)//2} {x} {yy-14}' "
                "stroke='rgba(0,0,0,.35)' fill='none'/>"
            )
        y += gap

    svg.append("</svg>")
    return "".join(svg)