Go backend: - LLM client with DeepSeek/Kimi/Ollama/OpenAI support (OpenAI-compat) - Agent loader: AGENT.md frontmatter, SOUL.md, memory read/write - Skill system following agentskills.io standard - Room orchestration: master assign→execute→review loop with streaming - Hub: GitHub repo clone and team package install - Echo HTTP server with WebSocket and full REST API React frontend: - Discord-style 3-panel layout with Tailwind v4 - Zustand store with WebSocket streaming message handling - Chat view: streaming messages, role styles, right panel, drawer buttons - Agent MD editor with Monaco Editor (AGENT.md + SOUL.md) - Market page for GitHub team install/publish Docs: - plan.md with full progress tracking and next steps Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
88 lines
3.6 KiB
TypeScript
88 lines
3.6 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import Editor from '@monaco-editor/react'
|
|
import { useStore } from '../store'
|
|
|
|
const API = '/api'
|
|
|
|
export function AgentsPage() {
|
|
const { agents, fetchAgents } = useStore()
|
|
const [selected, setSelected] = useState<string | null>(null)
|
|
const [tab, setTab] = useState<'AGENT.md' | 'SOUL.md'>('AGENT.md')
|
|
const [content, setContent] = useState('')
|
|
const [newName, setNewName] = useState('')
|
|
|
|
useEffect(() => { fetchAgents() }, [])
|
|
|
|
useEffect(() => {
|
|
if (!selected) return
|
|
fetch(`${API}/agents/${selected}/files/${tab}`).then(r => r.json()).then(d => setContent(d.content || ''))
|
|
}, [selected, tab])
|
|
|
|
const save = async () => {
|
|
if (!selected) return
|
|
await fetch(`${API}/agents/${selected}/files/${tab}`, {
|
|
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ content })
|
|
})
|
|
}
|
|
|
|
const create = async () => {
|
|
if (!newName.trim()) return
|
|
await fetch(`${API}/agents`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: newName.trim() }) })
|
|
setNewName('')
|
|
fetchAgents()
|
|
}
|
|
|
|
const del = async (name: string) => {
|
|
await fetch(`${API}/agents/${name}`, { method: 'DELETE' })
|
|
if (selected === name) setSelected(null)
|
|
fetchAgents()
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{/* Agent list */}
|
|
<div className="w-48 border-r border-gray-700 flex flex-col">
|
|
<div className="p-3 border-b border-gray-700">
|
|
<div className="flex gap-1">
|
|
<input className="flex-1 bg-gray-700 rounded px-2 py-1 text-xs" placeholder="新 agent 名" value={newName} onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && create()} />
|
|
<button onClick={create} className="bg-indigo-600 hover:bg-indigo-500 px-2 py-1 rounded text-xs">+</button>
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto">
|
|
{agents.map(a => (
|
|
<div key={a.name} onClick={() => setSelected(a.name)}
|
|
className={`flex items-center justify-between px-3 py-2 cursor-pointer text-sm hover:bg-gray-700 ${selected === a.name ? 'bg-gray-700' : ''}`}>
|
|
<span className="truncate">{a.name}</span>
|
|
<button onClick={e => { e.stopPropagation(); del(a.name) }} className="text-gray-500 hover:text-red-400 text-xs">✕</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Editor */}
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
{selected ? (
|
|
<>
|
|
<div className="flex items-center gap-2 px-4 py-2 border-b border-gray-700">
|
|
<span className="font-semibold text-sm">{selected}</span>
|
|
<div className="flex gap-1 ml-2">
|
|
{(['AGENT.md', 'SOUL.md'] as const).map(t => (
|
|
<button key={t} onClick={() => setTab(t)}
|
|
className={`text-xs px-3 py-1 rounded ${tab === t ? 'bg-indigo-600' : 'bg-gray-700 hover:bg-gray-600'}`}>{t}</button>
|
|
))}
|
|
</div>
|
|
<button onClick={save} className="ml-auto bg-green-700 hover:bg-green-600 px-3 py-1 rounded text-xs">保存</button>
|
|
</div>
|
|
<div className="flex-1">
|
|
<Editor height="100%" defaultLanguage="markdown" theme="vs-dark" value={content} onChange={v => setContent(v || '')} options={{ minimap: { enabled: false }, wordWrap: 'on', fontSize: 13 }} />
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="flex-1 flex items-center justify-center text-gray-400 text-sm">选择一个 agent 编辑</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|