Upload folder using huggingface_hub
Browse files- trackio_custom_frontend/app.js +555 -0
- trackio_custom_frontend/index.html +135 -0
- trackio_custom_frontend/styles.css +467 -0
trackio_custom_frontend/app.js
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const projectSelectEl = document.querySelector("#project-select");
|
| 2 |
+
const runListEl = document.querySelector("#run-list");
|
| 3 |
+
const metricsTitleEl = document.querySelector("#metrics-title");
|
| 4 |
+
const metricsSubtitleEl = document.querySelector("#metrics-subtitle");
|
| 5 |
+
const metricsGridEl = document.querySelector("#metrics-grid");
|
| 6 |
+
const tracesSubtitleEl = document.querySelector("#traces-subtitle");
|
| 7 |
+
const tracesBodyEl = document.querySelector("#traces-body");
|
| 8 |
+
const navButtons = Array.from(document.querySelectorAll(".nav-link"));
|
| 9 |
+
const pages = Array.from(document.querySelectorAll(".page"));
|
| 10 |
+
|
| 11 |
+
const state = {
|
| 12 |
+
projects: [],
|
| 13 |
+
selectedProject: null,
|
| 14 |
+
runs: [],
|
| 15 |
+
selectedRunIds: [],
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
/*
|
| 19 |
+
Trackio routes used by this starter today:
|
| 20 |
+
- /api/get_all_projects
|
| 21 |
+
- /api/get_runs_for_project
|
| 22 |
+
- /api/get_metrics_for_run
|
| 23 |
+
- /api/get_metric_values
|
| 24 |
+
- /api/get_traces
|
| 25 |
+
|
| 26 |
+
Useful routes for expanding this starter toward the full dashboard:
|
| 27 |
+
- /api/get_system_metrics_for_run
|
| 28 |
+
- /api/get_system_logs
|
| 29 |
+
- /api/get_system_logs_batch
|
| 30 |
+
- /api/get_logs
|
| 31 |
+
- /api/get_logs_batch
|
| 32 |
+
- /api/get_snapshot
|
| 33 |
+
- /api/get_alerts
|
| 34 |
+
- /api/query_project
|
| 35 |
+
- /api/get_project_summary
|
| 36 |
+
- /api/get_run_summary
|
| 37 |
+
- /api/get_project_files
|
| 38 |
+
- /api/get_settings
|
| 39 |
+
- /api/get_run_mutation_status
|
| 40 |
+
- /api/delete_run
|
| 41 |
+
- /api/rename_run
|
| 42 |
+
- /api/force_sync
|
| 43 |
+
- /api/bulk_upload_media
|
| 44 |
+
- /api/upload
|
| 45 |
+
|
| 46 |
+
File/media URLs:
|
| 47 |
+
- /file?path=ABSOLUTE_PATH_FROM_API
|
| 48 |
+
*/
|
| 49 |
+
const RUN_COLORS = [
|
| 50 |
+
"#1f77b4",
|
| 51 |
+
"#ff7f0e",
|
| 52 |
+
"#2ca02c",
|
| 53 |
+
"#d62728",
|
| 54 |
+
"#9467bd",
|
| 55 |
+
"#8c564b",
|
| 56 |
+
"#e377c2",
|
| 57 |
+
"#7f7f7f",
|
| 58 |
+
"#bcbd22",
|
| 59 |
+
"#17becf",
|
| 60 |
+
];
|
| 61 |
+
|
| 62 |
+
async function api(name, payload = {}) {
|
| 63 |
+
const response = await fetch(`/api/${name}`, {
|
| 64 |
+
method: "POST",
|
| 65 |
+
headers: { "Content-Type": "application/json" },
|
| 66 |
+
body: JSON.stringify(payload),
|
| 67 |
+
});
|
| 68 |
+
const json = await response.json();
|
| 69 |
+
if (!response.ok || json.error) {
|
| 70 |
+
throw new Error(json.error || `Request failed for ${name}`);
|
| 71 |
+
}
|
| 72 |
+
return json.data;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
function runKey(run) {
|
| 76 |
+
return run.id || run.name;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function colorForRun(run) {
|
| 80 |
+
const index = state.runs.findIndex((candidate) => runKey(candidate) === runKey(run));
|
| 81 |
+
return RUN_COLORS[((index >= 0 ? index : 0) % RUN_COLORS.length + RUN_COLORS.length) % RUN_COLORS.length];
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
function formatValue(value) {
|
| 85 |
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
| 86 |
+
return String(value);
|
| 87 |
+
}
|
| 88 |
+
if (Math.abs(value) >= 1000 || Math.abs(value) < 0.01) {
|
| 89 |
+
return value.toExponential(2);
|
| 90 |
+
}
|
| 91 |
+
return value.toFixed(3);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
function getQueryParams() {
|
| 95 |
+
return new URLSearchParams(window.location.search);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
function setQueryParams(params) {
|
| 99 |
+
const next = new URL(window.location.href);
|
| 100 |
+
for (const [key, value] of Object.entries(params)) {
|
| 101 |
+
if (value == null || value === "" || (Array.isArray(value) && value.length === 0)) {
|
| 102 |
+
next.searchParams.delete(key);
|
| 103 |
+
continue;
|
| 104 |
+
}
|
| 105 |
+
next.searchParams.set(key, Array.isArray(value) ? value.join(",") : value);
|
| 106 |
+
}
|
| 107 |
+
window.history.replaceState({}, "", next);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
function setActivePage(pageName) {
|
| 111 |
+
navButtons.forEach((button) => {
|
| 112 |
+
button.classList.toggle("active", button.dataset.pageTarget === pageName);
|
| 113 |
+
});
|
| 114 |
+
pages.forEach((page) => {
|
| 115 |
+
page.classList.toggle("active", page.dataset.page === pageName);
|
| 116 |
+
});
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
function bindNavigation() {
|
| 120 |
+
navButtons.forEach((button) => {
|
| 121 |
+
button.addEventListener("click", () => setActivePage(button.dataset.pageTarget));
|
| 122 |
+
});
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
function pickInitialProject(projects) {
|
| 126 |
+
const params = getQueryParams();
|
| 127 |
+
const project = params.get("project");
|
| 128 |
+
if (project && projects.includes(project)) {
|
| 129 |
+
return project;
|
| 130 |
+
}
|
| 131 |
+
return projects[0] || null;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
function pickInitialRunIds(runs) {
|
| 135 |
+
const params = getQueryParams();
|
| 136 |
+
const fromUrl = (params.get("run_ids") || "")
|
| 137 |
+
.split(",")
|
| 138 |
+
.map((item) => item.trim())
|
| 139 |
+
.filter(Boolean);
|
| 140 |
+
const validIds = runs.map(runKey);
|
| 141 |
+
const selected = fromUrl.filter((id) => validIds.includes(id));
|
| 142 |
+
if (selected.length) {
|
| 143 |
+
return selected;
|
| 144 |
+
}
|
| 145 |
+
return runs.slice(0, 2).map(runKey);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
function renderProjectSelect() {
|
| 149 |
+
projectSelectEl.innerHTML = "";
|
| 150 |
+
if (!state.projects.length) {
|
| 151 |
+
const option = document.createElement("option");
|
| 152 |
+
option.value = "";
|
| 153 |
+
option.textContent = "No projects";
|
| 154 |
+
projectSelectEl.appendChild(option);
|
| 155 |
+
projectSelectEl.disabled = true;
|
| 156 |
+
return;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
projectSelectEl.disabled = false;
|
| 160 |
+
for (const project of state.projects) {
|
| 161 |
+
const option = document.createElement("option");
|
| 162 |
+
option.value = project;
|
| 163 |
+
option.textContent = project;
|
| 164 |
+
option.selected = project === state.selectedProject;
|
| 165 |
+
projectSelectEl.appendChild(option);
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
function renderRunList() {
|
| 170 |
+
runListEl.innerHTML = "";
|
| 171 |
+
if (!state.runs.length) {
|
| 172 |
+
const empty = document.createElement("div");
|
| 173 |
+
empty.className = "sidebar-empty";
|
| 174 |
+
empty.textContent = "No runs yet";
|
| 175 |
+
runListEl.appendChild(empty);
|
| 176 |
+
return;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
for (const run of state.runs) {
|
| 180 |
+
const wrapper = document.createElement("label");
|
| 181 |
+
wrapper.className = "run-option";
|
| 182 |
+
|
| 183 |
+
const input = document.createElement("input");
|
| 184 |
+
input.type = "checkbox";
|
| 185 |
+
input.checked = state.selectedRunIds.includes(runKey(run));
|
| 186 |
+
input.addEventListener("change", async () => {
|
| 187 |
+
if (input.checked) {
|
| 188 |
+
state.selectedRunIds = [...new Set([...state.selectedRunIds, runKey(run)])];
|
| 189 |
+
} else {
|
| 190 |
+
state.selectedRunIds = state.selectedRunIds.filter((id) => id !== runKey(run));
|
| 191 |
+
}
|
| 192 |
+
setQueryParams({
|
| 193 |
+
project: state.selectedProject,
|
| 194 |
+
run_ids: state.selectedRunIds,
|
| 195 |
+
});
|
| 196 |
+
await renderDashboard();
|
| 197 |
+
});
|
| 198 |
+
|
| 199 |
+
const marker = document.createElement("span");
|
| 200 |
+
marker.className = "run-color-dot";
|
| 201 |
+
marker.style.backgroundColor = colorForRun(run);
|
| 202 |
+
|
| 203 |
+
const text = document.createElement("span");
|
| 204 |
+
text.className = "run-option-text";
|
| 205 |
+
text.innerHTML = `<strong>${run.name || "Unnamed run"}</strong>`;
|
| 206 |
+
|
| 207 |
+
wrapper.appendChild(input);
|
| 208 |
+
wrapper.appendChild(marker);
|
| 209 |
+
wrapper.appendChild(text);
|
| 210 |
+
runListEl.appendChild(wrapper);
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
function chartPoints(rows, width, height, padding, min, max) {
|
| 215 |
+
const span = max - min || 1;
|
| 216 |
+
return rows.map((row, index) => {
|
| 217 |
+
const x = padding + (index / Math.max(rows.length - 1, 1)) * (width - padding * 2);
|
| 218 |
+
const y = height - padding - ((row.value - min) / span) * (height - padding * 2);
|
| 219 |
+
return [x, y];
|
| 220 |
+
});
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
function pathFromPoints(points) {
|
| 224 |
+
return points.map(([x, y], index) => `${index === 0 ? "M" : "L"} ${x} ${y}`).join(" ");
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
function renderMetricCard(metricName, seriesByRun) {
|
| 228 |
+
const card = document.createElement("article");
|
| 229 |
+
card.className = "metric-card";
|
| 230 |
+
const nonEmptySeries = seriesByRun.filter((entry) => entry.rows.length);
|
| 231 |
+
if (!nonEmptySeries.length) {
|
| 232 |
+
card.innerHTML = `
|
| 233 |
+
<div class="metric-card-head">
|
| 234 |
+
<div>
|
| 235 |
+
<h3>${metricName}</h3>
|
| 236 |
+
<div class="metric-run">Selected runs</div>
|
| 237 |
+
</div>
|
| 238 |
+
</div>
|
| 239 |
+
<div class="metric-empty">No numeric values logged for this metric.</div>
|
| 240 |
+
`;
|
| 241 |
+
return card;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
const width = 640;
|
| 245 |
+
const height = 220;
|
| 246 |
+
const padding = 20;
|
| 247 |
+
const values = nonEmptySeries.flatMap((entry) => entry.rows.map((row) => row.value));
|
| 248 |
+
const min = Math.min(...values);
|
| 249 |
+
const max = Math.max(...values);
|
| 250 |
+
const lineMarkup = nonEmptySeries
|
| 251 |
+
.map((entry) => {
|
| 252 |
+
const points = chartPoints(entry.rows, width, height, padding, min, max);
|
| 253 |
+
const markers = points
|
| 254 |
+
.map(([x, y]) => `<circle class="plot-marker" cx="${x}" cy="${y}" r="3.5" style="stroke:${entry.color}"></circle>`)
|
| 255 |
+
.join("");
|
| 256 |
+
return `
|
| 257 |
+
<path class="plot-line" d="${pathFromPoints(points)}" stroke="${entry.color}"></path>
|
| 258 |
+
${markers}
|
| 259 |
+
`;
|
| 260 |
+
})
|
| 261 |
+
.join("");
|
| 262 |
+
const legendMarkup = nonEmptySeries
|
| 263 |
+
.map(
|
| 264 |
+
(entry) => `
|
| 265 |
+
<span class="metric-legend-item">
|
| 266 |
+
<span class="metric-legend-dot" style="background:${entry.color}"></span>
|
| 267 |
+
${entry.runName}
|
| 268 |
+
</span>
|
| 269 |
+
`,
|
| 270 |
+
)
|
| 271 |
+
.join("");
|
| 272 |
+
const latestSummary = nonEmptySeries
|
| 273 |
+
.map((entry) => `${entry.runName}: ${formatValue(entry.rows.at(-1).value)}`)
|
| 274 |
+
.join(" | ");
|
| 275 |
+
|
| 276 |
+
card.innerHTML = `
|
| 277 |
+
<div class="metric-card-head">
|
| 278 |
+
<div>
|
| 279 |
+
<h3>${metricName}</h3>
|
| 280 |
+
<div class="metric-run">${nonEmptySeries.length} run${nonEmptySeries.length === 1 ? "" : "s"} overlaid</div>
|
| 281 |
+
</div>
|
| 282 |
+
<div class="metric-latest">${latestSummary}</div>
|
| 283 |
+
</div>
|
| 284 |
+
<div class="plot-shell">
|
| 285 |
+
<svg viewBox="0 0 ${width} ${height}" role="img" aria-label="${metricName} line plot">
|
| 286 |
+
<line class="plot-axis" x1="${padding}" y1="${height - padding}" x2="${width - padding}" y2="${height - padding}"></line>
|
| 287 |
+
${lineMarkup}
|
| 288 |
+
</svg>
|
| 289 |
+
</div>
|
| 290 |
+
<div class="metric-legend">${legendMarkup}</div>
|
| 291 |
+
<div class="metric-meta">Comparing ${nonEmptySeries.length} selected runs on the same metric scale.</div>
|
| 292 |
+
`;
|
| 293 |
+
return card;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
function textFromContent(content) {
|
| 297 |
+
if (typeof content === "string") return content;
|
| 298 |
+
if (Array.isArray(content)) {
|
| 299 |
+
return content
|
| 300 |
+
.map((part) => {
|
| 301 |
+
if (typeof part === "string") return part;
|
| 302 |
+
if (typeof part?.text === "string") return part.text;
|
| 303 |
+
if (typeof part?.content === "string") return part.content;
|
| 304 |
+
return "";
|
| 305 |
+
})
|
| 306 |
+
.filter(Boolean)
|
| 307 |
+
.join(" ");
|
| 308 |
+
}
|
| 309 |
+
if (typeof content?.text === "string") return content.text;
|
| 310 |
+
return "";
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
function escapeHtml(value) {
|
| 314 |
+
return String(value)
|
| 315 |
+
.replaceAll("&", "&")
|
| 316 |
+
.replaceAll("<", "<")
|
| 317 |
+
.replaceAll(">", ">")
|
| 318 |
+
.replaceAll('"', """)
|
| 319 |
+
.replaceAll("'", "'");
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
function renderMessageContent(content) {
|
| 323 |
+
if (typeof content === "string") {
|
| 324 |
+
return `<div class="trace-message-text">${escapeHtml(content)}</div>`;
|
| 325 |
+
}
|
| 326 |
+
if (Array.isArray(content)) {
|
| 327 |
+
const items = content
|
| 328 |
+
.map((part) => {
|
| 329 |
+
if (typeof part === "string") {
|
| 330 |
+
return `<div class="trace-message-text">${escapeHtml(part)}</div>`;
|
| 331 |
+
}
|
| 332 |
+
if (typeof part?.text === "string") {
|
| 333 |
+
return `<div class="trace-message-text">${escapeHtml(part.text)}</div>`;
|
| 334 |
+
}
|
| 335 |
+
if (typeof part?.content === "string") {
|
| 336 |
+
return `<div class="trace-message-text">${escapeHtml(part.content)}</div>`;
|
| 337 |
+
}
|
| 338 |
+
return `<div class="trace-message-text trace-message-muted">[non-text content]</div>`;
|
| 339 |
+
})
|
| 340 |
+
.join("");
|
| 341 |
+
return items || '<div class="trace-message-text trace-message-muted">(empty)</div>';
|
| 342 |
+
}
|
| 343 |
+
if (typeof content?.text === "string") {
|
| 344 |
+
return `<div class="trace-message-text">${escapeHtml(content.text)}</div>`;
|
| 345 |
+
}
|
| 346 |
+
return '<div class="trace-message-text trace-message-muted">(empty)</div>';
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
function renderTraceDetail(trace) {
|
| 350 |
+
const messages = Array.isArray(trace.messages) ? trace.messages : [];
|
| 351 |
+
if (!messages.length) {
|
| 352 |
+
return '<div class="trace-message-text trace-message-muted">No trace messages.</div>';
|
| 353 |
+
}
|
| 354 |
+
return messages
|
| 355 |
+
.map((message) => {
|
| 356 |
+
const role = escapeHtml(message?.role || "unknown");
|
| 357 |
+
return `
|
| 358 |
+
<div class="trace-message">
|
| 359 |
+
<div class="trace-message-role">${role}</div>
|
| 360 |
+
${renderMessageContent(message?.content)}
|
| 361 |
+
</div>
|
| 362 |
+
`;
|
| 363 |
+
})
|
| 364 |
+
.join("");
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
function formatTraceTime(timestamp) {
|
| 368 |
+
if (!timestamp) return "—";
|
| 369 |
+
const date = new Date(timestamp);
|
| 370 |
+
if (Number.isNaN(date.getTime())) {
|
| 371 |
+
return timestamp;
|
| 372 |
+
}
|
| 373 |
+
return date.toLocaleString();
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
function renderTraceRows(traces) {
|
| 377 |
+
tracesBodyEl.innerHTML = "";
|
| 378 |
+
if (!traces.length) {
|
| 379 |
+
const row = document.createElement("tr");
|
| 380 |
+
row.innerHTML = '<td colspan="5" class="empty-row">No traces for the selected runs.</td>';
|
| 381 |
+
tracesBodyEl.appendChild(row);
|
| 382 |
+
return;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
for (const trace of traces) {
|
| 386 |
+
const request = textFromContent(
|
| 387 |
+
(trace.messages || []).find((message) => message?.role === "user")?.content,
|
| 388 |
+
) || "(no user message)";
|
| 389 |
+
const row = document.createElement("tr");
|
| 390 |
+
row.className = "trace-summary-row";
|
| 391 |
+
row.setAttribute("role", "button");
|
| 392 |
+
row.setAttribute("tabindex", "0");
|
| 393 |
+
row.setAttribute("aria-expanded", "false");
|
| 394 |
+
row.innerHTML = `
|
| 395 |
+
<td><span class="trace-id">${trace.id}</span></td>
|
| 396 |
+
<td class="trace-request">${request}</td>
|
| 397 |
+
<td>${trace.run || "—"}</td>
|
| 398 |
+
<td>${trace.step ?? "—"}</td>
|
| 399 |
+
<td>${formatTraceTime(trace.timestamp)}</td>
|
| 400 |
+
`;
|
| 401 |
+
const detailRow = document.createElement("tr");
|
| 402 |
+
detailRow.className = "trace-detail-row";
|
| 403 |
+
detailRow.hidden = true;
|
| 404 |
+
detailRow.innerHTML = `
|
| 405 |
+
<td colspan="5">
|
| 406 |
+
<div class="trace-detail-shell">
|
| 407 |
+
<div class="trace-detail-head">
|
| 408 |
+
<div>
|
| 409 |
+
<strong>${escapeHtml(trace.id)}</strong>
|
| 410 |
+
<div class="trace-detail-meta">${escapeHtml(trace.run || "—")} | step ${escapeHtml(trace.step ?? "—")} | ${escapeHtml(formatTraceTime(trace.timestamp))}</div>
|
| 411 |
+
</div>
|
| 412 |
+
</div>
|
| 413 |
+
<div class="trace-message-list">
|
| 414 |
+
${renderTraceDetail(trace)}
|
| 415 |
+
</div>
|
| 416 |
+
</div>
|
| 417 |
+
</td>
|
| 418 |
+
`;
|
| 419 |
+
const toggleRow = () => {
|
| 420 |
+
const expanded = row.getAttribute("aria-expanded") === "true";
|
| 421 |
+
row.setAttribute("aria-expanded", expanded ? "false" : "true");
|
| 422 |
+
row.classList.toggle("expanded", !expanded);
|
| 423 |
+
detailRow.hidden = expanded;
|
| 424 |
+
};
|
| 425 |
+
row.addEventListener("click", toggleRow);
|
| 426 |
+
row.addEventListener("keydown", (event) => {
|
| 427 |
+
if (event.key === "Enter" || event.key === " ") {
|
| 428 |
+
event.preventDefault();
|
| 429 |
+
toggleRow();
|
| 430 |
+
}
|
| 431 |
+
});
|
| 432 |
+
tracesBodyEl.appendChild(row);
|
| 433 |
+
tracesBodyEl.appendChild(detailRow);
|
| 434 |
+
}
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
async function loadRuns() {
|
| 438 |
+
if (!state.selectedProject) {
|
| 439 |
+
state.runs = [];
|
| 440 |
+
state.selectedRunIds = [];
|
| 441 |
+
renderRunList();
|
| 442 |
+
await renderDashboard();
|
| 443 |
+
return;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
state.runs = await api("get_runs_for_project", { project: state.selectedProject });
|
| 447 |
+
state.selectedRunIds = pickInitialRunIds(state.runs);
|
| 448 |
+
renderRunList();
|
| 449 |
+
await renderDashboard();
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
async function renderDashboard() {
|
| 453 |
+
metricsGridEl.innerHTML = "";
|
| 454 |
+
tracesBodyEl.innerHTML = "";
|
| 455 |
+
|
| 456 |
+
const selectedRuns = state.runs.filter((run) => state.selectedRunIds.includes(runKey(run)));
|
| 457 |
+
metricsTitleEl.textContent = state.selectedProject || "Metrics";
|
| 458 |
+
|
| 459 |
+
if (!state.selectedProject) {
|
| 460 |
+
metricsSubtitleEl.textContent = "No Trackio projects found.";
|
| 461 |
+
tracesSubtitleEl.textContent = "No traces available.";
|
| 462 |
+
return;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
if (!selectedRuns.length) {
|
| 466 |
+
metricsSubtitleEl.textContent = "Select one or more runs in the sidebar.";
|
| 467 |
+
tracesSubtitleEl.textContent = "Select one or more runs to load traces.";
|
| 468 |
+
metricsGridEl.innerHTML = '<div class="empty-panel">No runs selected.</div>';
|
| 469 |
+
renderTraceRows([]);
|
| 470 |
+
return;
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
metricsSubtitleEl.textContent = `Plot cards for ${selectedRuns.length} selected run${selectedRuns.length === 1 ? "" : "s"}.`;
|
| 474 |
+
tracesSubtitleEl.textContent = `Recent traces for ${selectedRuns.length} selected run${selectedRuns.length === 1 ? "" : "s"}.`;
|
| 475 |
+
|
| 476 |
+
const traceGroups = [];
|
| 477 |
+
const metricMap = new Map();
|
| 478 |
+
|
| 479 |
+
for (const run of selectedRuns) {
|
| 480 |
+
const metrics = await api("get_metrics_for_run", {
|
| 481 |
+
project: state.selectedProject,
|
| 482 |
+
run: run.name,
|
| 483 |
+
run_id: run.id,
|
| 484 |
+
});
|
| 485 |
+
|
| 486 |
+
const metricSeries = await Promise.all(
|
| 487 |
+
metrics.slice(0, 3).map(async (metricName) => ({
|
| 488 |
+
metricName,
|
| 489 |
+
rows: await api("get_metric_values", {
|
| 490 |
+
project: state.selectedProject,
|
| 491 |
+
run: run.name,
|
| 492 |
+
run_id: run.id,
|
| 493 |
+
metric_name: metricName,
|
| 494 |
+
}),
|
| 495 |
+
})),
|
| 496 |
+
);
|
| 497 |
+
|
| 498 |
+
metricSeries.forEach(({ metricName, rows }) => {
|
| 499 |
+
const numericRows = rows.filter((row) => typeof row.value === "number" && Number.isFinite(row.value));
|
| 500 |
+
if (!metricMap.has(metricName)) {
|
| 501 |
+
metricMap.set(metricName, []);
|
| 502 |
+
}
|
| 503 |
+
metricMap.get(metricName).push({
|
| 504 |
+
runName: run.name || "Unnamed run",
|
| 505 |
+
color: colorForRun(run),
|
| 506 |
+
rows: numericRows,
|
| 507 |
+
});
|
| 508 |
+
});
|
| 509 |
+
|
| 510 |
+
const runTraces = await api("get_traces", {
|
| 511 |
+
project: state.selectedProject,
|
| 512 |
+
run: run.name,
|
| 513 |
+
run_id: run.id,
|
| 514 |
+
sort: "request_time_desc",
|
| 515 |
+
limit: 6,
|
| 516 |
+
});
|
| 517 |
+
traceGroups.push(...runTraces);
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
for (const [metricName, seriesByRun] of metricMap.entries()) {
|
| 521 |
+
metricsGridEl.appendChild(renderMetricCard(metricName, seriesByRun));
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
if (!metricsGridEl.children.length) {
|
| 525 |
+
metricsGridEl.innerHTML = '<div class="empty-panel">No numeric metrics available.</div>';
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
traceGroups.sort((left, right) => String(right.timestamp || "").localeCompare(String(left.timestamp || "")));
|
| 529 |
+
renderTraceRows(traceGroups.slice(0, 12));
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
async function load() {
|
| 533 |
+
bindNavigation();
|
| 534 |
+
projectSelectEl.addEventListener("change", async () => {
|
| 535 |
+
state.selectedProject = projectSelectEl.value || null;
|
| 536 |
+
setQueryParams({ project: state.selectedProject, run_ids: null });
|
| 537 |
+
await loadRuns();
|
| 538 |
+
renderProjectSelect();
|
| 539 |
+
});
|
| 540 |
+
try {
|
| 541 |
+
state.projects = await api("get_all_projects");
|
| 542 |
+
state.selectedProject = pickInitialProject(state.projects);
|
| 543 |
+
renderProjectSelect();
|
| 544 |
+
await loadRuns();
|
| 545 |
+
} catch (error) {
|
| 546 |
+
projectSelectEl.innerHTML = '<option value="">Error</option>';
|
| 547 |
+
projectSelectEl.disabled = true;
|
| 548 |
+
metricsSubtitleEl.textContent = "Could not load Trackio data.";
|
| 549 |
+
metricsGridEl.innerHTML = '<div class="empty-panel">The starter could not reach the Trackio API.</div>';
|
| 550 |
+
tracesSubtitleEl.textContent = "Could not load traces.";
|
| 551 |
+
renderTraceRows([]);
|
| 552 |
+
}
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
load();
|
trackio_custom_frontend/index.html
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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" />
|
| 6 |
+
<title>Starter</title>
|
| 7 |
+
<link rel="stylesheet" href="./styles.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div class="app-shell">
|
| 11 |
+
<aside class="sidebar">
|
| 12 |
+
<div class="sidebar-scroll">
|
| 13 |
+
<div class="logo-section">
|
| 14 |
+
<img
|
| 15 |
+
src="/static/trackio/trackio_logo_type_light_transparent.png"
|
| 16 |
+
alt="Trackio"
|
| 17 |
+
class="logo"
|
| 18 |
+
/>
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<section class="sidebar-section">
|
| 22 |
+
<div class="section-label">Project</div>
|
| 23 |
+
<div class="dropdown-wrap">
|
| 24 |
+
<select id="project-select" class="project-select" aria-label="Project"></select>
|
| 25 |
+
<div class="dropdown-icon" aria-hidden="true">
|
| 26 |
+
<svg width="16" height="16" viewBox="0 0 18 18" fill="none">
|
| 27 |
+
<path d="M5.25 7.5L9 11.25L12.75 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
| 28 |
+
</svg>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
</section>
|
| 32 |
+
|
| 33 |
+
<section class="sidebar-section">
|
| 34 |
+
<div class="section-label">Runs</div>
|
| 35 |
+
<div id="run-list" class="run-list"></div>
|
| 36 |
+
</section>
|
| 37 |
+
|
| 38 |
+
<section class="sidebar-section">
|
| 39 |
+
<div class="section-label">Notes</div>
|
| 40 |
+
<p class="sidebar-note">
|
| 41 |
+
This starter mirrors the Trackio dashboard structure, but stays plain
|
| 42 |
+
HTML, CSS, and JavaScript so you can replace pieces quickly.
|
| 43 |
+
</p>
|
| 44 |
+
</section>
|
| 45 |
+
</div>
|
| 46 |
+
</aside>
|
| 47 |
+
|
| 48 |
+
<main class="main-shell">
|
| 49 |
+
<nav class="navbar">
|
| 50 |
+
<div class="nav-spacer"></div>
|
| 51 |
+
<div class="nav-tabs">
|
| 52 |
+
<button class="nav-link active" data-page-target="metrics">Metrics</button>
|
| 53 |
+
<button class="nav-link" data-page-target="traces">Traces</button>
|
| 54 |
+
<!--
|
| 55 |
+
Trackio's full dashboard also includes tabs like these.
|
| 56 |
+
Uncomment them when you implement the corresponding page sections.
|
| 57 |
+
|
| 58 |
+
<button class="nav-link" data-page-target="system">System Metrics</button>
|
| 59 |
+
<button class="nav-link" data-page-target="media">Media & Tables</button>
|
| 60 |
+
<button class="nav-link" data-page-target="reports">Alerts & Reports</button>
|
| 61 |
+
<button class="nav-link" data-page-target="runs">Runs</button>
|
| 62 |
+
<button class="nav-link" data-page-target="files">Files</button>
|
| 63 |
+
<button class="nav-link" data-page-target="settings">Settings</button>
|
| 64 |
+
-->
|
| 65 |
+
</div>
|
| 66 |
+
</nav>
|
| 67 |
+
|
| 68 |
+
<section class="page active" data-page="metrics">
|
| 69 |
+
<header class="page-header">
|
| 70 |
+
<div>
|
| 71 |
+
<p class="eyebrow">Starter Dashboard</p>
|
| 72 |
+
<h1 id="metrics-title">Metrics</h1>
|
| 73 |
+
<p id="metrics-subtitle" class="page-subtitle">Loading Trackio data.</p>
|
| 74 |
+
</div>
|
| 75 |
+
</header>
|
| 76 |
+
<div id="metrics-grid" class="metrics-grid"></div>
|
| 77 |
+
</section>
|
| 78 |
+
|
| 79 |
+
<section class="page" data-page="traces">
|
| 80 |
+
<header class="page-header">
|
| 81 |
+
<div>
|
| 82 |
+
<p class="eyebrow">Starter Dashboard</p>
|
| 83 |
+
<h1>Traces</h1>
|
| 84 |
+
<p id="traces-subtitle" class="page-subtitle">Recent traces for the selected runs.</p>
|
| 85 |
+
</div>
|
| 86 |
+
</header>
|
| 87 |
+
<div class="traces-table-wrap">
|
| 88 |
+
<table class="traces-table">
|
| 89 |
+
<thead>
|
| 90 |
+
<tr>
|
| 91 |
+
<th>Trace ID</th>
|
| 92 |
+
<th>Request</th>
|
| 93 |
+
<th>Run</th>
|
| 94 |
+
<th>Step</th>
|
| 95 |
+
<th>Request time</th>
|
| 96 |
+
</tr>
|
| 97 |
+
</thead>
|
| 98 |
+
<tbody id="traces-body"></tbody>
|
| 99 |
+
</table>
|
| 100 |
+
</div>
|
| 101 |
+
</section>
|
| 102 |
+
|
| 103 |
+
<!--
|
| 104 |
+
Future page shells you may want to add:
|
| 105 |
+
|
| 106 |
+
<section class="page" data-page="system">
|
| 107 |
+
Build this from /api/get_system_metrics_for_run and /api/get_system_logs.
|
| 108 |
+
</section>
|
| 109 |
+
|
| 110 |
+
<section class="page" data-page="media">
|
| 111 |
+
Build this from /api/get_logs, /api/get_snapshot, /api/get_project_files, and /file?path=...
|
| 112 |
+
</section>
|
| 113 |
+
|
| 114 |
+
<section class="page" data-page="reports">
|
| 115 |
+
Build this from /api/get_alerts and /api/query_project.
|
| 116 |
+
</section>
|
| 117 |
+
|
| 118 |
+
<section class="page" data-page="runs">
|
| 119 |
+
Build this from /api/get_runs_for_project, /api/get_run_summary, /api/delete_run, and /api/rename_run.
|
| 120 |
+
</section>
|
| 121 |
+
|
| 122 |
+
<section class="page" data-page="files">
|
| 123 |
+
Build this from /api/get_project_files and /file?path=...
|
| 124 |
+
</section>
|
| 125 |
+
|
| 126 |
+
<section class="page" data-page="settings">
|
| 127 |
+
Build this from /api/get_settings, /api/force_sync, and /api/get_run_mutation_status.
|
| 128 |
+
</section>
|
| 129 |
+
-->
|
| 130 |
+
</main>
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
<script type="module" src="./app.js"></script>
|
| 134 |
+
</body>
|
| 135 |
+
</html>
|
trackio_custom_frontend/styles.css
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
color-scheme: light;
|
| 3 |
+
--background-fill-primary: #ffffff;
|
| 4 |
+
--background-fill-secondary: #f9fafb;
|
| 5 |
+
--background-fill-tertiary: #f3f4f6;
|
| 6 |
+
--border-color-primary: #e5e7eb;
|
| 7 |
+
--border-color-accent: #d1d5db;
|
| 8 |
+
--body-text-color: #111827;
|
| 9 |
+
--body-text-color-subdued: #6b7280;
|
| 10 |
+
--body-text-color-soft: #9ca3af;
|
| 11 |
+
--accent: #1d4ed8;
|
| 12 |
+
--shadow-soft: 0 1px 2px rgba(16, 24, 40, 0.04);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
* {
|
| 16 |
+
box-sizing: border-box;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
html,
|
| 20 |
+
body {
|
| 21 |
+
margin: 0;
|
| 22 |
+
min-height: 100%;
|
| 23 |
+
background: var(--background-fill-secondary);
|
| 24 |
+
color: var(--body-text-color);
|
| 25 |
+
font-family:
|
| 26 |
+
ui-sans-serif,
|
| 27 |
+
system-ui,
|
| 28 |
+
-apple-system,
|
| 29 |
+
BlinkMacSystemFont,
|
| 30 |
+
"Segoe UI",
|
| 31 |
+
sans-serif;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
button,
|
| 35 |
+
input,
|
| 36 |
+
select,
|
| 37 |
+
table {
|
| 38 |
+
font: inherit;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.app-shell {
|
| 42 |
+
display: grid;
|
| 43 |
+
grid-template-columns: 320px minmax(0, 1fr);
|
| 44 |
+
min-height: 100vh;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.sidebar {
|
| 48 |
+
border-right: 1px solid var(--border-color-primary);
|
| 49 |
+
background: var(--background-fill-primary);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.sidebar-scroll {
|
| 53 |
+
height: 100vh;
|
| 54 |
+
overflow-y: auto;
|
| 55 |
+
padding: 18px 16px 28px;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.logo-section {
|
| 59 |
+
padding: 10px 10px 18px;
|
| 60 |
+
border-bottom: 1px solid var(--border-color-primary);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.logo {
|
| 64 |
+
display: block;
|
| 65 |
+
width: 138px;
|
| 66 |
+
max-width: 100%;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.sidebar-section {
|
| 70 |
+
padding: 18px 10px 0;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.section-label,
|
| 74 |
+
.eyebrow {
|
| 75 |
+
margin: 0 0 10px;
|
| 76 |
+
color: var(--body-text-color-subdued);
|
| 77 |
+
font-size: 12px;
|
| 78 |
+
font-weight: 600;
|
| 79 |
+
letter-spacing: 0.08em;
|
| 80 |
+
text-transform: uppercase;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.run-list {
|
| 84 |
+
display: grid;
|
| 85 |
+
gap: 8px;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.dropdown-wrap {
|
| 89 |
+
position: relative;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.project-select {
|
| 93 |
+
width: 100%;
|
| 94 |
+
padding: 10px 40px 10px 12px;
|
| 95 |
+
border: 1px solid var(--border-color-primary);
|
| 96 |
+
border-radius: 10px;
|
| 97 |
+
background: var(--background-fill-primary);
|
| 98 |
+
color: var(--body-text-color);
|
| 99 |
+
appearance: none;
|
| 100 |
+
-webkit-appearance: none;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.project-select:focus {
|
| 104 |
+
outline: none;
|
| 105 |
+
border-color: var(--border-color-accent);
|
| 106 |
+
box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.08);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.dropdown-icon {
|
| 110 |
+
position: absolute;
|
| 111 |
+
top: 50%;
|
| 112 |
+
right: 12px;
|
| 113 |
+
transform: translateY(-50%);
|
| 114 |
+
color: var(--body-text-color-subdued);
|
| 115 |
+
pointer-events: none;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.run-option {
|
| 119 |
+
display: grid;
|
| 120 |
+
grid-template-columns: 18px 10px minmax(0, 1fr);
|
| 121 |
+
gap: 10px;
|
| 122 |
+
align-items: start;
|
| 123 |
+
padding: 10px 12px;
|
| 124 |
+
border: 1px solid var(--border-color-primary);
|
| 125 |
+
border-radius: 10px;
|
| 126 |
+
background: var(--background-fill-primary);
|
| 127 |
+
cursor: pointer;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.run-option input {
|
| 131 |
+
margin: 2px 0 0;
|
| 132 |
+
accent-color: var(--accent);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.run-color-dot {
|
| 136 |
+
width: 10px;
|
| 137 |
+
height: 10px;
|
| 138 |
+
margin-top: 5px;
|
| 139 |
+
border-radius: 999px;
|
| 140 |
+
background: var(--accent);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.run-option-text strong,
|
| 144 |
+
.run-option-text span {
|
| 145 |
+
display: block;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.run-option-text strong {
|
| 149 |
+
color: var(--body-text-color);
|
| 150 |
+
font-size: 14px;
|
| 151 |
+
font-weight: 600;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.run-option-text span,
|
| 155 |
+
.sidebar-note,
|
| 156 |
+
.sidebar-empty {
|
| 157 |
+
color: var(--body-text-color-subdued);
|
| 158 |
+
font-size: 13px;
|
| 159 |
+
line-height: 1.5;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.main-shell {
|
| 163 |
+
display: flex;
|
| 164 |
+
flex-direction: column;
|
| 165 |
+
min-width: 0;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.navbar {
|
| 169 |
+
display: flex;
|
| 170 |
+
align-items: stretch;
|
| 171 |
+
min-height: 44px;
|
| 172 |
+
border-bottom: 1px solid var(--border-color-primary);
|
| 173 |
+
background: var(--background-fill-primary);
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.nav-spacer {
|
| 177 |
+
flex: 1 1 0;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.nav-tabs {
|
| 181 |
+
display: flex;
|
| 182 |
+
padding-right: 8px;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.nav-link {
|
| 186 |
+
padding: 10px 16px;
|
| 187 |
+
border: none;
|
| 188 |
+
border-bottom: 2px solid transparent;
|
| 189 |
+
background: none;
|
| 190 |
+
color: var(--body-text-color-subdued);
|
| 191 |
+
cursor: pointer;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.nav-link.active {
|
| 195 |
+
border-bottom-color: var(--body-text-color);
|
| 196 |
+
color: var(--body-text-color);
|
| 197 |
+
font-weight: 500;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.page {
|
| 201 |
+
display: none;
|
| 202 |
+
min-width: 0;
|
| 203 |
+
padding: 24px 28px 36px;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.page.active {
|
| 207 |
+
display: block;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.page-header {
|
| 211 |
+
margin-bottom: 22px;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.page-header h1 {
|
| 215 |
+
margin: 0;
|
| 216 |
+
font-size: 32px;
|
| 217 |
+
line-height: 1.1;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.page-subtitle {
|
| 221 |
+
margin: 8px 0 0;
|
| 222 |
+
color: var(--body-text-color-subdued);
|
| 223 |
+
font-size: 15px;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.metrics-grid {
|
| 227 |
+
display: grid;
|
| 228 |
+
gap: 18px;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.metric-card {
|
| 232 |
+
padding: 18px;
|
| 233 |
+
border: 1px solid var(--border-color-primary);
|
| 234 |
+
border-radius: 14px;
|
| 235 |
+
background: var(--background-fill-primary);
|
| 236 |
+
box-shadow: var(--shadow-soft);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.metric-card-head {
|
| 240 |
+
display: flex;
|
| 241 |
+
align-items: start;
|
| 242 |
+
justify-content: space-between;
|
| 243 |
+
gap: 16px;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.metric-card h3 {
|
| 247 |
+
margin: 0;
|
| 248 |
+
font-size: 18px;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.metric-run,
|
| 252 |
+
.metric-meta,
|
| 253 |
+
.metric-empty,
|
| 254 |
+
.metric-latest {
|
| 255 |
+
color: var(--body-text-color-subdued);
|
| 256 |
+
font-size: 13px;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.metric-latest {
|
| 260 |
+
color: var(--body-text-color);
|
| 261 |
+
max-width: 50%;
|
| 262 |
+
font-size: 13px;
|
| 263 |
+
font-weight: 600;
|
| 264 |
+
text-align: right;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.plot-shell {
|
| 268 |
+
margin-top: 14px;
|
| 269 |
+
padding: 10px 12px;
|
| 270 |
+
border: 1px solid var(--border-color-primary);
|
| 271 |
+
border-radius: 12px;
|
| 272 |
+
background: linear-gradient(180deg, #ffffff, #f9fafb);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.plot-shell svg {
|
| 276 |
+
display: block;
|
| 277 |
+
width: 100%;
|
| 278 |
+
height: auto;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.plot-axis {
|
| 282 |
+
stroke: var(--border-color-accent);
|
| 283 |
+
stroke-width: 1.2;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.plot-line {
|
| 287 |
+
fill: none;
|
| 288 |
+
stroke-width: 2.25;
|
| 289 |
+
stroke-linecap: round;
|
| 290 |
+
stroke-linejoin: round;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.plot-marker {
|
| 294 |
+
fill: var(--background-fill-primary);
|
| 295 |
+
stroke: var(--body-text-color);
|
| 296 |
+
stroke-width: 1.5;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.metric-legend {
|
| 300 |
+
display: flex;
|
| 301 |
+
flex-wrap: wrap;
|
| 302 |
+
gap: 10px 14px;
|
| 303 |
+
margin-top: 12px;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.metric-legend-item {
|
| 307 |
+
display: inline-flex;
|
| 308 |
+
align-items: center;
|
| 309 |
+
gap: 8px;
|
| 310 |
+
color: var(--body-text-color-subdued);
|
| 311 |
+
font-size: 13px;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.metric-legend-dot {
|
| 315 |
+
width: 10px;
|
| 316 |
+
height: 10px;
|
| 317 |
+
border-radius: 999px;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.traces-table-wrap {
|
| 321 |
+
overflow: auto;
|
| 322 |
+
border: 1px solid var(--border-color-primary);
|
| 323 |
+
border-radius: 14px;
|
| 324 |
+
background: var(--background-fill-primary);
|
| 325 |
+
box-shadow: var(--shadow-soft);
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.traces-table {
|
| 329 |
+
width: 100%;
|
| 330 |
+
border-collapse: collapse;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.traces-table thead {
|
| 334 |
+
background: var(--background-fill-secondary);
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.traces-table th,
|
| 338 |
+
.traces-table td {
|
| 339 |
+
padding: 14px 16px;
|
| 340 |
+
border-bottom: 1px solid var(--border-color-primary);
|
| 341 |
+
text-align: left;
|
| 342 |
+
vertical-align: top;
|
| 343 |
+
font-size: 14px;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.traces-table th {
|
| 347 |
+
color: var(--body-text-color-subdued);
|
| 348 |
+
font-size: 12px;
|
| 349 |
+
font-weight: 600;
|
| 350 |
+
letter-spacing: 0.04em;
|
| 351 |
+
text-transform: uppercase;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.trace-summary-row {
|
| 355 |
+
cursor: pointer;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
.trace-summary-row:hover {
|
| 359 |
+
background: var(--background-fill-secondary);
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.trace-summary-row.expanded {
|
| 363 |
+
background: var(--background-fill-secondary);
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.trace-id {
|
| 367 |
+
color: var(--body-text-color);
|
| 368 |
+
font-family:
|
| 369 |
+
ui-monospace,
|
| 370 |
+
SFMono-Regular,
|
| 371 |
+
Menlo,
|
| 372 |
+
monospace;
|
| 373 |
+
font-size: 12px;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.trace-request {
|
| 377 |
+
max-width: 520px;
|
| 378 |
+
color: var(--body-text-color);
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.trace-detail-row td {
|
| 382 |
+
padding: 0;
|
| 383 |
+
background: var(--background-fill-secondary);
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.trace-detail-shell {
|
| 387 |
+
padding: 18px 20px;
|
| 388 |
+
border-top: 1px solid var(--border-color-primary);
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.trace-detail-head strong {
|
| 392 |
+
display: block;
|
| 393 |
+
color: var(--body-text-color);
|
| 394 |
+
font-size: 14px;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.trace-detail-meta {
|
| 398 |
+
margin-top: 4px;
|
| 399 |
+
color: var(--body-text-color-subdued);
|
| 400 |
+
font-size: 12px;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.trace-message-list {
|
| 404 |
+
display: grid;
|
| 405 |
+
gap: 12px;
|
| 406 |
+
margin-top: 16px;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.trace-message {
|
| 410 |
+
padding: 12px 14px;
|
| 411 |
+
border: 1px solid var(--border-color-primary);
|
| 412 |
+
border-radius: 12px;
|
| 413 |
+
background: var(--background-fill-primary);
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.trace-message-role {
|
| 417 |
+
margin-bottom: 8px;
|
| 418 |
+
color: var(--body-text-color-subdued);
|
| 419 |
+
font-size: 12px;
|
| 420 |
+
font-weight: 600;
|
| 421 |
+
letter-spacing: 0.04em;
|
| 422 |
+
text-transform: uppercase;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.trace-message-text {
|
| 426 |
+
color: var(--body-text-color);
|
| 427 |
+
font-size: 14px;
|
| 428 |
+
line-height: 1.55;
|
| 429 |
+
white-space: pre-wrap;
|
| 430 |
+
overflow-wrap: anywhere;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.trace-message-muted {
|
| 434 |
+
color: var(--body-text-color-subdued);
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
.empty-row,
|
| 438 |
+
.empty-panel {
|
| 439 |
+
color: var(--body-text-color-subdued);
|
| 440 |
+
text-align: center;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.empty-panel {
|
| 444 |
+
padding: 48px 20px;
|
| 445 |
+
border: 1px dashed var(--border-color-accent);
|
| 446 |
+
border-radius: 14px;
|
| 447 |
+
background: var(--background-fill-primary);
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
@media (max-width: 960px) {
|
| 451 |
+
.app-shell {
|
| 452 |
+
grid-template-columns: 1fr;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
.sidebar {
|
| 456 |
+
border-right: none;
|
| 457 |
+
border-bottom: 1px solid var(--border-color-primary);
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.sidebar-scroll {
|
| 461 |
+
height: auto;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
.page {
|
| 465 |
+
padding: 18px;
|
| 466 |
+
}
|
| 467 |
+
}
|