apteva 0.4.31 → 0.4.41
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/dist/ActivityPage.7907h64p.js +3 -0
- package/dist/ApiDocsPage.k3jjenpq.js +4 -0
- package/dist/App.01nq20st.js +4 -0
- package/dist/App.1maqvamf.js +4 -0
- package/dist/App.2yjrh32f.js +4 -0
- package/dist/App.3qw8nben.js +20 -0
- package/dist/App.7fb3e7mp.js +4 -0
- package/dist/App.7sy3wq8c.js +4 -0
- package/dist/App.apjrmctz.js +57 -0
- package/dist/App.av6t2yhe.js +4 -0
- package/dist/App.jqj5a094.js +46 -0
- package/dist/App.mc7xf85h.js +4 -0
- package/dist/App.myxqcj9x.js +4 -0
- package/dist/App.nm91r1mp.js +13 -0
- package/dist/App.qcknavjz.js +221 -0
- package/dist/App.vc7vfhg4.js +4 -0
- package/dist/App.z4s9zkw5.js +4 -0
- package/dist/ConnectionsPage.z1pw5xe2.js +3 -0
- package/dist/McpPage.8vc97z0b.js +3 -0
- package/dist/SettingsPage.p61bz8kd.js +3 -0
- package/dist/SkillsPage.r9x43g3g.js +3 -0
- package/dist/TasksPage.1e0zkye4.js +3 -0
- package/dist/TelemetryPage.p9vbe4gf.js +3 -0
- package/dist/TestsPage.d4xy504e.js +3 -0
- package/dist/ThreadsPage.m016am3x.js +3 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +8 -7
- package/src/crypto.ts +4 -3
- package/src/db.ts +153 -28
- package/src/integrations/agentdojo.ts +94 -12
- package/src/integrations/index.ts +7 -0
- package/src/mcp-platform.ts +494 -121
- package/src/providers.ts +12 -12
- package/src/routes/api/agent-utils.ts +59 -46
- package/src/routes/api/agents.ts +52 -1
- package/src/routes/api/integrations.ts +11 -5
- package/src/routes/api/mcp.ts +5 -4
- package/src/routes/api/meta-agent.ts +35 -1
- package/src/routes/api/projects.ts +3 -3
- package/src/routes/api/providers.ts +121 -30
- package/src/routes/api/skills.ts +2 -3
- package/src/routes/api/system.ts +8 -13
- package/src/server.ts +31 -32
- package/src/triggers/agentdojo.ts +2 -2
- package/src/web/App.tsx +18 -10
- package/src/web/components/activity/ActivityPage.tsx +241 -388
- package/src/web/components/agents/AgentCard.tsx +5 -13
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/common/Select.tsx +4 -3
- package/src/web/components/dashboard/Dashboard.tsx +155 -30
- package/src/web/components/index.ts +1 -1
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/mcp/IntegrationsPanel.tsx +126 -35
- package/src/web/components/mcp/McpPage.tsx +10 -1
- package/src/web/components/meta-agent/MetaAgent.tsx +4 -2
- package/src/web/components/settings/SettingsPage.tsx +133 -48
- package/src/web/components/tasks/TasksPage.tsx +48 -16
- package/src/web/components/telemetry/TelemetryPage.tsx +184 -0
- package/src/web/components/threads/ThreadsPage.tsx +313 -0
- package/src/web/context/AuthContext.tsx +3 -3
- package/src/web/context/ProjectContext.tsx +3 -3
- package/src/web/context/TelemetryContext.tsx +24 -6
- package/src/web/context/index.ts +1 -1
- package/src/web/styles.css +20 -4
- package/src/web/types.ts +4 -3
- package/dist/ActivityPage.41nbye4r.js +0 -3
- package/dist/ApiDocsPage.4smnt8m3.js +0 -4
- package/dist/App.0sbax9et.js +0 -4
- package/dist/App.0ws427h8.js +0 -4
- package/dist/App.6q6bar8b.js +0 -4
- package/dist/App.80301vdb.js +0 -4
- package/dist/App.af2wg84v.js +0 -267
- package/dist/App.ca1rz1ph.js +0 -4
- package/dist/App.ensa6z0r.js +0 -4
- package/dist/App.f8g7tych.js +0 -13
- package/dist/App.mvtqv6qc.js +0 -20
- package/dist/App.ncgc9cxy.js +0 -4
- package/dist/App.p0fb1pds.js +0 -4
- package/dist/App.pmaq48sj.js +0 -4
- package/dist/App.yv87t9m5.js +0 -4
- package/dist/App.zjmfm8p6.js +0 -4
- package/dist/ConnectionsPage.anb3rv9a.js +0 -3
- package/dist/McpPage.y396h6fy.js +0 -3
- package/dist/SettingsPage.p1hc60gk.js +0 -3
- package/dist/SkillsPage.yj3xdsay.js +0 -3
- package/dist/TasksPage.sjv0khtv.js +0 -3
- package/dist/TelemetryPage.2qm4w16r.js +0 -3
- package/dist/TestsPage.zzs4qfj8.js +0 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
2
2
|
import { TasksIcon, CloseIcon, RecurringIcon, ScheduledIcon, TaskOnceIcon } from "../common/Icons";
|
|
3
|
+
import { Select } from "../common/Select";
|
|
3
4
|
import { useAuth, useProjects } from "../../context";
|
|
4
5
|
import { useTelemetry } from "../../context/TelemetryContext";
|
|
5
6
|
import type { Task, TaskTrajectoryStep, ToolUseBlock, ToolResultBlock } from "../../types";
|
|
@@ -14,6 +15,7 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
|
|
|
14
15
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
15
16
|
const [loading, setLoading] = useState(true);
|
|
16
17
|
const [filter, setFilter] = useState<string>("all");
|
|
18
|
+
const [agentFilter, setAgentFilter] = useState<string>("all");
|
|
17
19
|
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
|
18
20
|
const [loadingTask, setLoadingTask] = useState(false);
|
|
19
21
|
const lastProcessedEventRef = useRef<string | null>(null);
|
|
@@ -87,9 +89,23 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
|
|
|
87
89
|
}
|
|
88
90
|
}, [authFetch]);
|
|
89
91
|
|
|
92
|
+
// Extract unique agents from tasks for the agent filter
|
|
93
|
+
const uniqueAgents = useMemo(() => {
|
|
94
|
+
const map = new Map<string, string>();
|
|
95
|
+
for (const t of tasks) {
|
|
96
|
+
const id = t.agentId || (t as any).agent_id;
|
|
97
|
+
const name = t.agentName || (t as any).agent_name;
|
|
98
|
+
if (id && name && !map.has(id)) {
|
|
99
|
+
map.set(id, name);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return Array.from(map.entries()).sort((a, b) => a[1].localeCompare(b[1]));
|
|
103
|
+
}, [tasks]);
|
|
104
|
+
|
|
90
105
|
// Sort tasks: running first, then pending by next execution (soonest first), then completed/failed by date
|
|
91
106
|
const sortedTasks = useMemo(() => {
|
|
92
|
-
|
|
107
|
+
const filtered = agentFilter === "all" ? tasks : tasks.filter(t => (t.agentId || (t as any).agent_id) === agentFilter);
|
|
108
|
+
return [...filtered].sort((a, b) => {
|
|
93
109
|
// Running tasks first
|
|
94
110
|
if (a.status === "running" && b.status !== "running") return -1;
|
|
95
111
|
if (b.status === "running" && a.status !== "running") return 1;
|
|
@@ -111,7 +127,7 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
|
|
|
111
127
|
const bDate = b.completed_at || b.executed_at || b.created_at;
|
|
112
128
|
return new Date(bDate).getTime() - new Date(aDate).getTime();
|
|
113
129
|
});
|
|
114
|
-
}, [tasks]);
|
|
130
|
+
}, [tasks, agentFilter]);
|
|
115
131
|
|
|
116
132
|
const statusColors: Record<string, string> = {
|
|
117
133
|
pending: "bg-yellow-500/20 text-yellow-400",
|
|
@@ -141,20 +157,36 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
|
|
|
141
157
|
View tasks from all running agents
|
|
142
158
|
</p>
|
|
143
159
|
</div>
|
|
144
|
-
<div className="flex
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
<div className="flex items-center gap-3 flex-wrap pb-1">
|
|
161
|
+
<div className="flex gap-2 overflow-x-auto scrollbar-hide">
|
|
162
|
+
{filterOptions.map(opt => (
|
|
163
|
+
<button
|
|
164
|
+
key={opt.value}
|
|
165
|
+
onClick={() => setFilter(opt.value)}
|
|
166
|
+
className={`px-3 py-1.5 rounded text-sm transition whitespace-nowrap ${
|
|
167
|
+
filter === opt.value
|
|
168
|
+
? "bg-[#f97316] text-black"
|
|
169
|
+
: "bg-[#1a1a1a] hover:bg-[#222]"
|
|
170
|
+
}`}
|
|
171
|
+
>
|
|
172
|
+
{opt.label}
|
|
173
|
+
</button>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
{uniqueAgents.length > 0 && (
|
|
177
|
+
<div className="w-48">
|
|
178
|
+
<Select
|
|
179
|
+
value={agentFilter}
|
|
180
|
+
onChange={setAgentFilter}
|
|
181
|
+
placeholder="All agents"
|
|
182
|
+
compact
|
|
183
|
+
options={[
|
|
184
|
+
{ value: "all", label: "All agents" },
|
|
185
|
+
...uniqueAgents.map(([id, name]) => ({ value: id, label: name })),
|
|
186
|
+
]}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
158
190
|
</div>
|
|
159
191
|
</div>
|
|
160
192
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import React, { useState, useEffect, useMemo, useRef, useCallback } from "react";
|
|
2
2
|
import { Select } from "../common/Select";
|
|
3
3
|
import { useTelemetryContext, useProjects, useAuth, type TelemetryEvent } from "../../context";
|
|
4
|
+
import {
|
|
5
|
+
AreaChart, Area, BarChart, Bar,
|
|
6
|
+
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend,
|
|
7
|
+
} from "recharts";
|
|
4
8
|
|
|
5
9
|
interface TelemetryStats {
|
|
6
10
|
total_events: number;
|
|
@@ -20,6 +24,15 @@ interface UsageByAgent {
|
|
|
20
24
|
errors: number;
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
interface DailyUsage {
|
|
28
|
+
date: string;
|
|
29
|
+
input_tokens: number;
|
|
30
|
+
output_tokens: number;
|
|
31
|
+
llm_calls: number;
|
|
32
|
+
tool_calls: number;
|
|
33
|
+
errors: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
// Helper to extract stats from a single event
|
|
24
37
|
function extractEventStats(event: TelemetryEvent): {
|
|
25
38
|
llm_calls: number;
|
|
@@ -50,6 +63,7 @@ export function TelemetryPage() {
|
|
|
50
63
|
const [fetchedStats, setFetchedStats] = useState<TelemetryStats | null>(null);
|
|
51
64
|
const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
|
|
52
65
|
const [fetchedUsage, setFetchedUsage] = useState<UsageByAgent[]>([]);
|
|
66
|
+
const [dailyUsage, setDailyUsage] = useState<DailyUsage[]>([]);
|
|
53
67
|
const [loading, setLoading] = useState(true);
|
|
54
68
|
const [filter, setFilter] = useState({
|
|
55
69
|
level: "",
|
|
@@ -127,6 +141,18 @@ export function TelemetryPage() {
|
|
|
127
141
|
const usageRes = await authFetch(`/api/telemetry/usage?${usageParams}`);
|
|
128
142
|
const usageData = await usageRes.json();
|
|
129
143
|
setFetchedUsage(usageData.usage || []);
|
|
144
|
+
|
|
145
|
+
// Fetch daily usage for charts
|
|
146
|
+
const dailyParams = new URLSearchParams();
|
|
147
|
+
dailyParams.set("group_by", "day");
|
|
148
|
+
if (projectParam) dailyParams.set("project_id", projectParam);
|
|
149
|
+
const dailyRes = await authFetch(`/api/telemetry/usage?${dailyParams}`);
|
|
150
|
+
const dailyData = await dailyRes.json();
|
|
151
|
+
// Sort by date ascending for charts
|
|
152
|
+
const sorted = (dailyData.usage || []).sort((a: DailyUsage, b: DailyUsage) =>
|
|
153
|
+
a.date.localeCompare(b.date)
|
|
154
|
+
);
|
|
155
|
+
setDailyUsage(sorted);
|
|
130
156
|
} catch (e) {
|
|
131
157
|
console.error("Failed to fetch telemetry:", e);
|
|
132
158
|
}
|
|
@@ -371,6 +397,164 @@ export function TelemetryPage() {
|
|
|
371
397
|
</div>
|
|
372
398
|
)}
|
|
373
399
|
|
|
400
|
+
{/* Charts */}
|
|
401
|
+
{(() => {
|
|
402
|
+
// Use daily data if we have multiple days, otherwise aggregate events by hour
|
|
403
|
+
const useDaily = dailyUsage.length > 1;
|
|
404
|
+
const chartData = useDaily ? dailyUsage : (() => {
|
|
405
|
+
// Aggregate all visible events by hour
|
|
406
|
+
const buckets = new Map<string, { date: string; llm_calls: number; tool_calls: number; errors: number; input_tokens: number; output_tokens: number }>();
|
|
407
|
+
for (const event of allEvents) {
|
|
408
|
+
const d = new Date(event.timestamp);
|
|
409
|
+
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:00`;
|
|
410
|
+
if (!buckets.has(key)) {
|
|
411
|
+
buckets.set(key, { date: key, llm_calls: 0, tool_calls: 0, errors: 0, input_tokens: 0, output_tokens: 0 });
|
|
412
|
+
}
|
|
413
|
+
const b = buckets.get(key)!;
|
|
414
|
+
const s = extractEventStats(event);
|
|
415
|
+
b.llm_calls += s.llm_calls;
|
|
416
|
+
b.tool_calls += s.tool_calls;
|
|
417
|
+
b.errors += s.errors;
|
|
418
|
+
b.input_tokens += s.input_tokens;
|
|
419
|
+
b.output_tokens += s.output_tokens;
|
|
420
|
+
}
|
|
421
|
+
return Array.from(buckets.values()).sort((a, b) => a.date.localeCompare(b.date));
|
|
422
|
+
})();
|
|
423
|
+
const chartLabel = useDaily ? "Daily" : "Hourly";
|
|
424
|
+
|
|
425
|
+
if (chartData.length === 0) return null;
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
|
|
429
|
+
{/* Activity Chart */}
|
|
430
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4">
|
|
431
|
+
<h3 className="text-sm font-medium text-[#888] mb-4">{chartLabel} Activity</h3>
|
|
432
|
+
<ResponsiveContainer width="100%" height={200}>
|
|
433
|
+
<AreaChart data={chartData}>
|
|
434
|
+
<CartesianGrid strokeDasharray="3 3" stroke="#1a1a1a" />
|
|
435
|
+
<XAxis
|
|
436
|
+
dataKey="date"
|
|
437
|
+
stroke="#444"
|
|
438
|
+
tick={{ fill: "#666", fontSize: 11 }}
|
|
439
|
+
tickFormatter={(v) => {
|
|
440
|
+
if (!useDaily && v.includes(" ")) {
|
|
441
|
+
return v.split(" ")[1];
|
|
442
|
+
}
|
|
443
|
+
const d = new Date(v + "T00:00:00");
|
|
444
|
+
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
445
|
+
}}
|
|
446
|
+
/>
|
|
447
|
+
<YAxis stroke="#444" tick={{ fill: "#666", fontSize: 11 }} allowDecimals={false} />
|
|
448
|
+
<Tooltip
|
|
449
|
+
contentStyle={{
|
|
450
|
+
backgroundColor: "#111",
|
|
451
|
+
border: "1px solid #333",
|
|
452
|
+
borderRadius: "8px",
|
|
453
|
+
fontSize: 12,
|
|
454
|
+
}}
|
|
455
|
+
labelStyle={{ color: "#888" }}
|
|
456
|
+
cursor={{ stroke: "rgba(255,255,255,0.1)" }}
|
|
457
|
+
labelFormatter={(v) => useDaily ? new Date(v + "T00:00:00").toLocaleDateString() : v}
|
|
458
|
+
/>
|
|
459
|
+
<Legend
|
|
460
|
+
wrapperStyle={{ fontSize: 11 }}
|
|
461
|
+
iconType="circle"
|
|
462
|
+
iconSize={8}
|
|
463
|
+
/>
|
|
464
|
+
<Area
|
|
465
|
+
type="monotone"
|
|
466
|
+
dataKey="llm_calls"
|
|
467
|
+
name="LLM Calls"
|
|
468
|
+
stroke="#f97316"
|
|
469
|
+
fill="#f97316"
|
|
470
|
+
fillOpacity={0.15}
|
|
471
|
+
strokeWidth={1.5}
|
|
472
|
+
/>
|
|
473
|
+
<Area
|
|
474
|
+
type="monotone"
|
|
475
|
+
dataKey="tool_calls"
|
|
476
|
+
name="Tool Calls"
|
|
477
|
+
stroke="#fb923c"
|
|
478
|
+
fill="#fb923c"
|
|
479
|
+
fillOpacity={0.08}
|
|
480
|
+
strokeWidth={1.5}
|
|
481
|
+
/>
|
|
482
|
+
<Area
|
|
483
|
+
type="monotone"
|
|
484
|
+
dataKey="errors"
|
|
485
|
+
name="Errors"
|
|
486
|
+
stroke="#ef4444"
|
|
487
|
+
fill="#ef4444"
|
|
488
|
+
fillOpacity={0.1}
|
|
489
|
+
strokeWidth={1.5}
|
|
490
|
+
/>
|
|
491
|
+
</AreaChart>
|
|
492
|
+
</ResponsiveContainer>
|
|
493
|
+
</div>
|
|
494
|
+
|
|
495
|
+
{/* Token Usage Chart */}
|
|
496
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4">
|
|
497
|
+
<h3 className="text-sm font-medium text-[#888] mb-4">{chartLabel} Token Usage</h3>
|
|
498
|
+
<ResponsiveContainer width="100%" height={200}>
|
|
499
|
+
<BarChart data={chartData}>
|
|
500
|
+
<CartesianGrid strokeDasharray="3 3" stroke="#1a1a1a" />
|
|
501
|
+
<XAxis
|
|
502
|
+
dataKey="date"
|
|
503
|
+
stroke="#444"
|
|
504
|
+
tick={{ fill: "#666", fontSize: 11 }}
|
|
505
|
+
tickFormatter={(v) => {
|
|
506
|
+
if (!useDaily && v.includes(" ")) {
|
|
507
|
+
return v.split(" ")[1];
|
|
508
|
+
}
|
|
509
|
+
const d = new Date(v + "T00:00:00");
|
|
510
|
+
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
511
|
+
}}
|
|
512
|
+
/>
|
|
513
|
+
<YAxis
|
|
514
|
+
stroke="#444"
|
|
515
|
+
tick={{ fill: "#666", fontSize: 11 }}
|
|
516
|
+
tickFormatter={(v) => {
|
|
517
|
+
if (v >= 1000000) return `${(v / 1000000).toFixed(1)}M`;
|
|
518
|
+
if (v >= 1000) return `${(v / 1000).toFixed(0)}K`;
|
|
519
|
+
return v;
|
|
520
|
+
}}
|
|
521
|
+
/>
|
|
522
|
+
<Tooltip
|
|
523
|
+
contentStyle={{
|
|
524
|
+
backgroundColor: "#111",
|
|
525
|
+
border: "1px solid #333",
|
|
526
|
+
borderRadius: "8px",
|
|
527
|
+
fontSize: 12,
|
|
528
|
+
}}
|
|
529
|
+
labelStyle={{ color: "#888" }}
|
|
530
|
+
cursor={{ fill: "rgba(255,255,255,0.03)" }}
|
|
531
|
+
labelFormatter={(v) => useDaily ? new Date(v + "T00:00:00").toLocaleDateString() : v}
|
|
532
|
+
formatter={(value: number) => [value.toLocaleString(), undefined]}
|
|
533
|
+
/>
|
|
534
|
+
<Legend
|
|
535
|
+
wrapperStyle={{ fontSize: 11 }}
|
|
536
|
+
iconType="circle"
|
|
537
|
+
iconSize={8}
|
|
538
|
+
/>
|
|
539
|
+
<Bar
|
|
540
|
+
dataKey="input_tokens"
|
|
541
|
+
name="Input Tokens"
|
|
542
|
+
fill="#f97316"
|
|
543
|
+
radius={[2, 2, 0, 0]}
|
|
544
|
+
/>
|
|
545
|
+
<Bar
|
|
546
|
+
dataKey="output_tokens"
|
|
547
|
+
name="Output Tokens"
|
|
548
|
+
fill="#ea580c"
|
|
549
|
+
radius={[2, 2, 0, 0]}
|
|
550
|
+
/>
|
|
551
|
+
</BarChart>
|
|
552
|
+
</ResponsiveContainer>
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
);
|
|
556
|
+
})()}
|
|
557
|
+
|
|
374
558
|
{/* Usage by Agent */}
|
|
375
559
|
{usage.length > 0 && (
|
|
376
560
|
<div className="mb-6">
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
2
|
+
import { Chat, convertApiMessages } from "@apteva/apteva-kit";
|
|
3
|
+
import { useAgentActivity, useAuth, useProjects, useTelemetryContext } from "../../context";
|
|
4
|
+
import type { TelemetryEvent } from "../../context";
|
|
5
|
+
import type { Agent, Route } from "../../types";
|
|
6
|
+
|
|
7
|
+
interface Thread {
|
|
8
|
+
id: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
created_at: string;
|
|
11
|
+
updated_at: string;
|
|
12
|
+
message_count?: number;
|
|
13
|
+
agent_id: string;
|
|
14
|
+
agent_name: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ThreadsPageProps {
|
|
18
|
+
agents: Agent[];
|
|
19
|
+
onNavigate?: (route: Route) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ThreadsPage({ agents, onNavigate }: ThreadsPageProps) {
|
|
23
|
+
const { authFetch } = useAuth();
|
|
24
|
+
const { currentProjectId } = useProjects();
|
|
25
|
+
const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
|
|
26
|
+
|
|
27
|
+
const [threads, setThreads] = useState<Thread[]>([]);
|
|
28
|
+
const [selectedThread, setSelectedThread] = useState<Thread | null>(null);
|
|
29
|
+
const [newChatAgent, setNewChatAgent] = useState<Agent | null>(null);
|
|
30
|
+
const [initialMessages, setInitialMessages] = useState<any[]>([]);
|
|
31
|
+
const [loadingThreads, setLoadingThreads] = useState(true);
|
|
32
|
+
const [loadingMessages, setLoadingMessages] = useState(false);
|
|
33
|
+
const [historicalActivities, setHistoricalActivities] = useState<TelemetryEvent[]>([]);
|
|
34
|
+
const [showAgentPicker, setShowAgentPicker] = useState(false);
|
|
35
|
+
const [newChatKey, setNewChatKey] = useState(0);
|
|
36
|
+
|
|
37
|
+
const filteredAgents = useMemo(() => {
|
|
38
|
+
if (currentProjectId === null) return agents;
|
|
39
|
+
if (currentProjectId === "unassigned") return agents.filter(a => !a.projectId);
|
|
40
|
+
return agents.filter(a => a.projectId === currentProjectId);
|
|
41
|
+
}, [agents, currentProjectId]);
|
|
42
|
+
|
|
43
|
+
const runningAgents = useMemo(() => filteredAgents.filter(a => a.status === "running"), [filteredAgents]);
|
|
44
|
+
const agentIds = useMemo(() => new Set(filteredAgents.map(a => a.id)), [filteredAgents]);
|
|
45
|
+
|
|
46
|
+
// Fetch consolidated threads
|
|
47
|
+
const fetchThreads = useCallback(async () => {
|
|
48
|
+
try {
|
|
49
|
+
const projectParam = currentProjectId ? `?project_id=${encodeURIComponent(currentProjectId)}` : "";
|
|
50
|
+
const [threadsRes, activityRes] = await Promise.all([
|
|
51
|
+
authFetch(`/api/threads${projectParam}`).catch(() => null),
|
|
52
|
+
authFetch(`/api/telemetry/events?type=thread_activity&limit=100${projectParam ? `&${projectParam}` : ""}`).catch(() => null),
|
|
53
|
+
]);
|
|
54
|
+
if (threadsRes?.ok) {
|
|
55
|
+
const data = await threadsRes.json();
|
|
56
|
+
setThreads(data.threads || []);
|
|
57
|
+
}
|
|
58
|
+
if (activityRes?.ok) {
|
|
59
|
+
const data = await activityRes.json();
|
|
60
|
+
setHistoricalActivities(data.events || []);
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error("Failed to fetch threads:", e);
|
|
64
|
+
} finally {
|
|
65
|
+
setLoadingThreads(false);
|
|
66
|
+
}
|
|
67
|
+
}, [authFetch, currentProjectId]);
|
|
68
|
+
|
|
69
|
+
useEffect(() => { fetchThreads(); }, [fetchThreads, statusChangeCounter]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const interval = setInterval(fetchThreads, 15000);
|
|
73
|
+
return () => clearInterval(interval);
|
|
74
|
+
}, [fetchThreads]);
|
|
75
|
+
|
|
76
|
+
// Open an existing thread
|
|
77
|
+
const openThread = useCallback(async (thread: Thread) => {
|
|
78
|
+
setNewChatAgent(null);
|
|
79
|
+
setLoadingMessages(true);
|
|
80
|
+
setSelectedThread(thread);
|
|
81
|
+
try {
|
|
82
|
+
const res = await authFetch(`/api/agents/${thread.agent_id}/threads/${thread.id}/messages`);
|
|
83
|
+
if (res.ok) {
|
|
84
|
+
const data = await res.json();
|
|
85
|
+
setInitialMessages(convertApiMessages(data.messages || []));
|
|
86
|
+
} else {
|
|
87
|
+
setInitialMessages([]);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
setInitialMessages([]);
|
|
91
|
+
}
|
|
92
|
+
setLoadingMessages(false);
|
|
93
|
+
}, [authFetch]);
|
|
94
|
+
|
|
95
|
+
// Start a new conversation with an agent
|
|
96
|
+
const startNewChat = (agent: Agent) => {
|
|
97
|
+
setSelectedThread(null);
|
|
98
|
+
setInitialMessages([]);
|
|
99
|
+
setNewChatAgent(agent);
|
|
100
|
+
setNewChatKey(k => k + 1);
|
|
101
|
+
setShowAgentPicker(false);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Merge real-time + historical activity
|
|
105
|
+
const activities = useMemo(() => {
|
|
106
|
+
const realtimeThreadEvents = realtimeEvents.filter(e => e.type === "thread_activity" && !e.data?.parent_id);
|
|
107
|
+
const seen = new Set(realtimeThreadEvents.map(e => e.id));
|
|
108
|
+
const merged = [...realtimeThreadEvents];
|
|
109
|
+
for (const evt of historicalActivities) {
|
|
110
|
+
if (!seen.has(evt.id) && !evt.data?.parent_id) { merged.push(evt); seen.add(evt.id); }
|
|
111
|
+
}
|
|
112
|
+
return merged
|
|
113
|
+
.filter(e => agentIds.has(e.agent_id))
|
|
114
|
+
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
|
115
|
+
.slice(0, 100);
|
|
116
|
+
}, [realtimeEvents, historicalActivities, agentIds]);
|
|
117
|
+
|
|
118
|
+
// Group activities by thread_id
|
|
119
|
+
const activityByThread = useMemo(() => {
|
|
120
|
+
const map = new Map<string, TelemetryEvent[]>();
|
|
121
|
+
for (const evt of activities) {
|
|
122
|
+
const tid = evt.thread_id || evt.data?.thread_id as string;
|
|
123
|
+
if (tid) {
|
|
124
|
+
if (!map.has(tid)) map.set(tid, []);
|
|
125
|
+
map.get(tid)!.push(evt);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return map;
|
|
129
|
+
}, [activities]);
|
|
130
|
+
|
|
131
|
+
const runningCount = runningAgents.length;
|
|
132
|
+
|
|
133
|
+
// What's currently shown in chat
|
|
134
|
+
const chatAgentId = selectedThread?.agent_id || newChatAgent?.id;
|
|
135
|
+
const chatAgentName = selectedThread?.agent_name || newChatAgent?.name;
|
|
136
|
+
const chatThreadId = selectedThread?.id;
|
|
137
|
+
const chatKey = selectedThread
|
|
138
|
+
? `${selectedThread.agent_id}-${selectedThread.id}`
|
|
139
|
+
: newChatAgent
|
|
140
|
+
? `new-${newChatAgent.id}-${newChatKey}`
|
|
141
|
+
: null;
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
145
|
+
{/* Header */}
|
|
146
|
+
<div className="px-6 pt-6 pb-4 shrink-0">
|
|
147
|
+
<div className="flex items-center justify-between">
|
|
148
|
+
<h1 className="text-xl font-semibold">Threads</h1>
|
|
149
|
+
<span className="text-sm text-[#666]">
|
|
150
|
+
{threads.length} threads from {runningCount} running agents
|
|
151
|
+
</span>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Messenger layout: 1/4 threads | 3/4 chat */}
|
|
156
|
+
<div className="flex-1 flex min-h-0 overflow-hidden">
|
|
157
|
+
{/* Thread list — 1/4 */}
|
|
158
|
+
<div className="w-1/4 min-w-[260px] max-w-[360px] flex flex-col overflow-hidden">
|
|
159
|
+
{/* New conversation button */}
|
|
160
|
+
<div className="p-2 shrink-0">
|
|
161
|
+
<div className="relative">
|
|
162
|
+
<button
|
|
163
|
+
onClick={() => setShowAgentPicker(!showAgentPicker)}
|
|
164
|
+
disabled={runningAgents.length === 0}
|
|
165
|
+
className="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg bg-[#f97316]/10 text-[#f97316] text-sm font-medium hover:bg-[#f97316]/20 transition disabled:opacity-30 disabled:cursor-not-allowed"
|
|
166
|
+
>
|
|
167
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
168
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
169
|
+
</svg>
|
|
170
|
+
New conversation
|
|
171
|
+
</button>
|
|
172
|
+
|
|
173
|
+
{/* Agent picker dropdown */}
|
|
174
|
+
{showAgentPicker && (
|
|
175
|
+
<>
|
|
176
|
+
<div className="fixed inset-0 z-40" onClick={() => setShowAgentPicker(false)} />
|
|
177
|
+
<div className="absolute top-full left-0 right-0 mt-1 bg-[#111] border border-[#222] rounded-lg shadow-xl z-50 max-h-60 overflow-auto">
|
|
178
|
+
{runningAgents.map(agent => (
|
|
179
|
+
<button
|
|
180
|
+
key={agent.id}
|
|
181
|
+
onClick={() => startNewChat(agent)}
|
|
182
|
+
className="w-full text-left px-3 py-2.5 hover:bg-[#1a1a1a] transition"
|
|
183
|
+
>
|
|
184
|
+
<p className="text-sm font-medium truncate">{agent.name}</p>
|
|
185
|
+
<p className="text-[10px] text-[#555]">{agent.provider} · {agent.model}</p>
|
|
186
|
+
</button>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
189
|
+
</>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div className="flex-1 overflow-auto px-2 pb-2">
|
|
195
|
+
{loadingThreads ? (
|
|
196
|
+
<div className="p-6 text-center text-[#555] text-sm">Loading threads...</div>
|
|
197
|
+
) : threads.length === 0 ? (
|
|
198
|
+
<div className="p-6 text-center text-[#555] text-sm">
|
|
199
|
+
<p>No threads yet</p>
|
|
200
|
+
<p className="mt-1 text-[#444]">Start a conversation or wait for agents</p>
|
|
201
|
+
</div>
|
|
202
|
+
) : (
|
|
203
|
+
<div className="space-y-0.5">
|
|
204
|
+
{threads.map(thread => (
|
|
205
|
+
<ThreadRow
|
|
206
|
+
key={`${thread.agent_id}-${thread.id}`}
|
|
207
|
+
thread={thread}
|
|
208
|
+
selected={selectedThread?.id === thread.id && selectedThread?.agent_id === thread.agent_id}
|
|
209
|
+
activities={activityByThread.get(thread.id) || []}
|
|
210
|
+
onSelect={() => openThread(thread)}
|
|
211
|
+
/>
|
|
212
|
+
))}
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{/* Chat — 3/4 */}
|
|
219
|
+
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
|
220
|
+
{chatAgentId && chatKey ? (
|
|
221
|
+
loadingMessages ? (
|
|
222
|
+
<div className="flex-1 flex items-center justify-center text-[#666]">Loading messages...</div>
|
|
223
|
+
) : (
|
|
224
|
+
<Chat
|
|
225
|
+
key={chatKey}
|
|
226
|
+
agentId="default"
|
|
227
|
+
apiUrl={`/api/agents/${chatAgentId}`}
|
|
228
|
+
threadId={chatThreadId}
|
|
229
|
+
initialMessages={initialMessages}
|
|
230
|
+
placeholder={`Message ${chatAgentName}...`}
|
|
231
|
+
headerTitle={chatAgentName}
|
|
232
|
+
variant="terminal"
|
|
233
|
+
showHeader={true}
|
|
234
|
+
/>
|
|
235
|
+
)
|
|
236
|
+
) : (
|
|
237
|
+
<div className="flex-1 flex items-center justify-center">
|
|
238
|
+
<div className="text-center text-[#555]">
|
|
239
|
+
<svg className="w-12 h-12 mx-auto mb-3 text-[#333]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
240
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
|
241
|
+
</svg>
|
|
242
|
+
<p className="text-sm">Select a thread or start a new conversation</p>
|
|
243
|
+
<p className="text-xs text-[#444] mt-1">Chat with any running agent</p>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// --- Thread Row ---
|
|
254
|
+
|
|
255
|
+
function ThreadRow({ thread, selected, activities, onSelect }: {
|
|
256
|
+
thread: Thread;
|
|
257
|
+
selected: boolean;
|
|
258
|
+
activities: TelemetryEvent[];
|
|
259
|
+
onSelect: () => void;
|
|
260
|
+
}) {
|
|
261
|
+
const { isActive } = useAgentActivity(thread.agent_id);
|
|
262
|
+
const latestActivity = activities[0];
|
|
263
|
+
const activityText = latestActivity?.data?.activity as string | undefined;
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<button
|
|
267
|
+
onClick={onSelect}
|
|
268
|
+
className={`w-full text-left px-3 py-2.5 rounded-lg transition ${
|
|
269
|
+
selected
|
|
270
|
+
? "bg-[#f97316]/10"
|
|
271
|
+
: "hover:bg-[#151515]"
|
|
272
|
+
}`}
|
|
273
|
+
>
|
|
274
|
+
<div className="flex items-center justify-between gap-2 mb-1">
|
|
275
|
+
<span className="text-sm font-medium truncate">
|
|
276
|
+
{thread.title || `Thread ${thread.id.slice(0, 8)}`}
|
|
277
|
+
</span>
|
|
278
|
+
<span className="text-[10px] text-[#555] shrink-0">{timeAgo(thread.updated_at)}</span>
|
|
279
|
+
</div>
|
|
280
|
+
<div className="flex items-center gap-1.5">
|
|
281
|
+
<span
|
|
282
|
+
className={`w-1.5 h-1.5 rounded-full shrink-0 ${
|
|
283
|
+
isActive ? "bg-green-400 animate-pulse" : "bg-[#444]"
|
|
284
|
+
}`}
|
|
285
|
+
/>
|
|
286
|
+
<span className="text-[11px] text-[#f97316]">{thread.agent_name}</span>
|
|
287
|
+
{thread.message_count != null && (
|
|
288
|
+
<>
|
|
289
|
+
<span className="text-[#333]">·</span>
|
|
290
|
+
<span className="text-[10px] text-[#555]">{thread.message_count} msgs</span>
|
|
291
|
+
</>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
{activityText && (
|
|
295
|
+
<p className="text-[11px] text-[#555] truncate mt-1">{activityText}</p>
|
|
296
|
+
)}
|
|
297
|
+
</button>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// --- Helpers ---
|
|
302
|
+
|
|
303
|
+
function timeAgo(timestamp: string): string {
|
|
304
|
+
const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
|
|
305
|
+
if (seconds < 5) return "just now";
|
|
306
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
307
|
+
const minutes = Math.floor(seconds / 60);
|
|
308
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
309
|
+
const hours = Math.floor(minutes / 60);
|
|
310
|
+
if (hours < 24) return `${hours}h ago`;
|
|
311
|
+
const days = Math.floor(hours / 24);
|
|
312
|
+
return `${days}d ago`;
|
|
313
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { createContext, useContext, useState, useEffect, useCallback, useRef, type ReactNode } from "react";
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo, useRef, type ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
interface User {
|
|
4
4
|
id: string;
|
|
@@ -216,7 +216,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|
|
216
216
|
return () => clearInterval(refreshInterval);
|
|
217
217
|
}, [accessToken, refreshTokenInternal]);
|
|
218
218
|
|
|
219
|
-
const value
|
|
219
|
+
const value = useMemo<AuthContextValue>(() => ({
|
|
220
220
|
user,
|
|
221
221
|
isAuthenticated: !!user,
|
|
222
222
|
isLoading,
|
|
@@ -230,7 +230,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|
|
230
230
|
refreshToken,
|
|
231
231
|
checkAuth,
|
|
232
232
|
authFetch,
|
|
233
|
-
};
|
|
233
|
+
}), [user, isLoading, hasUsers, isDev, accessToken, onboardingComplete, setOnboardingComplete, login, logout, refreshToken, checkAuth, authFetch]);
|
|
234
234
|
|
|
235
235
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
236
236
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from "react";
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo, type ReactNode } from "react";
|
|
2
2
|
import { useAuth } from "./AuthContext";
|
|
3
3
|
|
|
4
4
|
export interface Project {
|
|
@@ -189,7 +189,7 @@ export function ProjectProvider({ children }: ProjectProviderProps) {
|
|
|
189
189
|
}
|
|
190
190
|
}, [authLoading, projectsEnabled, refreshProjects]);
|
|
191
191
|
|
|
192
|
-
const value
|
|
192
|
+
const value = useMemo<ProjectContextValue>(() => ({
|
|
193
193
|
projects,
|
|
194
194
|
currentProjectId,
|
|
195
195
|
currentProject,
|
|
@@ -203,7 +203,7 @@ export function ProjectProvider({ children }: ProjectProviderProps) {
|
|
|
203
203
|
updateProject,
|
|
204
204
|
deleteProject,
|
|
205
205
|
refreshProjects,
|
|
206
|
-
};
|
|
206
|
+
}), [projects, currentProjectId, currentProject, isLoading, error, unassignedCount, projectsEnabled, metaAgentEnabled, setCurrentProjectId, createProject, updateProject, deleteProject, refreshProjects]);
|
|
207
207
|
|
|
208
208
|
return <ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>;
|
|
209
209
|
}
|