@agentforge-ai/cli 0.5.4 → 0.6.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 +197 -0
- package/dist/default/agentforge.config.ts +126 -6
- package/dist/default/convex/agents.ts +15 -21
- package/dist/default/convex/chat.ts +302 -0
- package/dist/default/convex/mastraIntegration.ts +101 -69
- package/dist/default/dashboard/app/routes/chat.tsx +462 -167
- package/dist/default/skills/browser-automation/SKILL.md +137 -0
- package/dist/default/skills/browser-automation/config.json +11 -0
- package/dist/default/skills/browser-automation/index.ts +93 -0
- package/dist/default/skills/skill-creator/SKILL.md +69 -230
- package/dist/index.js +2455 -290
- package/dist/index.js.map +1 -1
- package/package.json +13 -12
- package/templates/default/agentforge.config.ts +126 -6
- package/templates/default/convex/agents.ts +15 -21
- package/templates/default/convex/chat.ts +302 -0
- package/templates/default/convex/mastraIntegration.ts +101 -69
- package/templates/default/dashboard/app/routes/chat.tsx +462 -167
- package/templates/default/skills/browser-automation/SKILL.md +137 -0
- package/templates/default/skills/browser-automation/config.json +11 -0
- package/templates/default/skills/browser-automation/index.ts +93 -0
- package/templates/default/skills/skill-creator/SKILL.md +69 -230
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
import { createFileRoute } from
|
|
2
|
-
import { DashboardLayout } from
|
|
3
|
-
import { useState, useEffect, useRef, useCallback } from
|
|
4
|
-
import { useQuery, useMutation } from
|
|
5
|
-
import { api } from
|
|
6
|
-
import {
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { DashboardLayout } from "../components/DashboardLayout";
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
|
+
import { useQuery, useMutation, useAction } from "convex/react";
|
|
5
|
+
import { api } from "../../convex/_generated/api";
|
|
6
|
+
import {
|
|
7
|
+
Send,
|
|
8
|
+
Plus,
|
|
9
|
+
Bot,
|
|
10
|
+
User,
|
|
11
|
+
Shield,
|
|
12
|
+
ShieldAlert,
|
|
13
|
+
Lock,
|
|
14
|
+
AlertTriangle,
|
|
15
|
+
Paperclip,
|
|
16
|
+
Mic,
|
|
17
|
+
Settings2,
|
|
18
|
+
Loader2,
|
|
19
|
+
MessageSquare,
|
|
20
|
+
Trash2,
|
|
21
|
+
} from "lucide-react";
|
|
7
22
|
|
|
8
23
|
// ============================================================
|
|
9
|
-
// SECRET DETECTION ENGINE
|
|
24
|
+
// SECRET DETECTION ENGINE (client-side mirror of vault patterns)
|
|
10
25
|
// ============================================================
|
|
11
26
|
const SECRET_PATTERNS = [
|
|
12
27
|
{ pattern: /sk-[a-zA-Z0-9]{20,}/g, category: "api_key", provider: "openai", name: "OpenAI API Key" },
|
|
@@ -15,12 +30,22 @@ const SECRET_PATTERNS = [
|
|
|
15
30
|
{ pattern: /AIza[a-zA-Z0-9_-]{35}/g, category: "api_key", provider: "google", name: "Google API Key" },
|
|
16
31
|
{ pattern: /xai-[a-zA-Z0-9]{20,}/g, category: "api_key", provider: "xai", name: "xAI API Key" },
|
|
17
32
|
{ pattern: /ghp_[a-zA-Z0-9]{36}/g, category: "token", provider: "github", name: "GitHub Token" },
|
|
33
|
+
{ pattern: /gho_[a-zA-Z0-9]{36}/g, category: "token", provider: "github", name: "GitHub OAuth" },
|
|
34
|
+
{ pattern: /glpat-[a-zA-Z0-9_-]{20,}/g, category: "token", provider: "gitlab", name: "GitLab Token" },
|
|
35
|
+
{ pattern: /xoxb-[a-zA-Z0-9-]+/g, category: "token", provider: "slack", name: "Slack Bot Token" },
|
|
36
|
+
{ pattern: /xoxp-[a-zA-Z0-9-]+/g, category: "token", provider: "slack", name: "Slack User Token" },
|
|
18
37
|
{ pattern: /AKIA[A-Z0-9]{16}/g, category: "credential", provider: "aws", name: "AWS Access Key" },
|
|
19
38
|
{ pattern: /sk_live_[a-zA-Z0-9]{24,}/g, category: "api_key", provider: "stripe", name: "Stripe Live Key" },
|
|
20
39
|
{ pattern: /sk_test_[a-zA-Z0-9]{24,}/g, category: "api_key", provider: "stripe", name: "Stripe Test Key" },
|
|
40
|
+
{ pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g, category: "api_key", provider: "sendgrid", name: "SendGrid Key" },
|
|
21
41
|
];
|
|
22
42
|
|
|
23
|
-
interface DetectedSecret {
|
|
43
|
+
interface DetectedSecret {
|
|
44
|
+
match: string;
|
|
45
|
+
category: string;
|
|
46
|
+
provider: string;
|
|
47
|
+
name: string;
|
|
48
|
+
}
|
|
24
49
|
|
|
25
50
|
function detectSecrets(text: string): DetectedSecret[] {
|
|
26
51
|
const detected: DetectedSecret[] = [];
|
|
@@ -42,87 +67,157 @@ function maskSecret(value: string): string {
|
|
|
42
67
|
function censorText(text: string, secrets: DetectedSecret[]): string {
|
|
43
68
|
let censored = text;
|
|
44
69
|
for (const secret of secrets) {
|
|
45
|
-
censored = censored.replace(secret.match, `[
|
|
70
|
+
censored = censored.replace(secret.match, `[${secret.name}: ${maskSecret(secret.match)}]`);
|
|
46
71
|
}
|
|
47
72
|
return censored;
|
|
48
73
|
}
|
|
49
74
|
|
|
50
|
-
|
|
75
|
+
// ============================================================
|
|
76
|
+
// Chat Page Component
|
|
77
|
+
// ============================================================
|
|
78
|
+
export const Route = createFileRoute("/chat")({ component: ChatPageComponent });
|
|
51
79
|
|
|
52
80
|
function ChatPageComponent() {
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const addMessage = useMutation(api.messages.add);
|
|
57
|
-
const storeSecret = useMutation(api.vault.store);
|
|
81
|
+
// ── Convex queries ──────────────────────────────────────────
|
|
82
|
+
const agents = useQuery(api.agents.listActive, {}) ?? [];
|
|
83
|
+
const threads = useQuery(api.chat.listThreads, {}) ?? [];
|
|
58
84
|
|
|
85
|
+
// ── Convex mutations & actions ──────────────────────────────
|
|
86
|
+
const createThread = useMutation(api.chat.createThread);
|
|
87
|
+
const sendMessageAction = useAction(api.chat.sendMessage);
|
|
88
|
+
|
|
89
|
+
// ── Local state ─────────────────────────────────────────────
|
|
59
90
|
const [currentThreadId, setCurrentThreadId] = useState<string | null>(null);
|
|
60
91
|
const [currentAgentId, setCurrentAgentId] = useState<string | null>(null);
|
|
61
|
-
const [input, setInput] = useState(
|
|
62
|
-
const [
|
|
92
|
+
const [input, setInput] = useState("");
|
|
93
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
63
94
|
const [secretWarning, setSecretWarning] = useState<DetectedSecret[] | null>(null);
|
|
64
95
|
const [pendingMessage, setPendingMessage] = useState<string | null>(null);
|
|
96
|
+
const [vaultNotification, setVaultNotification] = useState<string | null>(null);
|
|
97
|
+
const [error, setError] = useState<string | null>(null);
|
|
98
|
+
|
|
65
99
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
66
100
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
67
101
|
|
|
68
|
-
//
|
|
69
|
-
|
|
102
|
+
// ── Subscribe to messages for the current thread ────────────
|
|
103
|
+
// This is the real-time subscription — messages update automatically
|
|
104
|
+
// when new ones are inserted by the Convex action.
|
|
105
|
+
const messages = useQuery(
|
|
106
|
+
api.chat.getThreadMessages,
|
|
107
|
+
currentThreadId ? { threadId: currentThreadId as any } : "skip"
|
|
108
|
+
) ?? [];
|
|
70
109
|
|
|
71
|
-
// Auto-select first agent
|
|
110
|
+
// ── Auto-select first agent ─────────────────────────────────
|
|
72
111
|
useEffect(() => {
|
|
73
112
|
if (agents.length > 0 && !currentAgentId) {
|
|
74
113
|
setCurrentAgentId(agents[0].id);
|
|
75
114
|
}
|
|
76
115
|
}, [agents, currentAgentId]);
|
|
77
116
|
|
|
78
|
-
// Auto-
|
|
117
|
+
// ── Auto-select first thread or none ────────────────────────
|
|
79
118
|
useEffect(() => {
|
|
80
|
-
|
|
81
|
-
|
|
119
|
+
if (threads.length > 0 && !currentThreadId) {
|
|
120
|
+
setCurrentThreadId(threads[0]._id);
|
|
121
|
+
}
|
|
122
|
+
}, [threads, currentThreadId]);
|
|
82
123
|
|
|
83
|
-
|
|
124
|
+
// ── Auto-scroll to bottom ───────────────────────────────────
|
|
125
|
+
const scrollToBottom = () => {
|
|
126
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
127
|
+
};
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
scrollToBottom();
|
|
130
|
+
}, [messages, isGenerating]);
|
|
84
131
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (!input.trim()) return;
|
|
132
|
+
// ── Secret detection as user types ──────────────────────────
|
|
133
|
+
const inputHasSecrets = input.length > 8 ? detectSecrets(input) : [];
|
|
88
134
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
135
|
+
// ── Create new thread ───────────────────────────────────────
|
|
136
|
+
const handleNewThread = useCallback(async () => {
|
|
137
|
+
if (!currentAgentId) {
|
|
138
|
+
setError("Please select an agent first.");
|
|
93
139
|
return;
|
|
94
140
|
}
|
|
141
|
+
try {
|
|
142
|
+
const agent = agents.find((a) => a.id === currentAgentId);
|
|
143
|
+
const threadId = await createThread({
|
|
144
|
+
agentId: currentAgentId,
|
|
145
|
+
name: `Chat with ${agent?.name || "Agent"}`,
|
|
146
|
+
});
|
|
147
|
+
setCurrentThreadId(threadId);
|
|
148
|
+
setError(null);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
setError(`Failed to create thread: ${e instanceof Error ? e.message : String(e)}`);
|
|
151
|
+
}
|
|
152
|
+
}, [currentAgentId, agents, createThread]);
|
|
95
153
|
|
|
96
|
-
|
|
97
|
-
|
|
154
|
+
// ── Send message ────────────────────────────────────────────
|
|
155
|
+
const handleSendMessage = useCallback(
|
|
156
|
+
async (e: React.FormEvent) => {
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
if (!input.trim() || !currentAgentId) return;
|
|
98
159
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
160
|
+
// Check for secrets
|
|
161
|
+
const detectedSecrets = detectSecrets(input);
|
|
162
|
+
if (detectedSecrets.length > 0) {
|
|
163
|
+
setSecretWarning(detectedSecrets);
|
|
164
|
+
setPendingMessage(input);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await sendMessageToChat(input);
|
|
169
|
+
},
|
|
170
|
+
[input, currentAgentId, currentThreadId]
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const sendMessageToChat = async (text: string, secrets?: DetectedSecret[]) => {
|
|
174
|
+
if (!currentAgentId) return;
|
|
175
|
+
|
|
176
|
+
let messageText = text;
|
|
177
|
+
if (secrets && secrets.length > 0) {
|
|
178
|
+
messageText = censorText(text, secrets);
|
|
179
|
+
setVaultNotification(
|
|
180
|
+
`${secrets.length} secret${secrets.length > 1 ? "s" : ""} detected and redacted.`
|
|
181
|
+
);
|
|
182
|
+
setTimeout(() => setVaultNotification(null), 5000);
|
|
104
183
|
}
|
|
105
|
-
if (!threadId) return;
|
|
106
184
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
185
|
+
setInput("");
|
|
186
|
+
setIsGenerating(true);
|
|
187
|
+
setError(null);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
// If no thread exists, create one first
|
|
191
|
+
let threadId = currentThreadId;
|
|
192
|
+
if (!threadId) {
|
|
193
|
+
const agent = agents.find((a) => a.id === currentAgentId);
|
|
194
|
+
threadId = await createThread({
|
|
195
|
+
agentId: currentAgentId,
|
|
196
|
+
name: `Chat with ${agent?.name || "Agent"}`,
|
|
197
|
+
});
|
|
198
|
+
setCurrentThreadId(threadId);
|
|
199
|
+
}
|
|
110
200
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
201
|
+
// Call the Convex action — this stores user message, calls LLM,
|
|
202
|
+
// and stores assistant response. The useQuery subscription above
|
|
203
|
+
// will automatically pick up both new messages in real-time.
|
|
204
|
+
await sendMessageAction({
|
|
205
|
+
agentId: currentAgentId,
|
|
206
|
+
threadId: threadId as any,
|
|
207
|
+
content: messageText,
|
|
208
|
+
});
|
|
209
|
+
} catch (e) {
|
|
210
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
211
|
+
setError(`Failed to send message: ${msg}`);
|
|
212
|
+
} finally {
|
|
213
|
+
setIsGenerating(false);
|
|
214
|
+
}
|
|
116
215
|
};
|
|
117
216
|
|
|
118
|
-
const handleConfirmSendWithSecrets =
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
for (const secret of secretWarning) {
|
|
122
|
-
await storeSecret({ name: secret.name, category: secret.category, provider: secret.provider, value: secret.match });
|
|
217
|
+
const handleConfirmSendWithSecrets = () => {
|
|
218
|
+
if (pendingMessage && secretWarning) {
|
|
219
|
+
sendMessageToChat(pendingMessage, secretWarning);
|
|
123
220
|
}
|
|
124
|
-
const censored = censorText(pendingMessage, secretWarning);
|
|
125
|
-
await sendMessage(censored);
|
|
126
221
|
setSecretWarning(null);
|
|
127
222
|
setPendingMessage(null);
|
|
128
223
|
};
|
|
@@ -130,143 +225,343 @@ function ChatPageComponent() {
|
|
|
130
225
|
const handleCancelSendWithSecrets = () => {
|
|
131
226
|
setSecretWarning(null);
|
|
132
227
|
setPendingMessage(null);
|
|
228
|
+
inputRef.current?.focus();
|
|
133
229
|
};
|
|
134
230
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
231
|
+
// ── Derived state ───────────────────────────────────────────
|
|
232
|
+
const currentAgent = agents.find((a) => a.id === currentAgentId);
|
|
233
|
+
const hasAgents = agents.length > 0;
|
|
138
234
|
|
|
139
235
|
return (
|
|
140
236
|
<DashboardLayout>
|
|
141
|
-
<div className="flex h-
|
|
142
|
-
{/*
|
|
143
|
-
<
|
|
144
|
-
<div className="
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<option value="">No agents — create one first</option>
|
|
154
|
-
) : (
|
|
155
|
-
agents.map((a: any) => <option key={a.id} value={a.id}>{a.name}</option>)
|
|
237
|
+
<div className="flex flex-col h-full bg-background">
|
|
238
|
+
{/* Header */}
|
|
239
|
+
<header className="flex items-center justify-between p-3 border-b border-border bg-card/50">
|
|
240
|
+
<div className="flex items-center gap-3">
|
|
241
|
+
{/* Thread selector */}
|
|
242
|
+
<select
|
|
243
|
+
value={currentThreadId || ""}
|
|
244
|
+
onChange={(e) => setCurrentThreadId(e.target.value || null)}
|
|
245
|
+
className="bg-background border border-border rounded-lg px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
|
246
|
+
>
|
|
247
|
+
{threads.length === 0 && (
|
|
248
|
+
<option value="">No threads yet</option>
|
|
156
249
|
)}
|
|
250
|
+
{threads.map((thread) => (
|
|
251
|
+
<option key={thread._id} value={thread._id}>
|
|
252
|
+
{thread.name || "Untitled Thread"}
|
|
253
|
+
</option>
|
|
254
|
+
))}
|
|
157
255
|
</select>
|
|
256
|
+
<button
|
|
257
|
+
onClick={handleNewThread}
|
|
258
|
+
className="p-2 rounded-lg hover:bg-card border border-border"
|
|
259
|
+
title="New Thread"
|
|
260
|
+
>
|
|
261
|
+
<Plus className="w-4 h-4 text-muted-foreground" />
|
|
262
|
+
</button>
|
|
158
263
|
</div>
|
|
159
|
-
<div className="flex
|
|
160
|
-
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
264
|
+
<div className="flex items-center gap-3">
|
|
265
|
+
{/* Agent selector */}
|
|
266
|
+
<div className="flex items-center gap-2 px-3 py-1.5 bg-background border border-border rounded-lg">
|
|
267
|
+
<div
|
|
268
|
+
className={`w-2 h-2 rounded-full ${
|
|
269
|
+
currentAgent?.provider === "openai"
|
|
270
|
+
? "bg-green-500"
|
|
271
|
+
: currentAgent?.provider === "anthropic"
|
|
272
|
+
? "bg-orange-500"
|
|
273
|
+
: currentAgent?.provider === "openrouter"
|
|
274
|
+
? "bg-purple-500"
|
|
275
|
+
: "bg-blue-500"
|
|
276
|
+
}`}
|
|
277
|
+
/>
|
|
278
|
+
<select
|
|
279
|
+
value={currentAgentId || ""}
|
|
280
|
+
onChange={(e) => setCurrentAgentId(e.target.value || null)}
|
|
281
|
+
className="bg-transparent text-sm text-foreground focus:outline-none"
|
|
282
|
+
>
|
|
283
|
+
{agents.length === 0 && (
|
|
284
|
+
<option value="">No agents configured</option>
|
|
285
|
+
)}
|
|
286
|
+
{agents.map((agent) => (
|
|
287
|
+
<option key={agent.id} value={agent.id}>
|
|
288
|
+
{agent.name} ({agent.provider}/{agent.model})
|
|
289
|
+
</option>
|
|
290
|
+
))}
|
|
291
|
+
</select>
|
|
292
|
+
</div>
|
|
293
|
+
<button
|
|
294
|
+
className="p-2 rounded-lg hover:bg-card border border-border"
|
|
295
|
+
title="Chat Settings"
|
|
296
|
+
>
|
|
297
|
+
<Settings2 className="w-4 h-4 text-muted-foreground" />
|
|
298
|
+
</button>
|
|
170
299
|
</div>
|
|
171
|
-
</
|
|
172
|
-
|
|
173
|
-
{/*
|
|
174
|
-
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
300
|
+
</header>
|
|
301
|
+
|
|
302
|
+
{/* Vault Notification Banner */}
|
|
303
|
+
{vaultNotification && (
|
|
304
|
+
<div className="mx-4 mt-3 flex items-center gap-2 px-4 py-2.5 bg-green-900/30 border border-green-700/50 rounded-lg text-green-400 text-sm animate-in fade-in slide-in-from-top-2">
|
|
305
|
+
<Shield className="w-4 h-4 flex-shrink-0" />
|
|
306
|
+
<span>{vaultNotification}</span>
|
|
307
|
+
<button
|
|
308
|
+
onClick={() => setVaultNotification(null)}
|
|
309
|
+
className="ml-auto text-green-400/60 hover:text-green-400"
|
|
310
|
+
>
|
|
311
|
+
×
|
|
312
|
+
</button>
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{/* Error Banner */}
|
|
317
|
+
{error && (
|
|
318
|
+
<div className="mx-4 mt-3 flex items-center gap-2 px-4 py-2.5 bg-red-900/30 border border-red-700/50 rounded-lg text-red-400 text-sm">
|
|
319
|
+
<AlertTriangle className="w-4 h-4 flex-shrink-0" />
|
|
320
|
+
<span>{error}</span>
|
|
321
|
+
<button
|
|
322
|
+
onClick={() => setError(null)}
|
|
323
|
+
className="ml-auto text-red-400/60 hover:text-red-400"
|
|
324
|
+
>
|
|
325
|
+
×
|
|
326
|
+
</button>
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
{/* No Agents Warning */}
|
|
331
|
+
{!hasAgents && (
|
|
332
|
+
<div className="mx-4 mt-3 flex items-center gap-2 px-4 py-2.5 bg-yellow-900/30 border border-yellow-700/50 rounded-lg text-yellow-400 text-sm">
|
|
333
|
+
<AlertTriangle className="w-4 h-4 flex-shrink-0" />
|
|
334
|
+
<span>
|
|
335
|
+
No agents configured. Go to the{" "}
|
|
336
|
+
<a href="/agents" className="underline font-medium">
|
|
337
|
+
Agents
|
|
338
|
+
</a>{" "}
|
|
339
|
+
page to create one, or the chat will use a default assistant.
|
|
340
|
+
</span>
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
|
|
344
|
+
{/* Messages Area */}
|
|
345
|
+
<main className="flex-1 overflow-y-auto p-4 md:p-6">
|
|
346
|
+
<div className="max-w-3xl mx-auto space-y-4">
|
|
347
|
+
{/* Empty state */}
|
|
348
|
+
{messages.length === 0 && !isGenerating && (
|
|
349
|
+
<div className="flex flex-col items-center justify-center py-20 text-center">
|
|
350
|
+
<div className="w-16 h-16 rounded-full bg-card border border-border flex items-center justify-center mb-4">
|
|
351
|
+
<MessageSquare className="w-8 h-8 text-primary" />
|
|
352
|
+
</div>
|
|
353
|
+
<h3 className="text-lg font-semibold text-foreground mb-2">
|
|
354
|
+
{currentThreadId ? "No messages yet" : "Start a new conversation"}
|
|
355
|
+
</h3>
|
|
356
|
+
<p className="text-sm text-muted-foreground max-w-md">
|
|
357
|
+
{currentThreadId
|
|
358
|
+
? "Send a message to start chatting with your agent."
|
|
359
|
+
: "Select a thread from the dropdown or create a new one to begin."}
|
|
360
|
+
</p>
|
|
181
361
|
</div>
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
|
|
196
|
-
<User className="w-4 h-4 text-muted-foreground" />
|
|
197
|
-
</div>
|
|
198
|
-
)}
|
|
362
|
+
)}
|
|
363
|
+
|
|
364
|
+
{/* Message list */}
|
|
365
|
+
{messages.map((msg) => (
|
|
366
|
+
<div
|
|
367
|
+
key={msg._id}
|
|
368
|
+
className={`flex items-end gap-2.5 ${
|
|
369
|
+
msg.role === "user" ? "justify-end" : "justify-start"
|
|
370
|
+
}`}
|
|
371
|
+
>
|
|
372
|
+
{msg.role === "assistant" && (
|
|
373
|
+
<div className="w-8 h-8 rounded-full bg-card border border-border flex items-center justify-center flex-shrink-0">
|
|
374
|
+
<Bot className="w-4 h-4 text-primary" />
|
|
199
375
|
</div>
|
|
200
|
-
)
|
|
201
|
-
{
|
|
202
|
-
<div
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
376
|
+
)}
|
|
377
|
+
<div className={`max-w-[75%]`}>
|
|
378
|
+
<div
|
|
379
|
+
className={`px-4 py-2.5 rounded-2xl ${
|
|
380
|
+
msg.role === "user"
|
|
381
|
+
? "bg-primary text-primary-foreground rounded-br-md"
|
|
382
|
+
: msg.role === "system"
|
|
383
|
+
? "bg-yellow-900/30 border border-yellow-700/50 text-yellow-200"
|
|
384
|
+
: "bg-card border border-border rounded-bl-md"
|
|
385
|
+
}`}
|
|
386
|
+
>
|
|
387
|
+
<p className="text-sm whitespace-pre-wrap">{msg.content}</p>
|
|
388
|
+
</div>
|
|
389
|
+
<p
|
|
390
|
+
className={`text-xs mt-1 px-1 ${
|
|
391
|
+
msg.role === "user"
|
|
392
|
+
? "text-right text-muted-foreground"
|
|
393
|
+
: "text-muted-foreground"
|
|
394
|
+
}`}
|
|
395
|
+
>
|
|
396
|
+
{new Date(msg.createdAt).toLocaleTimeString([], {
|
|
397
|
+
hour: "2-digit",
|
|
398
|
+
minute: "2-digit",
|
|
399
|
+
})}
|
|
400
|
+
</p>
|
|
401
|
+
</div>
|
|
402
|
+
{msg.role === "user" && (
|
|
403
|
+
<div className="w-8 h-8 rounded-full bg-primary/20 border border-primary/30 flex items-center justify-center flex-shrink-0">
|
|
404
|
+
<User className="w-4 h-4 text-primary" />
|
|
207
405
|
</div>
|
|
208
406
|
)}
|
|
209
|
-
<div ref={messagesEndRef} />
|
|
210
407
|
</div>
|
|
211
|
-
)}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<div>
|
|
221
|
-
<
|
|
222
|
-
<
|
|
408
|
+
))}
|
|
409
|
+
|
|
410
|
+
{/* Typing indicator */}
|
|
411
|
+
{isGenerating && (
|
|
412
|
+
<div className="flex items-end gap-2.5 justify-start">
|
|
413
|
+
<div className="w-8 h-8 rounded-full bg-card border border-border flex items-center justify-center">
|
|
414
|
+
<Bot className="w-4 h-4 text-primary" />
|
|
415
|
+
</div>
|
|
416
|
+
<div className="px-4 py-3 rounded-2xl rounded-bl-md bg-card border border-border">
|
|
417
|
+
<div className="flex items-center gap-2">
|
|
418
|
+
<Loader2 className="w-4 h-4 animate-spin text-primary" />
|
|
419
|
+
<span className="text-sm text-muted-foreground">
|
|
420
|
+
{currentAgent?.name || "Agent"} is thinking...
|
|
421
|
+
</span>
|
|
223
422
|
</div>
|
|
224
423
|
</div>
|
|
225
|
-
|
|
424
|
+
</div>
|
|
425
|
+
)}
|
|
426
|
+
<div ref={messagesEndRef} />
|
|
427
|
+
</div>
|
|
428
|
+
</main>
|
|
429
|
+
|
|
430
|
+
{/* Secret Warning Modal */}
|
|
431
|
+
{secretWarning && (
|
|
432
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
|
433
|
+
<div className="bg-card border border-border rounded-xl shadow-2xl w-full max-w-md m-4 overflow-hidden">
|
|
434
|
+
<div className="p-4 bg-yellow-900/30 border-b border-yellow-700/50 flex items-center gap-3">
|
|
435
|
+
<ShieldAlert className="w-6 h-6 text-yellow-500" />
|
|
436
|
+
<div>
|
|
437
|
+
<h3 className="font-semibold text-foreground">
|
|
438
|
+
Secrets Detected
|
|
439
|
+
</h3>
|
|
226
440
|
<p className="text-sm text-muted-foreground">
|
|
227
|
-
|
|
441
|
+
Your message contains sensitive information
|
|
228
442
|
</p>
|
|
229
|
-
<div className="space-y-2">
|
|
230
|
-
{secretWarning.map((secret, i) => (
|
|
231
|
-
<div key={i} className="flex items-center gap-2 px-3 py-2 bg-background rounded-lg border border-border">
|
|
232
|
-
<Lock className="w-4 h-4 text-yellow-500 flex-shrink-0" />
|
|
233
|
-
<div className="min-w-0">
|
|
234
|
-
<p className="text-sm font-medium text-foreground">{secret.name}</p>
|
|
235
|
-
<p className="text-xs text-muted-foreground font-mono truncate">{maskSecret(secret.match)}</p>
|
|
236
|
-
</div>
|
|
237
|
-
<span className="ml-auto text-xs px-2 py-0.5 bg-yellow-900/30 text-yellow-400 rounded-full">{secret.category}</span>
|
|
238
|
-
</div>
|
|
239
|
-
))}
|
|
240
|
-
</div>
|
|
241
443
|
</div>
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
444
|
+
</div>
|
|
445
|
+
<div className="p-4 space-y-3">
|
|
446
|
+
<p className="text-sm text-muted-foreground">
|
|
447
|
+
The following secrets were detected in your message. They will
|
|
448
|
+
be{" "}
|
|
449
|
+
<strong className="text-foreground">
|
|
450
|
+
automatically redacted
|
|
451
|
+
</strong>{" "}
|
|
452
|
+
before being sent. The original values will{" "}
|
|
453
|
+
<strong className="text-foreground">
|
|
454
|
+
never appear in chat history
|
|
455
|
+
</strong>
|
|
456
|
+
.
|
|
457
|
+
</p>
|
|
458
|
+
<div className="space-y-2">
|
|
459
|
+
{secretWarning.map((secret, i) => (
|
|
460
|
+
<div
|
|
461
|
+
key={i}
|
|
462
|
+
className="flex items-center gap-2 px-3 py-2 bg-background rounded-lg border border-border"
|
|
463
|
+
>
|
|
464
|
+
<Lock className="w-4 h-4 text-yellow-500 flex-shrink-0" />
|
|
465
|
+
<div className="min-w-0">
|
|
466
|
+
<p className="text-sm font-medium text-foreground">
|
|
467
|
+
{secret.name}
|
|
468
|
+
</p>
|
|
469
|
+
<p className="text-xs text-muted-foreground font-mono truncate">
|
|
470
|
+
{maskSecret(secret.match)}
|
|
471
|
+
</p>
|
|
472
|
+
</div>
|
|
473
|
+
<span className="ml-auto text-xs px-2 py-0.5 bg-yellow-900/30 text-yellow-400 rounded-full">
|
|
474
|
+
{secret.category}
|
|
475
|
+
</span>
|
|
476
|
+
</div>
|
|
477
|
+
))}
|
|
247
478
|
</div>
|
|
248
479
|
</div>
|
|
480
|
+
<div className="p-4 border-t border-border flex justify-end gap-2">
|
|
481
|
+
<button
|
|
482
|
+
onClick={handleCancelSendWithSecrets}
|
|
483
|
+
className="px-4 py-2 text-sm rounded-lg bg-muted text-muted-foreground hover:bg-muted/80"
|
|
484
|
+
>
|
|
485
|
+
Cancel
|
|
486
|
+
</button>
|
|
487
|
+
<button
|
|
488
|
+
onClick={handleConfirmSendWithSecrets}
|
|
489
|
+
className="px-4 py-2 text-sm rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2"
|
|
490
|
+
>
|
|
491
|
+
<Shield className="w-4 h-4" />
|
|
492
|
+
Redact & Send
|
|
493
|
+
</button>
|
|
494
|
+
</div>
|
|
249
495
|
</div>
|
|
250
|
-
|
|
496
|
+
</div>
|
|
497
|
+
)}
|
|
251
498
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
499
|
+
{/* Input Area */}
|
|
500
|
+
<footer className="p-3 border-t border-border bg-card/50">
|
|
501
|
+
{/* Secret detection warning bar */}
|
|
502
|
+
{inputHasSecrets.length > 0 && (
|
|
503
|
+
<div className="mb-2 flex items-center gap-2 px-3 py-2 bg-yellow-900/20 border border-yellow-700/40 rounded-lg text-yellow-400 text-xs">
|
|
504
|
+
<AlertTriangle className="w-3.5 h-3.5 flex-shrink-0" />
|
|
505
|
+
<span>
|
|
506
|
+
<strong>
|
|
507
|
+
{inputHasSecrets.length} secret
|
|
508
|
+
{inputHasSecrets.length > 1 ? "s" : ""}
|
|
509
|
+
</strong>{" "}
|
|
510
|
+
detected ({inputHasSecrets.map((s) => s.name).join(", ")}). Will
|
|
511
|
+
be auto-redacted on send.
|
|
512
|
+
</span>
|
|
513
|
+
</div>
|
|
514
|
+
)}
|
|
515
|
+
<form
|
|
516
|
+
onSubmit={handleSendMessage}
|
|
517
|
+
className="max-w-3xl mx-auto flex items-center gap-2"
|
|
518
|
+
>
|
|
519
|
+
<button
|
|
520
|
+
type="button"
|
|
521
|
+
className="p-2.5 rounded-lg hover:bg-background border border-border"
|
|
522
|
+
title="Attach file"
|
|
523
|
+
>
|
|
524
|
+
<Paperclip className="w-4 h-4 text-muted-foreground" />
|
|
525
|
+
</button>
|
|
526
|
+
<div className="flex-1 relative">
|
|
527
|
+
<input
|
|
528
|
+
ref={inputRef}
|
|
529
|
+
type="text"
|
|
530
|
+
value={input}
|
|
531
|
+
onChange={(e) => setInput(e.target.value)}
|
|
532
|
+
placeholder={
|
|
533
|
+
isGenerating
|
|
534
|
+
? "Waiting for response..."
|
|
535
|
+
: "Type your message..."
|
|
536
|
+
}
|
|
537
|
+
disabled={isGenerating}
|
|
538
|
+
className={`w-full bg-background border rounded-lg pl-4 pr-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary disabled:opacity-50 ${
|
|
539
|
+
inputHasSecrets.length > 0
|
|
540
|
+
? "border-yellow-600/50"
|
|
541
|
+
: "border-border"
|
|
542
|
+
}`}
|
|
543
|
+
/>
|
|
544
|
+
</div>
|
|
545
|
+
<button
|
|
546
|
+
type="button"
|
|
547
|
+
className="p-2.5 rounded-lg hover:bg-background border border-border"
|
|
548
|
+
title="Voice input"
|
|
549
|
+
>
|
|
550
|
+
<Mic className="w-4 h-4 text-muted-foreground" />
|
|
551
|
+
</button>
|
|
552
|
+
<button
|
|
553
|
+
type="submit"
|
|
554
|
+
disabled={!input.trim() || isGenerating}
|
|
555
|
+
className="p-2.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
556
|
+
>
|
|
557
|
+
{isGenerating ? (
|
|
558
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
559
|
+
) : (
|
|
265
560
|
<Send className="w-4 h-4" />
|
|
266
|
-
|
|
267
|
-
</
|
|
268
|
-
</
|
|
269
|
-
</
|
|
561
|
+
)}
|
|
562
|
+
</button>
|
|
563
|
+
</form>
|
|
564
|
+
</footer>
|
|
270
565
|
</div>
|
|
271
566
|
</DashboardLayout>
|
|
272
567
|
);
|