copilot-lens 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +66 -0
- package/dist/cli.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +61 -0
- package/dist/server.js.map +1 -0
- package/dist/sessions.d.ts +40 -0
- package/dist/sessions.js +246 -0
- package/dist/sessions.js.map +1 -0
- package/package.json +35 -0
- package/public/app.js +398 -0
- package/public/index.html +79 -0
- package/public/style.css +364 -0
package/public/app.js
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// State
|
|
2
|
+
let sessions = [];
|
|
3
|
+
let analytics = null;
|
|
4
|
+
let charts = {};
|
|
5
|
+
|
|
6
|
+
// Directory color coding — sophisticated muted palette
|
|
7
|
+
const DIR_COLORS = [
|
|
8
|
+
{ border: "#6e7681", bg: "#161b2208" }, // slate
|
|
9
|
+
{ border: "#58a6ff", bg: "#58a6ff08" }, // blue
|
|
10
|
+
{ border: "#3fb950", bg: "#3fb95008" }, // green
|
|
11
|
+
{ border: "#d29922", bg: "#d2992208" }, // amber
|
|
12
|
+
{ border: "#bc8cff", bg: "#bc8cff08" }, // purple
|
|
13
|
+
{ border: "#f0883e", bg: "#f0883e08" }, // orange
|
|
14
|
+
{ border: "#56d4dd", bg: "#56d4dd08" }, // teal
|
|
15
|
+
{ border: "#db61a2", bg: "#db61a208" }, // rose
|
|
16
|
+
];
|
|
17
|
+
const dirColorMap = {};
|
|
18
|
+
let nextColorIdx = 0;
|
|
19
|
+
|
|
20
|
+
function getDirColor(dir) {
|
|
21
|
+
if (!dir) return DIR_COLORS[0];
|
|
22
|
+
// Normalize to project root
|
|
23
|
+
const key = dir.replace(/\\/g, "/").split("/").slice(0, -1).join("/") || dir;
|
|
24
|
+
if (!dirColorMap[key]) {
|
|
25
|
+
dirColorMap[key] = DIR_COLORS[nextColorIdx % DIR_COLORS.length];
|
|
26
|
+
nextColorIdx++;
|
|
27
|
+
}
|
|
28
|
+
return dirColorMap[key];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// DOM refs
|
|
32
|
+
const sessionList = document.getElementById("sessionList");
|
|
33
|
+
const sessionCount = document.getElementById("sessionCount");
|
|
34
|
+
const searchInput = document.getElementById("searchInput");
|
|
35
|
+
const timeFilter = document.getElementById("timeFilter");
|
|
36
|
+
const statusFilter = document.getElementById("statusFilter");
|
|
37
|
+
const dirFilter = document.getElementById("dirFilter");
|
|
38
|
+
const detailModal = document.getElementById("detailModal");
|
|
39
|
+
const detailContent = document.getElementById("detailContent");
|
|
40
|
+
const modalClose = document.getElementById("modalClose");
|
|
41
|
+
const refreshBtn = document.getElementById("refreshBtn");
|
|
42
|
+
|
|
43
|
+
// Navigation
|
|
44
|
+
document.querySelectorAll(".nav-btn").forEach((btn) => {
|
|
45
|
+
btn.addEventListener("click", () => {
|
|
46
|
+
document.querySelectorAll(".nav-btn").forEach((b) => b.classList.remove("active"));
|
|
47
|
+
document.querySelectorAll(".page").forEach((p) => p.classList.remove("active"));
|
|
48
|
+
btn.classList.add("active");
|
|
49
|
+
document.getElementById(btn.dataset.page + "Page").classList.add("active");
|
|
50
|
+
if (btn.dataset.page === "analytics") loadAnalytics();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Modal
|
|
55
|
+
modalClose.addEventListener("click", () => detailModal.classList.add("hidden"));
|
|
56
|
+
detailModal.addEventListener("click", (e) => {
|
|
57
|
+
if (e.target === detailModal) detailModal.classList.add("hidden");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Format helpers
|
|
61
|
+
function formatDuration(ms) {
|
|
62
|
+
if (ms < 1000) return "< 1s";
|
|
63
|
+
const s = Math.floor(ms / 1000);
|
|
64
|
+
if (s < 60) return s + "s";
|
|
65
|
+
const m = Math.floor(s / 60);
|
|
66
|
+
if (m < 60) return m + "m " + (s % 60) + "s";
|
|
67
|
+
const h = Math.floor(m / 60);
|
|
68
|
+
return h + "h " + (m % 60) + "m";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatTime(iso) {
|
|
72
|
+
if (!iso) return "—";
|
|
73
|
+
const d = new Date(iso);
|
|
74
|
+
const now = new Date();
|
|
75
|
+
const diff = now - d;
|
|
76
|
+
if (diff < 60000) return "just now";
|
|
77
|
+
if (diff < 3600000) return Math.floor(diff / 60000) + "m ago";
|
|
78
|
+
if (diff < 86400000) return Math.floor(diff / 3600000) + "h ago";
|
|
79
|
+
return d.toLocaleDateString() + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function shortId(id) {
|
|
83
|
+
return id.slice(0, 8);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function shortDir(dir) {
|
|
87
|
+
if (!dir) return "—";
|
|
88
|
+
const parts = dir.replace(/\\/g, "/").split("/");
|
|
89
|
+
return parts.slice(-2).join("/");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Filter sessions
|
|
93
|
+
function getFilteredSessions() {
|
|
94
|
+
let filtered = [...sessions];
|
|
95
|
+
const query = searchInput.value.toLowerCase();
|
|
96
|
+
if (query) {
|
|
97
|
+
filtered = filtered.filter(
|
|
98
|
+
(s) =>
|
|
99
|
+
s.id.toLowerCase().includes(query) ||
|
|
100
|
+
(s.cwd || "").toLowerCase().includes(query) ||
|
|
101
|
+
(s.branch || "").toLowerCase().includes(query)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const time = timeFilter.value;
|
|
106
|
+
if (time !== "all") {
|
|
107
|
+
const now = new Date();
|
|
108
|
+
filtered = filtered.filter((s) => {
|
|
109
|
+
const created = new Date(s.createdAt);
|
|
110
|
+
if (time === "today") return now - created < 86400000;
|
|
111
|
+
if (time === "week") return now - created < 604800000;
|
|
112
|
+
if (time === "month") return now - created < 2592000000;
|
|
113
|
+
return true;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const status = statusFilter.value;
|
|
118
|
+
if (status !== "all") {
|
|
119
|
+
filtered = filtered.filter((s) => s.status === status);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const dir = dirFilter.value;
|
|
123
|
+
if (dir !== "all") {
|
|
124
|
+
filtered = filtered.filter((s) => (s.cwd || "") === dir);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return filtered;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Render session list
|
|
131
|
+
function renderSessions() {
|
|
132
|
+
const filtered = getFilteredSessions();
|
|
133
|
+
sessionCount.textContent = `${filtered.length} session${filtered.length !== 1 ? "s" : ""} found`;
|
|
134
|
+
|
|
135
|
+
sessionList.innerHTML = filtered
|
|
136
|
+
.map(
|
|
137
|
+
(s) => {
|
|
138
|
+
const c = getDirColor(s.cwd);
|
|
139
|
+
return `
|
|
140
|
+
<div class="session-card" data-id="${s.id}" style="border-left: 3px solid ${c.border}">
|
|
141
|
+
<div class="top-row">
|
|
142
|
+
<span class="session-id">${shortId(s.id)}</span>
|
|
143
|
+
<span class="badge badge-${s.status}">${s.status === "running" ? "● Running" : s.status === "error" ? "✕ Error" : "✓ Completed"}</span>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="session-dir">${s.cwd || "—"}</div>
|
|
146
|
+
<div class="session-meta">
|
|
147
|
+
${s.branch ? `<span class="badge badge-branch">⎇ ${s.branch}</span>` : ""}
|
|
148
|
+
<span>${formatTime(s.createdAt)}</span>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
.join("");
|
|
155
|
+
|
|
156
|
+
// Click handlers
|
|
157
|
+
sessionList.querySelectorAll(".session-card").forEach((card) => {
|
|
158
|
+
card.addEventListener("click", () => openDetail(card.dataset.id));
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Open session detail
|
|
163
|
+
async function openDetail(id) {
|
|
164
|
+
detailContent.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-dim)">Loading...</div>';
|
|
165
|
+
detailModal.classList.remove("hidden");
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const res = await fetch(`/api/sessions/${id}`);
|
|
169
|
+
const session = await res.json();
|
|
170
|
+
renderDetail(session);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
detailContent.innerHTML = `<div style="color:var(--danger)">Failed to load session: ${err.message}</div>`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderDetail(s) {
|
|
177
|
+
const userMessages = s.events.filter((e) => e.type === "user.message");
|
|
178
|
+
const assistantMessages = s.events.filter((e) => e.type === "assistant.message");
|
|
179
|
+
const toolCalls = s.events.filter((e) => e.type === "tool.execution_start");
|
|
180
|
+
const errors = s.events.filter((e) => e.type === "session.error");
|
|
181
|
+
|
|
182
|
+
// Interleave conversation messages in order
|
|
183
|
+
const conversation = s.events
|
|
184
|
+
.filter((e) => e.type === "user.message" || e.type === "assistant.message")
|
|
185
|
+
.map((e) => {
|
|
186
|
+
const isUser = e.type === "user.message";
|
|
187
|
+
const content = e.data?.content || "";
|
|
188
|
+
// Truncate long messages
|
|
189
|
+
const display = content.length > 2000 ? content.slice(0, 2000) + "\n...(truncated)" : content;
|
|
190
|
+
return `<div class="message ${isUser ? "message-user" : "message-assistant"}">
|
|
191
|
+
<div class="message-label">${isUser ? "👤 You" : "🤖 Copilot"}</div>
|
|
192
|
+
${escapeHtml(display)}
|
|
193
|
+
</div>`;
|
|
194
|
+
})
|
|
195
|
+
.join("");
|
|
196
|
+
|
|
197
|
+
detailContent.innerHTML = `
|
|
198
|
+
<div class="detail-header">
|
|
199
|
+
<h2>Session ${s.id}</h2>
|
|
200
|
+
<div class="detail-meta">
|
|
201
|
+
<div><span>Directory:</span> <strong>${s.cwd || "—"}</strong></div>
|
|
202
|
+
<div><span>Branch:</span> <strong>${s.branch || "—"}</strong></div>
|
|
203
|
+
<div><span>Created:</span> <strong>${new Date(s.createdAt).toLocaleString()}</strong></div>
|
|
204
|
+
<div><span>Duration:</span> <strong>${formatDuration(s.duration)}</strong></div>
|
|
205
|
+
<div><span>Version:</span> <strong>${s.copilotVersion || "—"}</strong></div>
|
|
206
|
+
<div><span>Status:</span> <strong class="badge badge-${s.status}">${s.status === "running" ? "● Running" : s.status === "error" ? "✕ Error" : "✓ Completed"}</strong></div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div class="event-counts" style="margin-bottom:16px">
|
|
211
|
+
${Object.entries(s.eventCounts)
|
|
212
|
+
.sort((a, b) => b[1] - a[1])
|
|
213
|
+
.map(([type, count]) => `<span class="event-count-badge">${type}: ${count}</span>`)
|
|
214
|
+
.join("")}
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div class="detail-tabs">
|
|
218
|
+
<button class="detail-tab active" data-tab="conversation">Conversation (${userMessages.length + assistantMessages.length})</button>
|
|
219
|
+
<button class="detail-tab" data-tab="tools">Tools (${toolCalls.length})</button>
|
|
220
|
+
<button class="detail-tab" data-tab="errors">Errors (${errors.length})</button>
|
|
221
|
+
${s.planContent ? '<button class="detail-tab" data-tab="plan">Plan</button>' : ""}
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div class="detail-panel active" id="panel-conversation">
|
|
225
|
+
${conversation || '<div style="color:var(--text-dim)">No messages in this session</div>'}
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div class="detail-panel" id="panel-tools">
|
|
229
|
+
${
|
|
230
|
+
toolCalls.length
|
|
231
|
+
? toolCalls
|
|
232
|
+
.map((e) => `<div class="tool-item">${e.data?.tool || e.data?.toolName || "unknown"}</div>`)
|
|
233
|
+
.join("")
|
|
234
|
+
: '<div style="color:var(--text-dim)">No tool calls</div>'
|
|
235
|
+
}
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div class="detail-panel" id="panel-errors">
|
|
239
|
+
${
|
|
240
|
+
errors.length
|
|
241
|
+
? errors
|
|
242
|
+
.map(
|
|
243
|
+
(e) =>
|
|
244
|
+
`<div class="message" style="border-left:3px solid var(--danger)"><div class="message-label" style="color:var(--danger)">Error</div>${escapeHtml(e.data?.message || "Unknown error")}</div>`
|
|
245
|
+
)
|
|
246
|
+
.join("")
|
|
247
|
+
: '<div style="color:var(--text-dim)">No errors 🎉</div>'
|
|
248
|
+
}
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
${s.planContent ? `<div class="detail-panel" id="panel-plan"><div class="plan-content">${escapeHtml(s.planContent)}</div></div>` : ""}
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
// Tab switching
|
|
255
|
+
detailContent.querySelectorAll(".detail-tab").forEach((tab) => {
|
|
256
|
+
tab.addEventListener("click", () => {
|
|
257
|
+
detailContent.querySelectorAll(".detail-tab").forEach((t) => t.classList.remove("active"));
|
|
258
|
+
detailContent.querySelectorAll(".detail-panel").forEach((p) => p.classList.remove("active"));
|
|
259
|
+
tab.classList.add("active");
|
|
260
|
+
document.getElementById("panel-" + tab.dataset.tab).classList.add("active");
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function escapeHtml(str) {
|
|
266
|
+
const div = document.createElement("div");
|
|
267
|
+
div.textContent = str;
|
|
268
|
+
return div.innerHTML;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Analytics
|
|
272
|
+
async function loadAnalytics() {
|
|
273
|
+
try {
|
|
274
|
+
const res = await fetch("/api/analytics");
|
|
275
|
+
analytics = await res.json();
|
|
276
|
+
renderAnalytics();
|
|
277
|
+
} catch (err) {
|
|
278
|
+
statsCards.innerHTML = `<div style="color:var(--danger)">Failed to load analytics</div>`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renderAnalytics() {
|
|
283
|
+
if (!analytics) return;
|
|
284
|
+
|
|
285
|
+
// Stats cards
|
|
286
|
+
statsCards.innerHTML = `
|
|
287
|
+
<div class="stat-card"><div class="stat-value">${analytics.totalSessions}</div><div class="stat-label">Total Sessions</div></div>
|
|
288
|
+
<div class="stat-card"><div class="stat-value">${formatDuration(analytics.avgDuration)}</div><div class="stat-label">Avg Duration</div></div>
|
|
289
|
+
<div class="stat-card"><div class="stat-value">${formatDuration(analytics.maxDuration)}</div><div class="stat-label">Longest Session</div></div>
|
|
290
|
+
<div class="stat-card"><div class="stat-value">${formatDuration(analytics.totalDuration)}</div><div class="stat-label">Total Time</div></div>
|
|
291
|
+
`;
|
|
292
|
+
|
|
293
|
+
renderCharts();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function renderCharts() {
|
|
297
|
+
// Destroy existing charts
|
|
298
|
+
Object.values(charts).forEach((c) => c.destroy());
|
|
299
|
+
charts = {};
|
|
300
|
+
|
|
301
|
+
const chartColors = ["#58a6ff", "#3fb950", "#d29922", "#f85149", "#bc8cff", "#f0883e", "#56d4dd", "#db61a2"];
|
|
302
|
+
|
|
303
|
+
// Sessions per day
|
|
304
|
+
const days = Object.keys(analytics.sessionsPerDay).sort();
|
|
305
|
+
charts.perDay = new Chart(document.getElementById("sessionsPerDayChart"), {
|
|
306
|
+
type: "bar",
|
|
307
|
+
data: {
|
|
308
|
+
labels: days.map((d) => d.slice(5)), // MM-DD
|
|
309
|
+
datasets: [{ label: "Sessions", data: days.map((d) => analytics.sessionsPerDay[d]), backgroundColor: "#58a6ff88", borderColor: "#58a6ff", borderWidth: 1 }],
|
|
310
|
+
},
|
|
311
|
+
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { color: "#8b949e" } }, x: { ticks: { color: "#8b949e" } } } },
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Tool usage (top 10)
|
|
315
|
+
const tools = Object.entries(analytics.toolUsage)
|
|
316
|
+
.sort((a, b) => b[1] - a[1])
|
|
317
|
+
.slice(0, 10);
|
|
318
|
+
if (tools.length) {
|
|
319
|
+
charts.tools = new Chart(document.getElementById("toolUsageChart"), {
|
|
320
|
+
type: "doughnut",
|
|
321
|
+
data: {
|
|
322
|
+
labels: tools.map((t) => t[0]),
|
|
323
|
+
datasets: [{ data: tools.map((t) => t[1]), backgroundColor: chartColors }],
|
|
324
|
+
},
|
|
325
|
+
options: { responsive: true, plugins: { legend: { position: "right", labels: { color: "#e6edf3", font: { size: 11 } } } } },
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Top directories (top 8)
|
|
330
|
+
const dirs = Object.entries(analytics.topDirectories)
|
|
331
|
+
.sort((a, b) => b[1] - a[1])
|
|
332
|
+
.slice(0, 8);
|
|
333
|
+
if (dirs.length) {
|
|
334
|
+
charts.dirs = new Chart(document.getElementById("topDirsChart"), {
|
|
335
|
+
type: "bar",
|
|
336
|
+
data: {
|
|
337
|
+
labels: dirs.map((d) => shortDir(d[0])),
|
|
338
|
+
datasets: [{ label: "Sessions", data: dirs.map((d) => d[1]), backgroundColor: "#3fb95088", borderColor: "#3fb950", borderWidth: 1 }],
|
|
339
|
+
},
|
|
340
|
+
options: { indexAxis: "y", responsive: true, plugins: { legend: { display: false } }, scales: { x: { beginAtZero: true, ticks: { color: "#8b949e" } }, y: { ticks: { color: "#8b949e", font: { size: 11 } } } } },
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Branch activity (top 8)
|
|
345
|
+
const branches = Object.entries(analytics.branchActivity)
|
|
346
|
+
.sort((a, b) => b[1] - a[1])
|
|
347
|
+
.slice(0, 8);
|
|
348
|
+
if (branches.length) {
|
|
349
|
+
charts.branches = new Chart(document.getElementById("branchChart"), {
|
|
350
|
+
type: "bar",
|
|
351
|
+
data: {
|
|
352
|
+
labels: branches.map((b) => b[0]),
|
|
353
|
+
datasets: [{ label: "Sessions", data: branches.map((b) => b[1]), backgroundColor: "#d2992288", borderColor: "#d29922", borderWidth: 1 }],
|
|
354
|
+
},
|
|
355
|
+
options: { indexAxis: "y", responsive: true, plugins: { legend: { display: false } }, scales: { x: { beginAtZero: true, ticks: { color: "#8b949e" } }, y: { ticks: { color: "#8b949e", font: { size: 11 } } } } },
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Data loading
|
|
361
|
+
async function loadSessions() {
|
|
362
|
+
try {
|
|
363
|
+
const res = await fetch("/api/sessions");
|
|
364
|
+
sessions = await res.json();
|
|
365
|
+
updateDirFilter();
|
|
366
|
+
renderSessions();
|
|
367
|
+
} catch (err) {
|
|
368
|
+
sessionList.innerHTML = `<div style="color:var(--danger);padding:20px">Failed to load sessions: ${err.message}</div>`;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Event listeners
|
|
373
|
+
searchInput.addEventListener("input", renderSessions);
|
|
374
|
+
timeFilter.addEventListener("change", renderSessions);
|
|
375
|
+
statusFilter.addEventListener("change", renderSessions);
|
|
376
|
+
dirFilter.addEventListener("change", renderSessions);
|
|
377
|
+
|
|
378
|
+
// Populate directory filter from session data
|
|
379
|
+
function updateDirFilter() {
|
|
380
|
+
const dirs = [...new Set(sessions.map((s) => s.cwd || "").filter(Boolean))].sort();
|
|
381
|
+
const current = dirFilter.value;
|
|
382
|
+
dirFilter.innerHTML = '<option value="all">All Directories</option>' +
|
|
383
|
+
dirs.map((d) => `<option value="${d}">${shortDir(d)}</option>`).join("");
|
|
384
|
+
dirFilter.value = current;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Refresh button
|
|
388
|
+
refreshBtn.addEventListener("click", () => {
|
|
389
|
+
refreshBtn.classList.add("spinning");
|
|
390
|
+
loadSessions();
|
|
391
|
+
if (document.getElementById("analyticsPage").classList.contains("active")) {
|
|
392
|
+
loadAnalytics();
|
|
393
|
+
}
|
|
394
|
+
setTimeout(() => refreshBtn.classList.remove("spinning"), 600);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Init
|
|
398
|
+
loadSessions();
|
|
@@ -0,0 +1,79 @@
|
|
|
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.0">
|
|
6
|
+
<title>Copilot Lens</title>
|
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
|
8
|
+
<link rel="stylesheet" href="style.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header>
|
|
12
|
+
<div class="logo">👓 Copilot Lens</div>
|
|
13
|
+
<nav>
|
|
14
|
+
<button class="nav-btn active" data-page="sessions">Sessions</button>
|
|
15
|
+
<button class="nav-btn" data-page="analytics">Analytics</button>
|
|
16
|
+
</nav>
|
|
17
|
+
<button class="refresh-btn" id="refreshBtn" title="Refresh data">⟳ Refresh</button>
|
|
18
|
+
</header>
|
|
19
|
+
|
|
20
|
+
<main>
|
|
21
|
+
<!-- Sessions Page -->
|
|
22
|
+
<section id="sessionsPage" class="page active">
|
|
23
|
+
<div class="controls">
|
|
24
|
+
<input type="text" id="searchInput" placeholder="Search sessions (ID, branch)..." />
|
|
25
|
+
<select id="timeFilter">
|
|
26
|
+
<option value="all">All Time</option>
|
|
27
|
+
<option value="today">Today</option>
|
|
28
|
+
<option value="week">This Week</option>
|
|
29
|
+
<option value="month">This Month</option>
|
|
30
|
+
</select>
|
|
31
|
+
<select id="statusFilter">
|
|
32
|
+
<option value="all">All Status</option>
|
|
33
|
+
<option value="running">Running</option>
|
|
34
|
+
<option value="completed">Completed</option>
|
|
35
|
+
<option value="error">Error</option>
|
|
36
|
+
</select>
|
|
37
|
+
<select id="dirFilter">
|
|
38
|
+
<option value="all">All Directories</option>
|
|
39
|
+
</select>
|
|
40
|
+
</div>
|
|
41
|
+
<div id="sessionCount" class="session-count"></div>
|
|
42
|
+
<div id="sessionList" class="session-list"></div>
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<!-- Analytics Page -->
|
|
46
|
+
<section id="analyticsPage" class="page">
|
|
47
|
+
<div class="stats-cards" id="statsCards"></div>
|
|
48
|
+
<div class="charts-grid">
|
|
49
|
+
<div class="chart-card">
|
|
50
|
+
<h3>Sessions Per Day</h3>
|
|
51
|
+
<canvas id="sessionsPerDayChart"></canvas>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="chart-card">
|
|
54
|
+
<h3>Tool Usage</h3>
|
|
55
|
+
<canvas id="toolUsageChart"></canvas>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="chart-card">
|
|
58
|
+
<h3>Top Working Directories</h3>
|
|
59
|
+
<canvas id="topDirsChart"></canvas>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="chart-card">
|
|
62
|
+
<h3>Branch Activity</h3>
|
|
63
|
+
<canvas id="branchChart"></canvas>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</section>
|
|
67
|
+
|
|
68
|
+
<!-- Session Detail Modal -->
|
|
69
|
+
<div id="detailModal" class="modal hidden">
|
|
70
|
+
<div class="modal-content">
|
|
71
|
+
<button class="modal-close" id="modalClose">✕</button>
|
|
72
|
+
<div id="detailContent"></div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</main>
|
|
76
|
+
|
|
77
|
+
<script src="app.js"></script>
|
|
78
|
+
</body>
|
|
79
|
+
</html>
|