apteva 0.4.57 → 0.7.1
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/README.md +216 -54
- package/cli.js +35 -0
- package/install.js +92 -0
- package/package.json +15 -76
- package/LICENSE +0 -63
- package/bin/apteva.js +0 -196
- package/dist/ActivityPage.kxzzb4yc.js +0 -3
- package/dist/ApiDocsPage.zq998hbm.js +0 -4
- package/dist/App.55rea8mn.js +0 -61
- package/dist/App.5ywb23z4.js +0 -53
- package/dist/App.6thds120.js +0 -4
- package/dist/App.9tctxzqm.js +0 -8
- package/dist/App.a8r8ttaz.js +0 -4
- package/dist/App.agsv5bje.js +0 -4
- package/dist/App.cepapqmx.js +0 -4
- package/dist/App.dp041gb3.js +0 -221
- package/dist/App.fds72zb5.js +0 -4
- package/dist/App.fg9qj2dq.js +0 -4
- package/dist/App.ndfejbm9.js +0 -4
- package/dist/App.nxmfmq1h.js +0 -13
- package/dist/App.qdfyt8ba.js +0 -4
- package/dist/App.x2d0ygt6.js +0 -4
- package/dist/App.yt9p4nr3.js +0 -20
- package/dist/App.zn4mw16t.js +0 -1
- package/dist/ConnectionsPage.8r96ryw7.js +0 -3
- package/dist/McpPage.3cwh0gnd.js +0 -3
- package/dist/SettingsPage.ykgdh5ev.js +0 -3
- package/dist/SkillsPage.4np1s65b.js +0 -3
- package/dist/TasksPage.4g08t7p6.js +0 -3
- package/dist/TelemetryPage.72w9pwcp.js +0 -3
- package/dist/TestsPage.z4fk3r7r.js +0 -3
- package/dist/ThreadsPage.63tcajeh.js +0 -3
- package/dist/apteva-kit.css +0 -1
- package/dist/icon.png +0 -0
- package/dist/index.html +0 -16
- package/dist/styles.css +0 -1
- package/scripts/postinstall.mjs +0 -102
- package/src/auth/index.ts +0 -394
- package/src/auth/middleware.ts +0 -213
- package/src/binary.ts +0 -536
- package/src/channels/index.ts +0 -40
- package/src/channels/telegram.ts +0 -311
- package/src/crypto.ts +0 -301
- package/src/db-tests.ts +0 -174
- package/src/db.ts +0 -3133
- package/src/integrations/agentdojo.ts +0 -559
- package/src/integrations/composio.ts +0 -437
- package/src/integrations/index.ts +0 -87
- package/src/integrations/skillsmp.ts +0 -318
- package/src/mcp-client.ts +0 -605
- package/src/mcp-handler.ts +0 -394
- package/src/mcp-platform.ts +0 -2403
- package/src/openapi.ts +0 -2410
- package/src/providers.ts +0 -597
- package/src/routes/api/agent-utils.ts +0 -890
- package/src/routes/api/agents.ts +0 -916
- package/src/routes/api/api-keys.ts +0 -95
- package/src/routes/api/channels.ts +0 -182
- package/src/routes/api/helpers.ts +0 -12
- package/src/routes/api/integrations.ts +0 -639
- package/src/routes/api/mcp.ts +0 -574
- package/src/routes/api/meta-agent.ts +0 -195
- package/src/routes/api/projects.ts +0 -112
- package/src/routes/api/providers.ts +0 -424
- package/src/routes/api/skills.ts +0 -537
- package/src/routes/api/system.ts +0 -333
- package/src/routes/api/telemetry.ts +0 -203
- package/src/routes/api/tests.ts +0 -148
- package/src/routes/api/triggers.ts +0 -518
- package/src/routes/api/users.ts +0 -148
- package/src/routes/api/webhooks.ts +0 -171
- package/src/routes/api.ts +0 -53
- package/src/routes/auth.ts +0 -251
- package/src/routes/share.ts +0 -86
- package/src/routes/static.ts +0 -131
- package/src/server.ts +0 -642
- package/src/test-runner.ts +0 -598
- package/src/triggers/agentdojo.ts +0 -253
- package/src/triggers/composio.ts +0 -264
- package/src/triggers/index.ts +0 -71
- package/src/tui/AgentList.tsx +0 -145
- package/src/tui/App.tsx +0 -102
- package/src/tui/Login.tsx +0 -104
- package/src/tui/api.ts +0 -72
- package/src/tui/index.tsx +0 -7
- package/src/web/App.tsx +0 -455
- package/src/web/components/activity/ActivityPage.tsx +0 -314
- package/src/web/components/activity/index.ts +0 -1
- package/src/web/components/agents/AgentCard.tsx +0 -189
- package/src/web/components/agents/AgentPanel.tsx +0 -2244
- package/src/web/components/agents/AgentsView.tsx +0 -180
- package/src/web/components/agents/CreateAgentModal.tsx +0 -475
- package/src/web/components/agents/index.ts +0 -4
- package/src/web/components/api/ApiDocsPage.tsx +0 -842
- package/src/web/components/auth/CreateAccountStep.tsx +0 -176
- package/src/web/components/auth/LoginPage.tsx +0 -91
- package/src/web/components/auth/index.ts +0 -2
- package/src/web/components/common/Icons.tsx +0 -250
- package/src/web/components/common/LoadingSpinner.tsx +0 -44
- package/src/web/components/common/Modal.tsx +0 -199
- package/src/web/components/common/Select.tsx +0 -97
- package/src/web/components/common/index.ts +0 -20
- package/src/web/components/connections/ConnectionsPage.tsx +0 -54
- package/src/web/components/connections/IntegrationsTab.tsx +0 -170
- package/src/web/components/connections/OverviewTab.tsx +0 -137
- package/src/web/components/connections/TriggersTab.tsx +0 -1346
- package/src/web/components/dashboard/Dashboard.tsx +0 -572
- package/src/web/components/dashboard/index.ts +0 -1
- package/src/web/components/index.ts +0 -21
- package/src/web/components/layout/ErrorBanner.tsx +0 -18
- package/src/web/components/layout/Header.tsx +0 -332
- package/src/web/components/layout/Sidebar.tsx +0 -231
- package/src/web/components/layout/index.ts +0 -3
- package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
- package/src/web/components/mcp/McpPage.tsx +0 -2515
- package/src/web/components/mcp/index.ts +0 -1
- package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
- package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
- package/src/web/components/onboarding/index.ts +0 -1
- package/src/web/components/settings/SettingsPage.tsx +0 -2776
- package/src/web/components/settings/index.ts +0 -1
- package/src/web/components/skills/SkillsPage.tsx +0 -1200
- package/src/web/components/tasks/TasksPage.tsx +0 -1116
- package/src/web/components/tasks/index.ts +0 -1
- package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
- package/src/web/components/tests/TestsPage.tsx +0 -594
- package/src/web/components/threads/ThreadsPage.tsx +0 -315
- package/src/web/context/AuthContext.tsx +0 -242
- package/src/web/context/ProjectContext.tsx +0 -214
- package/src/web/context/TelemetryContext.tsx +0 -299
- package/src/web/context/ThemeContext.tsx +0 -90
- package/src/web/context/UIModeContext.tsx +0 -49
- package/src/web/context/index.ts +0 -12
- package/src/web/hooks/index.ts +0 -3
- package/src/web/hooks/useAgents.ts +0 -115
- package/src/web/hooks/useOnboarding.ts +0 -20
- package/src/web/hooks/useProviders.ts +0 -75
- package/src/web/icon.png +0 -0
- package/src/web/index.html +0 -16
- package/src/web/styles.css +0 -118
- package/src/web/themes.ts +0 -162
- package/src/web/types.ts +0 -298
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
2
|
-
import { Chat, convertApiMessages } from "@apteva/apteva-kit";
|
|
3
|
-
import { useAgentActivity, useAuth, useProjects, useTelemetryContext, useTheme } 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 { theme } = useTheme();
|
|
24
|
-
const { authFetch } = useAuth();
|
|
25
|
-
const { currentProjectId } = useProjects();
|
|
26
|
-
const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
|
|
27
|
-
|
|
28
|
-
const [threads, setThreads] = useState<Thread[]>([]);
|
|
29
|
-
const [selectedThread, setSelectedThread] = useState<Thread | null>(null);
|
|
30
|
-
const [newChatAgent, setNewChatAgent] = useState<Agent | null>(null);
|
|
31
|
-
const [initialMessages, setInitialMessages] = useState<any[]>([]);
|
|
32
|
-
const [loadingThreads, setLoadingThreads] = useState(true);
|
|
33
|
-
const [loadingMessages, setLoadingMessages] = useState(false);
|
|
34
|
-
const [historicalActivities, setHistoricalActivities] = useState<TelemetryEvent[]>([]);
|
|
35
|
-
const [showAgentPicker, setShowAgentPicker] = useState(false);
|
|
36
|
-
const [newChatKey, setNewChatKey] = useState(0);
|
|
37
|
-
|
|
38
|
-
const filteredAgents = useMemo(() => {
|
|
39
|
-
if (currentProjectId === null) return agents;
|
|
40
|
-
if (currentProjectId === "unassigned") return agents.filter(a => !a.projectId);
|
|
41
|
-
return agents.filter(a => a.projectId === currentProjectId);
|
|
42
|
-
}, [agents, currentProjectId]);
|
|
43
|
-
|
|
44
|
-
const runningAgents = useMemo(() => filteredAgents.filter(a => a.status === "running"), [filteredAgents]);
|
|
45
|
-
const agentIds = useMemo(() => new Set(filteredAgents.map(a => a.id)), [filteredAgents]);
|
|
46
|
-
|
|
47
|
-
// Fetch consolidated threads
|
|
48
|
-
const fetchThreads = useCallback(async () => {
|
|
49
|
-
try {
|
|
50
|
-
const projectParam = currentProjectId ? `?project_id=${encodeURIComponent(currentProjectId)}` : "";
|
|
51
|
-
const [threadsRes, activityRes] = await Promise.all([
|
|
52
|
-
authFetch(`/api/threads${projectParam}`).catch(() => null),
|
|
53
|
-
authFetch(`/api/telemetry/events?type=thread_activity&limit=100${projectParam ? `&${projectParam}` : ""}`).catch(() => null),
|
|
54
|
-
]);
|
|
55
|
-
if (threadsRes?.ok) {
|
|
56
|
-
const data = await threadsRes.json();
|
|
57
|
-
setThreads(data.threads || []);
|
|
58
|
-
}
|
|
59
|
-
if (activityRes?.ok) {
|
|
60
|
-
const data = await activityRes.json();
|
|
61
|
-
setHistoricalActivities(data.events || []);
|
|
62
|
-
}
|
|
63
|
-
} catch (e) {
|
|
64
|
-
console.error("Failed to fetch threads:", e);
|
|
65
|
-
} finally {
|
|
66
|
-
setLoadingThreads(false);
|
|
67
|
-
}
|
|
68
|
-
}, [authFetch, currentProjectId]);
|
|
69
|
-
|
|
70
|
-
useEffect(() => { fetchThreads(); }, [fetchThreads, statusChangeCounter]);
|
|
71
|
-
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
const interval = setInterval(fetchThreads, 15000);
|
|
74
|
-
return () => clearInterval(interval);
|
|
75
|
-
}, [fetchThreads]);
|
|
76
|
-
|
|
77
|
-
// Open an existing thread
|
|
78
|
-
const openThread = useCallback(async (thread: Thread) => {
|
|
79
|
-
setNewChatAgent(null);
|
|
80
|
-
setLoadingMessages(true);
|
|
81
|
-
setSelectedThread(thread);
|
|
82
|
-
try {
|
|
83
|
-
const res = await authFetch(`/api/agents/${thread.agent_id}/threads/${thread.id}/messages`);
|
|
84
|
-
if (res.ok) {
|
|
85
|
-
const data = await res.json();
|
|
86
|
-
setInitialMessages(convertApiMessages(data.messages || []));
|
|
87
|
-
} else {
|
|
88
|
-
setInitialMessages([]);
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
setInitialMessages([]);
|
|
92
|
-
}
|
|
93
|
-
setLoadingMessages(false);
|
|
94
|
-
}, [authFetch]);
|
|
95
|
-
|
|
96
|
-
// Start a new conversation with an agent
|
|
97
|
-
const startNewChat = (agent: Agent) => {
|
|
98
|
-
setSelectedThread(null);
|
|
99
|
-
setInitialMessages([]);
|
|
100
|
-
setNewChatAgent(agent);
|
|
101
|
-
setNewChatKey(k => k + 1);
|
|
102
|
-
setShowAgentPicker(false);
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
// Merge real-time + historical activity
|
|
106
|
-
const activities = useMemo(() => {
|
|
107
|
-
const realtimeThreadEvents = realtimeEvents.filter(e => e.type === "thread_activity" && !e.data?.parent_id);
|
|
108
|
-
const seen = new Set(realtimeThreadEvents.map(e => e.id));
|
|
109
|
-
const merged = [...realtimeThreadEvents];
|
|
110
|
-
for (const evt of historicalActivities) {
|
|
111
|
-
if (!seen.has(evt.id) && !evt.data?.parent_id) { merged.push(evt); seen.add(evt.id); }
|
|
112
|
-
}
|
|
113
|
-
return merged
|
|
114
|
-
.filter(e => agentIds.has(e.agent_id))
|
|
115
|
-
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
|
116
|
-
.slice(0, 100);
|
|
117
|
-
}, [realtimeEvents, historicalActivities, agentIds]);
|
|
118
|
-
|
|
119
|
-
// Group activities by thread_id
|
|
120
|
-
const activityByThread = useMemo(() => {
|
|
121
|
-
const map = new Map<string, TelemetryEvent[]>();
|
|
122
|
-
for (const evt of activities) {
|
|
123
|
-
const tid = evt.thread_id || evt.data?.thread_id as string;
|
|
124
|
-
if (tid) {
|
|
125
|
-
if (!map.has(tid)) map.set(tid, []);
|
|
126
|
-
map.get(tid)!.push(evt);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return map;
|
|
130
|
-
}, [activities]);
|
|
131
|
-
|
|
132
|
-
const runningCount = runningAgents.length;
|
|
133
|
-
|
|
134
|
-
// What's currently shown in chat
|
|
135
|
-
const chatAgentId = selectedThread?.agent_id || newChatAgent?.id;
|
|
136
|
-
const chatAgentName = selectedThread?.agent_name || newChatAgent?.name;
|
|
137
|
-
const chatThreadId = selectedThread?.id;
|
|
138
|
-
const chatKey = selectedThread
|
|
139
|
-
? `${selectedThread.agent_id}-${selectedThread.id}`
|
|
140
|
-
: newChatAgent
|
|
141
|
-
? `new-${newChatAgent.id}-${newChatKey}`
|
|
142
|
-
: null;
|
|
143
|
-
|
|
144
|
-
return (
|
|
145
|
-
<div className="flex-1 flex flex-col overflow-hidden">
|
|
146
|
-
{/* Header */}
|
|
147
|
-
<div className="px-6 pt-6 pb-4 shrink-0">
|
|
148
|
-
<div className="flex items-center justify-between">
|
|
149
|
-
<h1 className="text-xl font-semibold">Threads</h1>
|
|
150
|
-
<span className="text-sm text-[var(--color-text-muted)]">
|
|
151
|
-
{threads.length} threads from {runningCount} running agents
|
|
152
|
-
</span>
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
|
|
156
|
-
{/* Messenger layout: 1/4 threads | 3/4 chat */}
|
|
157
|
-
<div className="flex-1 flex min-h-0 overflow-hidden">
|
|
158
|
-
{/* Thread list — 1/4 */}
|
|
159
|
-
<div className="w-1/4 min-w-[260px] max-w-[360px] flex flex-col overflow-hidden">
|
|
160
|
-
{/* New conversation button */}
|
|
161
|
-
<div className="p-2 shrink-0">
|
|
162
|
-
<div className="relative">
|
|
163
|
-
<button
|
|
164
|
-
onClick={() => setShowAgentPicker(!showAgentPicker)}
|
|
165
|
-
disabled={runningAgents.length === 0}
|
|
166
|
-
className="w-full flex items-center justify-center gap-2 px-3 py-2 btn bg-[var(--color-accent-10)] text-[var(--color-accent)] text-sm font-medium hover:bg-[var(--color-accent-20)] transition disabled:opacity-30 disabled:cursor-not-allowed"
|
|
167
|
-
>
|
|
168
|
-
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
169
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
170
|
-
</svg>
|
|
171
|
-
New conversation
|
|
172
|
-
</button>
|
|
173
|
-
|
|
174
|
-
{/* Agent picker dropdown */}
|
|
175
|
-
{showAgentPicker && (
|
|
176
|
-
<>
|
|
177
|
-
<div className="fixed inset-0 z-40" onClick={() => setShowAgentPicker(false)} />
|
|
178
|
-
<div className="absolute top-full left-0 right-0 mt-1 bg-[var(--color-surface)] card shadow-xl z-50 max-h-60 overflow-auto">
|
|
179
|
-
{runningAgents.map(agent => (
|
|
180
|
-
<button
|
|
181
|
-
key={agent.id}
|
|
182
|
-
onClick={() => startNewChat(agent)}
|
|
183
|
-
className="w-full text-left px-3 py-2.5 hover:bg-[var(--color-surface-raised)] transition"
|
|
184
|
-
>
|
|
185
|
-
<p className="text-sm font-medium truncate">{agent.name}</p>
|
|
186
|
-
<p className="text-[10px] text-[var(--color-text-faint)]">{agent.provider} · {agent.model}</p>
|
|
187
|
-
</button>
|
|
188
|
-
))}
|
|
189
|
-
</div>
|
|
190
|
-
</>
|
|
191
|
-
)}
|
|
192
|
-
</div>
|
|
193
|
-
</div>
|
|
194
|
-
|
|
195
|
-
<div className="flex-1 overflow-auto px-2 pb-2">
|
|
196
|
-
{loadingThreads ? (
|
|
197
|
-
<div className="p-6 text-center text-[var(--color-text-faint)] text-sm">Loading threads...</div>
|
|
198
|
-
) : threads.length === 0 ? (
|
|
199
|
-
<div className="p-6 text-center text-[var(--color-text-faint)] text-sm">
|
|
200
|
-
<p>No threads yet</p>
|
|
201
|
-
<p className="mt-1 text-[var(--color-text-faint)]">Start a conversation or wait for agents</p>
|
|
202
|
-
</div>
|
|
203
|
-
) : (
|
|
204
|
-
<div className="space-y-0.5">
|
|
205
|
-
{threads.map(thread => (
|
|
206
|
-
<ThreadRow
|
|
207
|
-
key={`${thread.agent_id}-${thread.id}`}
|
|
208
|
-
thread={thread}
|
|
209
|
-
selected={selectedThread?.id === thread.id && selectedThread?.agent_id === thread.agent_id}
|
|
210
|
-
activities={activityByThread.get(thread.id) || []}
|
|
211
|
-
onSelect={() => openThread(thread)}
|
|
212
|
-
/>
|
|
213
|
-
))}
|
|
214
|
-
</div>
|
|
215
|
-
)}
|
|
216
|
-
</div>
|
|
217
|
-
</div>
|
|
218
|
-
|
|
219
|
-
{/* Chat — 3/4 */}
|
|
220
|
-
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
|
221
|
-
{chatAgentId && chatKey ? (
|
|
222
|
-
loadingMessages ? (
|
|
223
|
-
<div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">Loading messages...</div>
|
|
224
|
-
) : (
|
|
225
|
-
<Chat
|
|
226
|
-
key={chatKey}
|
|
227
|
-
agentId="default"
|
|
228
|
-
apiUrl={`/api/agents/${chatAgentId}`}
|
|
229
|
-
threadId={chatThreadId}
|
|
230
|
-
initialMessages={initialMessages}
|
|
231
|
-
placeholder={`Message ${chatAgentName}...`}
|
|
232
|
-
headerTitle={chatAgentName}
|
|
233
|
-
variant="terminal"
|
|
234
|
-
theme={theme.id as "light" | "dark"}
|
|
235
|
-
showHeader={true}
|
|
236
|
-
/>
|
|
237
|
-
)
|
|
238
|
-
) : (
|
|
239
|
-
<div className="flex-1 flex items-center justify-center">
|
|
240
|
-
<div className="text-center text-[var(--color-text-faint)]">
|
|
241
|
-
<svg className="w-12 h-12 mx-auto mb-3 text-[var(--color-border-light)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
242
|
-
<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" />
|
|
243
|
-
</svg>
|
|
244
|
-
<p className="text-sm">Select a thread or start a new conversation</p>
|
|
245
|
-
<p className="text-xs text-[var(--color-text-faint)] mt-1">Chat with any running agent</p>
|
|
246
|
-
</div>
|
|
247
|
-
</div>
|
|
248
|
-
)}
|
|
249
|
-
</div>
|
|
250
|
-
</div>
|
|
251
|
-
</div>
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// --- Thread Row ---
|
|
256
|
-
|
|
257
|
-
function ThreadRow({ thread, selected, activities, onSelect }: {
|
|
258
|
-
thread: Thread;
|
|
259
|
-
selected: boolean;
|
|
260
|
-
activities: TelemetryEvent[];
|
|
261
|
-
onSelect: () => void;
|
|
262
|
-
}) {
|
|
263
|
-
const { isActive } = useAgentActivity(thread.agent_id);
|
|
264
|
-
const latestActivity = activities[0];
|
|
265
|
-
const activityText = latestActivity?.data?.activity as string | undefined;
|
|
266
|
-
|
|
267
|
-
return (
|
|
268
|
-
<button
|
|
269
|
-
onClick={onSelect}
|
|
270
|
-
className={`w-full text-left px-3 py-2.5 rounded-lg transition ${
|
|
271
|
-
selected
|
|
272
|
-
? "bg-[var(--color-accent-10)]"
|
|
273
|
-
: "hover:bg-[var(--color-bg-secondary)]"
|
|
274
|
-
}`}
|
|
275
|
-
>
|
|
276
|
-
<div className="flex items-center justify-between gap-2 mb-1">
|
|
277
|
-
<span className="text-sm font-medium truncate">
|
|
278
|
-
{thread.title || `Thread ${thread.id.slice(0, 8)}`}
|
|
279
|
-
</span>
|
|
280
|
-
<span className="text-[10px] text-[var(--color-text-faint)] shrink-0">{timeAgo(thread.updated_at)}</span>
|
|
281
|
-
</div>
|
|
282
|
-
<div className="flex items-center gap-1.5">
|
|
283
|
-
<span
|
|
284
|
-
className={`w-1.5 h-1.5 rounded-full shrink-0 ${
|
|
285
|
-
isActive ? "bg-green-400 animate-pulse" : "bg-[var(--color-scrollbar)]"
|
|
286
|
-
}`}
|
|
287
|
-
/>
|
|
288
|
-
<span className="text-[11px] text-[var(--color-accent)]">{thread.agent_name}</span>
|
|
289
|
-
{thread.message_count != null && (
|
|
290
|
-
<>
|
|
291
|
-
<span className="text-[var(--color-border-light)]">·</span>
|
|
292
|
-
<span className="text-[10px] text-[var(--color-text-faint)]">{thread.message_count} msgs</span>
|
|
293
|
-
</>
|
|
294
|
-
)}
|
|
295
|
-
</div>
|
|
296
|
-
{activityText && (
|
|
297
|
-
<p className="text-[11px] text-[var(--color-text-faint)] truncate mt-1">{activityText}</p>
|
|
298
|
-
)}
|
|
299
|
-
</button>
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// --- Helpers ---
|
|
304
|
-
|
|
305
|
-
function timeAgo(timestamp: string): string {
|
|
306
|
-
const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
|
|
307
|
-
if (seconds < 5) return "just now";
|
|
308
|
-
if (seconds < 60) return `${seconds}s ago`;
|
|
309
|
-
const minutes = Math.floor(seconds / 60);
|
|
310
|
-
if (minutes < 60) return `${minutes}m ago`;
|
|
311
|
-
const hours = Math.floor(minutes / 60);
|
|
312
|
-
if (hours < 24) return `${hours}h ago`;
|
|
313
|
-
const days = Math.floor(hours / 24);
|
|
314
|
-
return `${days}d ago`;
|
|
315
|
-
}
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo, useRef, type ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
interface User {
|
|
4
|
-
id: string;
|
|
5
|
-
username: string;
|
|
6
|
-
role: "admin" | "user";
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface AuthStatus {
|
|
10
|
-
hasUsers: boolean;
|
|
11
|
-
authenticated: boolean;
|
|
12
|
-
isDev: boolean;
|
|
13
|
-
user?: User;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface AuthContextValue {
|
|
17
|
-
user: User | null;
|
|
18
|
-
isAuthenticated: boolean;
|
|
19
|
-
isLoading: boolean;
|
|
20
|
-
hasUsers: boolean | null;
|
|
21
|
-
isDev: boolean;
|
|
22
|
-
accessToken: string | null;
|
|
23
|
-
onboardingComplete: boolean | null;
|
|
24
|
-
setOnboardingComplete: (v: boolean) => void;
|
|
25
|
-
login: (username: string, password: string) => Promise<{ success: boolean; error?: string }>;
|
|
26
|
-
logout: () => Promise<void>;
|
|
27
|
-
refreshToken: () => Promise<boolean>;
|
|
28
|
-
checkAuth: () => Promise<void>;
|
|
29
|
-
authFetch: (url: string, options?: RequestInit) => Promise<Response>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
33
|
-
|
|
34
|
-
export function useAuth(): AuthContextValue {
|
|
35
|
-
const context = useContext(AuthContext);
|
|
36
|
-
if (!context) {
|
|
37
|
-
throw new Error("useAuth must be used within an AuthProvider");
|
|
38
|
-
}
|
|
39
|
-
return context;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface AuthProviderProps {
|
|
43
|
-
children: ReactNode;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function AuthProvider({ children }: AuthProviderProps) {
|
|
47
|
-
const [user, setUser] = useState<User | null>(null);
|
|
48
|
-
const [accessToken, setAccessToken] = useState<string | null>(null);
|
|
49
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
50
|
-
const [hasUsers, setHasUsers] = useState<boolean | null>(null);
|
|
51
|
-
const [isDev, setIsDev] = useState(false);
|
|
52
|
-
const [onboardingComplete, setOnboardingComplete] = useState<boolean | null>(null);
|
|
53
|
-
|
|
54
|
-
// Refs to track state without causing re-renders
|
|
55
|
-
const tokenRef = useRef<string | null>(null);
|
|
56
|
-
const refreshingRef = useRef(false);
|
|
57
|
-
const initializedRef = useRef(false);
|
|
58
|
-
|
|
59
|
-
// Helper to set token in both state and ref
|
|
60
|
-
const updateToken = useCallback((token: string | null) => {
|
|
61
|
-
tokenRef.current = token;
|
|
62
|
-
setAccessToken(token);
|
|
63
|
-
}, []);
|
|
64
|
-
|
|
65
|
-
// Internal refresh function - prevents concurrent refreshes
|
|
66
|
-
const refreshTokenInternal = useCallback(async (): Promise<boolean> => {
|
|
67
|
-
// Prevent concurrent refresh calls
|
|
68
|
-
if (refreshingRef.current) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
refreshingRef.current = true;
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const res = await fetch("/api/auth/refresh", {
|
|
75
|
-
method: "POST",
|
|
76
|
-
credentials: "include",
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
if (!res.ok) {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const data = await res.json();
|
|
84
|
-
updateToken(data.accessToken);
|
|
85
|
-
|
|
86
|
-
// User info + onboarding included in refresh response to avoid extra round trip
|
|
87
|
-
if (data.user) {
|
|
88
|
-
setUser(data.user);
|
|
89
|
-
}
|
|
90
|
-
if (data.onboarding) {
|
|
91
|
-
setOnboardingComplete(data.onboarding.completed || data.onboarding.has_any_keys);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return !!data.user;
|
|
95
|
-
} catch (e) {
|
|
96
|
-
console.error("Token refresh failed:", e);
|
|
97
|
-
return false;
|
|
98
|
-
} finally {
|
|
99
|
-
refreshingRef.current = false;
|
|
100
|
-
}
|
|
101
|
-
}, [updateToken]);
|
|
102
|
-
|
|
103
|
-
// Check auth status
|
|
104
|
-
const checkAuth = useCallback(async () => {
|
|
105
|
-
try {
|
|
106
|
-
const token = tokenRef.current;
|
|
107
|
-
const res = await fetch("/api/auth/check", {
|
|
108
|
-
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
109
|
-
});
|
|
110
|
-
const data: AuthStatus & { onboarding?: { completed: boolean; has_any_keys: boolean } } = await res.json();
|
|
111
|
-
|
|
112
|
-
setHasUsers(data.hasUsers);
|
|
113
|
-
setIsDev(data.isDev ?? false);
|
|
114
|
-
|
|
115
|
-
// Extract onboarding status (piggybacks on auth check to avoid extra round trip)
|
|
116
|
-
if (data.onboarding) {
|
|
117
|
-
setOnboardingComplete(data.onboarding.completed || data.onboarding.has_any_keys);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (data.authenticated && data.user) {
|
|
121
|
-
setUser(data.user as User);
|
|
122
|
-
} else {
|
|
123
|
-
setUser(null);
|
|
124
|
-
// Try to refresh if we have users (meaning there might be a cookie)
|
|
125
|
-
if (data.hasUsers) {
|
|
126
|
-
const refreshed = await refreshTokenInternal();
|
|
127
|
-
if (!refreshed) {
|
|
128
|
-
updateToken(null);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
} catch (e) {
|
|
133
|
-
console.error("Auth check failed:", e);
|
|
134
|
-
setUser(null);
|
|
135
|
-
updateToken(null);
|
|
136
|
-
} finally {
|
|
137
|
-
setIsLoading(false);
|
|
138
|
-
}
|
|
139
|
-
}, [refreshTokenInternal, updateToken]);
|
|
140
|
-
|
|
141
|
-
// Login
|
|
142
|
-
const login = useCallback(async (username: string, password: string): Promise<{ success: boolean; error?: string }> => {
|
|
143
|
-
try {
|
|
144
|
-
const res = await fetch("/api/auth/login", {
|
|
145
|
-
method: "POST",
|
|
146
|
-
headers: { "Content-Type": "application/json" },
|
|
147
|
-
credentials: "include",
|
|
148
|
-
body: JSON.stringify({ username, password }),
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const data = await res.json();
|
|
152
|
-
|
|
153
|
-
if (!res.ok) {
|
|
154
|
-
return { success: false, error: data.error || "Login failed" };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
updateToken(data.accessToken);
|
|
158
|
-
setUser(data.user);
|
|
159
|
-
setHasUsers(true);
|
|
160
|
-
|
|
161
|
-
return { success: true };
|
|
162
|
-
} catch (e) {
|
|
163
|
-
console.error("Login failed:", e);
|
|
164
|
-
return { success: false, error: "Login failed" };
|
|
165
|
-
}
|
|
166
|
-
}, [updateToken]);
|
|
167
|
-
|
|
168
|
-
// Logout
|
|
169
|
-
const logout = useCallback(async () => {
|
|
170
|
-
try {
|
|
171
|
-
const token = tokenRef.current;
|
|
172
|
-
await fetch("/api/auth/logout", {
|
|
173
|
-
method: "POST",
|
|
174
|
-
credentials: "include",
|
|
175
|
-
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
176
|
-
});
|
|
177
|
-
} catch (e) {
|
|
178
|
-
console.error("Logout failed:", e);
|
|
179
|
-
} finally {
|
|
180
|
-
setUser(null);
|
|
181
|
-
updateToken(null);
|
|
182
|
-
}
|
|
183
|
-
}, [updateToken]);
|
|
184
|
-
|
|
185
|
-
// Authenticated fetch wrapper - uses ref for latest token
|
|
186
|
-
const authFetch = useCallback(async (url: string, options: RequestInit = {}): Promise<Response> => {
|
|
187
|
-
const headers = new Headers(options.headers);
|
|
188
|
-
const token = tokenRef.current;
|
|
189
|
-
if (token) {
|
|
190
|
-
headers.set("Authorization", `Bearer ${token}`);
|
|
191
|
-
}
|
|
192
|
-
return fetch(url, { ...options, headers });
|
|
193
|
-
}, []);
|
|
194
|
-
|
|
195
|
-
// Public refresh function
|
|
196
|
-
const refreshToken = useCallback(async (): Promise<boolean> => {
|
|
197
|
-
return refreshTokenInternal();
|
|
198
|
-
}, [refreshTokenInternal]);
|
|
199
|
-
|
|
200
|
-
// Check auth on mount - only once
|
|
201
|
-
useEffect(() => {
|
|
202
|
-
if (initializedRef.current) return;
|
|
203
|
-
initializedRef.current = true;
|
|
204
|
-
checkAuth();
|
|
205
|
-
}, [checkAuth]);
|
|
206
|
-
|
|
207
|
-
// Set up token refresh interval
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
if (!accessToken) return;
|
|
210
|
-
|
|
211
|
-
// Refresh token 1 minute before expiry (tokens last 15 min)
|
|
212
|
-
const refreshInterval = setInterval(() => {
|
|
213
|
-
refreshTokenInternal();
|
|
214
|
-
}, 14 * 60 * 1000); // 14 minutes
|
|
215
|
-
|
|
216
|
-
return () => clearInterval(refreshInterval);
|
|
217
|
-
}, [accessToken, refreshTokenInternal]);
|
|
218
|
-
|
|
219
|
-
const value = useMemo<AuthContextValue>(() => ({
|
|
220
|
-
user,
|
|
221
|
-
isAuthenticated: !!user,
|
|
222
|
-
isLoading,
|
|
223
|
-
hasUsers,
|
|
224
|
-
isDev,
|
|
225
|
-
accessToken,
|
|
226
|
-
onboardingComplete,
|
|
227
|
-
setOnboardingComplete,
|
|
228
|
-
login,
|
|
229
|
-
logout,
|
|
230
|
-
refreshToken,
|
|
231
|
-
checkAuth,
|
|
232
|
-
authFetch,
|
|
233
|
-
}), [user, isLoading, hasUsers, isDev, accessToken, onboardingComplete, setOnboardingComplete, login, logout, refreshToken, checkAuth, authFetch]);
|
|
234
|
-
|
|
235
|
-
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Hook to get auth headers for API calls
|
|
239
|
-
export function useAuthHeaders(): Record<string, string> {
|
|
240
|
-
const { accessToken } = useAuth();
|
|
241
|
-
return accessToken ? { Authorization: `Bearer ${accessToken}` } : {};
|
|
242
|
-
}
|