feat: 完善 Server 层并添加 CLI 和 Web 前端

Server 层增强:
- 添加 Agent 适配层,支持动态加载 core 模块
- 实现 Token 认证机制,支持本地/远程模式
- WebSocket 集成 Agent 实时对话

CLI 模块 (packages/cli):
- serve 命令启动 HTTP Server
- attach 命令连接远程 Server
- API Client 封装

Web 前端 (packages/web):
- React 18 + Vite + Tailwind CSS
- 会话管理侧边栏
- WebSocket 实时聊天界面
- 流式消息显示
This commit is contained in:
2025-12-12 11:22:25 +08:00
parent 5e32375f0e
commit 168996a475
35 changed files with 4028 additions and 52 deletions
+124
View File
@@ -0,0 +1,124 @@
/**
* Sidebar Component
*/
import { useState, useEffect } from 'react';
import { Plus, MessageSquare, Trash2, RefreshCw } from 'lucide-react';
import clsx from 'clsx';
import { listSessions, createSession, deleteSession, type Session } from '../api/client';
interface SidebarProps {
currentSessionId: string | null;
onSelectSession: (id: string) => void;
onCreateSession: (session: Session) => void;
}
export function Sidebar({ currentSessionId, onSelectSession, onCreateSession }: SidebarProps) {
const [sessions, setSessions] = useState<Session[]>([]);
const [isLoading, setIsLoading] = useState(false);
const loadSessions = async () => {
setIsLoading(true);
try {
const { data } = await listSessions();
setSessions(data);
} catch (error) {
console.error('Failed to load sessions:', error);
} finally {
setIsLoading(false);
}
};
const handleCreate = async () => {
try {
const { data } = await createSession();
setSessions((prev) => [data, ...prev]);
onCreateSession(data);
} catch (error) {
console.error('Failed to create session:', error);
}
};
const handleDelete = async (id: string, e: React.MouseEvent) => {
e.stopPropagation();
try {
await deleteSession(id);
setSessions((prev) => prev.filter((s) => s.id !== id));
if (currentSessionId === id) {
const remaining = sessions.filter((s) => s.id !== id);
if (remaining.length > 0) {
onSelectSession(remaining[0].id);
}
}
} catch (error) {
console.error('Failed to delete session:', error);
}
};
useEffect(() => {
loadSessions();
}, []);
return (
<div className="w-64 bg-gray-800 border-r border-gray-700 flex flex-col">
{/* Header */}
<div className="p-4 border-b border-gray-700">
<button
onClick={handleCreate}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-primary-600 hover:bg-primary-700 rounded-lg transition-colors"
>
<Plus size={18} />
<span>New Chat</span>
</button>
</div>
{/* Session List */}
<div className="flex-1 overflow-y-auto">
{isLoading ? (
<div className="p-4 text-center text-gray-500">
<RefreshCw className="animate-spin inline-block" size={20} />
</div>
) : sessions.length === 0 ? (
<div className="p-4 text-center text-gray-500">
No conversations yet
</div>
) : (
<div className="p-2 space-y-1">
{sessions.map((session) => (
<div
key={session.id}
onClick={() => onSelectSession(session.id)}
className={clsx(
'flex items-center gap-2 p-3 rounded-lg cursor-pointer group',
'hover:bg-gray-700 transition-colors',
currentSessionId === session.id && 'bg-gray-700'
)}
>
<MessageSquare size={18} className="text-gray-400 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-sm truncate">
{session.name || `Chat ${session.id.slice(0, 8)}`}
</div>
<div className="text-xs text-gray-500">
{session.messageCount} messages
</div>
</div>
<button
onClick={(e) => handleDelete(session.id, e)}
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-gray-600 rounded transition-all"
>
<Trash2 size={14} className="text-gray-400" />
</button>
</div>
))}
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-700 text-center text-xs text-gray-500">
AI Assistant v1.0
</div>
</div>
);
}