@agentforge-ai/cli 0.4.3 → 0.5.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/dist/default/convex/agents.ts +204 -0
- package/dist/default/convex/apiKeys.ts +133 -0
- package/dist/default/convex/cronJobs.ts +224 -0
- package/dist/default/convex/files.ts +103 -0
- package/dist/default/convex/folders.ts +110 -0
- package/dist/default/convex/heartbeat.ts +371 -0
- package/dist/default/convex/logs.ts +66 -0
- package/dist/default/convex/mastraIntegration.ts +185 -0
- package/dist/default/convex/mcpConnections.ts +127 -0
- package/dist/default/convex/messages.ts +90 -0
- package/dist/default/convex/projects.ts +114 -0
- package/dist/default/convex/schema.ts +150 -83
- package/dist/default/convex/sessions.ts +174 -0
- package/dist/default/convex/settings.ts +79 -0
- package/dist/default/convex/skills.ts +178 -0
- package/dist/default/convex/threads.ts +100 -0
- package/dist/default/convex/usage.ts +195 -0
- package/dist/default/convex/vault.ts +397 -0
- package/dist/default/dashboard/app/main.tsx +7 -3
- package/dist/default/dashboard/app/routes/agents.tsx +103 -161
- package/dist/default/dashboard/app/routes/chat.tsx +163 -317
- package/dist/default/dashboard/app/routes/connections.tsx +247 -386
- package/dist/default/dashboard/app/routes/cron.tsx +127 -286
- package/dist/default/dashboard/app/routes/files.tsx +184 -167
- package/dist/default/dashboard/app/routes/index.tsx +63 -96
- package/dist/default/dashboard/app/routes/projects.tsx +106 -225
- package/dist/default/dashboard/app/routes/sessions.tsx +87 -253
- package/dist/default/dashboard/app/routes/settings.tsx +316 -532
- package/dist/default/dashboard/app/routes/skills.tsx +329 -216
- package/dist/default/dashboard/app/routes/usage.tsx +107 -150
- package/dist/default/dashboard/tsconfig.json +3 -2
- package/dist/default/dashboard/vite.config.ts +6 -0
- package/dist/index.js +256 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/default/convex/agents.ts +204 -0
- package/templates/default/convex/apiKeys.ts +133 -0
- package/templates/default/convex/cronJobs.ts +224 -0
- package/templates/default/convex/files.ts +103 -0
- package/templates/default/convex/folders.ts +110 -0
- package/templates/default/convex/heartbeat.ts +371 -0
- package/templates/default/convex/logs.ts +66 -0
- package/templates/default/convex/mastraIntegration.ts +185 -0
- package/templates/default/convex/mcpConnections.ts +127 -0
- package/templates/default/convex/messages.ts +90 -0
- package/templates/default/convex/projects.ts +114 -0
- package/templates/default/convex/schema.ts +150 -83
- package/templates/default/convex/sessions.ts +174 -0
- package/templates/default/convex/settings.ts +79 -0
- package/templates/default/convex/skills.ts +178 -0
- package/templates/default/convex/threads.ts +100 -0
- package/templates/default/convex/usage.ts +195 -0
- package/templates/default/convex/vault.ts +397 -0
- package/templates/default/dashboard/app/main.tsx +7 -3
- package/templates/default/dashboard/app/routes/agents.tsx +103 -161
- package/templates/default/dashboard/app/routes/chat.tsx +163 -317
- package/templates/default/dashboard/app/routes/connections.tsx +247 -386
- package/templates/default/dashboard/app/routes/cron.tsx +127 -286
- package/templates/default/dashboard/app/routes/files.tsx +184 -167
- package/templates/default/dashboard/app/routes/index.tsx +63 -96
- package/templates/default/dashboard/app/routes/projects.tsx +106 -225
- package/templates/default/dashboard/app/routes/sessions.tsx +87 -253
- package/templates/default/dashboard/app/routes/settings.tsx +316 -532
- package/templates/default/dashboard/app/routes/skills.tsx +329 -216
- package/templates/default/dashboard/app/routes/usage.tsx +107 -150
- package/templates/default/dashboard/tsconfig.json +3 -2
- package/templates/default/dashboard/vite.config.ts +6 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createFileRoute } from '@tanstack/react-router';
|
|
2
2
|
import { DashboardLayout } from '../components/DashboardLayout';
|
|
3
3
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { useQuery, useMutation } from 'convex/react';
|
|
5
|
+
import { api } from '@convex/_generated/api';
|
|
6
|
+
import { Send, Plus, Bot, User, Shield, ShieldAlert, Lock, AlertTriangle, Paperclip, Mic, Settings2, X, MessageSquare } from 'lucide-react';
|
|
5
7
|
|
|
6
8
|
// ============================================================
|
|
7
|
-
// SECRET DETECTION ENGINE
|
|
9
|
+
// SECRET DETECTION ENGINE
|
|
8
10
|
// ============================================================
|
|
9
11
|
const SECRET_PATTERNS = [
|
|
10
12
|
{ pattern: /sk-[a-zA-Z0-9]{20,}/g, category: "api_key", provider: "openai", name: "OpenAI API Key" },
|
|
@@ -13,23 +15,12 @@ const SECRET_PATTERNS = [
|
|
|
13
15
|
{ pattern: /AIza[a-zA-Z0-9_-]{35}/g, category: "api_key", provider: "google", name: "Google API Key" },
|
|
14
16
|
{ pattern: /xai-[a-zA-Z0-9]{20,}/g, category: "api_key", provider: "xai", name: "xAI API Key" },
|
|
15
17
|
{ pattern: /ghp_[a-zA-Z0-9]{36}/g, category: "token", provider: "github", name: "GitHub Token" },
|
|
16
|
-
{ pattern: /gho_[a-zA-Z0-9]{36}/g, category: "token", provider: "github", name: "GitHub OAuth" },
|
|
17
|
-
{ pattern: /glpat-[a-zA-Z0-9_-]{20,}/g, category: "token", provider: "gitlab", name: "GitLab Token" },
|
|
18
|
-
{ pattern: /xoxb-[a-zA-Z0-9-]+/g, category: "token", provider: "slack", name: "Slack Bot Token" },
|
|
19
|
-
{ pattern: /xoxp-[a-zA-Z0-9-]+/g, category: "token", provider: "slack", name: "Slack User Token" },
|
|
20
18
|
{ pattern: /AKIA[A-Z0-9]{16}/g, category: "credential", provider: "aws", name: "AWS Access Key" },
|
|
21
19
|
{ pattern: /sk_live_[a-zA-Z0-9]{24,}/g, category: "api_key", provider: "stripe", name: "Stripe Live Key" },
|
|
22
20
|
{ pattern: /sk_test_[a-zA-Z0-9]{24,}/g, category: "api_key", provider: "stripe", name: "Stripe Test Key" },
|
|
23
|
-
{ pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g, category: "api_key", provider: "sendgrid", name: "SendGrid Key" },
|
|
24
|
-
{ pattern: /eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g, category: "token", provider: "jwt", name: "JWT Token" },
|
|
25
21
|
];
|
|
26
22
|
|
|
27
|
-
interface DetectedSecret {
|
|
28
|
-
match: string;
|
|
29
|
-
category: string;
|
|
30
|
-
provider: string;
|
|
31
|
-
name: string;
|
|
32
|
-
}
|
|
23
|
+
interface DetectedSecret { match: string; category: string; provider: string; name: string; }
|
|
33
24
|
|
|
34
25
|
function detectSecrets(text: string): DetectedSecret[] {
|
|
35
26
|
const detected: DetectedSecret[] = [];
|
|
@@ -51,159 +42,87 @@ function maskSecret(value: string): string {
|
|
|
51
42
|
function censorText(text: string, secrets: DetectedSecret[]): string {
|
|
52
43
|
let censored = text;
|
|
53
44
|
for (const secret of secrets) {
|
|
54
|
-
censored = censored.replace(secret.match, `[
|
|
45
|
+
censored = censored.replace(secret.match, `[REDACTED: ${secret.name}]`);
|
|
55
46
|
}
|
|
56
47
|
return censored;
|
|
57
48
|
}
|
|
58
49
|
|
|
59
|
-
// ============================================================
|
|
60
|
-
// Mock data (replace with Convex queries)
|
|
61
|
-
// ============================================================
|
|
62
|
-
const mockSessions = [
|
|
63
|
-
{ id: 'ses_1', name: 'Main Session' },
|
|
64
|
-
{ id: 'ses_2', name: 'Customer Support' },
|
|
65
|
-
{ id: 'ses_3', name: 'Code Review' },
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
const mockAgents = [
|
|
69
|
-
{ id: 'agent_1', name: 'GPT-4.1 Mini', provider: 'openai' },
|
|
70
|
-
{ id: 'agent_2', name: 'Claude 3.5 Sonnet', provider: 'anthropic' },
|
|
71
|
-
{ id: 'agent_3', name: 'Grok 2', provider: 'xai' },
|
|
72
|
-
{ id: 'agent_4', name: 'Gemini Pro', provider: 'google' },
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
interface ChatMessage {
|
|
76
|
-
id: string;
|
|
77
|
-
text: string;
|
|
78
|
-
sender: 'user' | 'assistant' | 'system';
|
|
79
|
-
timestamp: string;
|
|
80
|
-
isRedacted?: boolean;
|
|
81
|
-
secretsCaptured?: Array<{ name: string; masked: string }>;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ============================================================
|
|
85
|
-
// Chat Page Component
|
|
86
|
-
// ============================================================
|
|
87
50
|
export const Route = createFileRoute('/chat')({ component: ChatPageComponent });
|
|
88
51
|
|
|
89
52
|
function ChatPageComponent() {
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
53
|
+
const agents = useQuery(api.agents.list, {}) ?? [];
|
|
54
|
+
const threads = useQuery(api.threads.list, {}) ?? [];
|
|
55
|
+
const createThread = useMutation(api.threads.create);
|
|
56
|
+
const addMessage = useMutation(api.messages.add);
|
|
57
|
+
const storeSecret = useMutation(api.vault.store);
|
|
58
|
+
|
|
59
|
+
const [currentThreadId, setCurrentThreadId] = useState<string | null>(null);
|
|
60
|
+
const [currentAgentId, setCurrentAgentId] = useState<string | null>(null);
|
|
94
61
|
const [input, setInput] = useState('');
|
|
95
62
|
const [isTyping, setIsTyping] = useState(false);
|
|
96
|
-
const [currentAgentId, setCurrentAgentId] = useState(mockAgents[0]?.id || null);
|
|
97
63
|
const [secretWarning, setSecretWarning] = useState<DetectedSecret[] | null>(null);
|
|
98
64
|
const [pendingMessage, setPendingMessage] = useState<string | null>(null);
|
|
99
|
-
const [vaultNotification, setVaultNotification] = useState<string | null>(null);
|
|
100
|
-
|
|
101
65
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
102
66
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
103
67
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
useEffect(() => { scrollToBottom(); }, [messages, isTyping]);
|
|
68
|
+
// Load messages for current thread
|
|
69
|
+
const messages = useQuery(api.messages.list, currentThreadId ? { threadId: currentThreadId as any } : "skip") ?? [];
|
|
109
70
|
|
|
71
|
+
// Auto-select first agent
|
|
110
72
|
useEffect(() => {
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
setIsTyping(true);
|
|
114
|
-
setTimeout(() => {
|
|
115
|
-
const agent = agents.find(a => a.id === currentAgentId);
|
|
116
|
-
setMessages([{
|
|
117
|
-
id: 'msg_welcome',
|
|
118
|
-
text: `Hello! I'm ${agent?.name || 'your agent'}. How can I help you today?`,
|
|
119
|
-
sender: 'assistant',
|
|
120
|
-
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
|
121
|
-
}]);
|
|
122
|
-
setIsTyping(false);
|
|
123
|
-
}, 500);
|
|
73
|
+
if (agents.length > 0 && !currentAgentId) {
|
|
74
|
+
setCurrentAgentId(agents[0].id);
|
|
124
75
|
}
|
|
125
|
-
}, [
|
|
76
|
+
}, [agents, currentAgentId]);
|
|
126
77
|
|
|
127
|
-
//
|
|
128
|
-
|
|
78
|
+
// Auto-scroll
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
81
|
+
}, [messages]);
|
|
129
82
|
|
|
130
|
-
const
|
|
131
|
-
e.preventDefault();
|
|
132
|
-
if (!input.trim() || !currentSessionId) return;
|
|
83
|
+
const inputHasSecrets = input ? detectSecrets(input) : [];
|
|
133
84
|
|
|
134
|
-
|
|
85
|
+
const handleSendMessage = async (e: React.FormEvent) => {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
if (!input.trim()) return;
|
|
135
88
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
setSecretWarning(
|
|
89
|
+
const secrets = detectSecrets(input);
|
|
90
|
+
if (secrets.length > 0) {
|
|
91
|
+
setSecretWarning(secrets);
|
|
139
92
|
setPendingMessage(input);
|
|
140
93
|
return;
|
|
141
94
|
}
|
|
142
95
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}, [input, currentSessionId, currentAgentId]);
|
|
146
|
-
|
|
147
|
-
const sendMessageToChat = (text: string, secrets?: DetectedSecret[]) => {
|
|
148
|
-
let displayText = text;
|
|
149
|
-
let secretsCaptured: Array<{ name: string; masked: string }> = [];
|
|
150
|
-
let isRedacted = false;
|
|
151
|
-
|
|
152
|
-
if (secrets && secrets.length > 0) {
|
|
153
|
-
// Censor the message and auto-store secrets
|
|
154
|
-
displayText = censorText(text, secrets);
|
|
155
|
-
isRedacted = true;
|
|
156
|
-
secretsCaptured = secrets.map(s => ({ name: s.name, masked: maskSecret(s.match) }));
|
|
157
|
-
|
|
158
|
-
// In production: call vault.censorMessage mutation here
|
|
159
|
-
// const result = await censorMessage({ text, userId, autoStore: true });
|
|
96
|
+
await sendMessage(input);
|
|
97
|
+
};
|
|
160
98
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
);
|
|
165
|
-
|
|
99
|
+
const sendMessage = async (text: string) => {
|
|
100
|
+
let threadId = currentThreadId;
|
|
101
|
+
if (!threadId && currentAgentId) {
|
|
102
|
+
threadId = await createThread({ agentId: currentAgentId, name: `Chat ${new Date().toLocaleString()}` }) as any;
|
|
103
|
+
setCurrentThreadId(threadId);
|
|
166
104
|
}
|
|
105
|
+
if (!threadId) return;
|
|
167
106
|
|
|
168
|
-
|
|
169
|
-
id: `msg_${Date.now()}`,
|
|
170
|
-
text: displayText,
|
|
171
|
-
sender: 'user',
|
|
172
|
-
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
|
173
|
-
isRedacted,
|
|
174
|
-
secretsCaptured: secretsCaptured.length > 0 ? secretsCaptured : undefined,
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
setMessages(prev => [...prev, userMessage]);
|
|
107
|
+
await addMessage({ threadId: threadId as any, role: 'user', content: text });
|
|
178
108
|
setInput('');
|
|
179
109
|
setIsTyping(true);
|
|
180
110
|
|
|
181
|
-
// Simulate agent response
|
|
182
|
-
setTimeout(() => {
|
|
183
|
-
|
|
184
|
-
let responseText: string;
|
|
185
|
-
|
|
186
|
-
if (isRedacted) {
|
|
187
|
-
responseText = `I noticed you shared ${secretsCaptured.length} secret${secretsCaptured.length > 1 ? 's' : ''}. They've been automatically encrypted and stored in your Secure Vault. The original values are never stored in chat history.\n\nHow else can I help you?`;
|
|
188
|
-
} else {
|
|
189
|
-
responseText = `I understand your request. Let me work on that for you.\n\n*This is a simulated response from ${agent?.name || 'the agent'}.*`;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const assistantResponse: ChatMessage = {
|
|
193
|
-
id: `msg_${Date.now() + 1}`,
|
|
194
|
-
text: responseText,
|
|
195
|
-
sender: 'assistant',
|
|
196
|
-
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
|
197
|
-
};
|
|
198
|
-
setMessages(prev => [...prev, assistantResponse]);
|
|
111
|
+
// Simulate agent response (will be replaced by Mastra integration)
|
|
112
|
+
setTimeout(async () => {
|
|
113
|
+
await addMessage({ threadId: threadId as any, role: 'assistant', content: 'This response will be powered by your configured AI provider once Mastra integration is active. For now, messages are stored in Convex.' });
|
|
199
114
|
setIsTyping(false);
|
|
200
115
|
}, 1500);
|
|
201
116
|
};
|
|
202
117
|
|
|
203
|
-
const handleConfirmSendWithSecrets = () => {
|
|
204
|
-
if (pendingMessage
|
|
205
|
-
|
|
118
|
+
const handleConfirmSendWithSecrets = async () => {
|
|
119
|
+
if (!pendingMessage || !secretWarning) return;
|
|
120
|
+
// Store secrets in vault
|
|
121
|
+
for (const secret of secretWarning) {
|
|
122
|
+
await storeSecret({ name: secret.name, category: secret.category, provider: secret.provider, value: secret.match });
|
|
206
123
|
}
|
|
124
|
+
const censored = censorText(pendingMessage, secretWarning);
|
|
125
|
+
await sendMessage(censored);
|
|
207
126
|
setSecretWarning(null);
|
|
208
127
|
setPendingMessage(null);
|
|
209
128
|
};
|
|
@@ -211,216 +130,143 @@ function ChatPageComponent() {
|
|
|
211
130
|
const handleCancelSendWithSecrets = () => {
|
|
212
131
|
setSecretWarning(null);
|
|
213
132
|
setPendingMessage(null);
|
|
214
|
-
inputRef.current?.focus();
|
|
215
133
|
};
|
|
216
134
|
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
setCurrentSessionId(newSessionId);
|
|
220
|
-
setMessages([]);
|
|
135
|
+
const handleNewChat = async () => {
|
|
136
|
+
setCurrentThreadId(null);
|
|
221
137
|
};
|
|
222
138
|
|
|
223
|
-
const currentAgent = agents.find(a => a.id === currentAgentId);
|
|
224
|
-
|
|
225
139
|
return (
|
|
226
140
|
<DashboardLayout>
|
|
227
|
-
<div className="flex
|
|
228
|
-
{/*
|
|
229
|
-
<
|
|
230
|
-
<div className="
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
onChange={(e) => setCurrentSessionId(e.target.value)}
|
|
234
|
-
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"
|
|
235
|
-
>
|
|
236
|
-
{sessions.map((session) => (
|
|
237
|
-
<option key={session.id} value={session.id}>{session.name}</option>
|
|
238
|
-
))}
|
|
239
|
-
</select>
|
|
240
|
-
<button onClick={handleNewSession} className="p-2 rounded-lg hover:bg-card border border-border" title="New Session">
|
|
241
|
-
<Plus className="w-4 h-4 text-muted-foreground" />
|
|
141
|
+
<div className="flex h-[calc(100vh-8rem)] bg-background rounded-lg border border-border overflow-hidden">
|
|
142
|
+
{/* Sidebar */}
|
|
143
|
+
<aside className="w-64 border-r border-border bg-card flex flex-col">
|
|
144
|
+
<div className="p-3 border-b border-border">
|
|
145
|
+
<button onClick={handleNewChat} className="w-full bg-primary text-primary-foreground px-3 py-2 rounded-lg text-sm font-medium hover:bg-primary/90 flex items-center justify-center gap-2">
|
|
146
|
+
<Plus className="w-4 h-4" /> New Chat
|
|
242
147
|
</button>
|
|
243
148
|
</div>
|
|
244
|
-
<div className="
|
|
245
|
-
<
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
value=
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
<option key={agent.id} value={agent.id}>{agent.name}</option>
|
|
254
|
-
))}
|
|
255
|
-
</select>
|
|
256
|
-
</div>
|
|
257
|
-
<button className="p-2 rounded-lg hover:bg-card border border-border" title="Chat Settings">
|
|
258
|
-
<Settings2 className="w-4 h-4 text-muted-foreground" />
|
|
259
|
-
</button>
|
|
149
|
+
<div className="p-3 border-b border-border">
|
|
150
|
+
<label className="text-xs font-medium text-muted-foreground mb-1 block">Agent</label>
|
|
151
|
+
<select value={currentAgentId || ''} onChange={(e) => setCurrentAgentId(e.target.value)} className="w-full bg-background border border-border rounded-md px-2 py-1.5 text-sm">
|
|
152
|
+
{agents.length === 0 ? (
|
|
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>)
|
|
156
|
+
)}
|
|
157
|
+
</select>
|
|
260
158
|
</div>
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
159
|
+
<div className="flex-1 overflow-y-auto p-2 space-y-1">
|
|
160
|
+
{threads.length === 0 ? (
|
|
161
|
+
<div className="text-center py-8 text-muted-foreground text-xs">No conversations yet</div>
|
|
162
|
+
) : (
|
|
163
|
+
threads.map((t: any) => (
|
|
164
|
+
<button key={t._id} onClick={() => setCurrentThreadId(t._id)} className={`w-full text-left px-3 py-2 rounded-lg text-sm truncate transition-colors ${currentThreadId === t._id ? 'bg-primary/10 text-primary' : 'text-muted-foreground hover:bg-muted'}`}>
|
|
165
|
+
<MessageSquare className="w-3.5 h-3.5 inline mr-2" />
|
|
166
|
+
{t.name || `Thread ${t._id.slice(-6)}`}
|
|
167
|
+
</button>
|
|
168
|
+
))
|
|
169
|
+
)}
|
|
269
170
|
</div>
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
{/*
|
|
273
|
-
<
|
|
274
|
-
<
|
|
275
|
-
{messages.
|
|
276
|
-
<div
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
{msg.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
171
|
+
</aside>
|
|
172
|
+
|
|
173
|
+
{/* Main Chat Area */}
|
|
174
|
+
<div className="flex-1 flex flex-col">
|
|
175
|
+
<main className="flex-1 overflow-y-auto p-4">
|
|
176
|
+
{messages.length === 0 && !currentThreadId ? (
|
|
177
|
+
<div className="flex flex-col items-center justify-center h-full text-center">
|
|
178
|
+
<Bot className="w-16 h-16 text-muted-foreground/30 mb-4" />
|
|
179
|
+
<h2 className="text-xl font-semibold mb-2">Start a conversation</h2>
|
|
180
|
+
<p className="text-muted-foreground max-w-md">Select an agent and type a message to begin. Your conversations are stored in Convex and synced in real-time.</p>
|
|
181
|
+
</div>
|
|
182
|
+
) : (
|
|
183
|
+
<div className="max-w-3xl mx-auto space-y-4">
|
|
184
|
+
{messages.map((msg: any) => (
|
|
185
|
+
<div key={msg._id} className={`flex gap-3 ${msg.role === 'user' ? 'justify-end' : ''}`}>
|
|
186
|
+
{msg.role !== 'user' && (
|
|
187
|
+
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
|
188
|
+
<Bot className="w-4 h-4 text-primary" />
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
<div className={`max-w-[80%] px-4 py-3 rounded-2xl text-sm ${msg.role === 'user' ? 'bg-primary text-primary-foreground rounded-br-md' : 'bg-card border border-border rounded-bl-md'}`}>
|
|
192
|
+
{msg.content}
|
|
193
|
+
</div>
|
|
194
|
+
{msg.role === 'user' && (
|
|
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" />
|
|
294
197
|
</div>
|
|
295
198
|
)}
|
|
296
|
-
<p className="text-sm whitespace-pre-wrap">{msg.text}</p>
|
|
297
199
|
</div>
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
</span>
|
|
305
|
-
))}
|
|
200
|
+
))}
|
|
201
|
+
{isTyping && (
|
|
202
|
+
<div className="flex gap-3">
|
|
203
|
+
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center"><Bot className="w-4 h-4 text-primary" /></div>
|
|
204
|
+
<div className="bg-card border border-border rounded-2xl rounded-bl-md px-4 py-3">
|
|
205
|
+
<div className="flex space-x-1"><div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" /><div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce [animation-delay:0.15s]" /><div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce [animation-delay:0.3s]" /></div>
|
|
306
206
|
</div>
|
|
307
|
-
)}
|
|
308
|
-
<p className={`text-xs mt-1 px-1 ${msg.sender === 'user' ? 'text-right text-muted-foreground' : 'text-muted-foreground'}`}>
|
|
309
|
-
{msg.timestamp}
|
|
310
|
-
</p>
|
|
311
|
-
</div>
|
|
312
|
-
{msg.sender === 'user' && (
|
|
313
|
-
<div className="w-8 h-8 rounded-full bg-primary/20 border border-primary/30 flex items-center justify-center flex-shrink-0">
|
|
314
|
-
<User className="w-4 h-4 text-primary" />
|
|
315
207
|
</div>
|
|
316
208
|
)}
|
|
317
|
-
|
|
318
|
-
))}
|
|
319
|
-
|
|
320
|
-
{isTyping && (
|
|
321
|
-
<div className="flex items-end gap-2.5 justify-start">
|
|
322
|
-
<div className="w-8 h-8 rounded-full bg-card border border-border flex items-center justify-center">
|
|
323
|
-
<Bot className="w-4 h-4 text-primary" />
|
|
324
|
-
</div>
|
|
325
|
-
<div className="px-4 py-3 rounded-2xl rounded-bl-md bg-card border border-border">
|
|
326
|
-
<div className="flex gap-1">
|
|
327
|
-
<span className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
|
328
|
-
<span className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
|
329
|
-
<span className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
|
330
|
-
</div>
|
|
331
|
-
</div>
|
|
209
|
+
<div ref={messagesEndRef} />
|
|
332
210
|
</div>
|
|
333
211
|
)}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<p className="text-sm text-muted-foreground">Your message contains sensitive information</p>
|
|
212
|
+
</main>
|
|
213
|
+
|
|
214
|
+
{/* Secret Warning Modal */}
|
|
215
|
+
{secretWarning && (
|
|
216
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
|
217
|
+
<div className="bg-card border border-border rounded-xl shadow-2xl w-full max-w-md m-4 overflow-hidden">
|
|
218
|
+
<div className="p-4 bg-yellow-900/30 border-b border-yellow-700/50 flex items-center gap-3">
|
|
219
|
+
<ShieldAlert className="w-6 h-6 text-yellow-500" />
|
|
220
|
+
<div>
|
|
221
|
+
<h3 className="font-semibold text-foreground">Secrets Detected</h3>
|
|
222
|
+
<p className="text-sm text-muted-foreground">Your message contains sensitive information</p>
|
|
223
|
+
</div>
|
|
347
224
|
</div>
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
225
|
+
<div className="p-4 space-y-3">
|
|
226
|
+
<p className="text-sm text-muted-foreground">
|
|
227
|
+
The following secrets were detected. They will be <strong className="text-foreground">automatically encrypted</strong> and stored in the Secure Vault. The original values will <strong className="text-foreground">never appear in chat history</strong>.
|
|
228
|
+
</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>
|
|
360
238
|
</div>
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
239
|
+
))}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
<div className="p-4 border-t border-border flex justify-end gap-2">
|
|
243
|
+
<button onClick={handleCancelSendWithSecrets} className="px-4 py-2 text-sm rounded-lg bg-muted text-muted-foreground hover:bg-muted/80">Cancel</button>
|
|
244
|
+
<button onClick={handleConfirmSendWithSecrets} className="px-4 py-2 text-sm rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2">
|
|
245
|
+
<Shield className="w-4 h-4" /> Redact & Send
|
|
246
|
+
</button>
|
|
364
247
|
</div>
|
|
365
248
|
</div>
|
|
366
|
-
<div className="p-4 border-t border-border flex justify-end gap-2">
|
|
367
|
-
<button
|
|
368
|
-
onClick={handleCancelSendWithSecrets}
|
|
369
|
-
className="px-4 py-2 text-sm rounded-lg bg-muted text-muted-foreground hover:bg-muted/80"
|
|
370
|
-
>
|
|
371
|
-
Cancel
|
|
372
|
-
</button>
|
|
373
|
-
<button
|
|
374
|
-
onClick={handleConfirmSendWithSecrets}
|
|
375
|
-
className="px-4 py-2 text-sm rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2"
|
|
376
|
-
>
|
|
377
|
-
<Shield className="w-4 h-4" />
|
|
378
|
-
Redact & Send
|
|
379
|
-
</button>
|
|
380
|
-
</div>
|
|
381
|
-
</div>
|
|
382
|
-
</div>
|
|
383
|
-
)}
|
|
384
|
-
|
|
385
|
-
{/* Input Area */}
|
|
386
|
-
<footer className="p-3 border-t border-border bg-card/50">
|
|
387
|
-
{/* Secret detection warning bar */}
|
|
388
|
-
{inputHasSecrets.length > 0 && (
|
|
389
|
-
<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">
|
|
390
|
-
<AlertTriangle className="w-3.5 h-3.5 flex-shrink-0" />
|
|
391
|
-
<span>
|
|
392
|
-
<strong>{inputHasSecrets.length} secret{inputHasSecrets.length > 1 ? 's' : ''}</strong> detected ({inputHasSecrets.map(s => s.name).join(', ')}). Will be auto-redacted on send.
|
|
393
|
-
</span>
|
|
394
249
|
</div>
|
|
395
250
|
)}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
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 ${
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
<button
|
|
416
|
-
type="submit"
|
|
417
|
-
disabled={!input.trim()}
|
|
418
|
-
className="p-2.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
419
|
-
>
|
|
420
|
-
<Send className="w-4 h-4" />
|
|
421
|
-
</button>
|
|
422
|
-
</form>
|
|
423
|
-
</footer>
|
|
251
|
+
|
|
252
|
+
{/* Input Area */}
|
|
253
|
+
<footer className="p-3 border-t border-border bg-card/50">
|
|
254
|
+
{inputHasSecrets.length > 0 && (
|
|
255
|
+
<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">
|
|
256
|
+
<AlertTriangle className="w-3.5 h-3.5 flex-shrink-0" />
|
|
257
|
+
<span><strong>{inputHasSecrets.length} secret{inputHasSecrets.length > 1 ? 's' : ''}</strong> detected. Will be auto-redacted on send.</span>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
<form onSubmit={handleSendMessage} className="max-w-3xl mx-auto flex items-center gap-2">
|
|
261
|
+
<div className="flex-1 relative">
|
|
262
|
+
<input ref={inputRef} type="text" value={input} onChange={(e) => setInput(e.target.value)} placeholder={agents.length === 0 ? "Create an agent first..." : "Type your message..."} disabled={agents.length === 0} 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 ${inputHasSecrets.length > 0 ? 'border-yellow-600/50' : 'border-border'}`} />
|
|
263
|
+
</div>
|
|
264
|
+
<button type="submit" disabled={!input.trim() || agents.length === 0} className="p-2.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
265
|
+
<Send className="w-4 h-4" />
|
|
266
|
+
</button>
|
|
267
|
+
</form>
|
|
268
|
+
</footer>
|
|
269
|
+
</div>
|
|
424
270
|
</div>
|
|
425
271
|
</DashboardLayout>
|
|
426
272
|
);
|