@standardagents/cli 0.10.1-next.bbd142a → 0.11.0-next.99fb790
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.txt +48 -0
- package/chat/next/app/layout.tsx +24 -0
- package/chat/next/app/page.tsx +21 -0
- package/chat/next/next-env.d.ts +6 -0
- package/chat/next/next.config.ts +15 -0
- package/chat/next/postcss.config.mjs +5 -0
- package/chat/next/tsconfig.json +27 -0
- package/chat/package.json +32 -0
- package/chat/src/App.tsx +130 -0
- package/chat/src/components/AgentSelector.tsx +102 -0
- package/chat/src/components/Chat.tsx +134 -0
- package/chat/src/components/EmptyState.tsx +437 -0
- package/chat/src/components/Login.tsx +139 -0
- package/chat/src/components/Logo.tsx +39 -0
- package/chat/src/components/Markdown.tsx +222 -0
- package/chat/src/components/MessageInput.tsx +197 -0
- package/chat/src/components/MessageList.tsx +850 -0
- package/chat/src/components/Sidebar.tsx +253 -0
- package/chat/src/hooks/useAuth.tsx +118 -0
- package/chat/src/hooks/useTheme.tsx +55 -0
- package/chat/src/hooks/useThreads.ts +131 -0
- package/chat/src/index.css +168 -0
- package/chat/tsconfig.json +24 -0
- package/chat/vite/favicon.svg +3 -0
- package/chat/vite/index.html +17 -0
- package/chat/vite/main.tsx +25 -0
- package/chat/vite/vite.config.ts +23 -0
- package/dist/index.js +671 -104
- package/dist/index.js.map +1 -1
- package/package.json +13 -9
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
|
|
4
|
+
import type { Thread } from '../hooks/useThreads'
|
|
5
|
+
import { Logo } from './Logo'
|
|
6
|
+
|
|
7
|
+
interface Agent {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
title?: string
|
|
11
|
+
description?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PendingAttachment {
|
|
15
|
+
id: string
|
|
16
|
+
name: string
|
|
17
|
+
mimeType: string
|
|
18
|
+
data: string // base64
|
|
19
|
+
width?: number
|
|
20
|
+
height?: number
|
|
21
|
+
preview?: string // data URL for preview
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface EmptyStateProps {
|
|
25
|
+
selectedAgentId: string
|
|
26
|
+
onAgentChange: (agentId: string) => void
|
|
27
|
+
onThreadCreated: (thread: Thread) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Helper to get API config
|
|
31
|
+
function getApiConfig() {
|
|
32
|
+
const isVite = typeof import.meta !== 'undefined' && import.meta.env
|
|
33
|
+
const baseUrl = isVite
|
|
34
|
+
? (import.meta.env.VITE_AGENTBUILDER_URL || '')
|
|
35
|
+
: (process.env.NEXT_PUBLIC_AGENTBUILDER_URL || '')
|
|
36
|
+
|
|
37
|
+
// Token is stored in localStorage by auth flow
|
|
38
|
+
const token = typeof localStorage !== 'undefined'
|
|
39
|
+
? localStorage.getItem('agentbuilder_auth_token') || ''
|
|
40
|
+
: ''
|
|
41
|
+
|
|
42
|
+
return { baseUrl, token }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Read file as base64
|
|
46
|
+
function readFileAsBase64(file: File): Promise<string> {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const reader = new FileReader()
|
|
49
|
+
reader.onload = () => {
|
|
50
|
+
const result = reader.result as string
|
|
51
|
+
const base64 = result.split(',')[1]
|
|
52
|
+
resolve(base64)
|
|
53
|
+
}
|
|
54
|
+
reader.onerror = reject
|
|
55
|
+
reader.readAsDataURL(file)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Get image dimensions
|
|
60
|
+
function getImageDimensions(file: File): Promise<{ width: number; height: number } | null> {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
if (!file.type.startsWith('image/')) {
|
|
63
|
+
resolve(null)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
const img = new Image()
|
|
67
|
+
img.onload = () => {
|
|
68
|
+
resolve({ width: img.naturalWidth, height: img.naturalHeight })
|
|
69
|
+
URL.revokeObjectURL(img.src)
|
|
70
|
+
}
|
|
71
|
+
img.onerror = () => resolve(null)
|
|
72
|
+
img.src = URL.createObjectURL(file)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function EmptyState({
|
|
77
|
+
selectedAgentId,
|
|
78
|
+
onAgentChange,
|
|
79
|
+
onThreadCreated,
|
|
80
|
+
}: EmptyStateProps) {
|
|
81
|
+
const [message, setMessage] = useState('')
|
|
82
|
+
const [attachments, setAttachments] = useState<PendingAttachment[]>([])
|
|
83
|
+
const [isCreating, setIsCreating] = useState(false)
|
|
84
|
+
const [agents, setAgents] = useState<Agent[]>([])
|
|
85
|
+
const [agentsLoading, setAgentsLoading] = useState(true)
|
|
86
|
+
const [dropdownOpen, setDropdownOpen] = useState(false)
|
|
87
|
+
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
88
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
89
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
90
|
+
|
|
91
|
+
const { baseUrl, token } = useMemo(() => getApiConfig(), [])
|
|
92
|
+
const headers = useMemo(() => {
|
|
93
|
+
const h: Record<string, string> = { 'Content-Type': 'application/json' }
|
|
94
|
+
if (token) h['Authorization'] = `Bearer ${token}`
|
|
95
|
+
return h
|
|
96
|
+
}, [token])
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const fetchAgents = async () => {
|
|
100
|
+
try {
|
|
101
|
+
const response = await fetch(`${baseUrl}/api/agents`, { headers })
|
|
102
|
+
if (!response.ok) throw new Error('Failed to fetch agents')
|
|
103
|
+
const data = await response.json()
|
|
104
|
+
const agentList = data.agents || []
|
|
105
|
+
console.log('Agents from API:', agentList)
|
|
106
|
+
setAgents(agentList)
|
|
107
|
+
if (!selectedAgentId && agentList.length > 0) {
|
|
108
|
+
onAgentChange(agentList[0].id)
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error('Failed to load agents:', err)
|
|
112
|
+
} finally {
|
|
113
|
+
setAgentsLoading(false)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
fetchAgents()
|
|
117
|
+
}, [selectedAgentId, onAgentChange, baseUrl, headers])
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
121
|
+
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
|
122
|
+
setDropdownOpen(false)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
126
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
127
|
+
}, [])
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const textarea = textareaRef.current
|
|
131
|
+
if (textarea) {
|
|
132
|
+
textarea.style.height = 'auto'
|
|
133
|
+
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'
|
|
134
|
+
}
|
|
135
|
+
}, [message])
|
|
136
|
+
|
|
137
|
+
const selectedAgent = agents.find(a => a.id === selectedAgentId)
|
|
138
|
+
|
|
139
|
+
const handleFileSelect = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
140
|
+
const files = e.target.files
|
|
141
|
+
if (!files || files.length === 0) return
|
|
142
|
+
|
|
143
|
+
const newAttachments: PendingAttachment[] = []
|
|
144
|
+
|
|
145
|
+
for (const file of Array.from(files)) {
|
|
146
|
+
try {
|
|
147
|
+
const base64 = await readFileAsBase64(file)
|
|
148
|
+
const dimensions = await getImageDimensions(file)
|
|
149
|
+
const preview = file.type.startsWith('image/')
|
|
150
|
+
? URL.createObjectURL(file)
|
|
151
|
+
: undefined
|
|
152
|
+
|
|
153
|
+
newAttachments.push({
|
|
154
|
+
id: crypto.randomUUID(),
|
|
155
|
+
name: file.name,
|
|
156
|
+
mimeType: file.type || 'application/octet-stream',
|
|
157
|
+
data: base64,
|
|
158
|
+
width: dimensions?.width,
|
|
159
|
+
height: dimensions?.height,
|
|
160
|
+
preview,
|
|
161
|
+
})
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error('Failed to read file:', err)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setAttachments(prev => [...prev, ...newAttachments])
|
|
168
|
+
// Reset file input and refocus textarea
|
|
169
|
+
if (fileInputRef.current) {
|
|
170
|
+
fileInputRef.current.value = ''
|
|
171
|
+
}
|
|
172
|
+
textareaRef.current?.focus()
|
|
173
|
+
}, [])
|
|
174
|
+
|
|
175
|
+
const removeAttachment = useCallback((id: string) => {
|
|
176
|
+
setAttachments(prev => {
|
|
177
|
+
const attachment = prev.find(a => a.id === id)
|
|
178
|
+
if (attachment?.preview) {
|
|
179
|
+
URL.revokeObjectURL(attachment.preview)
|
|
180
|
+
}
|
|
181
|
+
return prev.filter(a => a.id !== id)
|
|
182
|
+
})
|
|
183
|
+
}, [])
|
|
184
|
+
|
|
185
|
+
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
|
186
|
+
e.preventDefault()
|
|
187
|
+
if ((!message.trim() && attachments.length === 0) || !selectedAgentId || isCreating) return
|
|
188
|
+
|
|
189
|
+
setIsCreating(true)
|
|
190
|
+
const title = message.slice(0, 50) + (message.length > 50 ? '...' : '') || 'New conversation'
|
|
191
|
+
const currentAttachments = [...attachments]
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// Create thread
|
|
195
|
+
const threadRes = await fetch(`${baseUrl}/api/threads`, {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers,
|
|
198
|
+
body: JSON.stringify({ agent_id: selectedAgentId }),
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
if (!threadRes.ok) throw new Error('Failed to create thread')
|
|
202
|
+
const threadData = await threadRes.json()
|
|
203
|
+
const threadId = threadData.threadId
|
|
204
|
+
|
|
205
|
+
// Set title on the thread via tags
|
|
206
|
+
await fetch(`${baseUrl}/api/threads/${threadId}`, {
|
|
207
|
+
method: 'PATCH',
|
|
208
|
+
headers,
|
|
209
|
+
body: JSON.stringify({ tags: [`title:${title}`] }),
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// Build message payload with attachments
|
|
213
|
+
const messagePayload: {
|
|
214
|
+
role: string
|
|
215
|
+
content: string
|
|
216
|
+
attachments?: Array<{
|
|
217
|
+
name: string
|
|
218
|
+
mimeType: string
|
|
219
|
+
data: string
|
|
220
|
+
width?: number
|
|
221
|
+
height?: number
|
|
222
|
+
}>
|
|
223
|
+
} = {
|
|
224
|
+
role: 'user',
|
|
225
|
+
content: message,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (currentAttachments.length > 0) {
|
|
229
|
+
messagePayload.attachments = currentAttachments.map(a => ({
|
|
230
|
+
name: a.name,
|
|
231
|
+
mimeType: a.mimeType,
|
|
232
|
+
data: a.data,
|
|
233
|
+
...(a.width && a.height ? { width: a.width, height: a.height } : {}),
|
|
234
|
+
}))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Send the first message
|
|
238
|
+
await fetch(`${baseUrl}/api/threads/${threadId}/messages`, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers,
|
|
241
|
+
body: JSON.stringify(messagePayload),
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// Navigate to the thread - use agent info from selected agent
|
|
245
|
+
const selectedAgent = agents.find(a => a.id === selectedAgentId)
|
|
246
|
+
onThreadCreated({
|
|
247
|
+
id: threadId,
|
|
248
|
+
agent_id: selectedAgentId,
|
|
249
|
+
user_id: null,
|
|
250
|
+
tags: [`title:${title}`],
|
|
251
|
+
created_at: Math.floor(Date.now() / 1000), // API uses seconds
|
|
252
|
+
agent: {
|
|
253
|
+
name: selectedAgent?.name,
|
|
254
|
+
title: selectedAgent?.title,
|
|
255
|
+
type: 'ai_human',
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
setMessage('')
|
|
259
|
+
setAttachments([])
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error('Failed to create thread:', error)
|
|
262
|
+
} finally {
|
|
263
|
+
setIsCreating(false)
|
|
264
|
+
}
|
|
265
|
+
}, [message, attachments, selectedAgentId, isCreating, onThreadCreated, baseUrl, headers, agents])
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div className="flex-1 flex flex-col items-center px-4 py-8">
|
|
269
|
+
<div className="w-full max-w-2xl mt-[15vh]">
|
|
270
|
+
{/* Logo and Greeting */}
|
|
271
|
+
<div className="text-center mb-12">
|
|
272
|
+
<Logo className="h-16 text-[var(--text-secondary)] mx-auto mb-20" />
|
|
273
|
+
<h1 className="text-2xl font-medium text-[var(--text-secondary)]">
|
|
274
|
+
What can I help with?
|
|
275
|
+
</h1>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
{/* Input box */}
|
|
279
|
+
<div className="relative bg-[var(--bg-tertiary)] rounded-2xl border border-[var(--border-primary)] shadow-lg shadow-black/10">
|
|
280
|
+
{/* Attachment drawer */}
|
|
281
|
+
{attachments.length > 0 && (
|
|
282
|
+
<div className="px-3 pt-3 pb-2 border-b border-[var(--border-secondary)]">
|
|
283
|
+
<div className="flex flex-wrap gap-2">
|
|
284
|
+
{attachments.map((attachment) => (
|
|
285
|
+
<div key={attachment.id} className="relative group">
|
|
286
|
+
{attachment.preview ? (
|
|
287
|
+
<img
|
|
288
|
+
src={attachment.preview}
|
|
289
|
+
alt={attachment.name}
|
|
290
|
+
className="h-16 w-auto rounded-lg object-cover border border-[var(--border-secondary)]"
|
|
291
|
+
/>
|
|
292
|
+
) : (
|
|
293
|
+
<div className="h-16 px-3 flex items-center gap-2 rounded-lg bg-[var(--bg-hover)] border border-[var(--border-secondary)]">
|
|
294
|
+
<svg className="w-4 h-4 text-[var(--text-secondary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
295
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
296
|
+
</svg>
|
|
297
|
+
<span className="text-xs text-[var(--text-secondary)] max-w-[100px] truncate">{attachment.name}</span>
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
onClick={() => removeAttachment(attachment.id)}
|
|
303
|
+
className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-[var(--bg-active)] hover:bg-[var(--bg-hover)] rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
|
304
|
+
>
|
|
305
|
+
<svg className="w-3 h-3 text-[var(--text-primary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
306
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
307
|
+
</svg>
|
|
308
|
+
</button>
|
|
309
|
+
</div>
|
|
310
|
+
))}
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
)}
|
|
314
|
+
|
|
315
|
+
{/* Agent selector row */}
|
|
316
|
+
<div className="px-4 pt-3 pb-1" ref={dropdownRef}>
|
|
317
|
+
<button
|
|
318
|
+
onClick={() => setDropdownOpen(!dropdownOpen)}
|
|
319
|
+
disabled={agentsLoading || agents.length === 0}
|
|
320
|
+
className="inline-flex items-center gap-1.5 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors disabled:opacity-50"
|
|
321
|
+
>
|
|
322
|
+
{agentsLoading ? (
|
|
323
|
+
<span className="text-[var(--text-tertiary)]">Loading agents...</span>
|
|
324
|
+
) : agents.length === 0 ? (
|
|
325
|
+
<span className="text-[var(--text-tertiary)]">No agents available</span>
|
|
326
|
+
) : (
|
|
327
|
+
<>
|
|
328
|
+
<span className="text-[var(--text-primary)] font-medium">{selectedAgent?.title || selectedAgent?.name || 'Select agent'}</span>
|
|
329
|
+
<svg className={`w-3.5 h-3.5 text-[var(--text-tertiary)] transition-transform ${dropdownOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
330
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
331
|
+
</svg>
|
|
332
|
+
</>
|
|
333
|
+
)}
|
|
334
|
+
</button>
|
|
335
|
+
|
|
336
|
+
{/* Dropdown menu */}
|
|
337
|
+
{dropdownOpen && agents.length > 0 && (
|
|
338
|
+
<div className="absolute left-4 top-12 z-50 min-w-[280px] max-w-[360px] bg-[var(--bg-elevated)] border border-[var(--border-secondary)] rounded-xl shadow-xl overflow-hidden animate-fade-in">
|
|
339
|
+
{agents.map((agent) => (
|
|
340
|
+
<button
|
|
341
|
+
key={agent.id}
|
|
342
|
+
onClick={() => {
|
|
343
|
+
onAgentChange(agent.id)
|
|
344
|
+
setDropdownOpen(false)
|
|
345
|
+
}}
|
|
346
|
+
className={`w-full px-4 py-3 text-left transition-colors ${
|
|
347
|
+
agent.id === selectedAgentId
|
|
348
|
+
? 'bg-[var(--bg-active)]'
|
|
349
|
+
: 'hover:bg-[var(--bg-hover)]'
|
|
350
|
+
}`}
|
|
351
|
+
>
|
|
352
|
+
<div className={`text-sm font-medium ${agent.id === selectedAgentId ? 'text-[var(--text-primary)]' : 'text-[var(--text-secondary)]'}`}>
|
|
353
|
+
{agent.title || agent.name}
|
|
354
|
+
</div>
|
|
355
|
+
{agent.description && (
|
|
356
|
+
<div className="text-xs text-[var(--text-muted)] mt-0.5 line-clamp-2">
|
|
357
|
+
{agent.description}
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
360
|
+
</button>
|
|
361
|
+
))}
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
{/* Textarea */}
|
|
367
|
+
<form onSubmit={handleSubmit}>
|
|
368
|
+
<textarea
|
|
369
|
+
ref={textareaRef}
|
|
370
|
+
value={message}
|
|
371
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
372
|
+
onKeyDown={(e) => {
|
|
373
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
374
|
+
e.preventDefault()
|
|
375
|
+
handleSubmit(e)
|
|
376
|
+
}
|
|
377
|
+
}}
|
|
378
|
+
placeholder="Message..."
|
|
379
|
+
rows={1}
|
|
380
|
+
disabled={isCreating || !selectedAgentId}
|
|
381
|
+
className="w-full px-4 py-3 bg-transparent resize-none text-[var(--text-primary)] placeholder-[var(--text-tertiary)] focus:outline-none disabled:opacity-50 text-[15px] leading-relaxed"
|
|
382
|
+
/>
|
|
383
|
+
|
|
384
|
+
{/* Actions row */}
|
|
385
|
+
<div className="flex items-center justify-between px-3 pb-3">
|
|
386
|
+
{/* Attachment button */}
|
|
387
|
+
<div>
|
|
388
|
+
<input
|
|
389
|
+
ref={fileInputRef}
|
|
390
|
+
type="file"
|
|
391
|
+
multiple
|
|
392
|
+
accept="image/*,.pdf,.txt,.md,.json,.csv"
|
|
393
|
+
onChange={handleFileSelect}
|
|
394
|
+
className="hidden"
|
|
395
|
+
/>
|
|
396
|
+
<button
|
|
397
|
+
type="button"
|
|
398
|
+
onClick={() => fileInputRef.current?.click()}
|
|
399
|
+
disabled={isCreating || !selectedAgentId}
|
|
400
|
+
className="p-2 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
401
|
+
title="Attach files"
|
|
402
|
+
>
|
|
403
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
404
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1.5 1.5 0 002.112 2.13" />
|
|
405
|
+
</svg>
|
|
406
|
+
</button>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
{/* Submit button */}
|
|
410
|
+
<button
|
|
411
|
+
type="submit"
|
|
412
|
+
disabled={(!message.trim() && attachments.length === 0) || !selectedAgentId || isCreating}
|
|
413
|
+
className="p-2 rounded-xl bg-[var(--text-primary)] text-[var(--bg-primary)] hover:opacity-90 disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-150"
|
|
414
|
+
>
|
|
415
|
+
{isCreating ? (
|
|
416
|
+
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
417
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
418
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
419
|
+
</svg>
|
|
420
|
+
) : (
|
|
421
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
422
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
|
423
|
+
</svg>
|
|
424
|
+
)}
|
|
425
|
+
</button>
|
|
426
|
+
</div>
|
|
427
|
+
</form>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
{/* Subtle hint */}
|
|
431
|
+
<p className="text-center text-xs text-[var(--text-muted)] mt-4">
|
|
432
|
+
Press Enter to send, Shift+Enter for new line
|
|
433
|
+
</p>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
)
|
|
437
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, type FormEvent } from 'react'
|
|
4
|
+
import { useAuth } from '../hooks/useAuth'
|
|
5
|
+
import { Logo } from './Logo'
|
|
6
|
+
|
|
7
|
+
export function Login() {
|
|
8
|
+
const [username, setUsername] = useState('')
|
|
9
|
+
const [password, setPassword] = useState('')
|
|
10
|
+
const [error, setError] = useState('')
|
|
11
|
+
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
12
|
+
const { login } = useAuth()
|
|
13
|
+
|
|
14
|
+
const handleSubmit = useCallback(async (e: FormEvent) => {
|
|
15
|
+
e.preventDefault()
|
|
16
|
+
setError('')
|
|
17
|
+
setIsSubmitting(true)
|
|
18
|
+
|
|
19
|
+
const result = await login(username, password)
|
|
20
|
+
|
|
21
|
+
if (!result.success) {
|
|
22
|
+
setError(result.error || 'Login failed')
|
|
23
|
+
setIsSubmitting(false)
|
|
24
|
+
}
|
|
25
|
+
}, [username, password, login])
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="min-h-screen flex items-center justify-center px-4 relative overflow-hidden bg-[var(--bg-primary)]">
|
|
29
|
+
{/* Subtle gradient background */}
|
|
30
|
+
<div
|
|
31
|
+
className="absolute inset-0 opacity-50"
|
|
32
|
+
style={{
|
|
33
|
+
background: 'radial-gradient(ellipse at top, var(--bg-secondary) 0%, transparent 50%), radial-gradient(ellipse at bottom right, var(--bg-tertiary) 0%, transparent 50%)',
|
|
34
|
+
}}
|
|
35
|
+
/>
|
|
36
|
+
|
|
37
|
+
{/* Noise texture overlay */}
|
|
38
|
+
<div
|
|
39
|
+
className="absolute inset-0 opacity-[0.015] pointer-events-none"
|
|
40
|
+
style={{
|
|
41
|
+
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
|
|
42
|
+
}}
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
{/* Content */}
|
|
46
|
+
<div className="w-full max-w-md relative z-10">
|
|
47
|
+
{/* Logo above the card */}
|
|
48
|
+
<div className="flex justify-center mb-10">
|
|
49
|
+
<Logo className="w-72 h-auto text-[var(--text-secondary)] drop-shadow-sm" />
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Login card */}
|
|
53
|
+
<div className="bg-[var(--bg-secondary)]/80 backdrop-blur-xl rounded-3xl border border-[var(--border-primary)] shadow-2xl shadow-black/10 p-8">
|
|
54
|
+
<form onSubmit={handleSubmit} className="space-y-5">
|
|
55
|
+
{error && (
|
|
56
|
+
<div className="p-4 rounded-2xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm flex items-center gap-3">
|
|
57
|
+
<svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
58
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
|
59
|
+
</svg>
|
|
60
|
+
{error}
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
<div className="space-y-2">
|
|
65
|
+
<label
|
|
66
|
+
htmlFor="username"
|
|
67
|
+
className="block text-sm font-medium text-[var(--text-secondary)]"
|
|
68
|
+
>
|
|
69
|
+
Username
|
|
70
|
+
</label>
|
|
71
|
+
<input
|
|
72
|
+
id="username"
|
|
73
|
+
type="text"
|
|
74
|
+
value={username}
|
|
75
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
76
|
+
className="w-full px-4 py-3 rounded-xl bg-[var(--bg-primary)] border border-[var(--border-primary)] text-[var(--text-primary)] placeholder-[var(--text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--text-secondary)]/20 focus:border-[var(--border-secondary)] transition-all"
|
|
77
|
+
placeholder="Enter your username"
|
|
78
|
+
required
|
|
79
|
+
autoComplete="username"
|
|
80
|
+
disabled={isSubmitting}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="space-y-2">
|
|
85
|
+
<label
|
|
86
|
+
htmlFor="password"
|
|
87
|
+
className="block text-sm font-medium text-[var(--text-secondary)]"
|
|
88
|
+
>
|
|
89
|
+
Password
|
|
90
|
+
</label>
|
|
91
|
+
<input
|
|
92
|
+
id="password"
|
|
93
|
+
type="password"
|
|
94
|
+
value={password}
|
|
95
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
96
|
+
className="w-full px-4 py-3 rounded-xl bg-[var(--bg-primary)] border border-[var(--border-primary)] text-[var(--text-primary)] placeholder-[var(--text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--text-secondary)]/20 focus:border-[var(--border-secondary)] transition-all"
|
|
97
|
+
placeholder="Enter your password"
|
|
98
|
+
required
|
|
99
|
+
autoComplete="current-password"
|
|
100
|
+
disabled={isSubmitting}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<button
|
|
105
|
+
type="submit"
|
|
106
|
+
disabled={isSubmitting || !username || !password}
|
|
107
|
+
className="w-full py-3.5 px-4 rounded-xl bg-[var(--text-primary)] text-[var(--bg-primary)] font-semibold hover:opacity-90 hover:scale-[1.01] active:scale-[0.99] disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-150 shadow-lg shadow-black/10"
|
|
108
|
+
>
|
|
109
|
+
{isSubmitting ? (
|
|
110
|
+
<span className="flex items-center justify-center gap-2">
|
|
111
|
+
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
112
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
113
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
114
|
+
</svg>
|
|
115
|
+
Signing in...
|
|
116
|
+
</span>
|
|
117
|
+
) : (
|
|
118
|
+
'Sign in'
|
|
119
|
+
)}
|
|
120
|
+
</button>
|
|
121
|
+
</form>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{/* Subtle footer */}
|
|
125
|
+
<p className="text-center text-xs text-[var(--text-muted)] mt-6">
|
|
126
|
+
Powered by{' '}
|
|
127
|
+
<a
|
|
128
|
+
href="https://standardagentbuilder.com"
|
|
129
|
+
target="_blank"
|
|
130
|
+
rel="noopener noreferrer"
|
|
131
|
+
className="text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
|
132
|
+
>
|
|
133
|
+
AgentBuilder
|
|
134
|
+
</a>
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function LogoMark({ className }: { className?: string }) {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
viewBox="0 0 150 150"
|
|
6
|
+
fill="currentColor"
|
|
7
|
+
className={className}
|
|
8
|
+
>
|
|
9
|
+
<path d="M44.06,0v44.08H0v105.92h105.93v-44.07h44.07V0H44.06ZM19.09,130.91c-16.47-16.47-5.29-54.45,24.96-85.18v60.2h60.23c-30.73,30.27-68.71,41.47-85.2,24.98ZM105.93,104.29v-60.21h-60.21C76.46,13.8,114.42,2.6,130.91,19.09c16.51,16.49,5.31,54.47-24.98,85.2Z" />
|
|
10
|
+
</svg>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Logo({ className }: { className?: string }) {
|
|
15
|
+
return (
|
|
16
|
+
<svg
|
|
17
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
18
|
+
viewBox="0 0 877.15 150"
|
|
19
|
+
fill="currentColor"
|
|
20
|
+
className={className}
|
|
21
|
+
>
|
|
22
|
+
<path d="M44.06,0v44.08H0v105.92h105.93v-44.07h44.07V0H44.06ZM19.09,130.91c-16.47-16.47-5.29-54.45,24.96-85.18v60.2h60.23c-30.73,30.27-68.71,41.47-85.2,24.98ZM105.93,104.29v-60.21h-60.21C76.46,13.8,114.42,2.6,130.91,19.09c16.51,16.49,5.31,54.47-24.98,85.2Z" />
|
|
23
|
+
<g>
|
|
24
|
+
<path d="M203.01,112.34c-8.14,0-16.05-1.81-17.52-2.03-.79.68-1.36,2.03-1.47,2.26h-3.84c-.45-3.84-1.58-14.58-2.26-18.98l3.5-.68,3.73,5.31c3.62,5.2,10.17,7.46,18.87,7.46,10.85,0,16.84-4.18,16.84-13.56,0-10.51-10.51-14.01-19.1-16.72-11.87-3.73-22.6-7.46-22.6-22.94,0-9.38,8.14-20.23,23.28-20.23,3.84,0,6.67.56,9.04,1.13s4.29,1.36,6.44,1.58c.34,0,2.15-1.58,2.37-1.69h2.6l2.94,16.95-4.29.45-2.71-4.97c-2.15-3.96-7.91-7.57-16.5-7.57s-14.24,4.07-14.24,12.54c0,8.93,8.7,11.64,18.08,14.8,11.98,4.07,24.3,8.81,24.3,24.63,0,12.32-11.75,22.26-27.46,22.26Z" />
|
|
25
|
+
<path d="M276.01,63.52h-19.55v34.02c0,3.28.45,4.52,1.24,5.42,1.24,1.36,3.16,2.37,5.31,2.37,4.97,0,8.93-2.03,12.09-3.73v4.75c-2.83,2.26-10.51,6.21-14.92,6.21-7.46,0-13.9-3.73-13.9-10.06,0-2.71.45-10.06.45-20.91v-17.18l-6.44-1.58v-2.03c3.28-2.83,9.38-8.7,14.01-13.56h2.71s-.45,7.57-.57,10.28h20.68l-1.13,5.99Z" />
|
|
26
|
+
<path d="M323.35,112.56c-3.16,0-7.01-.79-8.25-4.75-5.2,2.15-11.53,4.52-17.4,4.52-8.93,0-14.24-4.97-14.24-13.11,0-6.55,2.26-10.62,10.96-12.88,5.54-1.47,12.09-2.37,20.12-3.5v-7.8c0-1.24,0-2.6-.34-4.75-.79-5.2-6.44-7.68-10.85-7.68-2.37,0-4.63.68-6.67,2.03-1.69,1.13-2.26,2.37-2.26,4.18l-8.36,2.03c-.23-.57-.34-1.13-.34-1.81,0-2.15,1.69-4.29,3.62-5.76,2.26-1.81,4.52-3.28,7.34-4.52,3.39-1.58,7.91-3.16,12.32-3.16,3.96,0,7.23,1.02,9.72,2.6,2.6,1.69,4.29,4.18,4.86,7.57.34,1.92.34,4.18.34,7.35v26.56c0,4.52,2.37,6.33,5.31,6.33s4.97-1.24,6.89-2.71l.9,3.5c-4.41,3.05-8.93,5.76-13.67,5.76ZM314.54,87.25c-7.01.79-11.41,1.58-16.61,2.94-4.52,1.13-5.65,3.05-5.65,6.67,0,5.2,3.5,9.72,9.49,9.72,4.18,0,8.48-1.47,12.77-3.05v-16.27Z" />
|
|
27
|
+
<path d="M376.35,110.76v-2.94l2.37-.79c2.49-.79,2.94-2.71,2.94-5.08v-24.18c0-5.2-.45-9.04-2.94-11.3-1.7-1.58-4.18-2.37-7.01-2.37-3.28,0-5.99.79-8.7,1.81-2.49.9-4.75,2.03-6.1,2.83v33.45c0,3.5.68,3.73,3.73,4.29l5.31.9v3.39h-23.96v-2.94l2.49-.79c2.49-.79,2.71-2.37,2.71-5.08v-30.51c0-3.5-.45-4.75-2.15-5.99l-2.6-1.92v-1.92l12.66-6.67,1.81.68v8.93c2.83-2.03,12.54-8.59,19.32-8.59,8.48,0,15.14,5.65,15.14,14.35v31.87c0,3.5.79,3.84,3.73,4.29l5.54.9v3.39h-24.3Z" />
|
|
28
|
+
<path d="M447.65,113.92l-1.13-.9-.68-6.89c-4.52,2.6-10.85,6.33-17.74,6.33-13.33,0-21.58-10.74-21.58-26.22s11.75-29.16,29.27-29.16c3.39,0,8.02.79,10.06,1.36v-13.56c0-4.29-.9-5.65-2.83-6.78l-2.6-1.47v-1.81l13.67-7.8,2.26.79s-.79,8.81-.79,12.77v62.38c0,2.49,1.02,4.07,3.5,4.07.34,0,.68,0,1.13-.11l4.41-.68v4.18l-16.95,3.5ZM445.84,66.91c-4.07-2.15-7.91-3.62-13.56-3.62-9.83,0-16.16,3.62-16.16,17.63s5.31,24.41,17.06,24.41c4.41,0,9.27-1.24,12.66-2.49v-35.94Z" />
|
|
29
|
+
<path d="M512.51,112.56c-3.16,0-7.01-.79-8.25-4.75-5.2,2.15-11.53,4.52-17.4,4.52-8.93,0-14.24-4.97-14.24-13.11,0-6.55,2.26-10.62,10.96-12.88,5.54-1.47,12.09-2.37,20.12-3.5v-7.8c0-1.24,0-2.6-.34-4.75-.79-5.2-6.44-7.68-10.85-7.68-2.37,0-4.63.68-6.67,2.03-1.69,1.13-2.26,2.37-2.26,4.18l-8.36,2.03c-.23-.57-.34-1.13-.34-1.81,0-2.15,1.69-4.29,3.62-5.76,2.26-1.81,4.52-3.28,7.34-4.52,3.39-1.58,7.91-3.16,12.32-3.16,3.96,0,7.23,1.02,9.72,2.6,2.6,1.69,4.29,4.18,4.86,7.57.34,1.92.34,4.18.34,7.35v26.56c0,4.52,2.37,6.33,5.31,6.33s4.97-1.24,6.89-2.71l.9,3.5c-4.41,3.05-8.93,5.76-13.67,5.76ZM503.7,87.25c-7.01.79-11.41,1.58-16.61,2.94-4.52,1.13-5.65,3.05-5.65,6.67,0,5.2,3.5,9.72,9.49,9.72,4.18,0,8.48-1.47,12.77-3.05v-16.27Z" />
|
|
30
|
+
<path d="M564.49,68.94h-.23c-1.47-.79-3.73-1.92-3.73-1.92-2.03-1.02-3.62-1.58-5.08-1.58-1.7,0-3.39.79-5.42,2.37l-3.96,3.05v31.3c0,3.05,1.58,3.5,3.73,3.84l8.14,1.36v3.39h-26.78v-2.94l2.49-.68c2.49-.68,2.71-2.6,2.71-5.2v-29.04c0-3.62-.11-5.31-2.03-7.01l-2.71-2.37v-1.92l12.66-6.67,1.81.68v10.17c1.7-1.7,9.27-7.68,9.27-7.68,2.71-2.26,4.29-3.16,5.54-3.16,1.36,0,2.26,1.13,3.84,3.73l2.03,3.5c-.57,2.03-1.58,5.09-2.26,6.78Z" />
|
|
31
|
+
<path d="M611.95,113.92l-1.13-.9-.68-6.89c-4.52,2.6-10.85,6.33-17.74,6.33-13.33,0-21.58-10.74-21.58-26.22s11.75-29.16,29.27-29.16c3.39,0,8.02.79,10.06,1.36v-13.56c0-4.29-.9-5.65-2.83-6.78l-2.6-1.47v-1.81l13.67-7.8,2.26.79s-.79,8.81-.79,12.77v62.38c0,2.49,1.02,4.07,3.5,4.07.34,0,.68,0,1.13-.11l4.41-.68v4.18l-16.95,3.5ZM610.14,66.91c-4.07-2.15-7.91-3.62-13.56-3.62-9.83,0-16.16,3.62-16.16,17.63s5.31,24.41,17.06,24.41c4.41,0,9.27-1.24,12.66-2.49v-35.94Z" />
|
|
32
|
+
<path d="M703.59,107.37c-5.99,3.05-14.46,5.2-24.07,5.2-26.33,0-39.1-20.34-39.1-40s13.45-40.34,40.34-40.34c7.91,0,13.67,1.58,20,4.18l2.49-3.05h3.39v19.1h-3.96c-1.02-2.83-2.26-5.88-3.39-7.57-4.86-4.07-11.53-6.22-18.19-6.22-14.01,0-28.25,9.27-28.25,32.88,0,20.45,12.21,34.69,29.72,34.69,5.54,0,11.08-1.24,16.27-3.84,1.47-1.13,3.28-3.62,4.52-5.99l3.62.79c-.68,3.5-2.49,8.7-3.39,10.17Z" />
|
|
33
|
+
<path d="M752.86,110.76v-2.94l2.37-.68c2.49-.68,2.83-2.6,2.83-5.2v-24.64c0-8.25-5.76-13.22-12.32-13.22-5.76,0-10.06,3.28-14.13,6.33v31.75c0,3.5.68,3.73,3.73,4.29l5.31.9v3.39h-23.96v-2.94l2.49-.68c2.49-.68,2.71-1.7,2.71-5.2v-55.83c0-4.86-.34-6.55-2.94-8.14l-2.26-1.36v-1.81l13.33-7.8,2.26.79s-.68,9.04-.68,18.31v19.44c5.54-5.54,13.67-9.61,18.99-9.61,8.48,0,17.18,8.02,17.18,17.63v28.59c0,3.5.79,3.84,3.73,4.29l5.42.9v3.39h-24.07Z" />
|
|
34
|
+
<path d="M824.05,112.56c-3.16,0-7.01-.79-8.25-4.75-5.2,2.15-11.53,4.52-17.4,4.52-8.93,0-14.24-4.97-14.24-13.11,0-6.55,2.26-10.62,10.96-12.88,5.54-1.47,12.09-2.37,20.12-3.5v-7.8c0-1.24,0-2.6-.34-4.75-.79-5.2-6.44-7.68-10.85-7.68-2.37,0-4.63.68-6.67,2.03-1.69,1.13-2.26,2.37-2.26,4.18l-8.36,2.03c-.23-.57-.34-1.13-.34-1.81,0-2.15,1.69-4.29,3.62-5.76,2.26-1.81,4.52-3.28,7.34-4.52,3.39-1.58,7.91-3.16,12.32-3.16,3.96,0,7.23,1.02,9.72,2.6,2.6,1.69,4.29,4.18,4.86,7.57.34,1.92.34,4.18.34,7.35v26.56c0,4.52,2.37,6.33,5.31,6.33s4.97-1.24,6.89-2.71l.9,3.5c-4.41,3.05-8.93,5.76-13.67,5.76ZM815.24,87.25c-7.01.79-11.41,1.58-16.61,2.94-4.52,1.13-5.65,3.05-5.65,6.67,0,5.2,3.5,9.72,9.49,9.72,4.18,0,8.48-1.47,12.77-3.05v-16.27Z" />
|
|
35
|
+
<path d="M876.03,63.52h-19.55v34.02c0,3.28.45,4.52,1.24,5.42,1.24,1.36,3.16,2.37,5.31,2.37,4.97,0,8.93-2.03,12.09-3.73v4.75c-2.83,2.26-10.51,6.21-14.92,6.21-7.46,0-13.9-3.73-13.9-10.06,0-2.71.45-10.06.45-20.91v-17.18l-6.44-1.58v-2.03c3.28-2.83,9.38-8.7,14.01-13.56h2.71s-.45,7.57-.57,10.28h20.68l-1.13,5.99Z" />
|
|
36
|
+
</g>
|
|
37
|
+
</svg>
|
|
38
|
+
)
|
|
39
|
+
}
|