windhover 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- windhover/__init__.py +5 -0
- windhover/config.py +32 -0
- windhover/demo_graph.py +48 -0
- windhover/extract.py +103 -0
- windhover/pricing.json +16 -0
- windhover/server.py +333 -0
- windhover/static/icon.svg +8 -0
- windhover/static/index.html +843 -0
- windhover/static/manifest.json +18 -0
- windhover/static/vendor/cytoscape-dagre.js +397 -0
- windhover/static/vendor/cytoscape.min.js +32 -0
- windhover/static/vendor/dagre.min.js +3809 -0
- windhover/static/vendor/fonts/fraunces-500.woff2 +0 -0
- windhover/static/vendor/fonts/fraunces-600-italic.woff2 +0 -0
- windhover/static/vendor/fonts/fraunces-600.woff2 +0 -0
- windhover/store.py +347 -0
- windhover/tracer.py +325 -0
- windhover-0.7.0.dist-info/METADATA +111 -0
- windhover-0.7.0.dist-info/RECORD +22 -0
- windhover-0.7.0.dist-info/WHEEL +4 -0
- windhover-0.7.0.dist-info/entry_points.txt +2 -0
- windhover-0.7.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
6
|
+
<meta name="theme-color" content="#0E0E1C" media="(prefers-color-scheme: dark)">
|
|
7
|
+
<meta name="theme-color" content="#F4F4F8" media="(prefers-color-scheme: light)">
|
|
8
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
9
|
+
<link rel="manifest" href="/manifest.json">
|
|
10
|
+
<title>Windhover</title>
|
|
11
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%20%3Cdefs%3E%3ClinearGradient%20id%3D%22bg%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%221%22%3E%20%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23A32C3E%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23571017%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%20%3Crect%20width%3D%22512%22%20height%3D%22512%22%20rx%3D%22112%22%20fill%3D%22url%28%23bg%29%22%2F%3E%20%3Cg%20transform%3D%22translate%2856.0%20116.7%29%20scale%280.62893%29%22%3E%20%3Cpath%20d%3D%22M312.90%2C425.29%20C310.64%2C423.25%20306.72%2C418.86%20304.18%2C415.54%20C301.65%2C412.22%20298.88%2C408.93%20298.03%2C408.23%20C295.97%2C406.53%20289.90%2C407.25%20282.33%2C410.11%20C274.09%2C413.23%20271.34%2C412.33%20268.20%2C405.50%20C262.04%2C392.12%20261.63%2C391.51%20258.52%2C391.16%20C256.88%2C390.98%20253.06%2C391.35%20250.02%2C391.99%20C242.01%2C393.68%20239.51%2C392.45%20236.41%2C385.25%20C230.11%2C370.65%20228.49%2C369.21%20220.40%2C371.02%20C217.79%2C371.61%20214.35%2C371.80%20212.76%2C371.45%20C209.08%2C370.64%20206.01%2C365.75%20206.00%2C360.69%20C206.00%2C357.52%20206.81%2C356.01%20210.82%2C351.69%20C213.47%2C348.84%20217.71%2C343.80%20220.23%2C340.50%20C222.75%2C337.20%20226.92%2C332.48%20229.51%2C330.00%20C232.09%2C327.52%20236.81%2C322.12%20240.01%2C318.00%20C243.20%2C313.88%20249.27%2C307.12%20253.49%2C303.00%20C257.72%2C298.88%20263.24%2C292.80%20265.78%2C289.50%20C268.31%2C286.20%20272.76%2C280.90%20275.66%2C277.73%20C282.46%2C270.28%20283.78%2C266.15%20281.64%2C258.90%20C278.68%2C248.89%20277.41%2C247.67%20267.38%2C245.23%20C260.71%2C243.60%20251.53%2C239.67%20230.50%2C229.45%20C215.10%2C221.96%20200.47%2C214.74%20198.00%2C213.41%20C195.53%2C212.08%20184.95%2C206.76%20174.50%2C201.60%20C164.05%2C196.44%20153.25%2C191.06%20150.50%2C189.65%20C147.75%2C188.24%20137.08%2C182.79%20126.78%2C177.55%20C114.19%2C171.14%20107.49%2C167.16%20106.33%2C165.38%20C104.65%2C162.82%20104.67%2C162.68%20106.91%2C160.87%20C108.24%2C159.80%20110.80%2C159.00%20112.91%2C159.00%20C116.69%2C159.00%20119.00%2C157.60%20119.00%2C155.31%20C119.00%2C154.60%20114.84%2C151.96%20109.75%2C149.44%20C99.17%2C144.20%2067.99%2C123.81%2066.21%2C120.97%20C65.56%2C119.93%2065.31%2C118.35%2065.66%2C117.45%20C66.19%2C116.07%2067.32%2C115.93%2073.06%2C116.51%20C81.49%2C117.35%2084.00%2C116.65%2084.00%2C113.45%20C84.00%2C111.44%2081.40%2C109.29%2068.75%2C100.84%20C52.97%2C90.30%2037.70%2C78.18%2035.86%2C74.74%20C35.05%2C73.23%2035.18%2C72.40%2036.40%2C71.18%20C37.85%2C69.72%2038.96%2C69.82%2049.38%2C72.36%20C62.67%2C75.61%2067.35%2C75.59%2067.81%2C72.29%20C68.05%2C70.60%2065.53%2C67.46%2057.31%2C59.22%20C46.57%2C48.44%2041.78%2C42.47%2040.57%2C38.36%20C39.71%2C35.46%2042.49%2C32.71%2045.36%2C33.60%20C46.53%2C33.97%2060.99%2C41.01%2077.47%2C49.25%20C93.96%2C57.49%20112.41%2C66.03%20118.47%2C68.23%20C146.15%2C78.27%20183.73%2C90.19%20202.92%2C95.00%20C219.11%2C99.06%20221.00%2C99.92%20227.79%2C106.30%20C230.93%2C109.26%20235.53%2C113.06%20238.00%2C114.75%20C240.47%2C116.45%20245.43%2C120.83%20249.00%2C124.50%20C252.57%2C128.17%20257.52%2C132.55%20260.00%2C134.25%20C262.48%2C135.94%20267.05%2C139.73%20270.18%2C142.66%20C277.53%2C149.58%20279.88%2C149.68%20287.17%2C143.41%20C290.10%2C140.89%20295.20%2C137.43%20298.50%2C135.71%20C303.95%2C132.88%20305.53%2C132.56%20315.72%2C132.19%20C331.48%2C131.62%20338.30%2C133.73%20347.42%2C142.00%20C356.10%2C149.87%20358.09%2C149.94%20365.82%2C142.66%20C368.95%2C139.73%20373.52%2C135.94%20376.00%2C134.25%20C378.48%2C132.55%20383.43%2C128.17%20387.00%2C124.49%20C390.57%2C120.82%20395.98%2C116.11%20399.00%2C114.01%20C402.02%2C111.92%20406.07%2C108.54%20408.00%2C106.51%20C412.56%2C101.70%20416.56%2C99.78%20427.50%2C97.09%20C442.99%2C93.30%20489.94%2C78.25%20517.53%2C68.23%20C523.59%2C66.03%20542.04%2C57.49%20558.53%2C49.25%20C575.01%2C41.01%20589.47%2C33.97%20590.64%2C33.60%20C592.03%2C33.17%20593.37%2C33.51%20594.43%2C34.57%20C597.63%2C37.77%20594.44%2C42.92%20579.49%2C58.73%20C573.72%2C64.83%20569.00%2C70.52%20569.00%2C71.38%20C569.00%2C74.33%20572.54%2C75.17%20578.74%2C73.67%20C591.83%2C70.50%20597.95%2C69.72%20599.51%2C71.01%20C603.83%2C74.59%20599.88%2C78.43%20573.00%2C96.80%20C566.67%2C101.12%20558.72%2C106.13%20555.32%2C107.93%20C548.71%2C111.43%20547.22%2C113.35%20549.22%2C115.76%20C550.25%2C117.00%20552.19%2C117.16%20560.13%2C116.64%20C568.29%2C116.10%20569.88%2C116.24%20570.38%2C117.54%20C570.70%2C118.39%20570.44%2C119.93%20569.79%2C120.97%20C568.01%2C123.81%20536.83%2C144.20%20526.25%2C149.44%20C521.16%2C151.96%20517.00%2C154.60%20517.00%2C155.31%20C517.00%2C157.60%20519.31%2C159.00%20523.09%2C159.00%20C525.20%2C159.00%20527.76%2C159.80%20529.09%2C160.87%20C533.98%2C164.83%20532.25%2C165.87%20461.87%2C201.06%20C425.53%2C219.23%20394.15%2C235.20%20392.12%2C236.54%20C388.01%2C239.26%20374.07%2C244.22%20365.58%2C245.98%20C361.45%2C246.84%20359.57%2C247.82%20358.19%2C249.82%20C355.43%2C253.80%20353.00%2C260.60%20353.00%2C264.32%20C353.00%2C267.82%20357.09%2C275.09%20360.98%2C278.50%20C362.23%2C279.60%20365.86%2C283.88%20369.04%2C288.00%20C372.21%2C292.12%20378.27%2C298.88%20382.50%2C303.00%20C386.73%2C307.12%20392.80%2C313.88%20396.00%2C318.00%20C399.19%2C322.12%20404.37%2C327.98%20407.50%2C331.00%20C415.03%2C338.28%20427.28%2C352.69%20429.35%2C356.70%20C431.73%2C361.32%20431.39%2C364.53%20428.13%2C368.18%20C425.01%2C371.67%20422.07%2C372.14%20412.93%2C370.56%20C406.63%2C369.48%20403.40%2C372.27%20401.99%2C380.00%20C400.84%2C386.26%20398.56%2C389.89%20394.54%2C391.86%20C392.28%2C392.97%20390.71%2C393.00%20386.11%2C392.02%20C383.00%2C391.36%20379.12%2C390.98%20377.48%2C391.16%20C374.37%2C391.51%20373.96%2C392.12%20367.80%2C405.50%20C364.66%2C412.33%20361.91%2C413.23%20353.67%2C410.11%20C346.10%2C407.25%20340.03%2C406.53%20337.97%2C408.23%20C337.12%2C408.93%20334.35%2C412.22%20331.82%2C415.54%20C326.95%2C421.91%20319.68%2C429.00%20318.00%2C429.00%20C317.45%2C429.00%20315.16%2C427.33%20312.90%2C425.29%20z%20M323.60%2C201.25%20C329.52%2C190.76%20330.86%2C189.21%20336.20%2C186.68%20C344.02%2C182.98%20348.00%2C178.02%20348.00%2C172.00%20C348.00%2C168.25%20346.43%2C165.00%20344.62%2C165.00%20C343.90%2C165.00%20340.29%2C167.93%20336.61%2C171.50%20C332.93%2C175.07%20329.50%2C178.00%20328.99%2C178.00%20C328.48%2C178.00%20326.47%2C180.49%20324.52%2C183.54%20C322.57%2C186.58%20320.28%2C189.34%20319.42%2C189.67%20C317.02%2C190.59%20313.64%2C188.28%20310.95%2C183.86%20C308.20%2C179.35%20294.66%2C167.00%20292.47%2C167.00%20C289.94%2C167.00%20287.69%2C171.70%20288.33%2C175.64%20C289.11%2C180.46%20291.29%2C182.65%20298.80%2C186.21%20C305.24%2C189.26%20306.28%2C190.41%20312.40%2C201.25%20C314.64%2C205.22%20315.56%2C206.00%20318.00%2C206.00%20C320.44%2C206.00%20321.36%2C205.22%20323.60%2C201.25%20z%22%20fill%3D%22%23fff%22%2F%3E%20%3C%2Fg%3E%20%3C%2Fsvg%3E">
|
|
12
|
+
<script src="/static/vendor/cytoscape.min.js"></script>
|
|
13
|
+
<script src="/static/vendor/dagre.min.js"></script>
|
|
14
|
+
<script src="/static/vendor/cytoscape-dagre.js"></script>
|
|
15
|
+
<style>
|
|
16
|
+
/* ---------- faces ---------- */
|
|
17
|
+
@font-face{font-family:"Fraunces";src:url(/static/vendor/fonts/fraunces-500.woff2) format("woff2");font-weight:500;font-style:normal;font-display:swap}
|
|
18
|
+
@font-face{font-family:"Fraunces";src:url(/static/vendor/fonts/fraunces-600.woff2) format("woff2");font-weight:600;font-style:normal;font-display:swap}
|
|
19
|
+
@font-face{font-family:"Fraunces";src:url(/static/vendor/fonts/fraunces-600-italic.woff2) format("woff2");font-weight:600;font-style:italic;font-display:swap}
|
|
20
|
+
/* ---------- design tokens — kestrel plumage ----------
|
|
21
|
+
ink (deep indigo-black) · dawn paper
|
|
22
|
+
rufous (the kestrel's chestnut back) = PRIMARY accent + nodes/runs
|
|
23
|
+
slate-blue (male kestrel's head) = LLM spans · indigo = tools */
|
|
24
|
+
:root{
|
|
25
|
+
--bg:#F4F4F8; --surface:#FFFFFF; --surface2:#ECECF3; --border:#DEDEE9; --border2:#C4C4D7;
|
|
26
|
+
--text:#1C1B33; --text2:#4A4A66; --muted:#8A8AA3;
|
|
27
|
+
--accent:#8E2434; --accent-weak:#F7E8EB; --accent-text:#6E1B28;
|
|
28
|
+
--rufous:#8E2434; --rufous-weak:#F7E8EB;
|
|
29
|
+
--ok:#2E7D4F; --ok-weak:#E7F3EC; --warn:#B45309; --warn-weak:#FCEEDD; --err:#D0552F; --err-weak:#FAEBE5;
|
|
30
|
+
--chip-node:#F7E8EB; --chip-node-t:#6E1B28; --chip-llm:#E8EEF4; --chip-llm-t:#3D5876; --chip-tool:#E9EAFB; --chip-tool-t:#3A3FC4; --chip-ret:#E1F1EE; --chip-ret-t:#1F6B60;
|
|
31
|
+
--rail:#14132B; --rail-text:#B9B9D6; --rail-text2:#75759E; --rail-line:#262544; --rail-on:#FFFFFF;
|
|
32
|
+
--shadow:0 1px 2px rgba(28,27,51,.05),0 3px 12px rgba(28,27,51,.05);
|
|
33
|
+
--shadow-lg:0 10px 40px rgba(28,27,51,.18);
|
|
34
|
+
--r:8px; --r-lg:12px;
|
|
35
|
+
--mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;
|
|
36
|
+
--display:"Fraunces",Georgia,"Times New Roman",serif;
|
|
37
|
+
}
|
|
38
|
+
:root[data-theme="dark"]{
|
|
39
|
+
--bg:#0E0E1C; --surface:#16162A; --surface2:#1D1D35; --border:#28284A; --border2:#3A3A5E;
|
|
40
|
+
--text:#E9E8F4; --text2:#A9A9C7; --muted:#6C6C8F;
|
|
41
|
+
--accent:#D4626F; --accent-weak:#331318; --accent-text:#EC9CA6;
|
|
42
|
+
--rufous:#D4626F; --rufous-weak:#331318;
|
|
43
|
+
--ok:#4CC38A; --ok-weak:#12291D; --warn:#E5A13D; --warn-weak:#2E2210; --err:#F08A5E; --err-weak:#331B10;
|
|
44
|
+
--chip-node:#331318; --chip-node-t:#EC9CA6; --chip-llm:#16283A; --chip-llm-t:#7FB2E5; --chip-tool:#232349; --chip-tool-t:#A9ACF8; --chip-ret:#0F2A26; --chip-ret-t:#5FC9B8;
|
|
45
|
+
--rail:#0A0A16; --rail-text:#A9A9C7; --rail-text2:#63638A; --rail-line:#202040; --rail-on:#FFFFFF;
|
|
46
|
+
--shadow:0 1px 2px rgba(0,0,0,.3),0 4px 16px rgba(0,0,0,.35);
|
|
47
|
+
--shadow-lg:0 12px 48px rgba(0,0,0,.55);
|
|
48
|
+
}
|
|
49
|
+
@media (prefers-color-scheme: dark){ :root:not([data-theme]){
|
|
50
|
+
--bg:#0E0E1C; --surface:#16162A; --surface2:#1D1D35; --border:#28284A; --border2:#3A3A5E;
|
|
51
|
+
--text:#E9E8F4; --text2:#A9A9C7; --muted:#6C6C8F;
|
|
52
|
+
--accent:#D4626F; --accent-weak:#331318; --accent-text:#EC9CA6;
|
|
53
|
+
--rufous:#D4626F; --rufous-weak:#331318;
|
|
54
|
+
--ok:#4CC38A; --ok-weak:#12291D; --warn:#E5A13D; --warn-weak:#2E2210; --err:#F08A5E; --err-weak:#331B10;
|
|
55
|
+
--chip-node:#331318; --chip-node-t:#EC9CA6; --chip-llm:#16283A; --chip-llm-t:#7FB2E5; --chip-tool:#232349; --chip-tool-t:#A9ACF8; --chip-ret:#0F2A26; --chip-ret-t:#5FC9B8;
|
|
56
|
+
--rail:#0A0A16; --rail-text:#A9A9C7; --rail-text2:#63638A; --rail-line:#202040; --rail-on:#FFFFFF;
|
|
57
|
+
--shadow:0 1px 2px rgba(0,0,0,.3),0 4px 16px rgba(0,0,0,.35);
|
|
58
|
+
--shadow-lg:0 12px 48px rgba(0,0,0,.55);
|
|
59
|
+
}}
|
|
60
|
+
/* ---------- base ---------- */
|
|
61
|
+
*{box-sizing:border-box}
|
|
62
|
+
html,body{margin:0;height:100%}
|
|
63
|
+
body{background:var(--bg);color:var(--text);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Inter","Segoe UI",Roboto,sans-serif;
|
|
64
|
+
-webkit-font-smoothing:antialiased;overscroll-behavior:none}
|
|
65
|
+
button{font:inherit;color:inherit;background:none;border:none;cursor:pointer;padding:0}
|
|
66
|
+
.num{font-variant-numeric:tabular-nums}
|
|
67
|
+
::-webkit-scrollbar{width:10px;height:10px}
|
|
68
|
+
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:6px;border:2px solid var(--bg)}
|
|
69
|
+
::-webkit-scrollbar-track{background:transparent}
|
|
70
|
+
|
|
71
|
+
/* ---------- app shell ---------- */
|
|
72
|
+
#app{display:flex;height:100dvh}
|
|
73
|
+
aside{width:216px;flex:none;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;padding:16px 10px calc(env(safe-area-inset-bottom) + 12px)}
|
|
74
|
+
.logo{display:flex;align-items:center;gap:10px;padding:4px 10px 16px;font-weight:700;font-size:15px;letter-spacing:.2px}
|
|
75
|
+
.logo svg{flex:none}
|
|
76
|
+
.nav{display:flex;flex-direction:column;gap:2px}
|
|
77
|
+
.nav button{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;color:var(--text2);font-weight:500;text-align:left;width:100%}
|
|
78
|
+
.nav button:hover{background:var(--surface2)}
|
|
79
|
+
.nav button.on{background:var(--accent-weak);color:var(--accent-text);font-weight:600}
|
|
80
|
+
.nav svg{width:16px;height:16px;flex:none}
|
|
81
|
+
.side-foot{margin-top:auto;padding:10px;color:var(--muted);font-size:11.5px;display:flex;align-items:center;gap:8px}
|
|
82
|
+
main{flex:1;display:flex;flex-direction:column;min-width:0}
|
|
83
|
+
.topbar{height:56px;flex:none;display:flex;align-items:center;gap:12px;padding:0 20px;background:var(--surface);border-bottom:1px solid var(--border)}
|
|
84
|
+
.crumb{font-weight:600;font-size:15px}
|
|
85
|
+
.pill{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);background:var(--surface2);border-radius:999px;padding:3px 10px;font-size:12px;color:var(--text2);font-family:var(--mono)}
|
|
86
|
+
.live-dot{width:7px;height:7px;border-radius:50%;background:var(--ok)}
|
|
87
|
+
.spacer{flex:1}
|
|
88
|
+
.icon-btn{width:34px;height:34px;display:grid;place-items:center;border:1px solid var(--border);border-radius:8px;color:var(--text2);background:var(--surface)}
|
|
89
|
+
.icon-btn:hover{background:var(--surface2)}
|
|
90
|
+
.btn{display:inline-flex;align-items:center;gap:7px;padding:8px 14px;border-radius:8px;font-weight:600;font-size:13.5px;white-space:nowrap}
|
|
91
|
+
.btn.primary{background:var(--accent);color:#fff;box-shadow:var(--shadow)}
|
|
92
|
+
.btn.primary:hover{filter:brightness(1.07)}
|
|
93
|
+
.btn.primary[disabled]{opacity:.55;cursor:default}
|
|
94
|
+
.btn.ghost{border:1px solid var(--border);color:var(--text2);background:var(--surface)}
|
|
95
|
+
.btn.ghost:hover{background:var(--surface2)}
|
|
96
|
+
.content{flex:1;overflow:auto;position:relative}
|
|
97
|
+
.page{padding:20px;max-width:1200px}
|
|
98
|
+
|
|
99
|
+
/* ---------- graph view ---------- */
|
|
100
|
+
#view-graph{position:absolute;inset:0}
|
|
101
|
+
#cy{position:absolute;inset:0}
|
|
102
|
+
.graph-tools{position:absolute;top:14px;left:14px;display:flex;gap:8px;z-index:5}
|
|
103
|
+
.legend{position:absolute;bottom:calc(env(safe-area-inset-bottom) + 14px);left:14px;display:flex;gap:10px;z-index:5;
|
|
104
|
+
background:var(--surface);border:1px solid var(--border);border-radius:999px;box-shadow:var(--shadow);padding:6px 14px;font-size:12px;color:var(--text2)}
|
|
105
|
+
.lg{display:flex;align-items:center;gap:6px}
|
|
106
|
+
.sw{width:9px;height:9px;border-radius:3px;border:1.5px solid var(--border2)}
|
|
107
|
+
.sw.run{border-color:var(--warn);background:var(--warn-weak)} .sw.done{border-color:var(--ok);background:var(--ok-weak)} .sw.err{border-color:var(--err);background:var(--err-weak)}
|
|
108
|
+
|
|
109
|
+
/* ---------- table ---------- */
|
|
110
|
+
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);box-shadow:var(--shadow);overflow:hidden}
|
|
111
|
+
table{width:100%;border-collapse:collapse;font-size:13.5px}
|
|
112
|
+
th{position:sticky;top:0;background:var(--surface);text-align:left;font-size:11.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;padding:10px 14px;border-bottom:1px solid var(--border);white-space:nowrap}
|
|
113
|
+
td{padding:11px 14px;border-bottom:1px solid var(--border);white-space:nowrap;vertical-align:middle}
|
|
114
|
+
tbody tr{cursor:pointer}
|
|
115
|
+
tbody tr:hover{background:var(--surface2)}
|
|
116
|
+
tbody tr:last-child td{border-bottom:none}
|
|
117
|
+
td.r,th.r{text-align:right}
|
|
118
|
+
.mono{font-family:var(--mono);font-size:12.5px;color:var(--text2)}
|
|
119
|
+
.status{display:inline-flex;align-items:center;gap:6px;font-weight:600;font-size:12px;padding:2px 9px;border-radius:999px}
|
|
120
|
+
.status .d{width:6px;height:6px;border-radius:50%}
|
|
121
|
+
.status.done{background:var(--ok-weak);color:var(--ok)} .status.done .d{background:var(--ok)}
|
|
122
|
+
.status.running{background:var(--warn-weak);color:var(--warn)} .status.running .d{background:var(--warn);animation:pulse 1.2s infinite}
|
|
123
|
+
.status.error{background:var(--err-weak);color:var(--err)} .status.error .d{background:var(--err)}
|
|
124
|
+
.status.interrupted{background:var(--warn-weak);color:var(--warn)} .status.interrupted .d{background:var(--warn)}
|
|
125
|
+
@keyframes pulse{50%{opacity:.35}}
|
|
126
|
+
.src{font-size:11px;border:1px solid var(--border);border-radius:5px;padding:1px 6px;color:var(--muted)}
|
|
127
|
+
.page-head{display:flex;align-items:baseline;gap:10px;margin:2px 2px 14px}
|
|
128
|
+
.page-head h1{font-size:17px;margin:0;font-weight:700}
|
|
129
|
+
.page-head .sub{color:var(--muted);font-size:13px}
|
|
130
|
+
.empty{padding:56px 20px;text-align:center;color:var(--muted)}
|
|
131
|
+
.empty .t{font-weight:600;color:var(--text2);margin-bottom:4px}
|
|
132
|
+
|
|
133
|
+
/* ---------- stats ---------- */
|
|
134
|
+
.metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:16px}
|
|
135
|
+
.metric{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);box-shadow:var(--shadow);padding:14px 16px}
|
|
136
|
+
.metric .v{font-size:22px;font-weight:700;letter-spacing:-.02em}
|
|
137
|
+
.metric .l{color:var(--muted);font-size:12px;margin-top:2px}
|
|
138
|
+
.latrow{display:grid;grid-template-columns:140px 1fr 74px;align-items:center;gap:12px;padding:9px 14px;border-bottom:1px solid var(--border)}
|
|
139
|
+
.latrow:last-child{border-bottom:none}
|
|
140
|
+
.latname{font-weight:600;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
141
|
+
.lattrack{height:8px;background:var(--surface2);border-radius:4px;overflow:hidden}
|
|
142
|
+
.latfill{height:100%;background:var(--accent);border-radius:4px;opacity:.85}
|
|
143
|
+
.latms{text-align:right;color:var(--text2);font-family:var(--mono);font-size:12px}
|
|
144
|
+
|
|
145
|
+
/* ---------- drawer (run detail) ---------- */
|
|
146
|
+
.scrim{position:fixed;inset:0;background:rgba(2,6,23,.45);opacity:0;pointer-events:none;transition:.18s;z-index:40}
|
|
147
|
+
.scrim.on{opacity:1;pointer-events:auto}
|
|
148
|
+
.drawer{position:fixed;top:0;right:0;bottom:0;width:min(600px,100vw);background:var(--surface);border-left:1px solid var(--border);
|
|
149
|
+
box-shadow:var(--shadow-lg);transform:translateX(102%);transition:transform .24s cubic-bezier(.3,.9,.35,1);z-index:50;display:flex;flex-direction:column}
|
|
150
|
+
.drawer.on{transform:none}
|
|
151
|
+
.dhead{padding:16px 20px 12px;border-bottom:1px solid var(--border)}
|
|
152
|
+
.dhead .row1{display:flex;align-items:center;gap:10px}
|
|
153
|
+
.dhead h2{margin:0;font-size:15px;font-weight:700}
|
|
154
|
+
.dhead .x{margin-left:auto}
|
|
155
|
+
.dmeta{display:flex;gap:14px;flex-wrap:wrap;margin-top:8px;color:var(--text2);font-size:12.5px}
|
|
156
|
+
.dmeta b{color:var(--text);font-weight:600}
|
|
157
|
+
.dbody{flex:1;overflow:auto;padding:14px 20px calc(env(safe-area-inset-bottom) + 18px)}
|
|
158
|
+
/* trace tree */
|
|
159
|
+
.tr-row{display:grid;grid-template-columns:1fr 120px 92px;align-items:center;gap:8px;padding:7px 8px;border-radius:8px;cursor:pointer}
|
|
160
|
+
.tr-row.flat{grid-template-columns:1fr auto}
|
|
161
|
+
.tr-row:hover{background:var(--surface2)}
|
|
162
|
+
.tr-main{display:flex;align-items:center;gap:8px;min-width:0}
|
|
163
|
+
.indent{flex:none;display:inline-block}
|
|
164
|
+
.caret{flex:none;width:14px;color:var(--muted);transition:.15s;font-size:10px}
|
|
165
|
+
.caret.open{transform:rotate(90deg)}
|
|
166
|
+
.chip{font-size:10.5px;font-weight:700;padding:1.5px 7px;border-radius:5px;flex:none;letter-spacing:.02em}
|
|
167
|
+
.chip.node{background:var(--chip-node);color:var(--chip-node-t)}
|
|
168
|
+
.chip.llm{background:var(--chip-llm);color:var(--chip-llm-t)}
|
|
169
|
+
.chip.tool{background:var(--chip-tool);color:var(--chip-tool-t)}
|
|
170
|
+
.chip.retriever{background:var(--chip-ret);color:var(--chip-ret-t)}
|
|
171
|
+
.chip.interrupt{background:var(--warn-weak);color:var(--warn)}
|
|
172
|
+
.tr-name{font-weight:600;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
173
|
+
.tr-name.err{color:var(--err)}
|
|
174
|
+
.tr-gantt{height:6px;background:var(--surface2);border-radius:3px;position:relative;overflow:hidden}
|
|
175
|
+
.tr-bar{position:absolute;top:0;bottom:0;border-radius:3px;background:var(--accent);opacity:.8;min-width:2px}
|
|
176
|
+
.tr-bar.llm{background:#8b5cf6}.tr-bar.tool{background:#10b981}
|
|
177
|
+
.tr-meta{text-align:right;color:var(--muted);font-family:var(--mono);font-size:11.5px}
|
|
178
|
+
.tr-detail{margin:2px 0 8px 30px;border-left:2px solid var(--border);padding-left:12px;display:none}
|
|
179
|
+
/* source viewer */
|
|
180
|
+
.code{display:grid;grid-template-columns:auto 1fr;column-gap:12px;font:11.5px/1.65 var(--mono);
|
|
181
|
+
background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:10px 12px;
|
|
182
|
+
max-height:340px;overflow:auto;margin-top:4px}
|
|
183
|
+
.code .ln{color:var(--muted);text-align:right;user-select:none;position:sticky;left:-12px;background:var(--surface2);padding-left:12px}
|
|
184
|
+
.code .cl{white-space:pre}
|
|
185
|
+
.code .errline{background:var(--err-weak)}
|
|
186
|
+
.code .cl.errline{box-shadow:inset 3px 0 0 var(--err);padding-left:6px;margin-left:-6px}
|
|
187
|
+
.errhint{font:11px var(--mono);color:var(--err);margin-top:3px;max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
188
|
+
.kv{margin:8px 0}
|
|
189
|
+
.kv .k{display:flex;align-items:center;gap:8px;font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--muted);margin-bottom:4px}
|
|
190
|
+
.copy{font-size:10.5px;color:var(--accent-text);background:var(--accent-weak);border-radius:5px;padding:1px 7px}
|
|
191
|
+
.kv pre{margin:0;white-space:pre-wrap;word-break:break-word;background:var(--surface2);border:1px solid var(--border);border-radius:8px;
|
|
192
|
+
padding:10px 12px;font:12px/1.55 var(--mono);color:var(--text);max-height:280px;overflow:auto}
|
|
193
|
+
.kv pre.err{color:var(--err);background:var(--err-weak);border-color:transparent}
|
|
194
|
+
|
|
195
|
+
/* ---------- modal (new run) ---------- */
|
|
196
|
+
.modal{position:fixed;inset:0;display:grid;place-items:center;z-index:60;opacity:0;pointer-events:none;transition:.15s}
|
|
197
|
+
.modal.on{opacity:1;pointer-events:auto}
|
|
198
|
+
.modal .box{width:min(560px,calc(100vw - 32px));background:var(--surface);border:1px solid var(--border);border-radius:16px;box-shadow:var(--shadow-lg);padding:18px 20px}
|
|
199
|
+
.modal h2{margin:0 0 4px;font-size:15px}
|
|
200
|
+
.modal .hint{color:var(--muted);font-size:12.5px;margin-bottom:10px}
|
|
201
|
+
textarea{width:100%;min-height:190px;background:var(--surface2);border:1px solid var(--border);border-radius:10px;color:var(--text);
|
|
202
|
+
font:12.5px/1.55 var(--mono);padding:12px;resize:vertical;outline:none}
|
|
203
|
+
textarea:focus{border-color:var(--accent)}
|
|
204
|
+
.modal .actions{display:flex;justify-content:flex-end;gap:8px;margin-top:12px}
|
|
205
|
+
|
|
206
|
+
/* ---------- toast ---------- */
|
|
207
|
+
.toast{position:fixed;right:20px;bottom:calc(env(safe-area-inset-bottom) + 20px);background:var(--surface);border:1px solid var(--border);
|
|
208
|
+
border-left:3px solid var(--accent);border-radius:10px;box-shadow:var(--shadow-lg);padding:11px 16px;font-size:13px;color:var(--text2);
|
|
209
|
+
z-index:70;opacity:0;transform:translateY(8px);transition:.2s;pointer-events:none;max-width:min(420px,90vw)}
|
|
210
|
+
.toast.show{opacity:1;transform:none}
|
|
211
|
+
|
|
212
|
+
/* ---------- runs toolbar / chips / pager ---------- */
|
|
213
|
+
.rtools{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px}
|
|
214
|
+
.rtools input[type=search],.rtools select,.scoreadd input{background:var(--surface);border:1px solid var(--border);
|
|
215
|
+
border-radius:8px;color:var(--text);padding:7px 10px;font-size:13px;outline:none}
|
|
216
|
+
.rtools input[type=search]{width:250px}
|
|
217
|
+
.rtools input:focus,.rtools select:focus,.scoreadd input:focus{border-color:var(--accent)}
|
|
218
|
+
#rbook.on{color:var(--warn);border-color:var(--warn)}
|
|
219
|
+
#xray.on{color:var(--accent-text);border-color:var(--accent)}
|
|
220
|
+
.star{cursor:pointer;color:var(--muted);font-size:15px;line-height:1}
|
|
221
|
+
.star.on{color:var(--warn)}
|
|
222
|
+
.tagchip{display:inline-block;background:var(--accent-weak);color:var(--accent-text);font-size:10.5px;font-weight:600;
|
|
223
|
+
padding:1.5px 7px;border-radius:5px;margin:1px 4px 1px 0;cursor:pointer}
|
|
224
|
+
.scorechip{display:inline-block;background:var(--ok-weak);color:var(--ok);font-size:10.5px;font-weight:600;
|
|
225
|
+
padding:1.5px 7px;border-radius:5px;margin:1px 4px 1px 0}
|
|
226
|
+
.pager{display:flex;align-items:center;gap:12px;justify-content:flex-end;margin-top:10px;color:var(--muted);font-size:12.5px}
|
|
227
|
+
.pager button{color:var(--text2);font-weight:600;padding:4px 10px;border:1px solid var(--border);border-radius:7px;background:var(--surface)}
|
|
228
|
+
.pager button:disabled{opacity:.4}
|
|
229
|
+
.scoreadd{display:flex;gap:6px;margin:6px 0 12px;flex-wrap:wrap}
|
|
230
|
+
.scoreadd input{padding:6px 9px;font-size:12.5px}
|
|
231
|
+
.scorerow{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:7px}
|
|
232
|
+
.scorerow:hover{background:var(--surface2)}
|
|
233
|
+
.scorerow .del{cursor:pointer;color:var(--muted);margin-left:auto}
|
|
234
|
+
.scorerow .del:hover{color:var(--err)}
|
|
235
|
+
/* ---------- stats charts ---------- */
|
|
236
|
+
.charts{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px}
|
|
237
|
+
.cwrap{padding:12px 14px 4px}
|
|
238
|
+
.cwrap b{font-size:13px}
|
|
239
|
+
.chartrow{display:flex;align-items:flex-end;gap:3px;height:110px;padding:10px 4px 0}
|
|
240
|
+
.cbar{flex:1;min-width:4px;background:var(--accent);opacity:.8;border-radius:3px 3px 0 0;position:relative}
|
|
241
|
+
.cbar .errpart{position:absolute;left:0;right:0;bottom:0;background:var(--err);border-radius:0 0 0 0}
|
|
242
|
+
.clabels{display:flex;justify-content:space-between;color:var(--muted);font-size:10.5px;padding:5px 6px 10px}
|
|
243
|
+
@media(max-width:840px){.charts{grid-template-columns:1fr}}
|
|
244
|
+
|
|
245
|
+
/* ---------- identity layer — the windhover voice ----------
|
|
246
|
+
ink rail (instrument housing) · Fraunces display moments ·
|
|
247
|
+
mono annunciators · rufous barring (the kestrel's tail) */
|
|
248
|
+
aside{background:var(--rail);border-right:1px solid var(--rail-line)}
|
|
249
|
+
.logo{font-family:var(--display);font-style:italic;font-weight:600;font-size:17px;color:var(--rail-on);letter-spacing:.01em}
|
|
250
|
+
.wordmark{display:flex;flex-direction:column;line-height:1.12}
|
|
251
|
+
.species{font-family:var(--mono);font-style:normal;font-weight:400;font-size:8.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--rail-text2);margin-top:3px}
|
|
252
|
+
@keyframes hover-float{0%,100%{transform:translateY(0)}50%{transform:translateY(-2.5px)}}
|
|
253
|
+
.logo svg{animation:hover-float 3.6s ease-in-out infinite}
|
|
254
|
+
@media (prefers-reduced-motion:reduce){.logo svg{animation:none}}
|
|
255
|
+
.nav button{color:var(--rail-text);font-family:var(--mono);font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:.13em;padding:9px 12px}
|
|
256
|
+
.nav button:hover{background:rgba(255,255,255,.05);color:var(--rail-on)}
|
|
257
|
+
.nav button.on{background:rgba(255,255,255,.07);color:var(--rail-on);font-weight:600;box-shadow:inset 2px 0 0 var(--rufous)}
|
|
258
|
+
.side-foot{color:var(--rail-text2)}
|
|
259
|
+
.crumb{font-family:var(--display);font-weight:600;font-size:17px;letter-spacing:.005em}
|
|
260
|
+
.pill{font-family:var(--mono);letter-spacing:.02em}
|
|
261
|
+
.plabel{font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
|
|
262
|
+
.page-head h1{font-family:var(--display);font-weight:600;letter-spacing:.005em}
|
|
263
|
+
.page-head h1::after{content:"";display:block;width:52px;height:4px;margin-top:7px;
|
|
264
|
+
background:repeating-linear-gradient(90deg,var(--rufous) 0 7px,transparent 7px 13px)}
|
|
265
|
+
.page-head h1[style]::after{display:none} /* barring marks the page title only, not section heads */
|
|
266
|
+
.metric .v{font-family:var(--display);font-weight:600;font-size:30px}
|
|
267
|
+
.empty .t{font-family:var(--display);font-weight:600}
|
|
268
|
+
th{font-family:var(--mono);letter-spacing:.11em;font-size:10.5px}
|
|
269
|
+
.tr-meta,.latms{font-family:var(--mono)}
|
|
270
|
+
.tr-bar{background:var(--accent)}
|
|
271
|
+
.tr-bar.llm{background:var(--chip-llm-t)}
|
|
272
|
+
.tr-bar.tool{background:var(--chip-tool-t)}
|
|
273
|
+
.tr-bar.retriever{background:var(--chip-ret-t)}
|
|
274
|
+
.tr-bar.interrupt{background:var(--warn)}
|
|
275
|
+
.charts .card:nth-child(2) .cbar{background:var(--chip-llm-t)}
|
|
276
|
+
#cy{background:radial-gradient(circle,var(--border) 1px,transparent 1.5px) 0 0/24px 24px}
|
|
277
|
+
.legend{font-family:var(--mono);font-size:10.5px;letter-spacing:.06em;text-transform:uppercase}
|
|
278
|
+
.bottom-nav{background:var(--rail) !important;border-top:1px solid var(--rail-line) !important}
|
|
279
|
+
.bottom-nav button{color:var(--rail-text2) !important;font-family:var(--mono);font-size:9.5px !important;letter-spacing:.12em;text-transform:uppercase}
|
|
280
|
+
.bottom-nav button.on{color:var(--rail-on) !important}
|
|
281
|
+
.dhead h2{font-family:var(--display);font-weight:600}
|
|
282
|
+
|
|
283
|
+
/* ---------- mobile ---------- */
|
|
284
|
+
.bottom-nav{display:none}
|
|
285
|
+
@media (max-width:840px){
|
|
286
|
+
aside{display:none}
|
|
287
|
+
.topbar{padding:calc(env(safe-area-inset-top)) 14px 0;height:calc(52px + env(safe-area-inset-top))}
|
|
288
|
+
.crumb{font-size:15px}
|
|
289
|
+
.page{padding:14px 12px calc(env(safe-area-inset-bottom) + 80px)}
|
|
290
|
+
.bottom-nav{display:flex;position:fixed;left:0;right:0;bottom:0;background:var(--surface);border-top:1px solid var(--border);
|
|
291
|
+
padding:6px 8px calc(env(safe-area-inset-bottom) + 6px);z-index:20;justify-content:space-around}
|
|
292
|
+
.bottom-nav button{display:flex;flex-direction:column;align-items:center;gap:3px;color:var(--muted);font-size:10.5px;font-weight:600;padding:4px 14px;border-radius:10px}
|
|
293
|
+
.bottom-nav button.on{color:var(--accent-text)}
|
|
294
|
+
.bottom-nav svg{width:20px;height:20px}
|
|
295
|
+
.drawer{width:100vw}
|
|
296
|
+
.legend{display:none}
|
|
297
|
+
#view-runs th:nth-child(5),#view-runs td:nth-child(5),
|
|
298
|
+
#view-runs th:nth-child(6),#view-runs td:nth-child(6),
|
|
299
|
+
#view-runs th:nth-child(8),#view-runs td:nth-child(8){display:none}
|
|
300
|
+
}
|
|
301
|
+
@media (max-width:600px){
|
|
302
|
+
.tr-row{grid-template-columns:1fr 86px}
|
|
303
|
+
.tr-gantt{display:none}
|
|
304
|
+
}
|
|
305
|
+
</style>
|
|
306
|
+
</head>
|
|
307
|
+
<body>
|
|
308
|
+
<div id="app">
|
|
309
|
+
<aside>
|
|
310
|
+
<div class="logo">
|
|
311
|
+
<svg width="22" height="22" viewBox="0 0 512 512"><rect width="512" height="512" rx="112" fill="var(--accent)"/><g transform="translate(56.0 116.7) scale(0.62893)"><path d="M312.90,425.29 C310.64,423.25 306.72,418.86 304.18,415.54 C301.65,412.22 298.88,408.93 298.03,408.23 C295.97,406.53 289.90,407.25 282.33,410.11 C274.09,413.23 271.34,412.33 268.20,405.50 C262.04,392.12 261.63,391.51 258.52,391.16 C256.88,390.98 253.06,391.35 250.02,391.99 C242.01,393.68 239.51,392.45 236.41,385.25 C230.11,370.65 228.49,369.21 220.40,371.02 C217.79,371.61 214.35,371.80 212.76,371.45 C209.08,370.64 206.01,365.75 206.00,360.69 C206.00,357.52 206.81,356.01 210.82,351.69 C213.47,348.84 217.71,343.80 220.23,340.50 C222.75,337.20 226.92,332.48 229.51,330.00 C232.09,327.52 236.81,322.12 240.01,318.00 C243.20,313.88 249.27,307.12 253.49,303.00 C257.72,298.88 263.24,292.80 265.78,289.50 C268.31,286.20 272.76,280.90 275.66,277.73 C282.46,270.28 283.78,266.15 281.64,258.90 C278.68,248.89 277.41,247.67 267.38,245.23 C260.71,243.60 251.53,239.67 230.50,229.45 C215.10,221.96 200.47,214.74 198.00,213.41 C195.53,212.08 184.95,206.76 174.50,201.60 C164.05,196.44 153.25,191.06 150.50,189.65 C147.75,188.24 137.08,182.79 126.78,177.55 C114.19,171.14 107.49,167.16 106.33,165.38 C104.65,162.82 104.67,162.68 106.91,160.87 C108.24,159.80 110.80,159.00 112.91,159.00 C116.69,159.00 119.00,157.60 119.00,155.31 C119.00,154.60 114.84,151.96 109.75,149.44 C99.17,144.20 67.99,123.81 66.21,120.97 C65.56,119.93 65.31,118.35 65.66,117.45 C66.19,116.07 67.32,115.93 73.06,116.51 C81.49,117.35 84.00,116.65 84.00,113.45 C84.00,111.44 81.40,109.29 68.75,100.84 C52.97,90.30 37.70,78.18 35.86,74.74 C35.05,73.23 35.18,72.40 36.40,71.18 C37.85,69.72 38.96,69.82 49.38,72.36 C62.67,75.61 67.35,75.59 67.81,72.29 C68.05,70.60 65.53,67.46 57.31,59.22 C46.57,48.44 41.78,42.47 40.57,38.36 C39.71,35.46 42.49,32.71 45.36,33.60 C46.53,33.97 60.99,41.01 77.47,49.25 C93.96,57.49 112.41,66.03 118.47,68.23 C146.15,78.27 183.73,90.19 202.92,95.00 C219.11,99.06 221.00,99.92 227.79,106.30 C230.93,109.26 235.53,113.06 238.00,114.75 C240.47,116.45 245.43,120.83 249.00,124.50 C252.57,128.17 257.52,132.55 260.00,134.25 C262.48,135.94 267.05,139.73 270.18,142.66 C277.53,149.58 279.88,149.68 287.17,143.41 C290.10,140.89 295.20,137.43 298.50,135.71 C303.95,132.88 305.53,132.56 315.72,132.19 C331.48,131.62 338.30,133.73 347.42,142.00 C356.10,149.87 358.09,149.94 365.82,142.66 C368.95,139.73 373.52,135.94 376.00,134.25 C378.48,132.55 383.43,128.17 387.00,124.49 C390.57,120.82 395.98,116.11 399.00,114.01 C402.02,111.92 406.07,108.54 408.00,106.51 C412.56,101.70 416.56,99.78 427.50,97.09 C442.99,93.30 489.94,78.25 517.53,68.23 C523.59,66.03 542.04,57.49 558.53,49.25 C575.01,41.01 589.47,33.97 590.64,33.60 C592.03,33.17 593.37,33.51 594.43,34.57 C597.63,37.77 594.44,42.92 579.49,58.73 C573.72,64.83 569.00,70.52 569.00,71.38 C569.00,74.33 572.54,75.17 578.74,73.67 C591.83,70.50 597.95,69.72 599.51,71.01 C603.83,74.59 599.88,78.43 573.00,96.80 C566.67,101.12 558.72,106.13 555.32,107.93 C548.71,111.43 547.22,113.35 549.22,115.76 C550.25,117.00 552.19,117.16 560.13,116.64 C568.29,116.10 569.88,116.24 570.38,117.54 C570.70,118.39 570.44,119.93 569.79,120.97 C568.01,123.81 536.83,144.20 526.25,149.44 C521.16,151.96 517.00,154.60 517.00,155.31 C517.00,157.60 519.31,159.00 523.09,159.00 C525.20,159.00 527.76,159.80 529.09,160.87 C533.98,164.83 532.25,165.87 461.87,201.06 C425.53,219.23 394.15,235.20 392.12,236.54 C388.01,239.26 374.07,244.22 365.58,245.98 C361.45,246.84 359.57,247.82 358.19,249.82 C355.43,253.80 353.00,260.60 353.00,264.32 C353.00,267.82 357.09,275.09 360.98,278.50 C362.23,279.60 365.86,283.88 369.04,288.00 C372.21,292.12 378.27,298.88 382.50,303.00 C386.73,307.12 392.80,313.88 396.00,318.00 C399.19,322.12 404.37,327.98 407.50,331.00 C415.03,338.28 427.28,352.69 429.35,356.70 C431.73,361.32 431.39,364.53 428.13,368.18 C425.01,371.67 422.07,372.14 412.93,370.56 C406.63,369.48 403.40,372.27 401.99,380.00 C400.84,386.26 398.56,389.89 394.54,391.86 C392.28,392.97 390.71,393.00 386.11,392.02 C383.00,391.36 379.12,390.98 377.48,391.16 C374.37,391.51 373.96,392.12 367.80,405.50 C364.66,412.33 361.91,413.23 353.67,410.11 C346.10,407.25 340.03,406.53 337.97,408.23 C337.12,408.93 334.35,412.22 331.82,415.54 C326.95,421.91 319.68,429.00 318.00,429.00 C317.45,429.00 315.16,427.33 312.90,425.29 z M323.60,201.25 C329.52,190.76 330.86,189.21 336.20,186.68 C344.02,182.98 348.00,178.02 348.00,172.00 C348.00,168.25 346.43,165.00 344.62,165.00 C343.90,165.00 340.29,167.93 336.61,171.50 C332.93,175.07 329.50,178.00 328.99,178.00 C328.48,178.00 326.47,180.49 324.52,183.54 C322.57,186.58 320.28,189.34 319.42,189.67 C317.02,190.59 313.64,188.28 310.95,183.86 C308.20,179.35 294.66,167.00 292.47,167.00 C289.94,167.00 287.69,171.70 288.33,175.64 C289.11,180.46 291.29,182.65 298.80,186.21 C305.24,189.26 306.28,190.41 312.40,201.25 C314.64,205.22 315.56,206.00 318.00,206.00 C320.44,206.00 321.36,205.22 323.60,201.25 z" fill="#fff"/></g></svg>
|
|
312
|
+
<span class="wordmark">Windhover<span class="species">LangGraph observability</span></span>
|
|
313
|
+
</div>
|
|
314
|
+
<nav class="nav">
|
|
315
|
+
<button data-v="graph" class="on">${ICON.graph} Graph</button>
|
|
316
|
+
<button data-v="runs">${ICON.list} Runs</button>
|
|
317
|
+
<button data-v="sessions">${ICON.layers} Sessions</button>
|
|
318
|
+
<button data-v="stats">${ICON.chart} Stats</button>
|
|
319
|
+
</nav>
|
|
320
|
+
<div class="side-foot"><span class="live-dot" id="live-dot" title="live topology"></span><span id="foot-note">watching</span><span class="spacer"></span><span class="mono" id="ver">v0.7</span></div>
|
|
321
|
+
</aside>
|
|
322
|
+
|
|
323
|
+
<main>
|
|
324
|
+
<div class="topbar">
|
|
325
|
+
<span class="crumb" id="crumb">Graph</span>
|
|
326
|
+
<span class="pill" id="gpill"><span class="live-dot"></span><span class="plabel">graph</span><span id="gname">—</span></span>
|
|
327
|
+
<div class="spacer"></div>
|
|
328
|
+
<button class="icon-btn" id="theme" title="theme">${ICON.moon}</button>
|
|
329
|
+
<button class="btn primary" id="newrun">${ICON.play} New run</button>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="content">
|
|
332
|
+
<div id="view-graph">
|
|
333
|
+
<div id="cy"></div>
|
|
334
|
+
<div class="graph-tools">
|
|
335
|
+
<button class="btn ghost" onclick="cy&&cy.fit(undefined,50)">Fit</button>
|
|
336
|
+
<button class="btn ghost" onclick="relayout()">Layout</button>
|
|
337
|
+
<button class="btn ghost" id="orient" onclick="toggleOrient()">⇅ Vertical</button>
|
|
338
|
+
<button class="btn ghost" id="xray" style="display:none" onclick="toggleXray()">◉ X-ray</button>
|
|
339
|
+
</div>
|
|
340
|
+
<div class="legend">
|
|
341
|
+
<span class="lg"><span class="sw"></span> idle</span>
|
|
342
|
+
<span class="lg"><span class="sw run"></span> running</span>
|
|
343
|
+
<span class="lg"><span class="sw done"></span> done</span>
|
|
344
|
+
<span class="lg"><span class="sw err"></span> error</span>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
<div id="view-runs" style="display:none">
|
|
348
|
+
<div class="page">
|
|
349
|
+
<div class="page-head"><h1>Runs</h1><span class="sub" id="runs-sub"></span></div>
|
|
350
|
+
<div class="rtools">
|
|
351
|
+
<input type="search" id="rq" placeholder="Search payloads, prompts, errors…">
|
|
352
|
+
<select id="rstatus"><option value="">All statuses</option>
|
|
353
|
+
<option value="done">done</option><option value="error">error</option>
|
|
354
|
+
<option value="interrupted">interrupted</option>
|
|
355
|
+
<option value="running">running</option></select>
|
|
356
|
+
<button class="btn ghost" id="rbook" title="only bookmarked">★</button>
|
|
357
|
+
<span id="rf-active"></span>
|
|
358
|
+
<span class="spacer"></span>
|
|
359
|
+
<button class="btn ghost" onclick="exportRuns('csv')">CSV</button>
|
|
360
|
+
<button class="btn ghost" onclick="exportRuns('json')">JSON</button>
|
|
361
|
+
</div>
|
|
362
|
+
<div class="card"><div id="runs-wrap"><div class="empty">Loading…</div></div></div>
|
|
363
|
+
<div class="pager" id="pager"></div>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
<div id="view-sessions" style="display:none">
|
|
367
|
+
<div class="page">
|
|
368
|
+
<div class="page-head"><h1>Sessions</h1><span class="sub" id="sess-sub"></span></div>
|
|
369
|
+
<div class="card"><div id="sess-wrap"><div class="empty">Loading…</div></div></div>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
<div id="view-stats" style="display:none">
|
|
373
|
+
<div class="page">
|
|
374
|
+
<div class="page-head"><h1>Stats</h1><span class="sub">all recorded runs</span></div>
|
|
375
|
+
<div id="stats-body"></div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
</main>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
<div class="bottom-nav">
|
|
383
|
+
<button data-v="graph" class="on">${ICON.graph}<span>Graph</span></button>
|
|
384
|
+
<button data-v="runs">${ICON.list}<span>Runs</span></button>
|
|
385
|
+
<button data-v="sessions">${ICON.layers}<span>Sessions</span></button>
|
|
386
|
+
<button data-v="stats">${ICON.chart}<span>Stats</span></button>
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
<div class="scrim" id="scrim" onclick="closeAll()"></div>
|
|
390
|
+
|
|
391
|
+
<div class="drawer" id="drawer">
|
|
392
|
+
<div class="dhead">
|
|
393
|
+
<div class="row1"><h2 id="d-title">Run</h2><span class="status done" id="d-status"><span class="d"></span><span id="d-status-t">done</span></span>
|
|
394
|
+
<button class="icon-btn x" onclick="closeAll()">${ICON.x}</button></div>
|
|
395
|
+
<div class="dmeta" id="d-meta"></div>
|
|
396
|
+
</div>
|
|
397
|
+
<div class="dbody" id="d-body"></div>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<div class="modal" id="modal">
|
|
401
|
+
<div class="box">
|
|
402
|
+
<h2>New run</h2>
|
|
403
|
+
<div class="hint">Input is derived from this graph's own state schema. Edit the JSON, then run.</div>
|
|
404
|
+
<textarea id="input-json" spellcheck="false"></textarea>
|
|
405
|
+
<div class="scoreadd" style="margin-top:10px">
|
|
406
|
+
<input id="run-session" placeholder="session (optional)" style="flex:1;min-width:130px">
|
|
407
|
+
<input id="run-tags" placeholder="tags, comma-separated (optional)" style="flex:1;min-width:160px">
|
|
408
|
+
</div>
|
|
409
|
+
<div class="actions">
|
|
410
|
+
<button class="btn ghost" onclick="closeAll()">Cancel</button>
|
|
411
|
+
<button class="btn primary" id="do-run">${ICON.play} Run</button>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<div class="toast" id="toast"></div>
|
|
417
|
+
|
|
418
|
+
<script>
|
|
419
|
+
/* ---------- icons ---------- */
|
|
420
|
+
const ICON = {
|
|
421
|
+
graph:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="5" r="2.4"/><circle cx="5.5" cy="18" r="2.4"/><circle cx="18.5" cy="18" r="2.4"/><path d="M12 7.5v4M10.6 13.2 7 15.8M13.4 13.2 17 15.8"/></svg>',
|
|
422
|
+
list:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M8 6h12M8 12h12M8 18h12"/><circle cx="4" cy="6" r="1.1" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1.1" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1.1" fill="currentColor" stroke="none"/></svg>',
|
|
423
|
+
chart:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M4 20V9M10 20V4M16 20v-7M21 20H3"/></svg>',
|
|
424
|
+
layers:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3 3 8l9 5 9-5-9-5z"/><path d="M3 13l9 5 9-5"/></svg>',
|
|
425
|
+
play:'<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M7 4.8v14.4c0 .8.9 1.3 1.6.9l11-7.2c.6-.4.6-1.4 0-1.8l-11-7.2c-.7-.4-1.6.1-1.6.9z"/></svg>',
|
|
426
|
+
moon:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M20 13.5A8 8 0 1 1 10.5 4 6.5 6.5 0 0 0 20 13.5z"/></svg>',
|
|
427
|
+
x:'<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>',
|
|
428
|
+
};
|
|
429
|
+
document.body.innerHTML = document.body.innerHTML.replaceAll(/\$\{ICON\.(\w+)\}/g,(m,k)=>ICON[k]||'');
|
|
430
|
+
|
|
431
|
+
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
|
|
432
|
+
let cy=null, VIEW='graph';
|
|
433
|
+
|
|
434
|
+
/* ---------- theme ---------- */
|
|
435
|
+
function setTheme(t){ if(t) document.documentElement.dataset.theme=t; else delete document.documentElement.dataset.theme;
|
|
436
|
+
localStorage.setItem('wh-theme',t||''); if(cy) applyCyStyle(); }
|
|
437
|
+
setTheme(localStorage.getItem('wh-theme')||'');
|
|
438
|
+
$('#theme').onclick=()=>{ const cur=document.documentElement.dataset.theme||
|
|
439
|
+
(matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');
|
|
440
|
+
setTheme(cur==='dark'?'light':'dark'); };
|
|
441
|
+
|
|
442
|
+
function cssv(n){ return getComputedStyle(document.documentElement).getPropertyValue(n).trim(); }
|
|
443
|
+
function cyStyle(){ return [
|
|
444
|
+
{selector:'node',style:{'shape':'round-rectangle','width':'label','height':40,'padding':'12px',
|
|
445
|
+
'background-color':cssv('--surface'),'border-width':1.5,'border-color':cssv('--border2'),
|
|
446
|
+
'label':'data(label)','color':cssv('--text'),'font-size':12.5,'font-weight':600,
|
|
447
|
+
'font-family':'ui-monospace, SF Mono, Menlo, Consolas, monospace',
|
|
448
|
+
'text-valign':'center','text-halign':'center'}},
|
|
449
|
+
{selector:'node[terminal]',style:{'shape':'round-rectangle','background-color':cssv('--surface2'),'color':cssv('--muted'),'font-size':11,'height':28,'border-color':cssv('--border')}},
|
|
450
|
+
{selector:'node.running',style:{'border-color':cssv('--warn'),'border-width':2.5,'background-color':cssv('--warn-weak')}},
|
|
451
|
+
{selector:'node.done',style:{'border-color':cssv('--ok'),'background-color':cssv('--ok-weak')}},
|
|
452
|
+
{selector:'node.err',style:{'border-color':cssv('--err'),'background-color':cssv('--err-weak')}},
|
|
453
|
+
{selector:'edge',style:{'width':1.6,'line-color':cssv('--border2'),'target-arrow-color':cssv('--border2'),'target-arrow-shape':'triangle','curve-style':'bezier','arrow-scale':1}},
|
|
454
|
+
{selector:'edge[?conditional]',style:{'line-style':'dashed'}},
|
|
455
|
+
{selector:'edge.flow',style:{'line-color':cssv('--accent'),'target-arrow-color':cssv('--accent'),'width':2.4}},
|
|
456
|
+
];}
|
|
457
|
+
function applyCyStyle(){ cy.style(cyStyle()); }
|
|
458
|
+
|
|
459
|
+
/* ---------- graph ---------- */
|
|
460
|
+
let XRAY=false;
|
|
461
|
+
window.toggleXray=()=>{ XRAY=!XRAY; const b=$('#xray'); if(b)b.classList.toggle('on',XRAY); loadGraph(true); };
|
|
462
|
+
async function loadGraph(force){
|
|
463
|
+
const t=await(await fetch('/api/graph')).json();
|
|
464
|
+
$('#gname').textContent=t.graph||'ingest-only';
|
|
465
|
+
const xb=$('#xray'); if(xb) xb.style.display=t.xray?'':'none';
|
|
466
|
+
if(!t.nodes.length){ $('#view-graph').innerHTML='<div class="empty" style="padding-top:120px"><div class="t">No local graph configured</div>Set <span class="mono">WINDHOVER_GRAPH</span>, or trace runs in from your app and open Runs.</div>'; return; }
|
|
467
|
+
const topo=(XRAY&&t.xray)?t.xray:t;
|
|
468
|
+
const els=[];
|
|
469
|
+
topo.nodes.forEach(n=>els.push({data:{id:n.id,label:n.label,terminal:n.terminal?1:undefined}}));
|
|
470
|
+
topo.edges.forEach(e=>els.push({data:{id:e.id,source:e.source,target:e.target,conditional:e.conditional?1:undefined}}));
|
|
471
|
+
if(cy&&!force){return;}
|
|
472
|
+
cy=cytoscape({container:$('#cy'),elements:els,style:cyStyle(),
|
|
473
|
+
layout:layoutOpts(),minZoom:.2,maxZoom:3,wheelSensitivity:.12});
|
|
474
|
+
cy.on('tap','node',ev=>{ const n=ev.target; if(!n.data('terminal')) openNode(n.id()); });
|
|
475
|
+
cy.on('mouseover','node',ev=>{ if(!ev.target.data('terminal')) $('#cy').style.cursor='pointer'; });
|
|
476
|
+
cy.on('mouseout','node',()=>{ $('#cy').style.cursor=''; });
|
|
477
|
+
}
|
|
478
|
+
/* middle-button drag pans the graph (and suppress the browser's autoscroll) */
|
|
479
|
+
(()=>{ const cont=$('#cy'); if(!cont) return;
|
|
480
|
+
cont.addEventListener('mousedown',e=>{ if(e.button!==1||!cy) return; e.preventDefault();
|
|
481
|
+
let px=e.clientX, py=e.clientY;
|
|
482
|
+
const move=ev=>{ cy.panBy({x:ev.clientX-px,y:ev.clientY-py}); px=ev.clientX; py=ev.clientY; };
|
|
483
|
+
const up=ev=>{ if(ev.button!==1) return;
|
|
484
|
+
window.removeEventListener('mousemove',move); window.removeEventListener('mouseup',up); };
|
|
485
|
+
window.addEventListener('mousemove',move); window.addEventListener('mouseup',up);
|
|
486
|
+
});
|
|
487
|
+
cont.addEventListener('auxclick',e=>{ if(e.button===1) e.preventDefault(); });
|
|
488
|
+
})();
|
|
489
|
+
|
|
490
|
+
let RANK=localStorage.getItem('wh-rank')||'TB';
|
|
491
|
+
function layoutOpts(){ return {name:'dagre',rankDir:RANK,nodeSep:30,rankSep:56}; }
|
|
492
|
+
function relayout(){ if(cy){ cy.layout(layoutOpts()).run(); cy.fit(undefined,50); } }
|
|
493
|
+
function orientLabel(){ const b=$('#orient'); if(b) b.textContent=RANK==='TB'?'⇅ Vertical':'⇄ Horizontal'; }
|
|
494
|
+
window.toggleOrient=()=>{ RANK=RANK==='TB'?'LR':'TB'; localStorage.setItem('wh-rank',RANK); orientLabel(); relayout(); };
|
|
495
|
+
orientLabel();
|
|
496
|
+
|
|
497
|
+
/* ---------- source viewer helpers ---------- */
|
|
498
|
+
function tbLines(err,src){ const out=new Set();
|
|
499
|
+
if(!err||!src) return out;
|
|
500
|
+
const base=(src.file||'').split('/').pop();
|
|
501
|
+
for(const m of String(err).matchAll(/File "([^"]+)", line (\d+)/g)){
|
|
502
|
+
if(m[1].split('/').pop()===base){ const ln=+m[2];
|
|
503
|
+
if(ln>=src.line_start&&ln<=src.line_end) out.add(ln); } }
|
|
504
|
+
return out; }
|
|
505
|
+
function codeBlock(src,hi){ const lines=src.code.replace(/\n$/,'').split('\n');
|
|
506
|
+
return `<div class="code">${lines.map((l,i)=>{ const ln=src.line_start+i, h=hi.has(ln);
|
|
507
|
+
return `<span class="ln ${h?'errline':''}">${ln}</span><span class="cl ${h?'errline':''}">${esc(l)||' '}</span>`;
|
|
508
|
+
}).join('')}</div>`; }
|
|
509
|
+
|
|
510
|
+
/* ---------- node pane (tap a node on the graph) ---------- */
|
|
511
|
+
async function openNode(name){
|
|
512
|
+
let d={}; try{ d=await(await fetch('/api/nodes/'+encodeURIComponent(name))).json(); }catch(e){}
|
|
513
|
+
let src=null; try{ const rs=await fetch('/api/nodes/'+encodeURIComponent(name)+'/source');
|
|
514
|
+
if(rs.ok) src=await rs.json(); }catch(e){}
|
|
515
|
+
const s=d.summary||{}, errs=s.errors||0;
|
|
516
|
+
$('#d-title').textContent=name;
|
|
517
|
+
const st=$('#d-status'); st.className='status '+(errs?'error':'done');
|
|
518
|
+
$('#d-status-t').textContent=errs?errs+' error'+(errs>1?'s':''):'healthy';
|
|
519
|
+
const inn=cy?cy.edges(`[target="${name}"]`).map(e=>e.source().id()):[];
|
|
520
|
+
const out=cy?cy.edges(`[source="${name}"]`).map(e=>e.target().id()):[];
|
|
521
|
+
$('#d-meta').innerHTML=[
|
|
522
|
+
`<span><b class="num">${s.n||0}</b> executions</span>`,
|
|
523
|
+
s.avg_ms!=null?`<span><b>${fmtms(Math.round(s.avg_ms))}</b> avg</span>`:'',
|
|
524
|
+
s.max_ms!=null?`<span><b>${fmtms(s.max_ms)}</b> max</span>`:'',
|
|
525
|
+
s.tokens?`<span><b class="num">${fmtk(s.tokens)}</b> tokens</span>`:'',
|
|
526
|
+
s.cost!=null?`<span><b class="num">$${(+s.cost).toFixed(4)}</b> cost</span>`:'',
|
|
527
|
+
].filter(Boolean).join('');
|
|
528
|
+
window.__nodeSrc=src;
|
|
529
|
+
const hiAll=new Set();
|
|
530
|
+
if(src)(d.recent||[]).forEach(r=>{ if(r.error) tbLines(r.error,src).forEach(x=>hiAll.add(x)); });
|
|
531
|
+
let html=`<div class="kv"><div class="k">wiring</div><pre>${esc((inn.join(', ')||'START')+' → '+name+' → '+(out.join(', ')||'END'))}</pre></div>`;
|
|
532
|
+
if(src) html+=`<div class="kv"><div class="k">source · ${esc((src.file||'').split('/').pop())}:${src.line_start}${hiAll.size?` · <span style="color:var(--err)">error line${hiAll.size>1?'s':''} ${[...hiAll].join(', ')}</span>`:''}
|
|
533
|
+
<button class="copy" onclick="navigator.clipboard.writeText(__nodeSrc.code);toast('Copied source')">copy</button></div>${codeBlock(src,hiAll)}</div>`;
|
|
534
|
+
html+=`<div class="kv"><div class="k">executions — newest first, expand for payloads</div></div>`;
|
|
535
|
+
html+=(d.recent||[]).length?d.recent.map((r,i)=>{
|
|
536
|
+
const did='nx'+i, open=i===0;
|
|
537
|
+
const kids=r.children||[];
|
|
538
|
+
const kidHtml=kids.map(k=>`
|
|
539
|
+
<div class="kv"><div class="k"><span class="chip ${k.type}">${k.type}</span> ${esc(k.name||k.type)}${
|
|
540
|
+
k.prompt_tokens!=null?` · ${k.prompt_tokens} in / ${k.completion_tokens||0} out`:''}${
|
|
541
|
+
k.dur_ms!=null?` · ${fmtms(k.dur_ms)}`:''}</div></div>
|
|
542
|
+
${k.error?`<div class="kv"><div class="k">error</div><pre class="err">${esc(k.error)}</pre></div>`:''}
|
|
543
|
+
${k.input!=null?kv(esc(k.name||k.type)+' input',k.input):''}
|
|
544
|
+
${k.output!=null?kv(esc(k.name||k.type)+' output',k.output):''}`).join('');
|
|
545
|
+
return `<div>
|
|
546
|
+
<div class="tr-row flat" onclick="tog('${did}',this)">
|
|
547
|
+
<div class="tr-main"><span class="caret ${open?'open':''}">▶</span><span class="chip ${r.type}">${r.type}</span>
|
|
548
|
+
<span class="tr-name ${r.status==='error'?'err':''}">${esc(r.graph||name)}</span>
|
|
549
|
+
<span class="mono" style="color:var(--muted);font-size:11px">${r.run_id.slice(0,8)}</span></div>
|
|
550
|
+
<div class="tr-meta">${[r.dur_ms!=null?fmtms(r.dur_ms):null,
|
|
551
|
+
(r.prompt_tokens!=null||r.completion_tokens!=null)?fmtk((r.prompt_tokens||0)+(r.completion_tokens||0))+'t':null,
|
|
552
|
+
ago(r.started_ms)].filter(Boolean).join(' · ')}</div>
|
|
553
|
+
</div>
|
|
554
|
+
<div class="tr-detail" id="${did}" style="${open?'display:block':''}">
|
|
555
|
+
${r.error?`<div class="kv"><div class="k">error</div><pre class="err">${esc(r.error)}</pre></div>`:''}
|
|
556
|
+
${r.input!=null?kv('input',r.input):''}
|
|
557
|
+
${r.output!=null?kv('output',r.output):''}
|
|
558
|
+
${kidHtml}
|
|
559
|
+
${(r.input==null&&r.output==null&&!kids.length&&!r.error)?'<div class="kv"><pre>no captured payload for this execution</pre></div>':''}
|
|
560
|
+
<div style="margin:6px 0 10px"><button class="btn ghost" onclick="openRun('${r.run_id}')">Open full trace</button></div>
|
|
561
|
+
</div></div>`;
|
|
562
|
+
}).join(''):'<div class="empty">No recorded executions of this node yet</div>';
|
|
563
|
+
$('#d-body').innerHTML=html;
|
|
564
|
+
$('#scrim').classList.add('on'); $('#drawer').classList.add('on');
|
|
565
|
+
}
|
|
566
|
+
function liveTopology(){ try{ const es=new EventSource('/api/events');
|
|
567
|
+
es.addEventListener('topology',async()=>{ await loadGraph(true); toast('Graph definition changed — view updated'); });
|
|
568
|
+
es.onerror=()=>{ es.close(); $('#foot-note').textContent='reconnecting…'; setTimeout(liveTopology,5000); };
|
|
569
|
+
es.onopen=()=>{ $('#foot-note').textContent='watching'; };
|
|
570
|
+
}catch(e){} }
|
|
571
|
+
|
|
572
|
+
/* ---------- views ---------- */
|
|
573
|
+
function switchView(v){ VIEW=v;
|
|
574
|
+
if(location.hash!=='#'+v) history.replaceState(null,'','#'+v);
|
|
575
|
+
$$('.nav button,.bottom-nav button').forEach(b=>b.classList.toggle('on',b.dataset.v===v));
|
|
576
|
+
$('#crumb').textContent=v[0].toUpperCase()+v.slice(1);
|
|
577
|
+
for(const k of ['graph','runs','sessions','stats'])
|
|
578
|
+
$('#view-'+k).style.display=v===k?'':'none';
|
|
579
|
+
if(v==='runs')loadRuns(); if(v==='sessions')loadSessions(); if(v==='stats')loadStats();
|
|
580
|
+
if(cy&&v==='graph')setTimeout(()=>cy.resize(),40);
|
|
581
|
+
}
|
|
582
|
+
$$('.nav button,.bottom-nav button').forEach(b=>b.onclick=()=>switchView(b.dataset.v));
|
|
583
|
+
|
|
584
|
+
/* ---------- new run ---------- */
|
|
585
|
+
$('#newrun').onclick=async()=>{ let tpl={}; try{tpl=(await(await fetch('/api/schema')).json()).template||{};}catch(e){}
|
|
586
|
+
$('#input-json').value=JSON.stringify(tpl,null,2); $('#scrim').classList.add('on'); $('#modal').classList.add('on'); };
|
|
587
|
+
$('#do-run').onclick=async()=>{ let input; try{input=JSON.parse($('#input-json').value||'{}');}catch(e){return toast('Invalid JSON input');}
|
|
588
|
+
const sess=$('#run-session').value.trim();
|
|
589
|
+
const tags=$('#run-tags').value.split(',').map(s=>s.trim()).filter(Boolean);
|
|
590
|
+
if(sess)input._session=sess; if(tags.length)input._tags=tags;
|
|
591
|
+
closeAll(); switchView('graph'); await runNow(input); };
|
|
592
|
+
|
|
593
|
+
async function runNow(input){
|
|
594
|
+
const b=$('#newrun'); b.disabled=true;
|
|
595
|
+
cy.nodes().removeClass('running done err'); cy.edges().removeClass('flow'); let prev='__start__';
|
|
596
|
+
try{
|
|
597
|
+
const res=await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(input)});
|
|
598
|
+
const rd=res.body.getReader(),dec=new TextDecoder(); let buf='';
|
|
599
|
+
while(true){ const {value,done}=await rd.read(); if(done)break; buf+=dec.decode(value,{stream:true});
|
|
600
|
+
let ps=buf.split('\n\n'); buf=ps.pop();
|
|
601
|
+
for(const p of ps){ const ev=(p.match(/event: (.*)/)||[])[1],dm=(p.match(/data: (.*)/s)||[])[1]; if(!ev)continue; const d=dm?JSON.parse(dm):{};
|
|
602
|
+
if(ev==='node'){ cy.$id(prev).addClass('done').removeClass('running'); cy.$id(d.node).addClass('running');
|
|
603
|
+
cy.edges(`[source="${prev}"][target="${d.node}"]`).addClass('flow'); prev=d.node; }
|
|
604
|
+
else if(ev==='interrupt'){ toast('Run paused at an interrupt — awaiting resume'); }
|
|
605
|
+
else if(ev==='interrupted'){ cy.$id(prev).addClass('running'); toast('Run interrupted — see Runs for the payload'); }
|
|
606
|
+
else if(ev==='done'){ cy.$id(prev).addClass('done').removeClass('running'); cy.$id('__end__').addClass('done'); toast('Run complete — saved to Runs'); }
|
|
607
|
+
else if(ev==='error'){ cy.$id(prev).addClass('err').removeClass('running'); toast('Run error: '+(d.message||'failed')); } } }
|
|
608
|
+
}catch(e){ toast('Run failed: '+e.message); }
|
|
609
|
+
b.disabled=false;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/* ---------- runs table (filters + pagination) ---------- */
|
|
613
|
+
const RF={q:'',status:'',tag:'',bookmarked:false,session:'',offset:0,limit:50};
|
|
614
|
+
function runParams(){ const p=new URLSearchParams();
|
|
615
|
+
if(RF.q)p.set('q',RF.q); if(RF.status)p.set('status',RF.status);
|
|
616
|
+
if(RF.tag)p.set('tag',RF.tag); if(RF.bookmarked)p.set('bookmarked','1');
|
|
617
|
+
if(RF.session)p.set('session',RF.session); return p; }
|
|
618
|
+
async function loadRuns(){
|
|
619
|
+
const p=runParams(); p.set('limit',RF.limit); p.set('offset',RF.offset);
|
|
620
|
+
const d=await(await fetch('/api/runs?'+p)).json();
|
|
621
|
+
const runs=d.runs||[];
|
|
622
|
+
$('#runs-sub').textContent=d.total?`${d.total} run${d.total===1?'':'s'}`:'';
|
|
623
|
+
$('#rf-active').innerHTML=[
|
|
624
|
+
RF.tag?`<span class="tagchip" onclick="setTag('')">tag: ${esc(RF.tag)} ×</span>`:'',
|
|
625
|
+
RF.session?`<span class="tagchip" onclick="RF.session='';RF.offset=0;loadRuns()">session: ${esc(RF.session)} ×</span>`:'',
|
|
626
|
+
].join('');
|
|
627
|
+
if(!runs.length){
|
|
628
|
+
$('#runs-wrap').innerHTML=`<div class="empty"><div class="t">${d.total===0&&!RF.q&&!RF.status&&!RF.tag&&!RF.bookmarked&&!RF.session?'No runs yet':'No matches'}</div>${d.total===0?'Run the graph, or add <span class="mono">WindhoverTracer</span> to your app.':'Try clearing filters.'}</div>`;
|
|
629
|
+
$('#pager').innerHTML=''; return; }
|
|
630
|
+
$('#runs-wrap').innerHTML=`<div style="overflow:auto"><table>
|
|
631
|
+
<thead><tr><th></th><th>Status</th><th>Run</th><th>Source</th><th class="r">Nodes</th><th class="r">LLM</th><th class="r">Tokens</th><th class="r">Cost</th><th>Scores</th><th class="r">Duration</th><th class="r">Age</th></tr></thead>
|
|
632
|
+
<tbody>${runs.map(r=>`<tr onclick="openRun('${r.id}')">
|
|
633
|
+
<td onclick="event.stopPropagation();toggleBook('${r.id}',${r.bookmarked?0:1})"><span class="star ${r.bookmarked?'on':''}">${r.bookmarked?'★':'☆'}</span></td>
|
|
634
|
+
<td><span class="status ${r.status}"><span class="d"></span>${r.status}</span></td>
|
|
635
|
+
<td><div style="font-weight:600">${esc(r.graph||'run')}</div><div class="mono">${r.id.slice(0,10)}</div>
|
|
636
|
+
${r.status==='error'&&r.error?`<div class="errhint" title="open for traceback + source">${esc(String(r.error).trim().split('\n').filter(Boolean).pop().slice(0,90))}</div>`:''}
|
|
637
|
+
${(r.tags||[]).map(t=>`<span class="tagchip" onclick="event.stopPropagation();setTag(decodeURIComponent('${encodeURIComponent(t)}'))">${esc(t)}</span>`).join('')}</td>
|
|
638
|
+
<td><span class="src">${r.source}</span>${r.session?`<div class="mono" style="font-size:10.5px;color:var(--muted);cursor:pointer" onclick="event.stopPropagation();RF.session=decodeURIComponent('${encodeURIComponent(r.session)}');RF.offset=0;loadRuns()">${esc(r.session)}</div>`:''}</td>
|
|
639
|
+
<td class="r num">${r.node_count??0}</td>
|
|
640
|
+
<td class="r num">${r.llm_calls||'—'}</td>
|
|
641
|
+
<td class="r num">${r.total_tokens?fmtk(r.total_tokens):'—'}</td>
|
|
642
|
+
<td class="r num">${r.cost_usd!=null?'$'+r.cost_usd.toFixed(4):'—'}</td>
|
|
643
|
+
<td>${r.scores?Object.entries(r.scores).map(([k,v])=>`<span class="scorechip">${esc(k)} ${v}</span>`).join(''):'<span style="color:var(--muted)">—</span>'}</td>
|
|
644
|
+
<td class="r num mono">${r.duration_ms!=null?fmtms(r.duration_ms):'…'}</td>
|
|
645
|
+
<td class="r" style="color:var(--muted)">${ago(r.started_ms)}</td></tr>`).join('')}</tbody></table></div>`;
|
|
646
|
+
const page=Math.floor(RF.offset/RF.limit)+1, pages=Math.max(1,Math.ceil(d.total/RF.limit));
|
|
647
|
+
$('#pager').innerHTML=pages>1?`
|
|
648
|
+
<button ${RF.offset===0?'disabled':''} onclick="RF.offset=Math.max(0,RF.offset-RF.limit);loadRuns()">‹ Prev</button>
|
|
649
|
+
<span>page ${page} / ${pages}</span>
|
|
650
|
+
<button ${RF.offset+RF.limit>=d.total?'disabled':''} onclick="RF.offset+=RF.limit;loadRuns()">Next ›</button>`:'';
|
|
651
|
+
}
|
|
652
|
+
window.setTag=t=>{RF.tag=t;RF.offset=0;loadRuns();};
|
|
653
|
+
window.toggleBook=async(id,b)=>{
|
|
654
|
+
await fetch('/api/runs/'+id,{method:'PATCH',headers:{'Content-Type':'application/json'},
|
|
655
|
+
body:JSON.stringify({bookmarked:!!b})});
|
|
656
|
+
loadRuns();};
|
|
657
|
+
window.exportRuns=fmt=>{const p=runParams();p.set('format',fmt);location='/api/export?'+p;};
|
|
658
|
+
let rqT; $('#rq').oninput=e=>{clearTimeout(rqT);rqT=setTimeout(()=>{RF.q=e.target.value.trim();RF.offset=0;loadRuns();},300);};
|
|
659
|
+
$('#rstatus').onchange=e=>{RF.status=e.target.value;RF.offset=0;loadRuns();};
|
|
660
|
+
$('#rbook').onclick=()=>{RF.bookmarked=!RF.bookmarked;$('#rbook').classList.toggle('on',RF.bookmarked);RF.offset=0;loadRuns();};
|
|
661
|
+
|
|
662
|
+
/* ---------- sessions ---------- */
|
|
663
|
+
async function loadSessions(){
|
|
664
|
+
const ss=await(await fetch('/api/sessions')).json();
|
|
665
|
+
$('#sess-sub').textContent=ss.length?`${ss.length} session${ss.length===1?'':'s'}`:'';
|
|
666
|
+
if(!ss.length){ $('#sess-wrap').innerHTML='<div class="empty"><div class="t">No sessions yet</div>Pass <span class="mono">session=</span> to <span class="mono">WindhoverTracer</span>, or <span class="mono">config={"metadata": {"windhover_session": …}}</span> in any LangChain/LangGraph app.</div>'; return; }
|
|
667
|
+
$('#sess-wrap').innerHTML=`<div style="overflow:auto"><table>
|
|
668
|
+
<thead><tr><th>Session</th><th class="r">Runs</th><th class="r">Errors</th><th class="r">Tokens</th><th class="r">Cost</th><th class="r">Total time</th><th class="r">Last active</th></tr></thead>
|
|
669
|
+
<tbody>${ss.map(s=>`<tr onclick="RF.session=decodeURIComponent('${encodeURIComponent(s.session)}');RF.offset=0;switchView('runs')">
|
|
670
|
+
<td style="font-weight:600" class="mono">${esc(s.session)}</td>
|
|
671
|
+
<td class="r num">${s.runs}</td>
|
|
672
|
+
<td class="r num" ${s.errors?'style="color:var(--err)"':''}>${s.errors||'—'}</td>
|
|
673
|
+
<td class="r num">${s.tokens?fmtk(s.tokens):'—'}</td>
|
|
674
|
+
<td class="r num">${s.cost!=null?'$'+s.cost.toFixed(4):'—'}</td>
|
|
675
|
+
<td class="r num mono">${s.duration_ms!=null?fmtms(s.duration_ms):'—'}</td>
|
|
676
|
+
<td class="r" style="color:var(--muted)">${ago(s.last_ms)}</td></tr>`).join('')}</tbody></table></div>`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/* ---------- run drawer (span tree) ---------- */
|
|
680
|
+
async function openRun(id){
|
|
681
|
+
const r=await(await fetch('/api/runs/'+id)).json();
|
|
682
|
+
window.__run=r;
|
|
683
|
+
$('#d-title').textContent=(r.graph||'run')+' · '+id.slice(0,8);
|
|
684
|
+
const st=$('#d-status'); st.className='status '+r.status; $('#d-status-t').textContent=r.status;
|
|
685
|
+
$('#d-meta').innerHTML=[
|
|
686
|
+
`<span><b>${fmtms(r.duration_ms||0)}</b> duration</span>`,
|
|
687
|
+
`<span><b class="num">${r.node_count||0}</b> nodes</span>`,
|
|
688
|
+
r.llm_calls?`<span><b class="num">${r.llm_calls}</b> LLM calls</span>`:'',
|
|
689
|
+
r.total_tokens?`<span><b class="num">${fmtk(r.total_tokens)}</b> tokens</span>`:'',
|
|
690
|
+
r.cost_usd!=null?`<span><b class="num">$${r.cost_usd.toFixed(5)}</b> cost</span>`:'',
|
|
691
|
+
r.session?`<span>session <b class="mono">${esc(r.session)}</b></span>`:'',
|
|
692
|
+
(r.tags||[]).length?`<span>${r.tags.map(t=>`<span class="tagchip">${esc(t)}</span>`).join('')}</span>`:'',
|
|
693
|
+
`<span>${new Date(r.started_ms).toLocaleString()}</span>`,
|
|
694
|
+
].filter(Boolean).join('');
|
|
695
|
+
const total=Math.max(1,...r.spans.map(s=>(s.offset_ms||0)+(s.dur_ms||0)));
|
|
696
|
+
const byParent={}; r.spans.forEach(s=>{(byParent[s.parent_id||'root']=byParent[s.parent_id||'root']||[]).push(s);});
|
|
697
|
+
let html = r.error?`<div class="kv"><div class="k">run error</div><pre class="err">${esc(r.error)}</pre></div>`:'';
|
|
698
|
+
if(r.input!=null) html += kv('run input', r.input);
|
|
699
|
+
html += renderSpans(byParent['root']||[],byParent,total,0);
|
|
700
|
+
const tops=r.spans.filter(s=>!s.parent_id&&s.type==='node'&&s.output&&typeof s.output==='object'&&!Array.isArray(s.output));
|
|
701
|
+
if(tops.length>1) html += `<div class="kv" style="margin-top:12px"><div class="k">state evolution — keys each node wrote</div></div>`+
|
|
702
|
+
tops.map(s=>`<div class="scorerow" title="${esc(JSON.stringify(s.output)).replace(/"/g,'"').slice(0,400)}">
|
|
703
|
+
<span class="chip node">${esc(s.name)}</span>
|
|
704
|
+
${Object.keys(s.output).map(k=>`<span class="tagchip" style="cursor:default">${esc(k)}</span>`).join('')}
|
|
705
|
+
<span style="color:var(--muted);font-size:11px;margin-left:auto" class="mono">${s.dur_ms!=null?fmtms(s.dur_ms):''}</span></div>`).join('');
|
|
706
|
+
html += `<div class="kv" style="margin-top:14px"><div class="k">scores</div></div>
|
|
707
|
+
${(r.scores||[]).map(sc=>`<div class="scorerow"><span class="scorechip">${esc(sc.name)} ${sc.value}</span>
|
|
708
|
+
${sc.comment?`<span style="color:var(--muted);font-size:12px">${esc(sc.comment)}</span>`:''}
|
|
709
|
+
<span class="del" onclick="delScore('${sc.id}','${id}')">×</span></div>`).join('')}
|
|
710
|
+
<div class="scoreadd">
|
|
711
|
+
<input id="sc-name" placeholder="name" style="width:110px">
|
|
712
|
+
<input id="sc-val" placeholder="value" type="number" step="any" style="width:80px">
|
|
713
|
+
<input id="sc-com" placeholder="comment (optional)" style="flex:1;min-width:120px">
|
|
714
|
+
<button class="btn ghost" onclick="addScore('${id}')">Add score</button>
|
|
715
|
+
</div>`;
|
|
716
|
+
html += `<div style="margin-top:6px"><button class="btn ghost" onclick='replay(${JSON.stringify(r.spans.filter(s=>s.type==="node").map(s=>({n:s.name,e:s.status==="error"})))})'>⟲ Replay on graph</button></div>`;
|
|
717
|
+
$('#d-body').innerHTML=html;
|
|
718
|
+
$('#scrim').classList.add('on'); $('#drawer').classList.add('on');
|
|
719
|
+
}
|
|
720
|
+
window.addScore=async(id)=>{
|
|
721
|
+
const name=$('#sc-name').value.trim(), value=parseFloat($('#sc-val').value);
|
|
722
|
+
if(!name||Number.isNaN(value)) return toast('Score needs a name and a numeric value');
|
|
723
|
+
const res=await fetch(`/api/runs/${id}/scores`,{method:'POST',
|
|
724
|
+
headers:{'Content-Type':'application/json'},
|
|
725
|
+
body:JSON.stringify({name,value,comment:$('#sc-com').value.trim()||null,source:'ui'})});
|
|
726
|
+
if(res.ok){ toast('Score added'); openRun(id); if(VIEW==='runs')loadRuns(); }
|
|
727
|
+
else toast('Score failed: '+((await res.json()).error||res.status));
|
|
728
|
+
};
|
|
729
|
+
window.delScore=async(sid,rid)=>{
|
|
730
|
+
await fetch('/api/scores/'+sid,{method:'DELETE'}); toast('Score removed'); openRun(rid);
|
|
731
|
+
if(VIEW==='runs')loadRuns();
|
|
732
|
+
};
|
|
733
|
+
function renderSpans(list,byParent,total,depth){
|
|
734
|
+
return list.map(s=>{
|
|
735
|
+
const kids=byParent[s.id]||[];
|
|
736
|
+
const left=((s.offset_ms||0)/total*100).toFixed(2), width=Math.max(1.2,(s.dur_ms||0)/total*100).toFixed(2);
|
|
737
|
+
const meta=[s.dur_ms!=null?s.dur_ms+'ms':null,
|
|
738
|
+
s.type==='llm'&&(s.prompt_tokens!=null||s.completion_tokens!=null)?fmtk((s.prompt_tokens||0)+(s.completion_tokens||0))+'t':null,
|
|
739
|
+
s.cost_usd!=null?'$'+s.cost_usd.toFixed(4):null].filter(Boolean).join(' · ');
|
|
740
|
+
const did='sp'+s.id;
|
|
741
|
+
return `<div>
|
|
742
|
+
<div class="tr-row" onclick="tog('${did}',this)">
|
|
743
|
+
<div class="tr-main"><span class="indent" style="width:${depth*18}px"></span>
|
|
744
|
+
<span class="caret">▶</span><span class="chip ${s.type}">${s.type}</span>
|
|
745
|
+
<span class="tr-name ${s.status==='error'?'err':''}">${esc(s.name||s.type)}</span></div>
|
|
746
|
+
<div class="tr-gantt"><div class="tr-bar ${s.type}" style="left:${left}%;width:${width}%"></div></div>
|
|
747
|
+
<div class="tr-meta">${meta}</div>
|
|
748
|
+
</div>
|
|
749
|
+
<div class="tr-detail" id="${did}">
|
|
750
|
+
${s.model?`<div class="kv"><div class="k">model</div><pre>${esc(s.model)}${s.prompt_tokens!=null?` · ${s.prompt_tokens} in / ${s.completion_tokens||0} out`:''}</pre></div>`:''}
|
|
751
|
+
${s.error?`<div class="kv"><div class="k">error${s.type==='node'?`<button class="copy" onclick="showSpanSource('${s.id}')">view source</button>`:''}</div><pre class="err">${esc(s.error)}</pre><div id="src-${s.id}"></div></div>`:''}
|
|
752
|
+
${s.input!=null?kv('input',s.input):''}
|
|
753
|
+
${s.output!=null?kv('output',s.output):''}
|
|
754
|
+
${(!s.error&&s.input==null&&s.output==null&&!s.model)?'<div class="kv"><pre>no captured payload</pre></div>':''}
|
|
755
|
+
</div>
|
|
756
|
+
${renderSpans(kids,byParent,total,depth+1)}
|
|
757
|
+
</div>`;
|
|
758
|
+
}).join('');
|
|
759
|
+
}
|
|
760
|
+
function kv(k,v){ const txt=typeof v==='string'?v:JSON.stringify(v,null,2);
|
|
761
|
+
return `<div class="kv"><div class="k">${k}<button class="copy" onclick="navigator.clipboard.writeText(this.closest('.kv').querySelector('pre').textContent);toast('Copied')">copy</button></div><pre>${esc(txt)}</pre></div>`; }
|
|
762
|
+
window.tog=(id,row)=>{ const el=document.getElementById(id); const open=el.style.display==='block';
|
|
763
|
+
el.style.display=open?'none':'block'; row.querySelector('.caret').classList.toggle('open',!open); };
|
|
764
|
+
window.replay=async(steps)=>{ closeAll(); switchView('graph');
|
|
765
|
+
cy.nodes().removeClass('running done err'); cy.edges().removeClass('flow'); let prev='__start__';
|
|
766
|
+
for(const raw of steps){ const st=typeof raw==='string'?{n:raw,e:false}:raw;
|
|
767
|
+
cy.$id(prev).addClass('done').removeClass('running');
|
|
768
|
+
cy.edges(`[source="${prev}"][target="${st.n}"]`).addClass('flow');
|
|
769
|
+
if(st.e){ cy.$id(st.n).addClass('err'); toast('Run failed at '+st.n+' — tap the node for its source'); return; }
|
|
770
|
+
cy.$id(st.n).addClass('running'); prev=st.n; await new Promise(r=>setTimeout(r,380)); }
|
|
771
|
+
cy.$id(prev).addClass('done').removeClass('running'); cy.$id('__end__').addClass('done'); };
|
|
772
|
+
window.showSpanSource=async(sid)=>{
|
|
773
|
+
const s=((window.__run||{}).spans||[]).find(x=>x.id===sid); if(!s) return;
|
|
774
|
+
const el=document.getElementById('src-'+sid); if(!el) return;
|
|
775
|
+
if(el.innerHTML){ el.innerHTML=''; return; }
|
|
776
|
+
let src=null; try{ const r=await fetch('/api/nodes/'+encodeURIComponent(s.name)+'/source');
|
|
777
|
+
if(r.ok) src=await r.json(); }catch(e){}
|
|
778
|
+
el.innerHTML=src?`<div class="k" style="margin-top:8px">source · ${esc((src.file||'').split('/').pop())}:${src.line_start}</div>${codeBlock(src,tbLines(s.error,src))}`
|
|
779
|
+
:'<pre>no source available for this node (external run, or inspect could not trace it)</pre>';
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
/* ---------- stats ---------- */
|
|
783
|
+
function chartCard(title,daily,val,errKey){
|
|
784
|
+
const max=Math.max(1,...daily.map(d=>val(d)||0));
|
|
785
|
+
return `<div class="card"><div class="cwrap"><b>${title}</b>
|
|
786
|
+
<div class="chartrow">${daily.map(d=>{
|
|
787
|
+
const v=val(d)||0,h=Math.max(v?4:1,v/max*100);
|
|
788
|
+
const ep=errKey&&v?Math.min(100,(d[errKey]||0)/v*100):0;
|
|
789
|
+
return `<div class="cbar" style="height:${h}%" title="${d.day}: ${fmtk(v)}${errKey&&d[errKey]?' ('+d[errKey]+' errors)':''}">${ep?`<div class="errpart" style="height:${ep}%"></div>`:''}</div>`;}).join('')}</div>
|
|
790
|
+
<div class="clabels"><span>${daily[0]?.day||''}</span><span>${daily[daily.length-1]?.day||''}</span></div></div></div>`;
|
|
791
|
+
}
|
|
792
|
+
async function loadStats(){
|
|
793
|
+
const s=await(await fetch('/api/stats?days=30')).json(); const t=s.totals||{};
|
|
794
|
+
const errRate=t.runs?Math.round((t.errors||0)/t.runs*100):0;
|
|
795
|
+
const max=Math.max(1,...(s.per_node||[]).map(n=>n.avg_ms||0));
|
|
796
|
+
const daily=s.daily||[], models=s.models||[];
|
|
797
|
+
const db=t.db_bytes!=null?(t.db_bytes/1048576).toFixed(1)+' MB db':'';
|
|
798
|
+
$('#stats-body').innerHTML=`
|
|
799
|
+
<div class="metrics">
|
|
800
|
+
<div class="metric"><div class="v num">${t.runs||0}</div><div class="l">Total runs</div></div>
|
|
801
|
+
<div class="metric"><div class="v num">${errRate}%</div><div class="l">Error rate</div></div>
|
|
802
|
+
<div class="metric"><div class="v num">${fmtk(t.tokens||0)}</div><div class="l">Tokens</div></div>
|
|
803
|
+
<div class="metric"><div class="v num">${t.cost!=null?'$'+(t.cost||0).toFixed(4):'—'}</div><div class="l">Estimated cost</div></div>
|
|
804
|
+
<div class="metric"><div class="v num">${t.llm_calls||0}</div><div class="l">LLM calls</div></div>
|
|
805
|
+
</div>
|
|
806
|
+
${daily.length?`<div class="page-head"><h1 style="font-size:14px">Last ${s.days} days</h1><span class="sub">${db}</span></div>
|
|
807
|
+
<div class="charts">
|
|
808
|
+
${chartCard('Runs / day',daily,d=>d.runs,'errors')}
|
|
809
|
+
${chartCard('Tokens / day',daily,d=>d.tokens)}
|
|
810
|
+
</div>`:''}
|
|
811
|
+
${models.length?`<div class="page-head"><h1 style="font-size:14px">Models</h1></div>
|
|
812
|
+
<div class="card" style="margin-bottom:14px"><div style="overflow:auto"><table>
|
|
813
|
+
<thead><tr><th>Model</th><th class="r">Calls</th><th class="r">In tokens</th><th class="r">Out tokens</th><th class="r">Cost</th><th class="r">Avg latency</th></tr></thead>
|
|
814
|
+
<tbody>${models.map(m=>`<tr>
|
|
815
|
+
<td style="font-weight:600">${esc(m.model)}</td>
|
|
816
|
+
<td class="r num">${m.calls}</td>
|
|
817
|
+
<td class="r num">${m.prompt_tokens!=null?fmtk(m.prompt_tokens):'—'}</td>
|
|
818
|
+
<td class="r num">${m.completion_tokens!=null?fmtk(m.completion_tokens):'—'}</td>
|
|
819
|
+
<td class="r num">${m.cost!=null?'$'+m.cost.toFixed(4):'—'}</td>
|
|
820
|
+
<td class="r num mono">${fmtms(Math.round(m.avg_ms||0))}</td></tr>`).join('')}</tbody></table></div></div>`:''}
|
|
821
|
+
<div class="page-head"><h1 style="font-size:14px">Average latency per node</h1></div>
|
|
822
|
+
<div class="card">${(s.per_node||[]).map(n=>`<div class="latrow"><div class="latname">${esc(n.name)}</div>
|
|
823
|
+
<div class="lattrack"><div class="latfill" style="width:${(n.avg_ms||0)/max*100}%"></div></div>
|
|
824
|
+
<div class="latms">${Math.round(n.avg_ms||0)} ms</div></div>`).join('')||'<div class="empty">No node timings yet</div>'}</div>`;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/* ---------- misc ---------- */
|
|
828
|
+
function closeAll(){ $('#scrim').classList.remove('on'); $('#drawer').classList.remove('on'); $('#modal').classList.remove('on'); }
|
|
829
|
+
function esc(s){ return String(s).replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c])); }
|
|
830
|
+
function fmtms(ms){ return ms<1000?ms+' ms':(ms/1000).toFixed(1)+' s'; }
|
|
831
|
+
function fmtk(n){ return n<1000?String(n):(n/1000).toFixed(1)+'k'; }
|
|
832
|
+
function ago(ms){ const s=(Date.now()-ms)/1000; if(s<60)return Math.round(s)+'s ago'; if(s<3600)return Math.round(s/60)+'m ago'; if(s<86400)return Math.round(s/3600)+'h ago'; return Math.round(s/86400)+'d ago'; }
|
|
833
|
+
let tT; function toast(m){ const t=$('#toast'); t.textContent=m; t.classList.add('show'); clearTimeout(tT); tT=setTimeout(()=>t.classList.remove('show'),2600); }
|
|
834
|
+
|
|
835
|
+
loadGraph(); liveTopology();
|
|
836
|
+
/* deep links: #runs #sessions #stats #run=<id> */
|
|
837
|
+
(function(){ const h=location.hash.slice(1);
|
|
838
|
+
if(h==='runs'||h==='sessions'||h==='stats') switchView(h);
|
|
839
|
+
else if(h.startsWith('run=')){ switchView('runs'); openRun(h.slice(4)); }
|
|
840
|
+
})();
|
|
841
|
+
</script>
|
|
842
|
+
</body>
|
|
843
|
+
</html>
|