Spaces:
Sleeping
Sleeping
Commit ·
64ae2f9
1
Parent(s): 1f42d7b
feat: add tool call visibility in step accordion
Browse files- emit tool_call step events for browser.navigate, html.parse, html.select, csv.generate
- show tool name, description, parameters, and result in frontend accordion
- add Zap icon for tool_call action with yellow highlight
- display tool call details section with structured parameter/result views
This enables users to see exactly which tools agents invoke during scraping.
backend/app/api/routes/scrape.py
CHANGED
|
@@ -929,15 +929,21 @@ async def _scrape_github_trending(
|
|
| 929 |
# Navigate to GitHub trending
|
| 930 |
trending_url = "https://github.com/trending"
|
| 931 |
|
|
|
|
| 932 |
step_num += 1
|
| 933 |
yield _record_step(
|
| 934 |
session,
|
| 935 |
ScrapeStep(
|
| 936 |
step_number=step_num,
|
| 937 |
-
action="
|
| 938 |
url=trending_url,
|
| 939 |
status="running",
|
| 940 |
-
message="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 941 |
timestamp=_now_iso(),
|
| 942 |
),
|
| 943 |
)
|
|
@@ -954,6 +960,28 @@ async def _scrape_github_trending(
|
|
| 954 |
nav_reward = 0.5 if nav_obs.page_html else 0.0
|
| 955 |
total_reward += nav_reward
|
| 956 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
# Update the navigation step with actual reward
|
| 958 |
step_num += 1
|
| 959 |
yield _record_step(
|
|
@@ -962,8 +990,8 @@ async def _scrape_github_trending(
|
|
| 962 |
step_number=step_num,
|
| 963 |
action="navigate",
|
| 964 |
url=trending_url,
|
| 965 |
-
status="completed" if
|
| 966 |
-
message=f"Navigated to {trending_url}" if
|
| 967 |
reward=nav_reward,
|
| 968 |
duration_ms=nav_info.get("step_duration_ms", 0),
|
| 969 |
timestamp=_now_iso(),
|
|
@@ -974,9 +1002,83 @@ async def _scrape_github_trending(
|
|
| 974 |
session["errors"].append("Failed to load GitHub trending page")
|
| 975 |
return
|
| 976 |
|
| 977 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 978 |
soup = parse_html(nav_obs.page_html)
|
| 979 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 980 |
step_num += 1
|
| 981 |
yield _record_step(
|
| 982 |
session,
|
|
@@ -991,9 +1093,6 @@ async def _scrape_github_trending(
|
|
| 991 |
),
|
| 992 |
)
|
| 993 |
|
| 994 |
-
# Find repository entries (GitHub trending structure)
|
| 995 |
-
repo_articles = soup.find_all("article", class_="Box-row") or soup.find_all("div", class_="Box-row")
|
| 996 |
-
|
| 997 |
for article in repo_articles[:20]: # Limit to first 20
|
| 998 |
try:
|
| 999 |
# Extract repo name and username
|
|
@@ -1055,6 +1154,28 @@ async def _scrape_github_trending(
|
|
| 1055 |
),
|
| 1056 |
)
|
| 1057 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1058 |
# Generate clean CSV output
|
| 1059 |
csv_buffer = io.StringIO()
|
| 1060 |
writer = csv.DictWriter(csv_buffer, fieldnames=["username", "repo_name", "stars", "forks"])
|
|
@@ -1062,6 +1183,27 @@ async def _scrape_github_trending(
|
|
| 1062 |
writer.writerows(trending_repos)
|
| 1063 |
clean_csv = csv_buffer.getvalue()
|
| 1064 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1065 |
# Store the clean CSV directly as extracted data for CSV output format
|
| 1066 |
if request.output_format == OutputFormat.CSV:
|
| 1067 |
session["extracted_data"] = {
|
|
@@ -1565,6 +1707,25 @@ async def _scrape_single_page(
|
|
| 1565 |
),
|
| 1566 |
)
|
| 1567 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1568 |
navigate_action = Action(
|
| 1569 |
action_type=ActionType.NAVIGATE,
|
| 1570 |
parameters={"url": url},
|
|
@@ -1575,6 +1736,23 @@ async def _scrape_single_page(
|
|
| 1575 |
|
| 1576 |
nav_success = nav_info.get("action_result", {}).get("success", bool(nav_obs.page_html))
|
| 1577 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1578 |
yield _record_step(
|
| 1579 |
session,
|
| 1580 |
ScrapeStep(
|
|
@@ -1598,14 +1776,20 @@ async def _scrape_single_page(
|
|
| 1598 |
|
| 1599 |
for field_name in fields_to_extract:
|
| 1600 |
step_num += 1
|
|
|
|
| 1601 |
yield _record_step(
|
| 1602 |
session,
|
| 1603 |
ScrapeStep(
|
| 1604 |
step_number=step_num,
|
| 1605 |
-
action="
|
| 1606 |
url=url,
|
| 1607 |
status="running",
|
| 1608 |
-
message=f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1609 |
timestamp=_now_iso(),
|
| 1610 |
),
|
| 1611 |
)
|
|
@@ -1624,6 +1808,24 @@ async def _scrape_single_page(
|
|
| 1624 |
extracted[field_name] = ef.value
|
| 1625 |
break
|
| 1626 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1627 |
yield _record_step(
|
| 1628 |
session,
|
| 1629 |
ScrapeStep(
|
|
|
|
| 929 |
# Navigate to GitHub trending
|
| 930 |
trending_url = "https://github.com/trending"
|
| 931 |
|
| 932 |
+
# Tool call: browser.navigate
|
| 933 |
step_num += 1
|
| 934 |
yield _record_step(
|
| 935 |
session,
|
| 936 |
ScrapeStep(
|
| 937 |
step_number=step_num,
|
| 938 |
+
action="tool_call",
|
| 939 |
url=trending_url,
|
| 940 |
status="running",
|
| 941 |
+
message=f"browser.navigate(url='{trending_url}')",
|
| 942 |
+
extracted_data={
|
| 943 |
+
"tool_name": "browser.navigate",
|
| 944 |
+
"tool_description": "Navigate browser to GitHub trending page",
|
| 945 |
+
"parameters": {"url": trending_url, "wait_for": "page_load"},
|
| 946 |
+
},
|
| 947 |
timestamp=_now_iso(),
|
| 948 |
),
|
| 949 |
)
|
|
|
|
| 960 |
nav_reward = 0.5 if nav_obs.page_html else 0.0
|
| 961 |
total_reward += nav_reward
|
| 962 |
|
| 963 |
+
nav_success = bool(nav_obs.page_html)
|
| 964 |
+
yield _record_step(
|
| 965 |
+
session,
|
| 966 |
+
ScrapeStep(
|
| 967 |
+
step_number=step_num,
|
| 968 |
+
action="tool_call",
|
| 969 |
+
url=trending_url,
|
| 970 |
+
status="completed" if nav_success else "failed",
|
| 971 |
+
message=f"browser.navigate() → {len(nav_obs.page_html) if nav_obs.page_html else 0} bytes",
|
| 972 |
+
reward=0.1,
|
| 973 |
+
extracted_data={
|
| 974 |
+
"tool_name": "browser.navigate",
|
| 975 |
+
"result": {
|
| 976 |
+
"success": nav_success,
|
| 977 |
+
"html_length": len(nav_obs.page_html) if nav_obs.page_html else 0,
|
| 978 |
+
"status_code": 200 if nav_success else 0,
|
| 979 |
+
},
|
| 980 |
+
},
|
| 981 |
+
timestamp=_now_iso(),
|
| 982 |
+
),
|
| 983 |
+
)
|
| 984 |
+
|
| 985 |
# Update the navigation step with actual reward
|
| 986 |
step_num += 1
|
| 987 |
yield _record_step(
|
|
|
|
| 990 |
step_number=step_num,
|
| 991 |
action="navigate",
|
| 992 |
url=trending_url,
|
| 993 |
+
status="completed" if nav_success else "failed",
|
| 994 |
+
message=f"Navigated to {trending_url}" if nav_success else "Navigation failed",
|
| 995 |
reward=nav_reward,
|
| 996 |
duration_ms=nav_info.get("step_duration_ms", 0),
|
| 997 |
timestamp=_now_iso(),
|
|
|
|
| 1002 |
session["errors"].append("Failed to load GitHub trending page")
|
| 1003 |
return
|
| 1004 |
|
| 1005 |
+
# Tool call: html.parse
|
| 1006 |
+
step_num += 1
|
| 1007 |
+
yield _record_step(
|
| 1008 |
+
session,
|
| 1009 |
+
ScrapeStep(
|
| 1010 |
+
step_number=step_num,
|
| 1011 |
+
action="tool_call",
|
| 1012 |
+
url=trending_url,
|
| 1013 |
+
status="running",
|
| 1014 |
+
message="html.parse(content)",
|
| 1015 |
+
extracted_data={
|
| 1016 |
+
"tool_name": "html.parse",
|
| 1017 |
+
"tool_description": "Parse HTML document into structured DOM",
|
| 1018 |
+
"parameters": {"parser": "html.parser", "content_length": len(nav_obs.page_html)},
|
| 1019 |
+
},
|
| 1020 |
+
timestamp=_now_iso(),
|
| 1021 |
+
),
|
| 1022 |
+
)
|
| 1023 |
+
|
| 1024 |
soup = parse_html(nav_obs.page_html)
|
| 1025 |
|
| 1026 |
+
yield _record_step(
|
| 1027 |
+
session,
|
| 1028 |
+
ScrapeStep(
|
| 1029 |
+
step_number=step_num,
|
| 1030 |
+
action="tool_call",
|
| 1031 |
+
url=trending_url,
|
| 1032 |
+
status="completed",
|
| 1033 |
+
message="html.parse() → DOM ready",
|
| 1034 |
+
reward=0.05,
|
| 1035 |
+
extracted_data={
|
| 1036 |
+
"tool_name": "html.parse",
|
| 1037 |
+
"result": {"parsed": True, "soup_type": "BeautifulSoup"},
|
| 1038 |
+
},
|
| 1039 |
+
timestamp=_now_iso(),
|
| 1040 |
+
),
|
| 1041 |
+
)
|
| 1042 |
+
|
| 1043 |
+
# Tool call: html.select
|
| 1044 |
+
step_num += 1
|
| 1045 |
+
yield _record_step(
|
| 1046 |
+
session,
|
| 1047 |
+
ScrapeStep(
|
| 1048 |
+
step_number=step_num,
|
| 1049 |
+
action="tool_call",
|
| 1050 |
+
url=trending_url,
|
| 1051 |
+
status="running",
|
| 1052 |
+
message="html.select(selector='article.Box-row')",
|
| 1053 |
+
extracted_data={
|
| 1054 |
+
"tool_name": "html.select",
|
| 1055 |
+
"tool_description": "Select repository elements from trending page",
|
| 1056 |
+
"parameters": {"selector": "article.Box-row", "fallback": "div.Box-row"},
|
| 1057 |
+
},
|
| 1058 |
+
timestamp=_now_iso(),
|
| 1059 |
+
),
|
| 1060 |
+
)
|
| 1061 |
+
|
| 1062 |
+
# Find repository entries (GitHub trending structure)
|
| 1063 |
+
repo_articles = soup.find_all("article", class_="Box-row") or soup.find_all("div", class_="Box-row")
|
| 1064 |
+
|
| 1065 |
+
yield _record_step(
|
| 1066 |
+
session,
|
| 1067 |
+
ScrapeStep(
|
| 1068 |
+
step_number=step_num,
|
| 1069 |
+
action="tool_call",
|
| 1070 |
+
url=trending_url,
|
| 1071 |
+
status="completed",
|
| 1072 |
+
message=f"html.select() → {len(repo_articles)} elements",
|
| 1073 |
+
reward=0.1,
|
| 1074 |
+
extracted_data={
|
| 1075 |
+
"tool_name": "html.select",
|
| 1076 |
+
"result": {"elements_found": len(repo_articles), "selector_used": "article.Box-row"},
|
| 1077 |
+
},
|
| 1078 |
+
timestamp=_now_iso(),
|
| 1079 |
+
),
|
| 1080 |
+
)
|
| 1081 |
+
|
| 1082 |
step_num += 1
|
| 1083 |
yield _record_step(
|
| 1084 |
session,
|
|
|
|
| 1093 |
),
|
| 1094 |
)
|
| 1095 |
|
|
|
|
|
|
|
|
|
|
| 1096 |
for article in repo_articles[:20]: # Limit to first 20
|
| 1097 |
try:
|
| 1098 |
# Extract repo name and username
|
|
|
|
| 1154 |
),
|
| 1155 |
)
|
| 1156 |
|
| 1157 |
+
# Tool call: csv.generate
|
| 1158 |
+
step_num += 1
|
| 1159 |
+
yield _record_step(
|
| 1160 |
+
session,
|
| 1161 |
+
ScrapeStep(
|
| 1162 |
+
step_number=step_num,
|
| 1163 |
+
action="tool_call",
|
| 1164 |
+
url=trending_url,
|
| 1165 |
+
status="running",
|
| 1166 |
+
message="csv.generate(data, fields=['username', 'repo_name', 'stars', 'forks'])",
|
| 1167 |
+
extracted_data={
|
| 1168 |
+
"tool_name": "csv.generate",
|
| 1169 |
+
"tool_description": "Generate CSV output from repository data",
|
| 1170 |
+
"parameters": {
|
| 1171 |
+
"fields": ["username", "repo_name", "stars", "forks"],
|
| 1172 |
+
"row_count": len(trending_repos),
|
| 1173 |
+
},
|
| 1174 |
+
},
|
| 1175 |
+
timestamp=_now_iso(),
|
| 1176 |
+
),
|
| 1177 |
+
)
|
| 1178 |
+
|
| 1179 |
# Generate clean CSV output
|
| 1180 |
csv_buffer = io.StringIO()
|
| 1181 |
writer = csv.DictWriter(csv_buffer, fieldnames=["username", "repo_name", "stars", "forks"])
|
|
|
|
| 1183 |
writer.writerows(trending_repos)
|
| 1184 |
clean_csv = csv_buffer.getvalue()
|
| 1185 |
|
| 1186 |
+
yield _record_step(
|
| 1187 |
+
session,
|
| 1188 |
+
ScrapeStep(
|
| 1189 |
+
step_number=step_num,
|
| 1190 |
+
action="tool_call",
|
| 1191 |
+
url=trending_url,
|
| 1192 |
+
status="completed",
|
| 1193 |
+
message=f"csv.generate() → {len(clean_csv)} bytes",
|
| 1194 |
+
reward=0.1,
|
| 1195 |
+
extracted_data={
|
| 1196 |
+
"tool_name": "csv.generate",
|
| 1197 |
+
"result": {
|
| 1198 |
+
"csv_length": len(clean_csv),
|
| 1199 |
+
"rows": len(trending_repos),
|
| 1200 |
+
"columns": 4,
|
| 1201 |
+
},
|
| 1202 |
+
},
|
| 1203 |
+
timestamp=_now_iso(),
|
| 1204 |
+
),
|
| 1205 |
+
)
|
| 1206 |
+
|
| 1207 |
# Store the clean CSV directly as extracted data for CSV output format
|
| 1208 |
if request.output_format == OutputFormat.CSV:
|
| 1209 |
session["extracted_data"] = {
|
|
|
|
| 1707 |
),
|
| 1708 |
)
|
| 1709 |
|
| 1710 |
+
# Tool call: browser.navigate
|
| 1711 |
+
step_num += 1
|
| 1712 |
+
yield _record_step(
|
| 1713 |
+
session,
|
| 1714 |
+
ScrapeStep(
|
| 1715 |
+
step_number=step_num,
|
| 1716 |
+
action="tool_call",
|
| 1717 |
+
url=url,
|
| 1718 |
+
status="running",
|
| 1719 |
+
message="browser.navigate(url)",
|
| 1720 |
+
extracted_data={
|
| 1721 |
+
"tool_name": "browser.navigate",
|
| 1722 |
+
"tool_description": "Navigate browser to target URL",
|
| 1723 |
+
"parameters": {"url": url},
|
| 1724 |
+
},
|
| 1725 |
+
timestamp=_now_iso(),
|
| 1726 |
+
),
|
| 1727 |
+
)
|
| 1728 |
+
|
| 1729 |
navigate_action = Action(
|
| 1730 |
action_type=ActionType.NAVIGATE,
|
| 1731 |
parameters={"url": url},
|
|
|
|
| 1736 |
|
| 1737 |
nav_success = nav_info.get("action_result", {}).get("success", bool(nav_obs.page_html))
|
| 1738 |
|
| 1739 |
+
yield _record_step(
|
| 1740 |
+
session,
|
| 1741 |
+
ScrapeStep(
|
| 1742 |
+
step_number=step_num,
|
| 1743 |
+
action="tool_call",
|
| 1744 |
+
url=url,
|
| 1745 |
+
status="completed" if nav_success else "failed",
|
| 1746 |
+
message="browser.navigate(url) → success" if nav_success else "browser.navigate(url) → failed",
|
| 1747 |
+
reward=0.05,
|
| 1748 |
+
extracted_data={
|
| 1749 |
+
"tool_name": "browser.navigate",
|
| 1750 |
+
"result": {"success": nav_success, "html_length": len(nav_obs.page_html) if nav_obs.page_html else 0},
|
| 1751 |
+
},
|
| 1752 |
+
timestamp=_now_iso(),
|
| 1753 |
+
),
|
| 1754 |
+
)
|
| 1755 |
+
|
| 1756 |
yield _record_step(
|
| 1757 |
session,
|
| 1758 |
ScrapeStep(
|
|
|
|
| 1776 |
|
| 1777 |
for field_name in fields_to_extract:
|
| 1778 |
step_num += 1
|
| 1779 |
+
# Tool call: html.extract
|
| 1780 |
yield _record_step(
|
| 1781 |
session,
|
| 1782 |
ScrapeStep(
|
| 1783 |
step_number=step_num,
|
| 1784 |
+
action="tool_call",
|
| 1785 |
url=url,
|
| 1786 |
status="running",
|
| 1787 |
+
message=f"html.extract(field='{field_name}')",
|
| 1788 |
+
extracted_data={
|
| 1789 |
+
"tool_name": "html.extract",
|
| 1790 |
+
"tool_description": f"Extract {field_name} from HTML document",
|
| 1791 |
+
"parameters": {"field_name": field_name},
|
| 1792 |
+
},
|
| 1793 |
timestamp=_now_iso(),
|
| 1794 |
),
|
| 1795 |
)
|
|
|
|
| 1808 |
extracted[field_name] = ef.value
|
| 1809 |
break
|
| 1810 |
|
| 1811 |
+
value_preview = str(extracted.get(field_name, ""))[:100]
|
| 1812 |
+
yield _record_step(
|
| 1813 |
+
session,
|
| 1814 |
+
ScrapeStep(
|
| 1815 |
+
step_number=step_num,
|
| 1816 |
+
action="tool_call",
|
| 1817 |
+
url=url,
|
| 1818 |
+
status="completed",
|
| 1819 |
+
message=f"html.extract(field='{field_name}') → {value_preview}",
|
| 1820 |
+
reward=0.05,
|
| 1821 |
+
extracted_data={
|
| 1822 |
+
"tool_name": "html.extract",
|
| 1823 |
+
"result": {field_name: extracted.get(field_name)},
|
| 1824 |
+
},
|
| 1825 |
+
timestamp=_now_iso(),
|
| 1826 |
+
),
|
| 1827 |
+
)
|
| 1828 |
+
|
| 1829 |
yield _record_step(
|
| 1830 |
session,
|
| 1831 |
ScrapeStep(
|
frontend/src/components/Dashboard.tsx
CHANGED
|
@@ -64,6 +64,8 @@ const getStepIcon = (action: string): LucideIcon => {
|
|
| 64 |
'complete': CheckCircle,
|
| 65 |
'mcp_search': Search,
|
| 66 |
'python_sandbox': Terminal,
|
|
|
|
|
|
|
| 67 |
'error': XCircle,
|
| 68 |
};
|
| 69 |
return iconMap[action] || Activity;
|
|
@@ -89,6 +91,8 @@ const getStepColor = (action: string, status: string): string => {
|
|
| 89 |
'complete': 'text-green-400 bg-green-500/20 border-green-500/30',
|
| 90 |
'mcp_search': 'text-cyan-400 bg-cyan-500/20 border-cyan-500/30',
|
| 91 |
'python_sandbox': 'text-yellow-400 bg-yellow-500/20 border-yellow-500/30',
|
|
|
|
|
|
|
| 92 |
};
|
| 93 |
return colorMap[action] || 'text-slate-400 bg-slate-500/20 border-slate-500/30';
|
| 94 |
};
|
|
@@ -110,6 +114,13 @@ const StepAccordionItem: React.FC<StepAccordionItemProps> = ({ step, isExpanded,
|
|
| 110 |
const Icon = getStepIcon(step.action);
|
| 111 |
const colorClasses = getStepColor(step.action, step.status);
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
return (
|
| 114 |
<div className={classNames(
|
| 115 |
'border rounded-lg overflow-hidden transition-all',
|
|
@@ -120,14 +131,14 @@ const StepAccordionItem: React.FC<StepAccordionItemProps> = ({ step, isExpanded,
|
|
| 120 |
onClick={onToggle}
|
| 121 |
className="w-full flex items-center justify-between px-4 py-3 hover:bg-white/5 transition-colors"
|
| 122 |
>
|
| 123 |
-
<div className="flex items-center gap-3">
|
| 124 |
-
<div className={classNames('p-2 rounded-lg', colorClasses.split(' ').slice(1, 3).join(' '))}>
|
| 125 |
<Icon className={classNames('w-4 h-4', colorClasses.split(' ')[0])} />
|
| 126 |
</div>
|
| 127 |
-
<div className="text-left">
|
| 128 |
<div className="flex items-center gap-2">
|
| 129 |
<span className="text-sm font-medium text-white">
|
| 130 |
-
{step.action.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}
|
| 131 |
</span>
|
| 132 |
<Badge
|
| 133 |
variant={step.status === 'completed' ? 'success' : step.status === 'failed' ? 'error' : 'info'}
|
|
@@ -136,10 +147,13 @@ const StepAccordionItem: React.FC<StepAccordionItemProps> = ({ step, isExpanded,
|
|
| 136 |
{step.status}
|
| 137 |
</Badge>
|
| 138 |
</div>
|
| 139 |
-
<p className="text-xs text-slate-400 truncate
|
|
|
|
|
|
|
|
|
|
| 140 |
</div>
|
| 141 |
</div>
|
| 142 |
-
<div className="flex items-center gap-3">
|
| 143 |
<div className="text-right">
|
| 144 |
<span className="text-xs text-slate-500">Step {step.step_number}</span>
|
| 145 |
{step.reward > 0 && (
|
|
@@ -156,6 +170,42 @@ const StepAccordionItem: React.FC<StepAccordionItemProps> = ({ step, isExpanded,
|
|
| 156 |
|
| 157 |
{isExpanded && (
|
| 158 |
<div className="px-4 py-3 border-t border-white/10 bg-slate-900/50 space-y-3">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
{/* Step Details */}
|
| 160 |
<div className="grid grid-cols-2 gap-4 text-xs">
|
| 161 |
<div>
|
|
@@ -188,8 +238,8 @@ const StepAccordionItem: React.FC<StepAccordionItemProps> = ({ step, isExpanded,
|
|
| 188 |
</div>
|
| 189 |
</div>
|
| 190 |
|
| 191 |
-
{/* Extracted Data */}
|
| 192 |
-
{step.extracted_data && Object.keys(step.extracted_data).length > 0 && (
|
| 193 |
<div className="mt-3">
|
| 194 |
<p className="text-xs text-slate-500 mb-2">Extracted Data:</p>
|
| 195 |
<pre className="text-xs text-slate-300 bg-slate-800/50 rounded-lg p-3 overflow-auto max-h-40 font-mono">
|
|
|
|
| 64 |
'complete': CheckCircle,
|
| 65 |
'mcp_search': Search,
|
| 66 |
'python_sandbox': Terminal,
|
| 67 |
+
'site_template': FileText,
|
| 68 |
+
'tool_call': Zap,
|
| 69 |
'error': XCircle,
|
| 70 |
};
|
| 71 |
return iconMap[action] || Activity;
|
|
|
|
| 91 |
'complete': 'text-green-400 bg-green-500/20 border-green-500/30',
|
| 92 |
'mcp_search': 'text-cyan-400 bg-cyan-500/20 border-cyan-500/30',
|
| 93 |
'python_sandbox': 'text-yellow-400 bg-yellow-500/20 border-yellow-500/30',
|
| 94 |
+
'site_template': 'text-violet-400 bg-violet-500/20 border-violet-500/30',
|
| 95 |
+
'tool_call': 'text-yellow-300 bg-yellow-500/20 border-yellow-500/30',
|
| 96 |
};
|
| 97 |
return colorMap[action] || 'text-slate-400 bg-slate-500/20 border-slate-500/30';
|
| 98 |
};
|
|
|
|
| 114 |
const Icon = getStepIcon(step.action);
|
| 115 |
const colorClasses = getStepColor(step.action, step.status);
|
| 116 |
|
| 117 |
+
// Check if this is a tool call
|
| 118 |
+
const isToolCall = step.action === 'tool_call';
|
| 119 |
+
const toolName = (step.extracted_data?.tool_name as string) || '';
|
| 120 |
+
const toolDescription = (step.extracted_data?.tool_description as string) || '';
|
| 121 |
+
const toolParameters = (step.extracted_data?.parameters as Record<string, any>) || {};
|
| 122 |
+
const toolResult = (step.extracted_data?.result as Record<string, any>) || {};
|
| 123 |
+
|
| 124 |
return (
|
| 125 |
<div className={classNames(
|
| 126 |
'border rounded-lg overflow-hidden transition-all',
|
|
|
|
| 131 |
onClick={onToggle}
|
| 132 |
className="w-full flex items-center justify-between px-4 py-3 hover:bg-white/5 transition-colors"
|
| 133 |
>
|
| 134 |
+
<div className="flex items-center gap-3 flex-1 min-w-0">
|
| 135 |
+
<div className={classNames('p-2 rounded-lg flex-shrink-0', colorClasses.split(' ').slice(1, 3).join(' '))}>
|
| 136 |
<Icon className={classNames('w-4 h-4', colorClasses.split(' ')[0])} />
|
| 137 |
</div>
|
| 138 |
+
<div className="text-left min-w-0 flex-1">
|
| 139 |
<div className="flex items-center gap-2">
|
| 140 |
<span className="text-sm font-medium text-white">
|
| 141 |
+
{isToolCall ? toolName : step.action.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}
|
| 142 |
</span>
|
| 143 |
<Badge
|
| 144 |
variant={step.status === 'completed' ? 'success' : step.status === 'failed' ? 'error' : 'info'}
|
|
|
|
| 147 |
{step.status}
|
| 148 |
</Badge>
|
| 149 |
</div>
|
| 150 |
+
<p className="text-xs text-slate-400 truncate">{step.message}</p>
|
| 151 |
+
{isToolCall && toolDescription && (
|
| 152 |
+
<p className="text-[10px] text-slate-500 mt-0.5">{toolDescription}</p>
|
| 153 |
+
)}
|
| 154 |
</div>
|
| 155 |
</div>
|
| 156 |
+
<div className="flex items-center gap-3 flex-shrink-0">
|
| 157 |
<div className="text-right">
|
| 158 |
<span className="text-xs text-slate-500">Step {step.step_number}</span>
|
| 159 |
{step.reward > 0 && (
|
|
|
|
| 170 |
|
| 171 |
{isExpanded && (
|
| 172 |
<div className="px-4 py-3 border-t border-white/10 bg-slate-900/50 space-y-3">
|
| 173 |
+
{/* Tool Call Specific Details */}
|
| 174 |
+
{isToolCall && (
|
| 175 |
+
<>
|
| 176 |
+
<div className="bg-slate-800/50 rounded-lg p-3 space-y-2">
|
| 177 |
+
<div className="flex items-center gap-2">
|
| 178 |
+
<Zap className="w-3 h-3 text-yellow-400" />
|
| 179 |
+
<span className="text-xs font-semibold text-yellow-400">Tool Call Details</span>
|
| 180 |
+
</div>
|
| 181 |
+
|
| 182 |
+
{toolDescription && (
|
| 183 |
+
<div className="text-xs text-slate-300">
|
| 184 |
+
<span className="text-slate-500">Description:</span> {toolDescription}
|
| 185 |
+
</div>
|
| 186 |
+
)}
|
| 187 |
+
|
| 188 |
+
{Object.keys(toolParameters).length > 0 && (
|
| 189 |
+
<div>
|
| 190 |
+
<span className="text-xs text-slate-500">Parameters:</span>
|
| 191 |
+
<pre className="text-xs text-cyan-300 bg-slate-900/70 rounded p-2 mt-1 overflow-auto max-h-20 font-mono">
|
| 192 |
+
{JSON.stringify(toolParameters, null, 2)}
|
| 193 |
+
</pre>
|
| 194 |
+
</div>
|
| 195 |
+
)}
|
| 196 |
+
|
| 197 |
+
{Object.keys(toolResult).length > 0 && (
|
| 198 |
+
<div>
|
| 199 |
+
<span className="text-xs text-slate-500">Result:</span>
|
| 200 |
+
<pre className="text-xs text-emerald-300 bg-slate-900/70 rounded p-2 mt-1 overflow-auto max-h-20 font-mono">
|
| 201 |
+
{JSON.stringify(toolResult, null, 2)}
|
| 202 |
+
</pre>
|
| 203 |
+
</div>
|
| 204 |
+
)}
|
| 205 |
+
</div>
|
| 206 |
+
</>
|
| 207 |
+
)}
|
| 208 |
+
|
| 209 |
{/* Step Details */}
|
| 210 |
<div className="grid grid-cols-2 gap-4 text-xs">
|
| 211 |
<div>
|
|
|
|
| 238 |
</div>
|
| 239 |
</div>
|
| 240 |
|
| 241 |
+
{/* Extracted Data (non-tool calls or if additional data exists) */}
|
| 242 |
+
{step.extracted_data && Object.keys(step.extracted_data).length > 0 && !isToolCall && (
|
| 243 |
<div className="mt-3">
|
| 244 |
<p className="text-xs text-slate-500 mb-2">Extracted Data:</p>
|
| 245 |
<pre className="text-xs text-slate-300 bg-slate-800/50 rounded-lg p-3 overflow-auto max-h-40 font-mono">
|