@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
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
PROPRIETARY SOFTWARE LICENSE
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2025 FormKit Inc. All Rights Reserved.
|
|
4
|
+
|
|
5
|
+
UNLICENSED - DO NOT USE
|
|
6
|
+
|
|
7
|
+
This software and associated documentation files (the "Software") are the sole
|
|
8
|
+
and exclusive property of FormKit Inc. ("FormKit").
|
|
9
|
+
|
|
10
|
+
USE RESTRICTIONS
|
|
11
|
+
|
|
12
|
+
The Software is UNLICENSED and proprietary. NO PERMISSION is granted to use,
|
|
13
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
14
|
+
the Software, or to permit persons to whom the Software is furnished to do so,
|
|
15
|
+
under any circumstances, without prior written authorization from FormKit Inc.
|
|
16
|
+
|
|
17
|
+
UNAUTHORIZED USE PROHIBITED
|
|
18
|
+
|
|
19
|
+
Any use of this Software without a valid, written license agreement signed by
|
|
20
|
+
authorized officers of FormKit Inc. is strictly prohibited and constitutes
|
|
21
|
+
unauthorized use and infringement of FormKit's intellectual property rights.
|
|
22
|
+
|
|
23
|
+
LICENSING INQUIRIES
|
|
24
|
+
|
|
25
|
+
Organizations interested in licensing this Software should contact:
|
|
26
|
+
enterprise@formkit.com
|
|
27
|
+
|
|
28
|
+
A written license agreement must be executed before any use of this Software
|
|
29
|
+
is authorized.
|
|
30
|
+
|
|
31
|
+
NO WARRANTY
|
|
32
|
+
|
|
33
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
34
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
35
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
36
|
+
FORMKIT INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
37
|
+
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
38
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
39
|
+
|
|
40
|
+
GOVERNING LAW
|
|
41
|
+
|
|
42
|
+
This license shall be governed by and construed in accordance with the laws
|
|
43
|
+
of the jurisdiction in which FormKit Inc. is incorporated, without regard to
|
|
44
|
+
its conflict of law provisions.
|
|
45
|
+
|
|
46
|
+
FormKit Inc.
|
|
47
|
+
https://formkit.com
|
|
48
|
+
enterprise@formkit.com
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { Inter } from 'next/font/google'
|
|
3
|
+
import '../../src/index.css'
|
|
4
|
+
|
|
5
|
+
const inter = Inter({ subsets: ['latin'] })
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: 'Chat',
|
|
9
|
+
description: 'Standard Agents Chat',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function RootLayout({
|
|
13
|
+
children,
|
|
14
|
+
}: {
|
|
15
|
+
children: React.ReactNode
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en" className="dark">
|
|
19
|
+
<body className={`${inter.className} bg-[#0a0a0b] text-zinc-100 antialiased`}>
|
|
20
|
+
{children}
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { AgentBuilderProvider } from '@standardagents/react'
|
|
4
|
+
import { ThemeProvider } from '../../src/hooks/useTheme'
|
|
5
|
+
import { AuthProvider } from '../../src/hooks/useAuth'
|
|
6
|
+
import App from '../../src/App'
|
|
7
|
+
|
|
8
|
+
// SDK endpoint - uses rewrites so just /api
|
|
9
|
+
const SDK_ENDPOINT = '/api'
|
|
10
|
+
|
|
11
|
+
export default function Page() {
|
|
12
|
+
return (
|
|
13
|
+
<ThemeProvider>
|
|
14
|
+
<AuthProvider>
|
|
15
|
+
<AgentBuilderProvider config={{ endpoint: SDK_ENDPOINT }}>
|
|
16
|
+
<App />
|
|
17
|
+
</AgentBuilderProvider>
|
|
18
|
+
</AuthProvider>
|
|
19
|
+
</ThemeProvider>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/// <reference types="next" />
|
|
2
|
+
/// <reference types="next/image-types/global" />
|
|
3
|
+
/// <reference path="./.next/types/routes.d.ts" />
|
|
4
|
+
|
|
5
|
+
// NOTE: This file should not be edited
|
|
6
|
+
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { NextConfig } from 'next'
|
|
2
|
+
|
|
3
|
+
const nextConfig: NextConfig = {
|
|
4
|
+
async rewrites() {
|
|
5
|
+
const builderUrl = process.env.NEXT_PUBLIC_AGENTBUILDER_URL || 'http://localhost:5173'
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
source: '/api/:path*',
|
|
9
|
+
destination: `${builderUrl}/api/:path*`,
|
|
10
|
+
},
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default nextConfig
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["../src/*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../src/**/*.ts", "../src/**/*.tsx"],
|
|
26
|
+
"exclude": ["node_modules"]
|
|
27
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@standardagents/chat",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev:vite": "vite --config vite/vite.config.ts",
|
|
8
|
+
"build:vite": "vite build --config vite/vite.config.ts",
|
|
9
|
+
"dev:next": "cd next && next dev --port 5175",
|
|
10
|
+
"build:next": "cd next && next build"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@standardagents/react": "workspace:*",
|
|
14
|
+
"next": "^15.1.0",
|
|
15
|
+
"react": "^19.0.0",
|
|
16
|
+
"react-dom": "^19.0.0",
|
|
17
|
+
"react-markdown": "^10.1.0",
|
|
18
|
+
"react-shiki": "^0.9.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
22
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
23
|
+
"@types/node": "^22.0.0",
|
|
24
|
+
"@types/react": "^19.0.0",
|
|
25
|
+
"@types/react-dom": "^19.0.0",
|
|
26
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
27
|
+
"postcss": "^8.5.0",
|
|
28
|
+
"tailwindcss": "^4.0.0",
|
|
29
|
+
"typescript": "^5.9.0",
|
|
30
|
+
"vite": "^6.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/chat/src/App.tsx
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
2
|
+
import { ThreadProvider } from '@standardagents/react'
|
|
3
|
+
import { Sidebar } from './components/Sidebar'
|
|
4
|
+
import { Chat } from './components/Chat'
|
|
5
|
+
import { EmptyState } from './components/EmptyState'
|
|
6
|
+
import { Login } from './components/Login'
|
|
7
|
+
import { useThreads, type Thread } from './hooks/useThreads'
|
|
8
|
+
import { useAuth } from './hooks/useAuth'
|
|
9
|
+
|
|
10
|
+
export default function App() {
|
|
11
|
+
const { isAuthenticated, isLoading: authLoading } = useAuth()
|
|
12
|
+
|
|
13
|
+
// Show loading state while checking auth
|
|
14
|
+
if (authLoading) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="h-screen bg-[var(--bg-primary)] flex items-center justify-center">
|
|
17
|
+
<div className="text-[var(--text-secondary)]">Loading...</div>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Show login if not authenticated
|
|
23
|
+
if (!isAuthenticated) {
|
|
24
|
+
return <Login />
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return <AuthenticatedApp />
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function AuthenticatedApp() {
|
|
31
|
+
const { threads, loading, refreshThreads, deleteThread, updateThreadTitle } = useThreads()
|
|
32
|
+
const [currentThreadId, setCurrentThreadId] = useState<string | null>(() => {
|
|
33
|
+
// Check URL for thread ID on initial load
|
|
34
|
+
const params = new URLSearchParams(window.location.search)
|
|
35
|
+
return params.get('thread')
|
|
36
|
+
})
|
|
37
|
+
const [selectedAgentId, setSelectedAgentId] = useState<string>('')
|
|
38
|
+
const [sidebarOpen, setSidebarOpen] = useState(true)
|
|
39
|
+
|
|
40
|
+
// Update URL when thread changes
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const url = new URL(window.location.href)
|
|
43
|
+
if (currentThreadId) {
|
|
44
|
+
url.searchParams.set('thread', currentThreadId)
|
|
45
|
+
} else {
|
|
46
|
+
url.searchParams.delete('thread')
|
|
47
|
+
}
|
|
48
|
+
window.history.replaceState({}, '', url.toString())
|
|
49
|
+
}, [currentThreadId])
|
|
50
|
+
|
|
51
|
+
const handleNewThread = useCallback(() => {
|
|
52
|
+
setCurrentThreadId(null)
|
|
53
|
+
}, [])
|
|
54
|
+
|
|
55
|
+
const handleSelectThread = useCallback((threadId: string) => {
|
|
56
|
+
setCurrentThreadId(threadId)
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
const handleThreadCreated = useCallback((thread: Thread) => {
|
|
60
|
+
refreshThreads()
|
|
61
|
+
setCurrentThreadId(thread.id)
|
|
62
|
+
}, [refreshThreads])
|
|
63
|
+
|
|
64
|
+
const handleAgentChange = useCallback((agentId: string) => {
|
|
65
|
+
setSelectedAgentId(agentId)
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
const handleDeleteThread = useCallback(async (threadId: string) => {
|
|
69
|
+
await deleteThread(threadId)
|
|
70
|
+
if (currentThreadId === threadId) {
|
|
71
|
+
setCurrentThreadId(null)
|
|
72
|
+
}
|
|
73
|
+
}, [deleteThread, currentThreadId])
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="h-screen bg-[var(--bg-primary)]">
|
|
77
|
+
<Sidebar
|
|
78
|
+
threads={threads}
|
|
79
|
+
currentThreadId={currentThreadId}
|
|
80
|
+
onSelectThread={handleSelectThread}
|
|
81
|
+
onNewThread={handleNewThread}
|
|
82
|
+
onDeleteThread={handleDeleteThread}
|
|
83
|
+
isOpen={sidebarOpen}
|
|
84
|
+
onToggle={() => setSidebarOpen(!sidebarOpen)}
|
|
85
|
+
loading={loading}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
{/* Main content with animated margin */}
|
|
89
|
+
<main
|
|
90
|
+
className={`
|
|
91
|
+
h-full flex flex-col min-w-0 relative
|
|
92
|
+
transition-[margin] duration-300 ease-out
|
|
93
|
+
${sidebarOpen ? 'ml-72' : 'ml-0'}
|
|
94
|
+
`}
|
|
95
|
+
>
|
|
96
|
+
{/* Expand sidebar button - shown when collapsed */}
|
|
97
|
+
<button
|
|
98
|
+
onClick={() => setSidebarOpen(true)}
|
|
99
|
+
className={`
|
|
100
|
+
fixed top-[5.5px] left-3 z-30 p-2 rounded-lg hover:bg-[var(--bg-hover)]
|
|
101
|
+
transition-all duration-300 ease-out
|
|
102
|
+
${sidebarOpen ? 'opacity-0 pointer-events-none -translate-x-2' : 'opacity-100 translate-x-0'}
|
|
103
|
+
`}
|
|
104
|
+
title="Expand sidebar"
|
|
105
|
+
>
|
|
106
|
+
{/* ChatGPT drawer icon */}
|
|
107
|
+
<svg className="w-5 h-5 text-[var(--text-secondary)]" viewBox="0 0 20 20" fill="currentColor">
|
|
108
|
+
<path d="M6.835 4c-.451.004-.82.012-1.137.038-.386.032-.659.085-.876.162l-.2.086c-.44.224-.807.564-1.063.982l-.103.184c-.126.247-.206.562-.248 1.076-.043.523-.043 1.19-.043 2.135v2.664c0 .944 0 1.612.043 2.135.042.515.122.829.248 1.076l.103.184c.256.418.624.758 1.063.982l.2.086c.217.077.49.13.876.162.316.026.685.034 1.136.038zm11.33 7.327c0 .922 0 1.654-.048 2.243-.043.522-.125.977-.305 1.395l-.082.177a4 4 0 0 1-1.473 1.593l-.276.155c-.465.237-.974.338-1.57.387-.59.048-1.322.048-2.244.048H7.833c-.922 0-1.654 0-2.243-.048-.522-.042-.977-.126-1.395-.305l-.176-.082a4 4 0 0 1-1.594-1.473l-.154-.275c-.238-.466-.34-.975-.388-1.572-.048-.589-.048-1.32-.048-2.243V8.663c0-.922 0-1.654.048-2.243.049-.597.15-1.106.388-1.571l.154-.276a4 4 0 0 1 1.594-1.472l.176-.083c.418-.18.873-.263 1.395-.305.589-.048 1.32-.048 2.243-.048h4.334c.922 0 1.654 0 2.243.048.597.049 1.106.15 1.571.388l.276.154a4 4 0 0 1 1.473 1.594l.082.176c.18.418.262.873.305 1.395.048.589.048 1.32.048 2.243zm-10 4.668h4.002c.944 0 1.612 0 2.135-.043.514-.042.829-.122 1.076-.248l.184-.103c.418-.256.758-.624.982-1.063l.086-.2c.077-.217.13-.49.162-.876.043-.523.043-1.19.043-2.135V8.663c0-.944 0-1.612-.043-2.135-.032-.386-.085-.659-.162-.876l-.086-.2a2.67 2.67 0 0 0-.982-1.063l-.184-.103c-.247-.126-.562-.206-1.076-.248-.523-.043-1.19-.043-2.135-.043H8.164L8.165 4z" />
|
|
109
|
+
</svg>
|
|
110
|
+
</button>
|
|
111
|
+
|
|
112
|
+
{currentThreadId ? (
|
|
113
|
+
<ThreadProvider key={currentThreadId} threadId={currentThreadId} live useWorkblocks>
|
|
114
|
+
<Chat
|
|
115
|
+
thread={threads.find(t => t.id === currentThreadId)}
|
|
116
|
+
onUpdateTitle={(title) => updateThreadTitle(currentThreadId, title)}
|
|
117
|
+
sidebarOpen={sidebarOpen}
|
|
118
|
+
/>
|
|
119
|
+
</ThreadProvider>
|
|
120
|
+
) : (
|
|
121
|
+
<EmptyState
|
|
122
|
+
selectedAgentId={selectedAgentId}
|
|
123
|
+
onAgentChange={handleAgentChange}
|
|
124
|
+
onThreadCreated={handleThreadCreated}
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
</main>
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from 'react'
|
|
4
|
+
|
|
5
|
+
interface Agent {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
title?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface AgentSelectorProps {
|
|
12
|
+
selectedAgentId: string
|
|
13
|
+
onSelectAgent: (agentId: string) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AgentSelector({ selectedAgentId, onSelectAgent }: AgentSelectorProps) {
|
|
17
|
+
const [agents, setAgents] = useState<Agent[]>([])
|
|
18
|
+
const [loading, setLoading] = useState(true)
|
|
19
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
20
|
+
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const fetchAgents = async () => {
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch('/api/agents')
|
|
26
|
+
if (!response.ok) throw new Error('Failed to fetch agents')
|
|
27
|
+
const data = await response.json()
|
|
28
|
+
const agentList = data.agents || []
|
|
29
|
+
setAgents(agentList)
|
|
30
|
+
if (!selectedAgentId && agentList.length > 0) {
|
|
31
|
+
onSelectAgent(agentList[0].id)
|
|
32
|
+
}
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error('Failed to load agents:', err)
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
fetchAgents()
|
|
40
|
+
}, [selectedAgentId, onSelectAgent])
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
44
|
+
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
|
45
|
+
setIsOpen(false)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
49
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
52
|
+
const selectedAgent = agents.find(a => a.id === selectedAgentId)
|
|
53
|
+
|
|
54
|
+
if (loading) {
|
|
55
|
+
return (
|
|
56
|
+
<div className="inline-flex items-center gap-2 px-4 py-2 bg-zinc-800 rounded-full text-sm text-zinc-400">
|
|
57
|
+
Loading agents...
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (agents.length === 0) {
|
|
63
|
+
return (
|
|
64
|
+
<div className="inline-flex items-center gap-2 px-4 py-2 bg-zinc-800 rounded-full text-sm text-zinc-400">
|
|
65
|
+
No agents available
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div ref={dropdownRef} className="relative inline-block">
|
|
72
|
+
<button
|
|
73
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
74
|
+
className="inline-flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-full text-sm transition-colors"
|
|
75
|
+
>
|
|
76
|
+
<span className="text-zinc-300">{selectedAgent?.title || selectedAgent?.name || 'Select agent'}</span>
|
|
77
|
+
<svg className={`w-4 h-4 text-zinc-500 transition-transform ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
78
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
79
|
+
</svg>
|
|
80
|
+
</button>
|
|
81
|
+
|
|
82
|
+
{isOpen && (
|
|
83
|
+
<div className="absolute top-full mt-2 left-1/2 -translate-x-1/2 min-w-[200px] bg-zinc-800 border border-zinc-700 rounded-xl shadow-xl overflow-hidden z-50">
|
|
84
|
+
{agents.map((agent) => (
|
|
85
|
+
<button
|
|
86
|
+
key={agent.id}
|
|
87
|
+
onClick={() => {
|
|
88
|
+
onSelectAgent(agent.id)
|
|
89
|
+
setIsOpen(false)
|
|
90
|
+
}}
|
|
91
|
+
className={`w-full px-4 py-3 text-left text-sm hover:bg-zinc-700 transition-colors ${
|
|
92
|
+
agent.id === selectedAgentId ? 'bg-zinc-700 text-white' : 'text-zinc-300'
|
|
93
|
+
}`}
|
|
94
|
+
>
|
|
95
|
+
{agent.title || agent.name}
|
|
96
|
+
</button>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
4
|
+
import { useThread } from '@standardagents/react'
|
|
5
|
+
import { MessageList } from './MessageList'
|
|
6
|
+
import { MessageInput } from './MessageInput'
|
|
7
|
+
import { type Thread, getThreadTitle } from '../hooks/useThreads'
|
|
8
|
+
|
|
9
|
+
interface ChatProps {
|
|
10
|
+
thread?: Thread
|
|
11
|
+
onUpdateTitle?: (title: string) => void
|
|
12
|
+
sidebarOpen?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Chat({ thread, onUpdateTitle, sidebarOpen = true }: ChatProps) {
|
|
16
|
+
const { status } = useThread()
|
|
17
|
+
const [isEditingTitle, setIsEditingTitle] = useState(false)
|
|
18
|
+
const [titleValue, setTitleValue] = useState('')
|
|
19
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
20
|
+
|
|
21
|
+
const agentName = thread?.agent?.title || thread?.agent?.name || 'Agent'
|
|
22
|
+
const threadTitle = thread ? getThreadTitle(thread) : 'Untitled'
|
|
23
|
+
|
|
24
|
+
// Focus input when entering edit mode
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (isEditingTitle && inputRef.current) {
|
|
27
|
+
inputRef.current.focus()
|
|
28
|
+
inputRef.current.select()
|
|
29
|
+
}
|
|
30
|
+
}, [isEditingTitle])
|
|
31
|
+
|
|
32
|
+
const handleStartEdit = useCallback(() => {
|
|
33
|
+
const currentTitle = thread ? getThreadTitle(thread) : ''
|
|
34
|
+
setTitleValue(currentTitle === 'Untitled' ? '' : currentTitle)
|
|
35
|
+
setIsEditingTitle(true)
|
|
36
|
+
}, [thread])
|
|
37
|
+
|
|
38
|
+
const handleSaveTitle = useCallback(() => {
|
|
39
|
+
const trimmed = titleValue.trim()
|
|
40
|
+
if (trimmed && onUpdateTitle) {
|
|
41
|
+
onUpdateTitle(trimmed)
|
|
42
|
+
}
|
|
43
|
+
setIsEditingTitle(false)
|
|
44
|
+
}, [titleValue, onUpdateTitle])
|
|
45
|
+
|
|
46
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
47
|
+
if (e.key === 'Enter') {
|
|
48
|
+
handleSaveTitle()
|
|
49
|
+
} else if (e.key === 'Escape') {
|
|
50
|
+
setIsEditingTitle(false)
|
|
51
|
+
}
|
|
52
|
+
}, [handleSaveTitle])
|
|
53
|
+
|
|
54
|
+
// Connection status LED color
|
|
55
|
+
const ledColor = status === 'connected'
|
|
56
|
+
? 'bg-emerald-500'
|
|
57
|
+
: status === 'connecting' || status === 'reconnecting'
|
|
58
|
+
? 'bg-yellow-500 animate-pulse'
|
|
59
|
+
: 'bg-red-500'
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="flex-1 flex flex-col w-full">
|
|
63
|
+
{/* Header bar - frosted glass effect, fixed on scroll */}
|
|
64
|
+
<div className={`
|
|
65
|
+
sticky top-0 z-20 h-[47px] flex items-center border-b border-[var(--border-primary)] bg-[var(--bg-primary)]/60 backdrop-blur-md
|
|
66
|
+
transition-[padding] duration-300 ease-out
|
|
67
|
+
${sidebarOpen ? 'px-4' : 'pl-14 pr-4'}
|
|
68
|
+
`}>
|
|
69
|
+
<div className="flex items-center justify-between w-full">
|
|
70
|
+
{/* Editable title on the left */}
|
|
71
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
72
|
+
{isEditingTitle ? (
|
|
73
|
+
<input
|
|
74
|
+
ref={inputRef}
|
|
75
|
+
type="text"
|
|
76
|
+
value={titleValue}
|
|
77
|
+
onChange={(e) => setTitleValue(e.target.value)}
|
|
78
|
+
onBlur={handleSaveTitle}
|
|
79
|
+
onKeyDown={handleKeyDown}
|
|
80
|
+
className="text-sm font-medium text-[var(--text-primary)] bg-transparent outline-none w-full max-w-xs"
|
|
81
|
+
placeholder="Enter title..."
|
|
82
|
+
/>
|
|
83
|
+
) : (
|
|
84
|
+
<>
|
|
85
|
+
<span className="text-sm font-medium text-[var(--text-primary)] truncate max-w-xs">
|
|
86
|
+
{threadTitle}
|
|
87
|
+
</span>
|
|
88
|
+
<button
|
|
89
|
+
onClick={handleStartEdit}
|
|
90
|
+
className="p-1 text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors shrink-0"
|
|
91
|
+
title="Edit title"
|
|
92
|
+
>
|
|
93
|
+
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
94
|
+
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
|
95
|
+
<path d="m15 5 4 4" />
|
|
96
|
+
</svg>
|
|
97
|
+
</button>
|
|
98
|
+
</>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Agent name and connection status on the right */}
|
|
103
|
+
<div className="flex items-center gap-3 ml-4">
|
|
104
|
+
<span className="text-sm text-[var(--text-tertiary)] hidden sm:inline">{agentName}</span>
|
|
105
|
+
<div
|
|
106
|
+
className={`w-2 h-2 rounded-full ${ledColor}`}
|
|
107
|
+
title={status === 'connected' ? 'Connected' : status === 'connecting' || status === 'reconnecting' ? 'Connecting...' : 'Disconnected'}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div
|
|
114
|
+
className="flex-1 overflow-y-auto"
|
|
115
|
+
style={{
|
|
116
|
+
maskImage: 'linear-gradient(to bottom, black calc(100% - 80px), transparent 100%)',
|
|
117
|
+
WebkitMaskImage: 'linear-gradient(to bottom, black calc(100% - 80px), transparent 100%)',
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<div className="max-w-3xl mx-auto w-full pb-20">
|
|
121
|
+
<MessageList />
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div className="sticky bottom-0 z-20 pb-4 px-4 pointer-events-none">
|
|
126
|
+
{/* Opaque blocker behind input - covers bottom half of input area down */}
|
|
127
|
+
<div className="absolute bottom-0 left-0 right-0 h-12 bg-[var(--bg-primary)] pointer-events-none" />
|
|
128
|
+
<div className="relative max-w-3xl mx-auto w-full pointer-events-auto">
|
|
129
|
+
<MessageInput />
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|