File size: 4,252 Bytes
824b65d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Renders a job's trace events into an interactive HTML timeline.

Events are simple dicts (see local_runner.py for the exact shapes emitted):
    {"ts": "...", "kind": "tool_call" | "tool_result" | "note" | "task_update" | "status",
     "label": "short one-liner", "detail": "optional longer text", "ok": true/false/None}

Rendered with plain <details>/<summary> so expand/collapse works with no JS,
which keeps it safe to drop straight into a Gradio gr.HTML component.
"""

from __future__ import annotations

import html as _html

_ICONS = {
    "tool_call": "\U0001F527",  # wrench
    "tool_result_ok": "✅",
    "tool_result_fail": "❌",
    "note": "\U0001F4AC",
    "task_update": "\U0001F4CB",
    "status_start": "▶️",
    "status_end": "\U0001F3C1",
}

_STYLE = """
<style>
.pr-trace { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
.pr-trace .pr-empty {
  color: var(--body-text-color-subdued, #888); font-style: italic; padding: 22px;
  text-align: center; border: 1px dashed var(--border-color-primary, #ddd); border-radius: 12px;
}
.pr-trace ol { list-style: none; margin: 0; padding: 4px 0 0; border-left: 2px solid var(--border-color-primary, #d0d5dd); max-height: 480px; overflow-y: auto; }
.pr-trace li { position: relative; padding: 6px 0 6px 18px; margin-left: 4px; }
.pr-trace li::before {
  content: ""; position: absolute; left: -7px; top: 14px;
  width: 10px; height: 10px; border-radius: 50%;
  background: #9aa4b2; border: 2px solid var(--background-fill-secondary, #fff);
}
.pr-trace li.ok::before { background: #0d9488; }
.pr-trace li.fail::before { background: #b91c1c; }
.pr-trace summary { cursor: pointer; display: flex; gap: 8px; align-items: baseline; padding: 2px 4px; border-radius: 6px; }
.pr-trace summary:hover { background: rgba(13,148,136,0.08); }
.pr-trace summary .pr-icon { flex: none; }
.pr-trace summary .pr-label { font-weight: 600; }
.pr-trace summary .pr-ts { margin-left: auto; font-size: 0.78em; color: var(--body-text-color-subdued, #888); white-space: nowrap; }
.pr-trace .pr-detail {
  margin: 6px 0 0 22px; padding: 8px 10px; border-radius: 6px;
  background: rgba(127,127,127,0.08); font-size: 0.85em; white-space: pre-wrap;
  font-family: ui-monospace, 'Fira Code', monospace; max-height: 260px; overflow: auto;
}
.pr-trace .pr-live {
  color: #0d9488; font-weight: 600; margin-bottom: 8px; padding: 6px 10px;
  background: rgba(13,148,136,0.1); border-radius: 8px; display: inline-block;
}
</style>
"""


def _icon_for(event: dict) -> str:
    kind = event.get("kind")
    if kind == "tool_result":
        return _ICONS["tool_result_ok"] if event.get("ok") else _ICONS["tool_result_fail"]
    if kind == "status":
        return _ICONS["status_end"] if event.get("ok") is not None else _ICONS["status_start"]
    return _ICONS.get(kind, "•")


def _row_class(event: dict) -> str:
    if event.get("ok") is True:
        return "ok"
    if event.get("ok") is False:
        return "fail"
    return ""


def render_trace(events: list[dict], running: bool = False) -> str:
    if not events:
        empty_msg = "Waiting for the local runner to pick this job up..." if running else "No trace recorded yet."
        return f'{_STYLE}<div class="pr-trace"><div class="pr-empty">{_html.escape(empty_msg)}</div></div>'

    items = []
    for event in events:
        icon = _icon_for(event)
        label = _html.escape(str(event.get("label", "")))
        ts = _html.escape(str(event.get("ts", "")))
        detail = event.get("detail")
        row_class = _row_class(event)
        detail_html = ""
        if detail:
            detail_html = f'<div class="pr-detail">{_html.escape(str(detail))}</div>'
        items.append(
            f'<li class="{row_class}"><details{" open" if detail and row_class == "fail" else ""}>'
            f'<summary><span class="pr-icon">{icon}</span>'
            f'<span class="pr-label">{label}</span>'
            f'<span class="pr-ts">{ts}</span></summary>'
            f"{detail_html}</details></li>"
        )

    live_banner = '<div class="pr-live">⏳ Run in progress — refreshing automatically...</div>' if running else ""
    return f'{_STYLE}<div class="pr-trace">{live_banner}<ol>{"".join(items)}</ol></div>'