mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-06-10 15:29:06 +08:00
fix ui
This commit is contained in:
@@ -32,7 +32,7 @@ export const GlobalDialog: React.FC<{
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div className="fixed inset-0 z-[130] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
<motion.div className="ui-overlay-strong fixed inset-0 z-[130] backdrop-blur-sm flex items-center justify-center p-4"
|
||||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
||||
<motion.div className="brand-card w-full max-w-md border border-zinc-700 shadow-2xl"
|
||||
initial={{ scale: 0.95, y: 8 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.95, y: 8 }}>
|
||||
|
||||
@@ -63,14 +63,14 @@ const Header: React.FC = () => {
|
||||
</button>
|
||||
<div className="hidden md:flex items-center gap-3 rounded-xl px-2 py-1.5 min-w-0">
|
||||
<div className="brand-badge w-9 h-9 rounded-xl flex items-center justify-center shadow-lg shrink-0">
|
||||
<Terminal className="w-5 h-5 text-white" />
|
||||
<Terminal className="w-5 h-5 text-zinc-950" />
|
||||
</div>
|
||||
{!sidebarCollapsed && (
|
||||
<span className="font-semibold text-lg md:text-xl tracking-tight truncate">{t('appName')}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="brand-badge md:hidden w-8 h-8 rounded-xl flex items-center justify-center shadow-lg shrink-0">
|
||||
<Terminal className="w-4 h-4 text-white" />
|
||||
<Terminal className="w-4 h-4 text-zinc-950" />
|
||||
</div>
|
||||
<span className="md:hidden font-semibold text-lg tracking-tight truncate">{t('appName')}</span>
|
||||
</div>
|
||||
@@ -79,13 +79,13 @@ const Header: React.FC = () => {
|
||||
<div className="flex items-center gap-1.5 md:gap-2.5 bg-zinc-900 border border-zinc-800 px-2 md:px-3 py-1 rounded-lg max-w-[140px] md:max-w-none overflow-hidden">
|
||||
<span className="hidden md:inline text-sm font-medium text-zinc-400">{t('gatewayStatus')}:</span>
|
||||
{isGatewayOnline ? (
|
||||
<div className="flex items-center gap-1.5 bg-emerald-500/10 text-emerald-400 px-2.5 py-0.5 rounded-md text-xs font-semibold border border-emerald-500/20">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.8)]" />
|
||||
<div className="status-pill-online flex items-center gap-1.5 px-2.5 py-0.5 rounded-md text-xs font-semibold border">
|
||||
<div className="status-dot-online w-1.5 h-1.5 rounded-full" />
|
||||
{t('online')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 bg-red-500/10 text-red-400 px-2.5 py-0.5 rounded-md text-xs font-semibold border border-red-500/20">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.8)]" />
|
||||
<div className="status-pill-offline flex items-center gap-1.5 px-2.5 py-0.5 rounded-md text-xs font-semibold border">
|
||||
<div className="status-dot-offline w-1.5 h-1.5 rounded-full" />
|
||||
{t('offline')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -15,7 +15,7 @@ const Layout: React.FC = () => {
|
||||
<div className="flex flex-1 min-h-0">
|
||||
<Sidebar />
|
||||
{sidebarOpen && (
|
||||
<button className="fixed inset-0 top-16 bg-black/40 z-30 md:hidden" onClick={() => setSidebarOpen(false)} aria-label="close sidebar" />
|
||||
<button className="ui-overlay-soft fixed inset-0 top-16 z-30 md:hidden" onClick={() => setSidebarOpen(false)} aria-label="close sidebar" />
|
||||
)}
|
||||
<main className="flex-1 flex flex-col min-w-0 relative app-main-surface">
|
||||
<AnimatePresence mode="wait">
|
||||
|
||||
@@ -59,7 +59,7 @@ const PrimitiveArrayEditor: React.FC<{
|
||||
{value.map((item, idx) => (
|
||||
<span key={`${item}-${idx}`} className="inline-flex items-center gap-1 px-2 py-1 rounded-xl ui-soft-panel text-xs font-mono text-zinc-700 dark:text-zinc-200">
|
||||
{String(item)}
|
||||
<button onClick={() => removeAt(idx)} className="text-zinc-400 hover:text-red-400">×</button>
|
||||
<button onClick={() => removeAt(idx)} className="ui-text-danger-hover text-zinc-400">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -11,12 +11,12 @@ const Sidebar: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const [expandedSections, setExpandedSections] = React.useState<Record<string, boolean>>({
|
||||
main: true,
|
||||
agents: true,
|
||||
ops: true,
|
||||
config: true,
|
||||
knowledge: true,
|
||||
insights: true,
|
||||
channels: true,
|
||||
agents: false,
|
||||
ops: false,
|
||||
config: false,
|
||||
knowledge: false,
|
||||
insights: false,
|
||||
channels: false,
|
||||
});
|
||||
|
||||
const sections = [
|
||||
@@ -158,7 +158,7 @@ const Sidebar: React.FC = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="hidden md:flex justify-center p-3 border-t border-zinc-800 bg-zinc-900/20">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-indigo-500 shadow-[0_0_14px_rgba(240,90,40,0.38)]" title={t('gatewayToken')} />
|
||||
<div className="gateway-token-indicator w-2.5 h-2.5 rounded-full" title={t('gatewayToken')} />
|
||||
</div>
|
||||
)}
|
||||
<div className={`hidden md:flex border-t border-zinc-800 bg-zinc-900/20 ${sidebarCollapsed ? 'justify-center p-3' : 'justify-end p-3'}`}>
|
||||
|
||||
@@ -32,9 +32,22 @@ export const SpaceParticles: React.FC = () => {
|
||||
resize();
|
||||
|
||||
let observer: MutationObserver | null = null;
|
||||
let themeStyles = typeof document !== 'undefined' ? getComputedStyle(document.documentElement) : null;
|
||||
|
||||
const readThemeColor = (name: string, fallback: string) => {
|
||||
const value = themeStyles?.getPropertyValue(name).trim();
|
||||
return value || fallback;
|
||||
};
|
||||
|
||||
const readThemeNumber = (name: string, fallback: number) => {
|
||||
const value = Number.parseFloat(themeStyles?.getPropertyValue(name).trim() || '');
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
};
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
observer = new MutationObserver(() => {
|
||||
isDark = document.documentElement.classList.contains('theme-dark');
|
||||
themeStyles = getComputedStyle(document.documentElement);
|
||||
});
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||
}
|
||||
@@ -60,18 +73,18 @@ export const SpaceParticles: React.FC = () => {
|
||||
canvas.width / 2, canvas.height / 2, 0,
|
||||
canvas.width / 2, canvas.height / 2, Math.max(canvas.width, canvas.height)
|
||||
);
|
||||
if (isDark) {
|
||||
gradient.addColorStop(0, 'rgba(255, 133, 82, 0.16)');
|
||||
gradient.addColorStop(0.55, 'rgba(112, 41, 22, 0.20)');
|
||||
gradient.addColorStop(1, 'rgba(28, 12, 8, 0.88)');
|
||||
} else {
|
||||
gradient.addColorStop(0, 'rgba(255, 243, 233, 0.30)');
|
||||
gradient.addColorStop(0.55, 'rgba(255, 179, 107, 0.16)');
|
||||
gradient.addColorStop(1, 'rgba(255, 226, 209, 0.55)');
|
||||
}
|
||||
gradient.addColorStop(0, readThemeColor('--particle-glow-start', 'rgba(255, 243, 233, 0.30)'));
|
||||
gradient.addColorStop(0.55, readThemeColor('--particle-glow-mid', 'rgba(255, 179, 107, 0.16)'));
|
||||
gradient.addColorStop(1, readThemeColor('--particle-glow-end', 'rgba(255, 226, 209, 0.55)'));
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const particleRGB = readThemeColor('--particle-dot-rgb', isDark ? '255, 179, 107' : '217, 72, 28');
|
||||
const particleOpacityFloor = readThemeNumber('--particle-dot-opacity-floor', isDark ? 0.1 : 0.08);
|
||||
const particleOpacityScale = readThemeNumber('--particle-dot-opacity-scale', isDark ? 1 : 0.65);
|
||||
const lineRGB = readThemeColor('--particle-line-rgb', isDark ? '240, 90, 40' : '164, 58, 24');
|
||||
const lineOpacityScale = readThemeNumber('--particle-line-opacity-scale', isDark ? 1 : 0.85);
|
||||
|
||||
particles.forEach((p) => {
|
||||
p.x += p.speedX;
|
||||
p.y += p.speedY;
|
||||
@@ -83,9 +96,7 @@ export const SpaceParticles: React.FC = () => {
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = isDark
|
||||
? `rgba(255, 179, 107, ${p.opacity})`
|
||||
: `rgba(217, 72, 28, ${Math.max(0.08, p.opacity * 0.65)})`;
|
||||
ctx.fillStyle = `rgba(${particleRGB}, ${Math.max(particleOpacityFloor, p.opacity * particleOpacityScale)})`;
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
@@ -100,9 +111,7 @@ export const SpaceParticles: React.FC = () => {
|
||||
if (dist < 120) {
|
||||
ctx.beginPath();
|
||||
const lineOpacity = 0.16 * (1 - dist / 120);
|
||||
ctx.strokeStyle = isDark
|
||||
? `rgba(240, 90, 40, ${lineOpacity})`
|
||||
: `rgba(164, 58, 24, ${lineOpacity * 0.85})`;
|
||||
ctx.strokeStyle = `rgba(${lineRGB}, ${lineOpacity * lineOpacityScale})`;
|
||||
ctx.moveTo(particles[i].x, particles[i].y);
|
||||
ctx.lineTo(particles[j].x, particles[j].y);
|
||||
ctx.stroke();
|
||||
|
||||
@@ -8,7 +8,7 @@ interface StatCardProps {
|
||||
|
||||
const StatCard: React.FC<StatCardProps> = ({ title, value, icon }) => (
|
||||
<div className="brand-card h-full min-h-[124px] border border-zinc-800 p-6 flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-zinc-800/50 flex items-center justify-center border border-zinc-700/50 shadow-[inset_0_1px_0_rgba(255,255,255,0.18)] relative z-[1]">
|
||||
<div className="card-icon-shell w-12 h-12 rounded-2xl bg-zinc-800/50 flex items-center justify-center border border-zinc-700/50 relative z-[1]">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="relative z-[1]">
|
||||
|
||||
@@ -114,7 +114,7 @@ export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
|
||||
<AnimatePresence>
|
||||
{loading && (
|
||||
<motion.div className="fixed inset-0 z-[120] bg-black/55 backdrop-blur-sm flex items-center justify-center"
|
||||
<motion.div className="ui-overlay-medium fixed inset-0 z-[120] backdrop-blur-sm flex items-center justify-center"
|
||||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
||||
<div className="px-6 py-5 rounded-2xl border border-zinc-700 bg-zinc-900/95 shadow-2xl min-w-[240px] text-center">
|
||||
<div className="mx-auto mb-3 h-8 w-8 border-2 border-zinc-600 border-t-indigo-400 rounded-full animate-spin" />
|
||||
@@ -134,7 +134,7 @@ export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
|
||||
<AnimatePresence>
|
||||
{customModal && (
|
||||
<motion.div className="fixed inset-0 z-[125] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
<motion.div className="ui-overlay-strong fixed inset-0 z-[125] backdrop-blur-sm flex items-center justify-center p-4"
|
||||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
||||
<motion.div className="w-full max-w-4xl rounded-2xl border border-zinc-700 bg-zinc-900 shadow-2xl overflow-hidden"
|
||||
initial={{ scale: 0.96 }} animate={{ scale: 1 }} exit={{ scale: 0.96 }}>
|
||||
|
||||
@@ -30,10 +30,16 @@ const resources = {
|
||||
whatsappStateDisconnected: 'Disconnected',
|
||||
whatsappBridgeDevHintTitle: 'How to use',
|
||||
whatsappBridgeDevHint: '1. Start the bridge with `clawgo channel whatsapp bridge run`.\n2. Scan the QR code here with WhatsApp.\n3. Keep `make dev` running to receive WhatsApp messages in ClawGo.',
|
||||
whatsappFieldEnabledHint: 'Master switch for receiving WhatsApp messages through the bridge.',
|
||||
whatsappFieldBridgeURLHint: 'WebSocket address of the local WhatsApp companion bridge service.',
|
||||
whatsappFieldAllowFromHint: 'One sender JID per line. Only these senders can trigger ClawGo.',
|
||||
whatsappFieldEnableGroupsHint: 'Allow messages from WhatsApp groups to enter the channel.',
|
||||
whatsappFieldRequireMentionHint: 'When enabled, group messages must mention the bot before being handled.',
|
||||
whatsappFieldAllowFromFootnote: 'Supports one JID per line, and also accepts comma-separated values.',
|
||||
whatsappLogoutTitle: 'Logout WhatsApp Session',
|
||||
whatsappLogoutMessage: 'Unlink the current WhatsApp companion session and request a new QR code?',
|
||||
mcpServices: 'MCP',
|
||||
mcpServicesHint: 'Manage MCP servers, install packages, and inspect discovered remote tools.',
|
||||
mcpServicesHint: 'Manage MCP servers and install service packages.',
|
||||
cronJobs: 'Cron Jobs',
|
||||
nodes: 'Nodes',
|
||||
nodeArtifacts: 'Node Artifacts',
|
||||
@@ -435,9 +441,6 @@ const resources = {
|
||||
configMCPArgsEnterHint: 'Type one argument and press Enter',
|
||||
configMCPCommandMissing: 'MCP command is unavailable.',
|
||||
configMCPInstallSuggested: 'Suggested package: {{pkg}}',
|
||||
configMCPDiscoveredTools: 'Discovered MCP Tools',
|
||||
configMCPDiscoveredToolsCount: '{{count}} discovered',
|
||||
configNoMCPDiscoveredTools: 'No MCP tools discovered yet.',
|
||||
configDeleteMCPServerConfirmTitle: 'Delete MCP Server',
|
||||
configDeleteMCPServerConfirmMessage: 'Delete MCP server "{{name}}" from current config?',
|
||||
configNoGroups: 'No config groups found.',
|
||||
@@ -682,10 +685,16 @@ const resources = {
|
||||
whatsappStateDisconnected: '已断开',
|
||||
whatsappBridgeDevHintTitle: '使用方式',
|
||||
whatsappBridgeDevHint: '1. 先执行 `clawgo channel whatsapp bridge run` 启动 bridge。\n2. 在这里用 WhatsApp 扫描二维码。\n3. 再保持 `make dev` 运行,让 ClawGo 接收 WhatsApp 消息。',
|
||||
whatsappFieldEnabledHint: '总开关,控制是否通过 bridge 接收 WhatsApp 消息。',
|
||||
whatsappFieldBridgeURLHint: '本地 WhatsApp companion bridge 服务的 WebSocket 地址。',
|
||||
whatsappFieldAllowFromHint: '每行一个发送者 JID,只有这些发送者可以触发 ClawGo。',
|
||||
whatsappFieldEnableGroupsHint: '允许来自 WhatsApp 群组的消息进入该通道。',
|
||||
whatsappFieldRequireMentionHint: '开启后,群消息必须显式 @ 提及机器人才会被处理。',
|
||||
whatsappFieldAllowFromFootnote: '支持每行一个 JID,也支持逗号分隔后自动拆分。',
|
||||
whatsappLogoutTitle: '退出 WhatsApp 会话',
|
||||
whatsappLogoutMessage: '是否解除当前 WhatsApp companion 会话,并重新申请新的二维码?',
|
||||
mcpServices: 'MCP',
|
||||
mcpServicesHint: '管理 MCP 服务、安装服务包,并查看已发现的远端工具。',
|
||||
mcpServicesHint: '管理 MCP 服务并安装服务包。',
|
||||
cronJobs: '定时任务',
|
||||
nodes: '节点',
|
||||
nodeArtifacts: '节点工件',
|
||||
@@ -1087,9 +1096,6 @@ const resources = {
|
||||
configMCPArgsEnterHint: '输入一个参数后按回车添加',
|
||||
configMCPCommandMissing: 'MCP 命令不可用。',
|
||||
configMCPInstallSuggested: '建议安装包:{{pkg}}',
|
||||
configMCPDiscoveredTools: '已发现的 MCP 工具',
|
||||
configMCPDiscoveredToolsCount: '已发现 {{count}} 个',
|
||||
configNoMCPDiscoveredTools: '暂未发现 MCP 工具。',
|
||||
configDeleteMCPServerConfirmTitle: '删除 MCP 服务',
|
||||
configDeleteMCPServerConfirmMessage: '确认从当前配置中删除 MCP 服务 “{{name}}”吗?',
|
||||
configNoGroups: '未找到配置分组。',
|
||||
|
||||
@@ -20,6 +20,24 @@
|
||||
--color-indigo-600: #ea580c;
|
||||
--color-indigo-700: #c2410c;
|
||||
--color-indigo-800: #9a3412;
|
||||
--color-emerald-300: #6ee7b7;
|
||||
--color-emerald-400: #34d399;
|
||||
--color-emerald-500: #10b981;
|
||||
--color-red-400: #f87171;
|
||||
--color-red-500: #ef4444;
|
||||
--color-amber-300: #fcd34d;
|
||||
--color-amber-400: #fbbf24;
|
||||
--color-amber-500: #f59e0b;
|
||||
--color-orange-400: #fb923c;
|
||||
--color-sky-300: #7dd3fc;
|
||||
--color-sky-400: #38bdf8;
|
||||
--color-sky-500: #0ea5e9;
|
||||
--color-violet-300: #c4b5fd;
|
||||
--color-violet-400: #a78bfa;
|
||||
--color-violet-500: #8b5cf6;
|
||||
--color-rose-300: #fda4af;
|
||||
--color-rose-500: #f43f5e;
|
||||
--color-fuchsia-400: #e879f9;
|
||||
}
|
||||
|
||||
html {
|
||||
@@ -76,6 +94,75 @@ html {
|
||||
--button-danger-bg-hover: rgb(254 205 211 / 0.98);
|
||||
--button-danger-border: rgb(244 63 94 / 0.28);
|
||||
--button-danger-text: rgb(159 18 57 / 0.98);
|
||||
--brand-badge-start: #ffb36b;
|
||||
--brand-badge-mid: #ff8552;
|
||||
--brand-badge-end: #d9481c;
|
||||
--brand-badge-shadow: rgb(234 88 12 / 0.2);
|
||||
--card-icon-inset: rgb(255 255 255 / 0.18);
|
||||
--status-online-bg: rgb(16 185 129 / 0.1);
|
||||
--status-online-border: rgb(16 185 129 / 0.2);
|
||||
--status-online-text: var(--color-emerald-500);
|
||||
--status-online-dot: var(--color-emerald-500);
|
||||
--status-online-glow: rgb(16 185 129 / 0.8);
|
||||
--status-offline-bg: rgb(239 68 68 / 0.1);
|
||||
--status-offline-border: rgb(239 68 68 / 0.2);
|
||||
--status-offline-text: var(--color-red-500);
|
||||
--status-offline-dot: var(--color-red-500);
|
||||
--status-offline-glow: rgb(239 68 68 / 0.8);
|
||||
--token-indicator-glow: rgb(240 90 40 / 0.38);
|
||||
--topology-node-highlight-shadow: rgb(245 158 11 / 0.2);
|
||||
--topology-node-base-shadow: rgb(0 0 0 / 0.6);
|
||||
--topology-node-hover-shadow: rgb(255 255 255 / 0.05);
|
||||
--topology-node-inner-border: rgb(255 255 255 / 0.05);
|
||||
--topology-node-highlight-border: rgb(245 158 11 / 0.8);
|
||||
--topology-online-glow: rgb(16 185 129 / 0.8);
|
||||
--topology-line-highlight-track: rgb(245 158 11 / 0.15);
|
||||
--topology-line-track: rgb(161 161 170 / 0.05);
|
||||
--topology-line-highlight-flow: rgb(245 158 11 / 0.9);
|
||||
--topology-line-flow: rgb(161 161 170 / 0.5);
|
||||
--topology-tooltip-shadow: rgb(0 0 0 / 0.5);
|
||||
--particle-glow-start: rgba(255, 243, 233, 0.30);
|
||||
--particle-glow-mid: rgba(255, 179, 107, 0.16);
|
||||
--particle-glow-end: rgba(255, 226, 209, 0.55);
|
||||
--particle-dot-rgb: 217, 72, 28;
|
||||
--particle-dot-opacity-floor: 0.08;
|
||||
--particle-dot-opacity-scale: 0.65;
|
||||
--particle-line-rgb: 164, 58, 24;
|
||||
--particle-line-opacity-scale: 0.85;
|
||||
--ekg-bar-start: rgb(245 158 11 / 0.7);
|
||||
--ekg-bar-end: rgb(251 146 60 / 0.8);
|
||||
--overlay-scrim-soft: rgb(15 23 42 / 0.4);
|
||||
--overlay-scrim-medium: rgb(15 23 42 / 0.55);
|
||||
--overlay-scrim-strong: rgb(15 23 42 / 0.6);
|
||||
--media-surface: rgb(15 23 42 / 0.2);
|
||||
--media-surface-strong: rgb(15 23 42 / 0.3);
|
||||
--media-border: var(--color-zinc-800);
|
||||
--notice-warning-bg: rgb(254 243 199 / 0.72);
|
||||
--notice-warning-border: rgb(245 158 11 / 0.28);
|
||||
--notice-warning-text: rgb(146 64 14 / 0.96);
|
||||
--notice-danger-bg: rgb(255 228 230 / 0.72);
|
||||
--notice-danger-border: rgb(244 63 94 / 0.24);
|
||||
--notice-danger-text: rgb(159 18 57 / 0.96);
|
||||
--pill-warning-bg: rgb(254 243 199 / 0.82);
|
||||
--pill-warning-border: rgb(245 158 11 / 0.24);
|
||||
--pill-warning-text: rgb(146 64 14 / 0.96);
|
||||
--pill-danger-bg: rgb(255 228 230 / 0.8);
|
||||
--pill-danger-border: rgb(244 63 94 / 0.22);
|
||||
--pill-danger-text: rgb(159 18 57 / 0.96);
|
||||
--pill-success-bg: rgb(220 252 231 / 0.82);
|
||||
--pill-success-border: rgb(52 211 153 / 0.24);
|
||||
--pill-success-text: rgb(6 95 70 / 0.96);
|
||||
--pill-info-bg: rgb(224 242 254 / 0.82);
|
||||
--pill-info-border: rgb(14 165 233 / 0.22);
|
||||
--pill-info-text: rgb(3 105 161 / 0.96);
|
||||
--pill-accent-bg: rgb(237 233 254 / 0.82);
|
||||
--pill-accent-border: rgb(139 92 246 / 0.22);
|
||||
--pill-accent-text: rgb(109 40 217 / 0.96);
|
||||
--pill-neutral-bg: rgb(226 232 240 / 0.82);
|
||||
--pill-neutral-border: rgb(203 213 225 / 0.24);
|
||||
--pill-neutral-text: rgb(71 85 105 / 0.96);
|
||||
--code-danger-text: rgb(190 24 93 / 0.96);
|
||||
--code-warning-text: rgb(180 83 9 / 0.96);
|
||||
--chip-bg: rgb(226 232 240 / 0.82);
|
||||
--chip-bg-hover: rgb(203 213 225 / 0.92);
|
||||
--chip-border: rgb(203 213 225 / 0.96);
|
||||
@@ -165,6 +252,75 @@ html.theme-dark {
|
||||
--button-danger-bg-hover: rgb(244 63 94 / 0.22);
|
||||
--button-danger-border: rgb(251 113 133 / 0.24);
|
||||
--button-danger-text: rgb(255 228 230 / 0.96);
|
||||
--brand-badge-start: #f8c58d;
|
||||
--brand-badge-mid: #ee9852;
|
||||
--brand-badge-end: #af4f16;
|
||||
--brand-badge-shadow: rgb(217 107 37 / 0.18);
|
||||
--card-icon-inset: rgb(255 255 255 / 0.1);
|
||||
--status-online-bg: rgb(16 185 129 / 0.14);
|
||||
--status-online-border: rgb(52 211 153 / 0.24);
|
||||
--status-online-text: rgb(209 250 229 / 0.96);
|
||||
--status-online-dot: var(--color-emerald-400);
|
||||
--status-online-glow: rgb(16 185 129 / 0.78);
|
||||
--status-offline-bg: rgb(244 63 94 / 0.14);
|
||||
--status-offline-border: rgb(251 113 133 / 0.24);
|
||||
--status-offline-text: rgb(255 228 230 / 0.96);
|
||||
--status-offline-dot: var(--color-red-400);
|
||||
--status-offline-glow: rgb(244 63 94 / 0.72);
|
||||
--token-indicator-glow: rgb(232 132 58 / 0.42);
|
||||
--topology-node-highlight-shadow: rgb(245 158 11 / 0.22);
|
||||
--topology-node-base-shadow: rgb(0 0 0 / 0.7);
|
||||
--topology-node-hover-shadow: rgb(255 255 255 / 0.06);
|
||||
--topology-node-inner-border: rgb(255 255 255 / 0.06);
|
||||
--topology-node-highlight-border: rgb(251 191 36 / 0.72);
|
||||
--topology-online-glow: rgb(52 211 153 / 0.8);
|
||||
--topology-line-highlight-track: rgb(245 158 11 / 0.16);
|
||||
--topology-line-track: rgb(148 163 184 / 0.08);
|
||||
--topology-line-highlight-flow: rgb(251 191 36 / 0.88);
|
||||
--topology-line-flow: rgb(148 163 184 / 0.5);
|
||||
--topology-tooltip-shadow: rgb(0 0 0 / 0.56);
|
||||
--particle-glow-start: rgba(255, 133, 82, 0.16);
|
||||
--particle-glow-mid: rgba(112, 41, 22, 0.20);
|
||||
--particle-glow-end: rgba(28, 12, 8, 0.88);
|
||||
--particle-dot-rgb: 255, 179, 107;
|
||||
--particle-dot-opacity-floor: 0.1;
|
||||
--particle-dot-opacity-scale: 1;
|
||||
--particle-line-rgb: 240, 90, 40;
|
||||
--particle-line-opacity-scale: 1;
|
||||
--ekg-bar-start: rgb(251 191 36 / 0.78);
|
||||
--ekg-bar-end: rgb(232 132 58 / 0.86);
|
||||
--overlay-scrim-soft: rgb(0 0 0 / 0.4);
|
||||
--overlay-scrim-medium: rgb(0 0 0 / 0.55);
|
||||
--overlay-scrim-strong: rgb(0 0 0 / 0.62);
|
||||
--media-surface: rgb(0 0 0 / 0.2);
|
||||
--media-surface-strong: rgb(0 0 0 / 0.3);
|
||||
--media-border: var(--color-zinc-700);
|
||||
--notice-warning-bg: rgb(245 158 11 / 0.14);
|
||||
--notice-warning-border: rgb(251 191 36 / 0.22);
|
||||
--notice-warning-text: rgb(254 243 199 / 0.96);
|
||||
--notice-danger-bg: rgb(244 63 94 / 0.14);
|
||||
--notice-danger-border: rgb(251 113 133 / 0.22);
|
||||
--notice-danger-text: rgb(255 228 230 / 0.96);
|
||||
--pill-warning-bg: rgb(245 158 11 / 0.14);
|
||||
--pill-warning-border: rgb(251 191 36 / 0.22);
|
||||
--pill-warning-text: rgb(254 243 199 / 0.96);
|
||||
--pill-danger-bg: rgb(244 63 94 / 0.14);
|
||||
--pill-danger-border: rgb(251 113 133 / 0.22);
|
||||
--pill-danger-text: rgb(255 228 230 / 0.96);
|
||||
--pill-success-bg: rgb(16 185 129 / 0.14);
|
||||
--pill-success-border: rgb(52 211 153 / 0.22);
|
||||
--pill-success-text: rgb(209 250 229 / 0.96);
|
||||
--pill-info-bg: rgb(14 165 233 / 0.14);
|
||||
--pill-info-border: rgb(56 189 248 / 0.22);
|
||||
--pill-info-text: rgb(224 242 254 / 0.96);
|
||||
--pill-accent-bg: rgb(139 92 246 / 0.14);
|
||||
--pill-accent-border: rgb(167 139 250 / 0.22);
|
||||
--pill-accent-text: rgb(237 233 254 / 0.96);
|
||||
--pill-neutral-bg: rgb(36 49 69 / 0.9);
|
||||
--pill-neutral-border: rgb(93 109 135 / 0.22);
|
||||
--pill-neutral-text: rgb(148 163 184 / 0.96);
|
||||
--code-danger-text: rgb(251 113 133 / 0.96);
|
||||
--code-warning-text: rgb(253 224 71 / 0.96);
|
||||
--chip-bg: rgb(36 49 69 / 0.9);
|
||||
--chip-bg-hover: rgb(48 63 87 / 0.96);
|
||||
--chip-border: rgb(93 109 135 / 0.82);
|
||||
@@ -247,8 +403,8 @@ html.theme-dark .app-header {
|
||||
}
|
||||
|
||||
.brand-badge {
|
||||
background: linear-gradient(135deg, #ffb36b 0%, #ff8552 48%, #d9481c 100%);
|
||||
box-shadow: 0 12px 28px rgb(234 88 12 / 0.2);
|
||||
background: linear-gradient(135deg, var(--brand-badge-start) 0%, var(--brand-badge-mid) 48%, var(--brand-badge-end) 100%);
|
||||
box-shadow: 0 12px 28px var(--brand-badge-shadow);
|
||||
border-radius: var(--radius-badge);
|
||||
}
|
||||
|
||||
@@ -335,6 +491,233 @@ html.theme-dark .brand-card-subtle {
|
||||
box-shadow: 0 8px 20px var(--button-shadow);
|
||||
}
|
||||
|
||||
.status-pill-online {
|
||||
background: var(--status-online-bg);
|
||||
border-color: var(--status-online-border);
|
||||
color: var(--status-online-text);
|
||||
}
|
||||
|
||||
.status-pill-offline {
|
||||
background: var(--status-offline-bg);
|
||||
border-color: var(--status-offline-border);
|
||||
color: var(--status-offline-text);
|
||||
}
|
||||
|
||||
.status-dot-online {
|
||||
background: var(--status-online-dot);
|
||||
box-shadow: 0 0 8px var(--status-online-glow);
|
||||
}
|
||||
|
||||
.status-dot-offline {
|
||||
background: var(--status-offline-dot);
|
||||
box-shadow: 0 0 8px var(--status-offline-glow);
|
||||
}
|
||||
|
||||
.gateway-token-indicator {
|
||||
background: var(--color-indigo-500);
|
||||
box-shadow: 0 0 14px var(--token-indicator-glow);
|
||||
}
|
||||
|
||||
.card-icon-shell {
|
||||
box-shadow: inset 0 1px 0 var(--card-icon-inset);
|
||||
}
|
||||
|
||||
.topology-node-highlight {
|
||||
box-shadow: 0 0 30px var(--topology-node-highlight-shadow);
|
||||
}
|
||||
|
||||
.topology-node-base {
|
||||
box-shadow: 0 20px 40px var(--topology-node-base-shadow);
|
||||
}
|
||||
|
||||
.group:hover .topology-node-base {
|
||||
box-shadow:
|
||||
0 20px 40px var(--topology-node-base-shadow),
|
||||
0 0 20px var(--topology-node-hover-shadow);
|
||||
}
|
||||
|
||||
.topology-node-inner-border {
|
||||
border-color: var(--topology-node-inner-border);
|
||||
}
|
||||
|
||||
.topology-node-border-highlight {
|
||||
border-color: var(--topology-node-highlight-border);
|
||||
}
|
||||
|
||||
.topology-online-indicator {
|
||||
box-shadow: 0 0 10px var(--topology-online-glow);
|
||||
}
|
||||
|
||||
.topology-tooltip {
|
||||
box-shadow: 0 24px 48px var(--topology-tooltip-shadow);
|
||||
}
|
||||
|
||||
.ekg-bar-fill {
|
||||
background: linear-gradient(90deg, var(--ekg-bar-start), var(--ekg-bar-end));
|
||||
}
|
||||
|
||||
.ui-overlay-soft {
|
||||
background: var(--overlay-scrim-soft);
|
||||
}
|
||||
|
||||
.ui-overlay-medium {
|
||||
background: var(--overlay-scrim-medium);
|
||||
}
|
||||
|
||||
.ui-overlay-strong {
|
||||
background: var(--overlay-scrim-strong);
|
||||
}
|
||||
|
||||
.ui-media-surface {
|
||||
background: var(--media-surface);
|
||||
border-color: var(--media-border);
|
||||
}
|
||||
|
||||
.ui-media-surface-strong {
|
||||
background: var(--media-surface-strong);
|
||||
border-color: var(--media-border);
|
||||
}
|
||||
|
||||
.ui-notice-warning {
|
||||
background: var(--notice-warning-bg);
|
||||
border-color: var(--notice-warning-border);
|
||||
color: var(--notice-warning-text);
|
||||
}
|
||||
|
||||
.ui-notice-danger {
|
||||
background: var(--notice-danger-bg);
|
||||
border-color: var(--notice-danger-border);
|
||||
color: var(--notice-danger-text);
|
||||
}
|
||||
|
||||
.ui-pill {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.ui-pill-warning {
|
||||
background: var(--pill-warning-bg);
|
||||
border-color: var(--pill-warning-border);
|
||||
color: var(--pill-warning-text);
|
||||
}
|
||||
|
||||
.ui-pill-danger {
|
||||
background: var(--pill-danger-bg);
|
||||
border-color: var(--pill-danger-border);
|
||||
color: var(--pill-danger-text);
|
||||
}
|
||||
|
||||
.ui-pill-success {
|
||||
background: var(--pill-success-bg);
|
||||
border-color: var(--pill-success-border);
|
||||
color: var(--pill-success-text);
|
||||
}
|
||||
|
||||
.ui-pill-info {
|
||||
background: var(--pill-info-bg);
|
||||
border-color: var(--pill-info-border);
|
||||
color: var(--pill-info-text);
|
||||
}
|
||||
|
||||
.ui-pill-accent {
|
||||
background: var(--pill-accent-bg);
|
||||
border-color: var(--pill-accent-border);
|
||||
color: var(--pill-accent-text);
|
||||
}
|
||||
|
||||
.ui-pill-neutral {
|
||||
background: var(--pill-neutral-bg);
|
||||
border-color: var(--pill-neutral-border);
|
||||
color: var(--pill-neutral-text);
|
||||
}
|
||||
|
||||
.ui-code-danger {
|
||||
color: var(--code-danger-text);
|
||||
}
|
||||
|
||||
.ui-code-warning {
|
||||
color: var(--code-warning-text);
|
||||
}
|
||||
|
||||
.avatar-user,
|
||||
.avatar-agent,
|
||||
.avatar-tool,
|
||||
.avatar-system,
|
||||
.avatar-tone-1,
|
||||
.avatar-tone-2,
|
||||
.avatar-tone-3,
|
||||
.avatar-tone-4,
|
||||
.avatar-tone-5,
|
||||
.avatar-tone-6,
|
||||
.avatar-tone-7 {
|
||||
color: var(--color-zinc-950);
|
||||
}
|
||||
|
||||
.avatar-user {
|
||||
background: color-mix(in srgb, var(--color-indigo-600) 88%, transparent);
|
||||
}
|
||||
|
||||
.avatar-agent {
|
||||
background: color-mix(in srgb, var(--color-emerald-500) 80%, transparent);
|
||||
}
|
||||
|
||||
.avatar-tool {
|
||||
background: color-mix(in srgb, var(--color-amber-500) 80%, transparent);
|
||||
}
|
||||
|
||||
.avatar-system {
|
||||
background: color-mix(in srgb, var(--color-zinc-700) 92%, transparent);
|
||||
color: var(--color-zinc-100);
|
||||
}
|
||||
|
||||
.avatar-tone-1 { background: color-mix(in srgb, var(--color-emerald-500) 80%, transparent); }
|
||||
.avatar-tone-2 { background: color-mix(in srgb, var(--color-sky-500) 80%, transparent); }
|
||||
.avatar-tone-3 { background: color-mix(in srgb, var(--color-violet-500) 80%, transparent); }
|
||||
.avatar-tone-4 { background: color-mix(in srgb, var(--color-amber-500) 80%, transparent); }
|
||||
.avatar-tone-5 { background: color-mix(in srgb, var(--color-rose-500) 80%, transparent); }
|
||||
.avatar-tone-6 { background: color-mix(in srgb, var(--color-sky-400) 80%, transparent); }
|
||||
.avatar-tone-7 { background: color-mix(in srgb, var(--color-fuchsia-400) 80%, transparent); }
|
||||
|
||||
.chat-meta-user {
|
||||
color: rgb(255 255 255 / 0.75);
|
||||
}
|
||||
|
||||
.chat-submeta-user {
|
||||
color: rgb(255 255 255 / 0.7);
|
||||
}
|
||||
|
||||
.chat-meta-tool {
|
||||
color: color-mix(in srgb, var(--button-warning-text) 78%, transparent);
|
||||
}
|
||||
|
||||
.chat-submeta-tool {
|
||||
color: color-mix(in srgb, var(--button-warning-text) 84%, transparent);
|
||||
}
|
||||
|
||||
.ui-dot-live {
|
||||
background: var(--color-emerald-500);
|
||||
}
|
||||
|
||||
.ui-dot-neutral {
|
||||
background: var(--color-zinc-600);
|
||||
}
|
||||
|
||||
.ui-text-danger-hover:hover {
|
||||
color: var(--color-red-500);
|
||||
}
|
||||
|
||||
.ui-card-active-warning {
|
||||
border-color: var(--color-amber-500);
|
||||
background: var(--pill-warning-bg);
|
||||
}
|
||||
|
||||
.topology-accent-warning {
|
||||
background: var(--color-amber-500);
|
||||
}
|
||||
|
||||
.topology-icon-danger {
|
||||
color: var(--color-red-500);
|
||||
}
|
||||
|
||||
html.theme-dark .brand-button {
|
||||
box-shadow: 0 10px 24px var(--button-shadow);
|
||||
}
|
||||
|
||||
@@ -68,13 +68,13 @@ function formatAgentName(agentID?: string): string {
|
||||
|
||||
function avatarSeed(key?: string): string {
|
||||
const palette = [
|
||||
'bg-emerald-600/80 text-white',
|
||||
'bg-sky-600/80 text-white',
|
||||
'bg-violet-600/80 text-white',
|
||||
'bg-amber-600/80 text-white',
|
||||
'bg-rose-600/80 text-white',
|
||||
'bg-cyan-600/80 text-white',
|
||||
'bg-fuchsia-600/80 text-white',
|
||||
'avatar-tone-1',
|
||||
'avatar-tone-2',
|
||||
'avatar-tone-3',
|
||||
'avatar-tone-4',
|
||||
'avatar-tone-5',
|
||||
'avatar-tone-6',
|
||||
'avatar-tone-7',
|
||||
];
|
||||
const source = String(key || 'agent');
|
||||
let hash = 0;
|
||||
@@ -192,12 +192,12 @@ const Chat: React.FC = () => {
|
||||
|
||||
const actorName = role === 'user' ? t('user') : role === 'tool' || role === 'exec' ? t('exec') : role === 'system' ? t('system') : t('agent');
|
||||
const avatarClassName = role === 'user'
|
||||
? 'bg-indigo-600/90 text-white'
|
||||
? 'avatar-user'
|
||||
: role === 'tool' || role === 'exec'
|
||||
? 'bg-amber-600/80 text-white'
|
||||
? 'avatar-tool'
|
||||
: role === 'system'
|
||||
? 'bg-zinc-700 text-zinc-100'
|
||||
: 'bg-emerald-600/80 text-white';
|
||||
? 'avatar-system'
|
||||
: 'avatar-agent';
|
||||
|
||||
return {
|
||||
id: `${targetSessionKey}-${index}`,
|
||||
@@ -312,7 +312,7 @@ const Chat: React.FC = () => {
|
||||
label: t('user'),
|
||||
actorName: t('user'),
|
||||
avatarText: 'U',
|
||||
avatarClassName: 'bg-indigo-600/90 text-white',
|
||||
avatarClassName: 'avatar-user',
|
||||
}]);
|
||||
|
||||
const currentMsg = msg;
|
||||
@@ -334,7 +334,7 @@ const Chat: React.FC = () => {
|
||||
label: t('agent'),
|
||||
actorName: t('agent'),
|
||||
avatarText: 'A',
|
||||
avatarClassName: 'bg-emerald-600/80 text-white',
|
||||
avatarClassName: 'avatar-agent',
|
||||
}]);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
@@ -656,23 +656,23 @@ const Chat: React.FC = () => {
|
||||
const active = dispatchAgentID === agent.agent_id;
|
||||
const badge = runtimeBadgeByAgent[String(agent.agent_id || '')];
|
||||
const badgeClass = badge?.status === 'running'
|
||||
? 'bg-emerald-500/15 text-emerald-300 border-emerald-500/30'
|
||||
? 'ui-pill-success'
|
||||
: badge?.status === 'waiting'
|
||||
? 'bg-amber-500/15 text-amber-300 border-amber-500/30'
|
||||
? 'ui-pill-warning'
|
||||
: badge?.status === 'failed'
|
||||
? 'bg-rose-500/15 text-rose-300 border-rose-500/30'
|
||||
? 'ui-pill-danger'
|
||||
: badge?.status === 'completed'
|
||||
? 'bg-sky-500/15 text-sky-300 border-sky-500/30'
|
||||
: 'bg-zinc-800 text-zinc-400 border-zinc-700';
|
||||
? 'ui-pill-info'
|
||||
: 'ui-pill-neutral';
|
||||
return (
|
||||
<button
|
||||
key={agent.agent_id}
|
||||
onClick={() => setDispatchAgentID(String(agent.agent_id || ''))}
|
||||
className={`w-full text-left rounded-2xl border px-3 py-2.5 ${active ? 'border-amber-500 bg-amber-500/10' : 'border-zinc-800 bg-zinc-900/50 hover:bg-zinc-900/70'}`}
|
||||
className={`w-full text-left rounded-2xl border px-3 py-2.5 ${active ? 'ui-card-active-warning' : 'border-zinc-800 bg-zinc-900/50 hover:bg-zinc-900/70'}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="text-sm font-medium text-zinc-100">{formatAgentName(agent.display_name || agent.agent_id)}</div>
|
||||
<span className={`inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] ${badgeClass}`}>{badge?.text || t('idle')}</span>
|
||||
<span className={`ui-pill inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] ${badgeClass}`}>{badge?.text || t('idle')}</span>
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500">{agent.agent_id} · {agent.role || '-'}</div>
|
||||
</button>
|
||||
@@ -711,14 +711,14 @@ const Chat: React.FC = () => {
|
||||
? 'chat-bubble-system rounded-bl-sm'
|
||||
: 'chat-bubble-agent rounded-bl-sm';
|
||||
const metaClass = isUser
|
||||
? 'text-white/75'
|
||||
? 'chat-meta-user'
|
||||
: isExec
|
||||
? 'text-amber-800/75 dark:text-amber-100/75'
|
||||
? 'chat-meta-tool'
|
||||
: 'text-zinc-500 dark:text-zinc-400';
|
||||
const subLabelClass = isUser
|
||||
? 'text-white/70'
|
||||
? 'chat-submeta-user'
|
||||
: isExec
|
||||
? 'text-amber-700/80 dark:text-amber-100/70'
|
||||
? 'chat-submeta-tool'
|
||||
: 'text-zinc-500 dark:text-zinc-400';
|
||||
|
||||
return (
|
||||
@@ -729,7 +729,7 @@ const Chat: React.FC = () => {
|
||||
className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div className={`flex items-start gap-2 max-w-full sm:max-w-[96%] ${isUser ? 'flex-row-reverse' : 'flex-row'}`}>
|
||||
<div className={`w-9 h-9 mt-1 rounded-full text-[11px] font-bold flex items-center justify-center shrink-0 ${m.avatarClassName || (isUser ? 'bg-indigo-600/90 text-white' : 'bg-emerald-600/80 text-white')}`}>{m.avatarText || (isUser ? 'U' : 'A')}</div>
|
||||
<div className={`w-9 h-9 mt-1 rounded-full text-[11px] font-bold flex items-center justify-center shrink-0 ${m.avatarClassName || (isUser ? 'avatar-user' : 'avatar-agent')}`}>{m.avatarText || (isUser ? 'U' : 'A')}</div>
|
||||
<div className={`max-w-[calc(100vw-6rem)] sm:max-w-[92%] rounded-[24px] px-4 py-3 shadow-sm ${bubbleClass}`}>
|
||||
<div className="flex items-center justify-between gap-3 mb-1">
|
||||
<div className={`text-[11px] font-medium ${metaClass}`}>{m.actorName || m.label || (isUser ? t('user') : isExec ? t('exec') : isSystem ? t('system') : t('agent'))}</div>
|
||||
@@ -774,7 +774,7 @@ const Chat: React.FC = () => {
|
||||
<button
|
||||
onClick={send}
|
||||
disabled={chatTab !== 'main' || (!msg.trim() && !fileSelected)}
|
||||
className="absolute right-2 p-2.5 brand-button disabled:opacity-50 text-white rounded-full transition-colors"
|
||||
className="absolute right-2 p-2.5 brand-button disabled:opacity-50 text-zinc-950 rounded-full transition-colors"
|
||||
>
|
||||
<Send className="w-4 h-4 ml-0.5" />
|
||||
</button>
|
||||
|
||||
@@ -598,7 +598,7 @@ const Config: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{showDiff && (
|
||||
<div className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4">
|
||||
<div className="ui-overlay-strong fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-4xl max-h-[85vh] brand-card border border-zinc-800 rounded-[30px] overflow-hidden flex flex-col">
|
||||
<div className="px-4 py-3 border-b border-zinc-800 flex items-center justify-between">
|
||||
<div className="font-semibold">{t('configDiffPreviewCount', { count: diffRows.length })}</div>
|
||||
|
||||
@@ -253,7 +253,7 @@ const Cron: React.FC = () => {
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setIsCronModalOpen(false)}
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
className="ui-overlay-strong absolute inset-0 backdrop-blur-sm"
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
|
||||
@@ -122,16 +122,16 @@ const Dashboard: React.FC = () => {
|
||||
{t('webui')}: <span className="font-mono text-zinc-300">{webuiVersion}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={refreshAll} className="brand-button flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-colors shrink-0 text-white">
|
||||
<button onClick={refreshAll} className="brand-button flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-colors shrink-0 text-zinc-950">
|
||||
<RefreshCw className="w-4 h-4" /> {t('refreshAll')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-6 gap-4">
|
||||
<StatCard title={t('gatewayStatus')} value={isGatewayOnline ? t('online') : t('offline')} icon={<Activity className={`w-6 h-6 ${isGatewayOnline ? 'text-emerald-400' : 'text-red-400'}`} />} />
|
||||
<StatCard title={t('activeSessions')} value={sessions.length} icon={<MessageSquare className="w-6 h-6 text-blue-400" />} />
|
||||
<StatCard title={t('skills')} value={skills.length} icon={<Sparkles className="w-6 h-6 text-pink-400" />} />
|
||||
<StatCard title={t('subagentsRuntime')} value={subagentCount} icon={<Wrench className="w-6 h-6 text-cyan-400" />} />
|
||||
<StatCard title={t('activeSessions')} value={sessions.length} icon={<MessageSquare className="w-6 h-6 text-sky-400" />} />
|
||||
<StatCard title={t('skills')} value={skills.length} icon={<Sparkles className="w-6 h-6 text-rose-300" />} />
|
||||
<StatCard title={t('subagentsRuntime')} value={subagentCount} icon={<Wrench className="w-6 h-6 text-sky-300" />} />
|
||||
<StatCard title={t('taskAudit')} value={recentTasks.length} icon={<Activity className="w-6 h-6 text-amber-400" />} />
|
||||
<StatCard title={t('nodeP2P')} value={p2pEnabled ? `${p2pSessions} · ${p2pTransport}` : t('disabled')} icon={<Workflow className="w-6 h-6 text-violet-400" />} />
|
||||
</div>
|
||||
@@ -172,7 +172,7 @@ const Dashboard: React.FC = () => {
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500 mt-1">{t('nodeArtifactsRetentionHint')}</div>
|
||||
</div>
|
||||
<div className={`rounded-full px-2.5 py-1 text-[11px] font-medium ${artifactRetentionEnabled ? 'bg-emerald-500/10 text-emerald-300' : 'bg-zinc-800 text-zinc-400'}`}>
|
||||
<div className={`ui-pill rounded-full px-2.5 py-1 text-[11px] font-medium ${artifactRetentionEnabled ? 'ui-pill-success' : 'ui-pill-neutral'}`}>
|
||||
{artifactRetentionEnabled ? t('enabled') : t('disabled')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,7 +214,7 @@ const Dashboard: React.FC = () => {
|
||||
<div className="text-sm font-medium text-zinc-100 truncate">{String(alert?.title || '-')}</div>
|
||||
<div className="text-xs text-zinc-500 mt-1 truncate">{String(alert?.node || '-')} · {String(alert?.kind || '-')}</div>
|
||||
</div>
|
||||
<div className={`shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium ${severity === 'critical' ? 'bg-rose-500/10 text-rose-300' : 'bg-amber-500/10 text-amber-300'}`}>
|
||||
<div className={`ui-pill shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium ${severity === 'critical' ? 'ui-pill-danger' : 'ui-pill-warning'}`}>
|
||||
{severity}
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,7 +242,7 @@ const Dashboard: React.FC = () => {
|
||||
<div className="text-sm font-medium text-zinc-200 truncate">{task.task_id || `task-${index + 1}`}</div>
|
||||
<div className="text-xs text-zinc-500 truncate">{task.channel || '-'} · {task.source || '-'}</div>
|
||||
</div>
|
||||
<div className={`shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium ${String(task.status || '').toLowerCase() === 'error' ? 'bg-rose-500/10 text-rose-300' : String(task.status || '').toLowerCase() === 'running' ? 'bg-emerald-500/10 text-emerald-300' : 'bg-zinc-800 text-zinc-400'}`}>
|
||||
<div className={`ui-pill shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium ${String(task.status || '').toLowerCase() === 'error' ? 'ui-pill-danger' : String(task.status || '').toLowerCase() === 'running' ? 'ui-pill-success' : 'ui-pill-neutral'}`}>
|
||||
{task.status || '-'}
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,7 +304,7 @@ const Dashboard: React.FC = () => {
|
||||
{t('dashboardNodeP2PSessionCreated')}: {session.createdAt}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium ${isOpen ? 'bg-emerald-500/10 text-emerald-300' : isConnecting ? 'bg-amber-500/10 text-amber-300' : 'bg-rose-500/10 text-rose-300'}`}>
|
||||
<div className={`ui-pill shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium ${isOpen ? 'ui-pill-success' : isConnecting ? 'ui-pill-warning' : 'ui-pill-danger'}`}>
|
||||
{session.status}
|
||||
</div>
|
||||
</div>
|
||||
@@ -356,7 +356,7 @@ const Dashboard: React.FC = () => {
|
||||
<div className="text-sm font-medium text-zinc-100 truncate">{`${item.node} · ${item.action}`}</div>
|
||||
<div className="text-xs text-zinc-500 mt-1">{item.time}</div>
|
||||
</div>
|
||||
<div className={`shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium ${item.ok ? 'bg-emerald-500/10 text-emerald-300' : 'bg-rose-500/10 text-rose-300'}`}>
|
||||
<div className={`ui-pill shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium ${item.ok ? 'ui-pill-success' : 'ui-pill-danger'}`}>
|
||||
{item.ok ? 'ok' : 'error'}
|
||||
</div>
|
||||
</div>
|
||||
@@ -407,13 +407,13 @@ const Dashboard: React.FC = () => {
|
||||
<div className="text-[11px] text-zinc-500">{String(artifact?.storage || '-')}</div>
|
||||
</div>
|
||||
{isImage && dataUrl && (
|
||||
<img src={dataUrl} alt={String(artifact?.name || 'artifact')} className="max-h-48 rounded-xl border border-zinc-800 object-contain bg-black/30" />
|
||||
<img src={dataUrl} alt={String(artifact?.name || 'artifact')} className="ui-media-surface-strong max-h-48 rounded-xl border object-contain" />
|
||||
)}
|
||||
{isVideo && dataUrl && (
|
||||
<video src={dataUrl} controls className="max-h-48 w-full rounded-xl border border-zinc-800 bg-black/30" />
|
||||
<video src={dataUrl} controls className="ui-media-surface-strong max-h-48 w-full rounded-xl border" />
|
||||
)}
|
||||
{!isImage && !isVideo && String(artifact?.content_text || '').trim() !== '' && (
|
||||
<pre className="rounded-xl border border-zinc-800 bg-black/20 p-3 text-[11px] text-zinc-300 whitespace-pre-wrap overflow-auto max-h-48">{String(artifact?.content_text || '')}</pre>
|
||||
<pre className="ui-media-surface rounded-xl border p-3 text-[11px] text-zinc-300 whitespace-pre-wrap overflow-auto max-h-48">{String(artifact?.content_text || '')}</pre>
|
||||
)}
|
||||
{!isImage && !isVideo && String(artifact?.content_text || '').trim() === '' && (
|
||||
<div className="text-[11px] text-zinc-500 break-all">
|
||||
|
||||
@@ -58,7 +58,7 @@ function KVDistributionCard({
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-zinc-800 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-gradient-to-r from-amber-500/70 to-orange-400/80"
|
||||
className="ekg-bar-fill h-full rounded-full"
|
||||
style={{ width: `${maxValue > 0 ? (value / maxValue) * 100 : 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -164,7 +164,7 @@ const EKG: React.FC = () => {
|
||||
<option value="24h">24h</option>
|
||||
<option value="7d">7d</option>
|
||||
</select>
|
||||
<button onClick={fetchData} className="brand-button inline-flex items-center gap-2 px-4 py-2 rounded-xl text-sm text-white">
|
||||
<button onClick={fetchData} className="brand-button inline-flex items-center gap-2 px-4 py-2 rounded-xl text-sm text-zinc-950">
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
{loading ? t('loading') : t('refresh')}
|
||||
</button>
|
||||
@@ -172,10 +172,10 @@ const EKG: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
<StatCard title={t('ekgEscalations')} value={escalationCount} subtitle={`${ekgWindow} window`} accent="bg-amber-500/10 text-amber-300 border border-amber-500/20" icon={<AlertTriangle className="w-5 h-5" />} />
|
||||
<StatCard title={t('ekgSourceStats')} value={sourceCount} subtitle={t('ekgActiveSources')} accent="bg-sky-500/10 text-sky-300 border border-sky-500/20" icon={<Workflow className="w-5 h-5" />} />
|
||||
<StatCard title={t('ekgChannelStats')} value={channelCount} subtitle={t('ekgActiveChannels')} accent="bg-violet-500/10 text-violet-300 border border-violet-500/20" icon={<Route className="w-5 h-5" />} />
|
||||
<StatCard title={t('ekgTopProvidersWorkload')} value={topWorkloadProvider} subtitle={`${t('ekgErrorsCount')} ${totalErrorHits}`} accent="bg-rose-500/10 text-rose-300 border border-rose-500/20" icon={<ServerCrash className="w-5 h-5" />} />
|
||||
<StatCard title={t('ekgEscalations')} value={escalationCount} subtitle={`${ekgWindow} window`} accent="ui-pill ui-pill-warning border" icon={<AlertTriangle className="w-5 h-5" />} />
|
||||
<StatCard title={t('ekgSourceStats')} value={sourceCount} subtitle={t('ekgActiveSources')} accent="ui-pill ui-pill-info border" icon={<Workflow className="w-5 h-5" />} />
|
||||
<StatCard title={t('ekgChannelStats')} value={channelCount} subtitle={t('ekgActiveChannels')} accent="ui-pill ui-pill-accent border" icon={<Route className="w-5 h-5" />} />
|
||||
<StatCard title={t('ekgTopProvidersWorkload')} value={topWorkloadProvider} subtitle={`${t('ekgErrorsCount')} ${totalErrorHits}`} accent="ui-pill ui-pill-danger border" icon={<ServerCrash className="w-5 h-5" />} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-[1.1fr_1fr] gap-6 min-h-0">
|
||||
|
||||
@@ -156,7 +156,7 @@ const Logs: React.FC = () => {
|
||||
switch ((level || 'INFO').toUpperCase()) {
|
||||
case 'ERROR': return 'text-red-400';
|
||||
case 'WARN': return 'text-amber-400';
|
||||
case 'DEBUG': return 'text-blue-400';
|
||||
case 'DEBUG': return 'text-sky-400';
|
||||
default: return 'text-emerald-400';
|
||||
}
|
||||
};
|
||||
@@ -166,10 +166,10 @@ const Logs: React.FC = () => {
|
||||
<div className="flex items-start justify-between gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{t('logs')}</h1>
|
||||
<div className={`flex items-center gap-1.5 px-2.5 py-0.5 rounded-md text-[10px] font-bold uppercase tracking-wider border ${
|
||||
isStreaming ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20' : 'bg-zinc-800 text-zinc-500 border-zinc-700'
|
||||
<div className={`ui-pill flex items-center gap-1.5 px-2.5 py-0.5 rounded-md text-[10px] font-bold uppercase tracking-wider border ${
|
||||
isStreaming ? 'ui-pill-success' : 'ui-pill-neutral'
|
||||
}`}>
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${isStreaming ? 'bg-emerald-500 animate-pulse' : 'bg-zinc-600'}`} />
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${isStreaming ? 'ui-dot-live animate-pulse' : 'ui-dot-neutral'}`} />
|
||||
{isStreaming ? t('live') : t('paused')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,6 @@ const MCP: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { cfg, setCfg, q, loadConfig, setConfigEditing } = useAppContext();
|
||||
const ui = useUI();
|
||||
const [mcpTools, setMcpTools] = useState<Array<{ name: string; description?: string; mcp?: { server?: string; remote_tool?: string } }>>([]);
|
||||
const [mcpServerChecks, setMcpServerChecks] = useState<Array<{ name: string; status?: string; message?: string; package?: string; installer?: string; installable?: boolean; resolved?: string }>>([]);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingName, setEditingName] = useState<string | null>(null);
|
||||
@@ -61,12 +60,10 @@ const MCP: React.FC = () => {
|
||||
if (!r.ok) throw new Error('Failed to load tools');
|
||||
const data = await r.json();
|
||||
if (!cancelled) {
|
||||
setMcpTools(Array.isArray(data?.mcp_tools) ? data.mcp_tools : []);
|
||||
setMcpServerChecks(Array.isArray(data?.mcp_server_checks) ? data.mcp_server_checks : []);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setMcpTools([]);
|
||||
setMcpServerChecks([]);
|
||||
}
|
||||
}
|
||||
@@ -383,14 +380,13 @@ const MCP: React.FC = () => {
|
||||
{serverEntries.map(([name, server]) => {
|
||||
const transport = String(server?.transport || 'stdio');
|
||||
const check = mcpServerChecks.find((item) => item.name === name);
|
||||
const discoveredCount = mcpTools.filter((tool) => tool.mcp?.server === name).length;
|
||||
return (
|
||||
<div key={name} className="brand-card ui-panel rounded-[24px] p-4 space-y-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="font-mono text-sm text-zinc-100">{name}</div>
|
||||
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] ${server?.enabled ? 'bg-emerald-500/12 text-emerald-300 border border-emerald-500/20' : 'bg-zinc-800 text-zinc-400 border border-zinc-700/70'}`}>
|
||||
<span className={`ui-pill inline-flex items-center rounded-full px-2 py-0.5 text-[11px] border ${server?.enabled ? 'ui-pill-success' : 'ui-pill-neutral'}`}>
|
||||
{server?.enabled ? t('enable') : t('paused')}
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-[11px] bg-zinc-900/70 text-zinc-400 border border-zinc-800">
|
||||
@@ -427,14 +423,10 @@ const MCP: React.FC = () => {
|
||||
<div className="text-zinc-500">permission</div>
|
||||
<div className="mt-1 text-zinc-200">{String(server?.permission || 'workspace')}</div>
|
||||
</div>
|
||||
<div className="ui-code-panel px-3 py-2">
|
||||
<div className="text-zinc-500">{t('configMCPDiscoveredTools')}</div>
|
||||
<div className="mt-1 text-zinc-200">{discoveredCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{check && check.status !== 'ok' && check.status !== 'disabled' && check.status !== 'not_applicable' && (
|
||||
<div className="rounded-2xl border border-amber-800/60 bg-amber-950/30 px-3 py-2 text-xs text-amber-100">
|
||||
<div className="ui-notice-warning rounded-2xl border px-3 py-2 text-xs">
|
||||
<div>{check.message || t('configMCPCommandMissing')}</div>
|
||||
{check.package && (
|
||||
<div className="mt-1 text-amber-300/80">{t('configMCPInstallSuggested', { pkg: check.package })}</div>
|
||||
@@ -452,31 +444,6 @@ const MCP: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="brand-card-subtle ui-subpanel rounded-2xl p-3 space-y-3">
|
||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||
<div className="text-sm font-semibold text-zinc-200">{t('configMCPDiscoveredTools')}</div>
|
||||
<div className="text-xs text-zinc-500">{t('configMCPDiscoveredToolsCount', { count: mcpTools.length })}</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{mcpTools.map((tool) => (
|
||||
<div key={tool.name} className="ui-soft-panel rounded-xl p-3">
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div className="font-mono text-xs text-zinc-200">{tool.name}</div>
|
||||
<div className="text-[11px] text-zinc-500">
|
||||
{(tool.mcp?.server || '-')}{' · '}{(tool.mcp?.remote_tool || '-')}
|
||||
</div>
|
||||
</div>
|
||||
{tool.description && (
|
||||
<div className="mt-2 text-xs text-zinc-400">{tool.description}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{mcpTools.length === 0 && (
|
||||
<div className="text-xs text-zinc-500">{t('configNoMCPDiscoveredTools')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{modalOpen && (
|
||||
<motion.div
|
||||
@@ -486,7 +453,7 @@ const MCP: React.FC = () => {
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
className="ui-overlay-strong absolute inset-0 backdrop-blur-sm"
|
||||
onClick={closeModal}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
@@ -610,7 +577,7 @@ const MCP: React.FC = () => {
|
||||
)}
|
||||
|
||||
{activeCheck && activeCheck.status !== 'ok' && activeCheck.status !== 'disabled' && activeCheck.status !== 'not_applicable' && (
|
||||
<div className="rounded-2xl border border-amber-800/60 bg-amber-950/30 px-4 py-3 text-xs text-amber-100 space-y-2">
|
||||
<div className="ui-notice-warning rounded-2xl border px-4 py-3 text-xs space-y-2">
|
||||
<div>{activeCheck.message || t('configMCPCommandMissing')}</div>
|
||||
{activeCheck.package && (
|
||||
<div className="text-amber-300/80">{t('configMCPInstallSuggested', { pkg: activeCheck.package })}</div>
|
||||
|
||||
@@ -251,10 +251,10 @@ const NodeArtifacts: React.FC = () => {
|
||||
const isVideo = kind === 'video' || mime.startsWith('video/');
|
||||
const dataUrl = dataUrlForArtifact(selected);
|
||||
if (isImage && dataUrl) {
|
||||
return <img src={dataUrl} alt={String(selected?.name || 'artifact')} className="max-h-[420px] rounded-2xl border border-zinc-800 object-contain bg-black/30" />;
|
||||
return <img src={dataUrl} alt={String(selected?.name || 'artifact')} className="ui-media-surface-strong max-h-[420px] rounded-2xl border object-contain" />;
|
||||
}
|
||||
if (isVideo && dataUrl) {
|
||||
return <video src={dataUrl} controls className="max-h-[420px] w-full rounded-2xl border border-zinc-800 bg-black/30" />;
|
||||
return <video src={dataUrl} controls className="ui-media-surface-strong max-h-[420px] w-full rounded-2xl border" />;
|
||||
}
|
||||
if (String(selected?.content_text || '').trim() !== '') {
|
||||
return <pre className="ui-code-panel p-3 text-[12px] whitespace-pre-wrap overflow-auto max-h-[420px]">{String(selected?.content_text || '')}</pre>;
|
||||
|
||||
@@ -348,10 +348,10 @@ const Nodes: React.FC = () => {
|
||||
{selectedNodeAlerts.length > 0 ? selectedNodeAlerts.map((alert: any, index: number) => {
|
||||
const severity = String(alert?.severity || 'warning');
|
||||
return (
|
||||
<div key={`${alert?.kind || index}-${index}`} className={`rounded-2xl border p-3 ${severity === 'critical' ? 'border-rose-900/60 bg-rose-950/20' : 'border-amber-900/60 bg-amber-950/20'}`}>
|
||||
<div key={`${alert?.kind || index}-${index}`} className={`rounded-2xl border p-3 ${severity === 'critical' ? 'ui-notice-danger' : 'ui-notice-warning'}`}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-medium text-zinc-100">{String(alert?.title || '-')}</div>
|
||||
<div className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${severity === 'critical' ? 'bg-rose-500/10 text-rose-300' : 'bg-amber-500/10 text-amber-300'}`}>{severity}</div>
|
||||
<div className={`ui-pill rounded-full px-2 py-0.5 text-[10px] font-medium ${severity === 'critical' ? 'ui-pill-danger' : 'ui-pill-warning'}`}>{severity}</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-zinc-300 whitespace-pre-wrap break-words">{String(alert?.detail || '-')}</div>
|
||||
</div>
|
||||
@@ -382,7 +382,7 @@ const Nodes: React.FC = () => {
|
||||
<div className="text-zinc-500 text-xs mb-1">{t('agentTree')}</div>
|
||||
<div className="ui-code-panel p-3 space-y-2">
|
||||
{Array.isArray(selectedTree?.items) && selectedTree.items.length > 0 ? selectedTree.items.map((item: any, index: number) => (
|
||||
<div key={`${item?.agent_id || index}`} className="rounded-xl border border-zinc-800/80 bg-black/20 p-3">
|
||||
<div key={`${item?.agent_id || index}`} className="ui-media-surface rounded-xl border border-zinc-800/80 p-3">
|
||||
<div className="text-sm font-medium text-zinc-100">{String(item?.display_name || item?.agent_id || '-')}</div>
|
||||
<div className="text-xs text-zinc-500 mt-1">{String(item?.agent_id || '-')} · {String(item?.transport || '-')} · {String(item?.role || '-')}</div>
|
||||
</div>
|
||||
@@ -512,7 +512,7 @@ const Nodes: React.FC = () => {
|
||||
<div>
|
||||
<div className="text-zinc-500 text-xs mb-1">{t('nodeReplayResult')}</div>
|
||||
{replayError ? (
|
||||
<div className="rounded-2xl border border-red-900/50 bg-red-950/20 p-3 text-xs whitespace-pre-wrap text-red-300">{replayError}</div>
|
||||
<div className="ui-notice-danger rounded-2xl border p-3 text-xs whitespace-pre-wrap">{replayError}</div>
|
||||
) : (
|
||||
<pre className="ui-code-panel p-3 text-xs overflow-auto">{replayResult ? JSON.stringify(replayResult, null, 2) : '-'}</pre>
|
||||
)}
|
||||
@@ -529,16 +529,16 @@ const Nodes: React.FC = () => {
|
||||
const isVideo = kind === 'video' || mime.startsWith('video/');
|
||||
const dataUrl = dataUrlForArtifact(artifact);
|
||||
return (
|
||||
<div key={`artifact-${artifactIndex}`} className="rounded-xl border border-zinc-800/80 bg-black/20 p-3">
|
||||
<div key={`artifact-${artifactIndex}`} className="ui-media-surface rounded-xl border border-zinc-800/80 p-3">
|
||||
<div className="text-xs font-medium text-zinc-200 truncate">{String(artifact?.name || artifact?.source_path || `artifact-${artifactIndex + 1}`)}</div>
|
||||
<div className="text-[11px] text-zinc-500 mt-1 truncate">
|
||||
{[artifact?.kind, artifact?.mime_type, formatBytes(artifact?.size_bytes)].filter(Boolean).join(' · ')}
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{isImage && dataUrl && <img src={dataUrl} alt={String(artifact?.name || 'artifact')} className="max-h-56 rounded-xl border border-zinc-800 object-contain bg-black/30" />}
|
||||
{isVideo && dataUrl && <video src={dataUrl} controls className="max-h-56 w-full rounded-xl border border-zinc-800 bg-black/30" />}
|
||||
{isImage && dataUrl && <img src={dataUrl} alt={String(artifact?.name || 'artifact')} className="ui-media-surface-strong max-h-56 rounded-xl border object-contain" />}
|
||||
{isVideo && dataUrl && <video src={dataUrl} controls className="ui-media-surface-strong max-h-56 w-full rounded-xl border" />}
|
||||
{!isImage && !isVideo && String(artifact?.content_text || '').trim() !== '' && (
|
||||
<pre className="rounded-xl border border-zinc-800 bg-black/20 p-3 text-[11px] text-zinc-300 whitespace-pre-wrap overflow-auto max-h-48">{String(artifact?.content_text || '')}</pre>
|
||||
<pre className="ui-media-surface rounded-xl border p-3 text-[11px] text-zinc-300 whitespace-pre-wrap overflow-auto max-h-48">{String(artifact?.content_text || '')}</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -202,7 +202,7 @@ const Skills: React.FC = () => {
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div className={`text-xs px-2.5 py-1 rounded-md border font-medium ${clawhubInstalled ? 'text-emerald-700 border-emerald-300 bg-emerald-50' : 'text-amber-700 border-amber-300 bg-amber-50'}`} title={clawhubPath || t('skillsClawhubNotFound')}>
|
||||
<div className={`ui-pill text-xs px-2.5 py-1 rounded-md border font-medium ${clawhubInstalled ? 'ui-pill-success' : 'ui-pill-warning'}`} title={clawhubPath || t('skillsClawhubNotFound')}>
|
||||
{t('skillsClawhubStatus')}: {clawhubInstalled ? t('installed') : t('notInstalled')}
|
||||
</div>
|
||||
{!clawhubInstalled && (
|
||||
@@ -225,7 +225,7 @@ const Skills: React.FC = () => {
|
||||
{!clawhubInstalled && (
|
||||
<div className="rounded-2xl border border-zinc-800/80 bg-zinc-950/45 p-4 text-sm shadow-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-amber-400/20 bg-amber-500/10 text-amber-300">
|
||||
<div className="ui-pill ui-pill-warning mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border">
|
||||
<Zap className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
@@ -275,7 +275,7 @@ const Skills: React.FC = () => {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteSkill(s.id)}
|
||||
className="p-2 bg-red-500/10 text-red-500 hover:bg-red-500/20 rounded-lg transition-colors"
|
||||
className="ui-pill ui-pill-danger p-2 rounded-lg transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -287,7 +287,7 @@ const Skills: React.FC = () => {
|
||||
<AnimatePresence>
|
||||
{isFileModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={() => setIsFileModalOpen(false)} className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={() => setIsFileModalOpen(false)} className="ui-overlay-strong absolute inset-0 backdrop-blur-sm" />
|
||||
<motion.div initial={{ opacity: 0, scale: 0.96 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.96 }} className="relative w-full max-w-6xl h-[80vh] bg-zinc-900 border border-zinc-800 rounded-3xl shadow-2xl overflow-hidden flex">
|
||||
<aside className="w-72 border-r border-zinc-800 bg-zinc-950/60 p-3 overflow-y-auto">
|
||||
<div className="text-sm font-semibold mb-3">{activeSkill} {t('files')}</div>
|
||||
@@ -301,7 +301,7 @@ const Skills: React.FC = () => {
|
||||
<div className="px-4 py-3 border-b border-zinc-800 flex items-center justify-between">
|
||||
<div className="text-sm text-zinc-300 font-mono truncate">{activeFile || t('noFileSelected')}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={saveFile} className="px-3 py-1.5 rounded bg-emerald-600 hover:bg-emerald-500 text-white text-xs flex items-center gap-1"><Save className="w-3 h-3"/>{t('save')}</button>
|
||||
<button onClick={saveFile} className="ui-button ui-button-success px-3 py-1.5 rounded text-xs flex items-center gap-1"><Save className="w-3 h-3"/>{t('save')}</button>
|
||||
<button onClick={() => setIsFileModalOpen(false)} className="p-2 hover:bg-zinc-800 rounded-full transition-colors text-zinc-400"><X className="w-4 h-4" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -159,7 +159,7 @@ type GraphCardSpec = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
meta: string[];
|
||||
accent: string;
|
||||
accentTone: 'success' | 'danger' | 'warning' | 'info' | 'accent' | 'neutral';
|
||||
online?: boolean;
|
||||
clickable?: boolean;
|
||||
highlighted?: boolean;
|
||||
@@ -169,6 +169,30 @@ type GraphCardSpec = {
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
function graphAccentBackgroundClass(accentTone: GraphCardSpec['accentTone']) {
|
||||
switch (accentTone) {
|
||||
case 'success': return 'bg-gradient-to-br from-transparent to-emerald-500';
|
||||
case 'danger': return 'bg-gradient-to-br from-transparent to-red-500';
|
||||
case 'warning': return 'bg-gradient-to-br from-transparent to-amber-400';
|
||||
case 'info': return 'bg-gradient-to-br from-transparent to-sky-400';
|
||||
case 'accent': return 'bg-gradient-to-br from-transparent to-violet-400';
|
||||
case 'neutral':
|
||||
default: return 'bg-gradient-to-br from-transparent to-zinc-500';
|
||||
}
|
||||
}
|
||||
|
||||
function graphAccentIconClass(accentTone: GraphCardSpec['accentTone']) {
|
||||
switch (accentTone) {
|
||||
case 'success': return 'text-emerald-500';
|
||||
case 'danger': return 'topology-icon-danger';
|
||||
case 'warning': return 'text-amber-400';
|
||||
case 'info': return 'text-sky-400';
|
||||
case 'accent': return 'text-violet-400';
|
||||
case 'neutral':
|
||||
default: return 'text-zinc-500';
|
||||
}
|
||||
}
|
||||
|
||||
type GraphLineSpec = {
|
||||
path: string;
|
||||
dashed?: boolean;
|
||||
@@ -328,27 +352,27 @@ function GraphCard({
|
||||
>
|
||||
{/* Sleek Glass Node Background */}
|
||||
<div className={`absolute inset-0 rounded-full transition-all duration-300 backdrop-blur-md ${card.highlighted
|
||||
? 'shadow-[0_0_30px_rgba(245,158,11,0.2)]'
|
||||
: 'shadow-xl shadow-black/60 group-hover:shadow-[0_0_20px_rgba(255,255,255,0.05)]'
|
||||
? 'topology-node-highlight'
|
||||
: 'topology-node-base'
|
||||
}`}>
|
||||
{/* Base dark glass */}
|
||||
<div className="absolute inset-0 rounded-full bg-gradient-to-b from-zinc-800/95 to-zinc-950/95" />
|
||||
|
||||
{/* Subtle accent glow */}
|
||||
<div className={`absolute inset-0 rounded-full opacity-20 ${card.accent.replace('bg-', 'bg-gradient-to-br from-transparent to-')}`} />
|
||||
<div className={`absolute inset-0 rounded-full opacity-20 ${graphAccentBackgroundClass(card.accentTone)}`} />
|
||||
|
||||
{/* Inner depth ring */}
|
||||
<div className="absolute inset-[1px] rounded-full border border-white/5" />
|
||||
<div className="topology-node-inner-border absolute inset-[1px] rounded-full border" />
|
||||
|
||||
{/* Border ring */}
|
||||
<div className={`absolute inset-0 rounded-full border-[1.5px] ${card.highlighted ? 'border-amber-500/80' : 'border-zinc-700/80 group-hover:border-zinc-500/80'
|
||||
<div className={`absolute inset-0 rounded-full border-[1.5px] ${card.highlighted ? 'topology-node-border-highlight' : 'border-zinc-700/80 group-hover:border-zinc-500/80'
|
||||
}`} />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 flex flex-col items-center justify-center w-full px-4 text-center">
|
||||
<div className={`flex items-center justify-center w-10 h-10 mb-1 rounded-full bg-zinc-950/60 border border-zinc-700/50 shadow-inner backdrop-blur-sm`}>
|
||||
<Icon className={`w-5 h-5 ${card.accent.replace('bg-', 'text-')}`} />
|
||||
<Icon className={`w-5 h-5 ${graphAccentIconClass(card.accentTone)}`} />
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
@@ -357,7 +381,7 @@ function GraphCard({
|
||||
</div>
|
||||
|
||||
{card.online !== undefined && (
|
||||
<div className={`absolute top-6 right-6 w-2.5 h-2.5 rounded-full border border-zinc-900 ${card.online ? 'bg-emerald-500 shadow-[0_0_10px_rgba(16,185,129,0.8)]' : 'bg-red-500'}`} />
|
||||
<div className={`absolute top-6 right-6 w-2.5 h-2.5 rounded-full border border-zinc-900 ${card.online ? 'status-dot-online topology-online-indicator' : 'status-dot-offline'}`} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -616,7 +640,7 @@ const Subagents: React.FC = () => {
|
||||
(localMainRegistry?.inherited_tools || []).length ? `inherits: ${(localMainRegistry?.inherited_tools || []).join(', ')}` : 'inherits: -',
|
||||
localMainStats.active[0] ? `task: ${localMainStats.active[0].title}` : t('noLiveTasks'),
|
||||
],
|
||||
accent: localMainStats.running > 0 ? 'bg-emerald-500' : localMainStats.latestStatus === 'failed' ? 'bg-red-500' : 'bg-amber-400',
|
||||
accentTone: localMainStats.running > 0 ? 'success' : localMainStats.latestStatus === 'failed' ? 'danger' : 'warning',
|
||||
clickable: true,
|
||||
scale,
|
||||
onClick: () => {
|
||||
@@ -654,7 +678,7 @@ const Subagents: React.FC = () => {
|
||||
(childRegistry?.inherited_tools || []).length ? `inherits: ${(childRegistry?.inherited_tools || []).join(', ')}` : 'inherits: -',
|
||||
stats.active[0] ? `task: ${stats.active[0].title}` : task ? `last: ${summarizeTask(task.task, task.label)}` : t('noLiveTasks'),
|
||||
],
|
||||
accent: stats.running > 0 ? 'bg-emerald-500' : stats.latestStatus === 'failed' ? 'bg-red-500' : 'bg-sky-400',
|
||||
accentTone: stats.running > 0 ? 'success' : stats.latestStatus === 'failed' ? 'danger' : 'info',
|
||||
clickable: true,
|
||||
scale,
|
||||
onClick: () => {
|
||||
@@ -699,7 +723,7 @@ const Subagents: React.FC = () => {
|
||||
`source=${normalizeTitle(treeRoot.managed_by, tree.source || '-')}`,
|
||||
t('remoteTasksUnavailable'),
|
||||
],
|
||||
accent: !tree.online ? 'bg-zinc-500' : normalizeTitle(p2pSession?.status, '').toLowerCase() === 'open' ? 'bg-emerald-400' : normalizeTitle(p2pSession?.status, '').toLowerCase() === 'connecting' ? 'bg-amber-400' : 'bg-fuchsia-400',
|
||||
accentTone: !tree.online ? 'neutral' : normalizeTitle(p2pSession?.status, '').toLowerCase() === 'open' ? 'success' : normalizeTitle(p2pSession?.status, '').toLowerCase() === 'connecting' ? 'warning' : 'accent',
|
||||
clickable: true,
|
||||
scale,
|
||||
onClick: () => {
|
||||
@@ -737,7 +761,7 @@ const Subagents: React.FC = () => {
|
||||
`source=${normalizeTitle(child.managed_by, 'remote_webui')}`,
|
||||
t('remoteTasksUnavailable'),
|
||||
],
|
||||
accent: normalizeTitle(p2pSession?.status, '').toLowerCase() === 'open' ? 'bg-emerald-400' : normalizeTitle(p2pSession?.status, '').toLowerCase() === 'connecting' ? 'bg-amber-400' : 'bg-violet-400',
|
||||
accentTone: normalizeTitle(p2pSession?.status, '').toLowerCase() === 'open' ? 'success' : normalizeTitle(p2pSession?.status, '').toLowerCase() === 'connecting' ? 'warning' : 'accent',
|
||||
clickable: true,
|
||||
scale,
|
||||
onClick: () => {
|
||||
@@ -1134,7 +1158,7 @@ const Subagents: React.FC = () => {
|
||||
<div className="h-full p-4 md:p-6 xl:p-8 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<h1 className="text-xl md:text-2xl font-semibold">{t('subagentsRuntime')}</h1>
|
||||
<button onClick={() => load()} className="brand-button px-3 py-1.5 rounded-xl text-sm text-white">{t('refresh')}</button>
|
||||
<button onClick={() => load()} className="brand-button px-3 py-1.5 rounded-xl text-sm text-zinc-950">{t('refresh')}</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 brand-card border border-zinc-800 p-4 flex flex-col gap-3">
|
||||
@@ -1285,7 +1309,7 @@ const Subagents: React.FC = () => {
|
||||
<path
|
||||
d={line.path}
|
||||
fill="none"
|
||||
stroke={line.highlighted ? 'rgba(245,158,11,0.15)' : 'rgba(161,161,170,0.05)'}
|
||||
stroke={line.highlighted ? 'var(--topology-line-highlight-track)' : 'var(--topology-line-track)'}
|
||||
strokeWidth={line.highlighted ? '6' : '2'}
|
||||
strokeLinecap="round"
|
||||
className="transition-all duration-300"
|
||||
@@ -1294,7 +1318,7 @@ const Subagents: React.FC = () => {
|
||||
<path
|
||||
d={line.path}
|
||||
fill="none"
|
||||
stroke={line.highlighted ? 'rgba(245,158,11,0.9)' : 'rgba(161,161,170,0.5)'}
|
||||
stroke={line.highlighted ? 'var(--topology-line-highlight-flow)' : 'var(--topology-line-flow)'}
|
||||
strokeWidth={line.highlighted ? '2.5' : '1.5'}
|
||||
strokeDasharray={line.highlighted ? "6 18" : "4 20"}
|
||||
className={line.highlighted ? "animate-flow-fast" : "animate-flow"}
|
||||
@@ -1312,11 +1336,11 @@ const Subagents: React.FC = () => {
|
||||
</div>
|
||||
{topologyTooltip && (
|
||||
<div
|
||||
className="pointer-events-none fixed z-50 w-[360px] max-w-[min(360px,calc(100vw-24px))] brand-card-subtle border border-zinc-700/80 p-4 shadow-2xl shadow-black/50 backdrop-blur-md transition-opacity duration-200"
|
||||
className="topology-tooltip pointer-events-none fixed z-50 w-[360px] max-w-[min(360px,calc(100vw-24px))] brand-card-subtle border border-zinc-700/80 p-4 backdrop-blur-md transition-opacity duration-200"
|
||||
style={{ left: topologyTooltip.x, top: topologyTooltip.y }}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-2 h-2 rounded-full bg-amber-500" />
|
||||
<div className="topology-accent-warning w-2 h-2 rounded-full" />
|
||||
<div className="text-sm font-semibold text-zinc-100">{topologyTooltip.title}</div>
|
||||
</div>
|
||||
<div className="text-xs text-zinc-400 mb-3 pb-3 border-b border-zinc-800/60">{topologyTooltip.subtitle}</div>
|
||||
|
||||
@@ -190,12 +190,12 @@ const TaskAudit: React.FC = () => {
|
||||
|
||||
<div>
|
||||
<div className="text-zinc-500 text-xs mb-1">{t('error')}</div>
|
||||
<div className="ui-code-panel p-2 whitespace-pre-wrap text-red-300">{selected.error || '-'}</div>
|
||||
<div className="ui-code-panel ui-code-danger p-2 whitespace-pre-wrap">{selected.error || '-'}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-zinc-500 text-xs mb-1">{t('blockReason')}</div>
|
||||
<div className="ui-code-panel p-2 whitespace-pre-wrap text-amber-200">{selected.block_reason || '-'}</div>
|
||||
<div className="ui-code-panel ui-code-warning p-2 whitespace-pre-wrap">{selected.block_reason || '-'}</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
@@ -284,7 +284,7 @@ const TaskAudit: React.FC = () => {
|
||||
|
||||
<div>
|
||||
<div className="text-zinc-500 text-xs mb-1">{t('error')}</div>
|
||||
<div className="ui-code-panel p-2 whitespace-pre-wrap text-red-300">{selectedNode.error || '-'}</div>
|
||||
<div className="ui-code-panel ui-code-danger p-2 whitespace-pre-wrap">{selectedNode.error || '-'}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -303,10 +303,10 @@ const TaskAudit: React.FC = () => {
|
||||
{[artifact?.kind, artifact?.mime_type, formatBytes(artifact?.size_bytes)].filter(Boolean).join(' · ')}
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{isImage && dataUrl && <img src={dataUrl} alt={String(artifact?.name || 'artifact')} className="max-h-48 rounded-xl border border-zinc-800 object-contain bg-black/30" />}
|
||||
{isVideo && dataUrl && <video src={dataUrl} controls className="max-h-48 w-full rounded-xl border border-zinc-800 bg-black/30" />}
|
||||
{isImage && dataUrl && <img src={dataUrl} alt={String(artifact?.name || 'artifact')} className="ui-media-surface-strong max-h-48 rounded-xl border object-contain" />}
|
||||
{isVideo && dataUrl && <video src={dataUrl} controls className="ui-media-surface-strong max-h-48 w-full rounded-xl border" />}
|
||||
{!isImage && !isVideo && String(artifact?.content_text || '').trim() !== '' && (
|
||||
<pre className="rounded-xl border border-zinc-800 bg-black/20 p-3 text-[11px] text-zinc-300 whitespace-pre-wrap overflow-auto max-h-48">{String(artifact?.content_text || '')}</pre>
|
||||
<pre className="ui-media-surface rounded-xl border p-3 text-[11px] text-zinc-300 whitespace-pre-wrap overflow-auto max-h-48">{String(artifact?.content_text || '')}</pre>
|
||||
)}
|
||||
{!isImage && !isVideo && String(artifact?.content_text || '').trim() === '' && (
|
||||
<div className="text-[11px] text-zinc-500 break-all mt-2">{String(artifact?.source_path || artifact?.path || artifact?.url || '-')}</div>
|
||||
|
||||
Reference in New Issue
Block a user