Files
OpenClaw-bot-review/app/workspace/page.tsx
T
liweiqiu797-cmyk 38ed48aceb feat: add Agent workspace and Daily board pages
- Add /workspace page: Agent grid with real-time status
- Add /daily page: Daily report board with TTS playback
- Add new API routes: /api/agents, /api/daily
- Add new components: agent-card, agent-grid
- Add sidebar navigation for new pages
- Add i18n labels (zh-CN + en)
2026-03-31 17:00:33 +08:00

180 lines
6.5 KiB
TypeScript

// app/workspace/page.tsx
'use client';
import { useEffect, useState } from 'react';
interface AgentState {
agentId: string;
state: 'working' | 'online' | 'idle' | 'offline';
lastActive: number | null;
}
interface Skill {
name: string;
description: string;
version: string;
status: string;
}
const STATE_CONFIG = {
working: { label: '工作中', emoji: '🔥', color: 'bg-green-500' },
online: { label: '在线', emoji: '🟢', color: 'bg-blue-500' },
idle: { label: '空闲', emoji: '😴', color: 'bg-yellow-500' },
offline: { label: '离线', emoji: '⚫', color: 'bg-gray-500' },
};
function AgentCard({ agent }: { agent: AgentState }) {
const config = STATE_CONFIG[agent.state] || STATE_CONFIG.offline;
const lastActiveText = agent.lastActive
? new Date(agent.lastActive).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
: '无记录';
return (
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-4 hover:border-[var(--accent)]/50 transition-all">
<div className="flex items-center gap-3 mb-3">
<div className={`w-3 h-3 rounded-full ${config.color}`} />
<span className="text-2xl">{config.emoji}</span>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-sm truncate">{agent.agentId}</h3>
<p className="text-xs text-[var(--text-muted)]">{config.label}</p>
</div>
</div>
<div className="text-xs text-[var(--text-muted)]">
最后活跃: {lastActiveText}
</div>
</div>
);
}
function SkillCard({ skill }: { skill: Skill }) {
return (
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-3 hover:border-[var(--accent)]/50 transition-all">
<div className="flex items-start justify-between gap-2 mb-2">
<h4 className="font-medium text-sm truncate">{skill.name}</h4>
<span className={`px-2 py-0.5 rounded-full text-[10px] font-medium ${
skill.status === 'running'
? 'bg-green-500/20 text-green-400'
: 'bg-gray-500/20 text-gray-400'
}`}>
{skill.status === 'running' ? '运行中' : '已禁用'}
</span>
</div>
<p className="text-xs text-[var(--text-muted)] line-clamp-2">
{skill.description || '无描述'}
</p>
<p className="text-[10px] text-[var(--accent)] mt-2">
v{skill.version}
</p>
</div>
);
}
export default function WorkspacePage() {
const [agents, setAgents] = useState<AgentState[]>([]);
const [skills, setSkills] = useState<Skill[]>([]);
const [loading, setLoading] = useState(true);
const [lastRefresh, setLastRefresh] = useState(new Date());
useEffect(() => {
async function fetchData() {
try {
const [agentRes, skillsRes] = await Promise.all([
fetch('/api/agent-status'),
fetch('/api/skills'),
]);
const agentData = await agentRes.json();
const skillsData = await skillsRes.json();
// Handle both old and new skill format
if (Array.isArray(skillsData.skills)) {
setSkills(skillsData.skills.map((s: any) => ({
name: s.name || s.id || '未知',
description: s.description || '',
version: s.version || 'unknown',
status: 'running',
})));
}
setAgents(agentData.statuses || []);
} catch (error) {
console.error('Failed to fetch data:', error);
} finally {
setLoading(false);
setLastRefresh(new Date());
}
}
fetchData();
const interval = setInterval(fetchData, 15000); // 15秒刷新
return () => clearInterval(interval);
}, []);
const workingCount = agents.filter(a => a.state === 'working').length;
const onlineCount = agents.filter(a => a.state === 'online' || a.state === 'working').length;
return (
<main className="min-h-screen p-4 md:p-8 max-w-7xl mx-auto">
{/* 头部 */}
<div className="flex flex-col gap-4 mb-8 md:flex-row md:items-center md:justify-between">
<div>
<h1 className="text-2xl font-bold">🤖 Agent 工作台</h1>
<p className="text-sm text-[var(--text-muted)] mt-1">
实时监控所有 Agent 状态 · {lastRefresh.toLocaleTimeString('zh-CN')} 更新
</p>
</div>
<div className="flex gap-3">
<div className="px-3 py-1.5 rounded-lg bg-[var(--card)] border border-[var(--border)] text-xs">
<span className="text-[var(--text-muted)]">在线:</span>
<span className="font-semibold ml-1">{onlineCount}/{agents.length}</span>
</div>
<div className="px-3 py-1.5 rounded-lg bg-[var(--card)] border border-[var(--border)] text-xs">
<span className="text-[var(--text-muted)]">工作中:</span>
<span className="font-semibold ml-1 text-green-400">{workingCount}</span>
</div>
</div>
</div>
{/* Agent 网格 */}
<section className="mb-8">
<h2 className="text-lg font-semibold mb-4">📊 Agent 状态</h2>
{loading ? (
<div className="text-center py-8 text-[var(--text-muted)]">加载中...</div>
) : agents.length === 0 ? (
<div className="text-center py-8 text-[var(--text-muted)] rounded-xl border border-[var(--border)] bg-[var(--card)]">
暂无 Agent 数据
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{agents.map((agent) => (
<AgentCard key={agent.agentId} agent={agent} />
))}
</div>
)}
</section>
{/* 技能列表 */}
<section>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">📦 已安装技能</h2>
<span className="text-xs text-[var(--text-muted)]">
{skills.length} 个技能
</span>
</div>
{loading ? (
<div className="text-center py-8 text-[var(--text-muted)]">加载中...</div>
) : skills.length === 0 ? (
<div className="text-center py-8 text-[var(--text-muted)] rounded-xl border border-[var(--border)] bg-[var(--card)]">
暂无技能
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{skills.map((skill) => (
<SkillCard key={skill.name} skill={skill} />
))}
</div>
)}
</section>
</main>
);
}