File size: 3,306 Bytes
3c5146b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386a178
 
3c5146b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Plan versioning — save and restore named snapshots of a ProjectPlan."""

import copy
import uuid
from datetime import datetime, timezone

from core.planner import ProjectPlan

MAX_VERSIONS = 20


def save_version(state: dict, label: str = "") -> dict:
    """
    Snapshot the current plan into state["versions"].
    Each version: {"id": uuid, "label": str, "timestamp": ISO str, "snapshot": plan_dict}
    If label is empty, auto-generate: "Version N" where N = len(versions)+1
    Returns updated state dict (deep copy).
    Max 20 versions kept (drop oldest if exceeded).
    """
    state = copy.deepcopy(state)
    versions: list = state.setdefault("versions", [])

    if not label:
        label = f"Version {len(versions) + 1}"

    plan = state.get("plan")
    if isinstance(plan, ProjectPlan):
        snapshot = plan.model_dump(exclude_none=True)
    elif isinstance(plan, dict):
        snapshot = plan
    else:
        snapshot = {}

    version_entry = {
        "id": str(uuid.uuid4()),
        "label": label,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "snapshot": snapshot,
    }

    versions.append(version_entry)

    if len(versions) > MAX_VERSIONS:
        versions[:] = versions[-MAX_VERSIONS:]

    return state


def restore_version(version_id: str, state: dict) -> dict:
    """
    Restore a saved version by ID into state["plan"].
    Returns updated state dict. If version_id not found, return state unchanged.
    """
    versions: list = state.get("versions", [])
    match = next((v for v in versions if v["id"] == version_id), None)

    if match is None:
        return state

    state = copy.deepcopy(state)
    # snapshot is already a dict — use model_validate, not parse_plan_json (which expects a string)
    state["plan"] = ProjectPlan.model_validate(match["snapshot"]).model_dump()
    return state


def list_versions(state: dict) -> list[dict]:
    """
    Return list of version summaries: [{"id", "label", "timestamp"}]
    Most recent first.
    """
    versions: list = state.get("versions", [])
    summaries = [
        {"id": v["id"], "label": v["label"], "timestamp": v["timestamp"]}
        for v in versions
    ]
    return list(reversed(summaries))


def versions_html(state: dict) -> str:
    """
    Render version history as an HTML list for display in Gradio.
    Each entry shows label + timestamp. Empty state shows placeholder text.
    Uses dark theme colors (#111111 bg, #e8e8e8 text, #2a2a2a border).
    """
    summaries = list_versions(state)

    container_style = (
        "background:#111111;color:#e8e8e8;border:1px solid #2a2a2a;"
        "border-radius:6px;padding:12px;font-family:sans-serif;"
    )

    if not summaries:
        return (
            f'<div style="{container_style}opacity:0.5;">'
            "No saved versions yet.</div>"
        )

    items_html = "".join(
        f'<li style="padding:6px 0;border-bottom:1px solid #2a2a2a;">'
        f'<strong>{v["label"]}</strong>'
        f'<span style="margin-left:12px;opacity:0.6;font-size:0.85em;">{v["timestamp"]}</span>'
        f"</li>"
        for v in summaries
    )

    return (
        f'<div style="{container_style}">'
        f'<ul style="list-style:none;margin:0;padding:0;">{items_html}</ul>'
        f"</div>"
    )