feat(swarm): major premium visual overhaul and restore missing view navigation
This commit is contained in:
parent
b5f1f57143
commit
2d50b31654
3 changed files with 203 additions and 51 deletions
|
|
@ -137,7 +137,7 @@ export function UnifiedShell({
|
|||
{/* LEFT PANEL: 20rem generic tree or 20rem swarm mission picker */}
|
||||
{view === 'swarm' ? (
|
||||
<div className="border-r bg-[var(--color-bg-base)] h-full overflow-y-auto">
|
||||
<SwarmMissionPicker />
|
||||
<SwarmMissionPicker issues={issues} />
|
||||
</div>
|
||||
) : (
|
||||
<LeftPanel
|
||||
|
|
|
|||
|
|
@ -1,37 +1,110 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { useUrlState } from '../../hooks/use-url-state';
|
||||
import { useUrlState, type ViewType } from '../../hooks/use-url-state';
|
||||
import type { BeadIssue } from '../../lib/types';
|
||||
import { FolderGit2 } from 'lucide-react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
// Mock hook for now, would connect to actual beads data
|
||||
const useMissionList = () => ({
|
||||
missions: [
|
||||
{ id: '1', title: 'Sample Mission', progress: 50 }
|
||||
]
|
||||
});
|
||||
export function SwarmMissionPicker({ issues }: { issues: BeadIssue[] }) {
|
||||
const { view, setView, setSwarmId, swarmId } = useUrlState();
|
||||
|
||||
export function SwarmMissionPicker() {
|
||||
const { setView, setSwarmId, swarmId } = useUrlState();
|
||||
const { missions } = useMissionList();
|
||||
const views: Array<{ id: ViewType; label: string }> = [
|
||||
{ id: 'social', label: 'Social' },
|
||||
{ id: 'graph', label: 'Graph' },
|
||||
{ id: 'swarm', label: 'Swarm' },
|
||||
];
|
||||
|
||||
// Filter issues to find epics (Missions)
|
||||
const missions = issues.filter(i => i.issue_type === 'epic' && i.status !== 'closed');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-2 w-full">
|
||||
<h2 className="px-2 text-sm font-semibold text-muted-foreground">Active Missions</h2>
|
||||
{missions.map(m => (
|
||||
<button
|
||||
key={m.id}
|
||||
onClick={() => {
|
||||
setView('swarm');
|
||||
setSwarmId(m.id);
|
||||
}}
|
||||
className={`flex flex-col items-start p-3 min-h-[44px] rounded-md hover:bg-accent transition-colors w-full focus:outline-none focus:ring-2 focus:ring-ring ${swarmId === m.id ? 'bg-accent border' : ''}`}
|
||||
>
|
||||
<span className="font-medium text-sm">{m.title}</span>
|
||||
<div className="w-full bg-secondary h-1 mt-2 rounded">
|
||||
<div className="bg-primary h-1 rounded transition-all duration-300" style={{ width: `${m.progress}%` }} />
|
||||
<aside className="flex h-full flex-col bg-[var(--ui-bg-shell)] shadow-[inset_-1px_0_0_rgba(0,0,0,0.55),24px_0_40px_-34px_rgba(0,0,0,0.98)]" data-testid="left-panel-swarm">
|
||||
<div className="px-4 py-3 shadow-[0_14px_24px_-20px_rgba(0,0,0,0.92)]">
|
||||
<div className="mb-3 flex items-center gap-1 rounded-xl bg-[#101c2b] p-1 shadow-[0_12px_24px_-18px_rgba(0,0,0,0.88)]">
|
||||
{views.map((item) => {
|
||||
const active = view === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => setView(item.id)}
|
||||
className={cn(
|
||||
'flex-1 rounded-lg px-2 py-1 text-xs font-semibold uppercase tracking-[0.12em] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-accent-info)]',
|
||||
active
|
||||
? 'bg-[#183149] text-[var(--ui-text-primary)]'
|
||||
: 'text-[var(--ui-text-muted)] hover:text-[var(--ui-text-primary)]',
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<FolderGit2 className="w-4 h-4 text-[var(--ui-accent-info)]" />
|
||||
<h2 className="font-mono text-[10px] uppercase tracking-[0.16em] text-[var(--ui-text-muted)]">Active Missions</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-3 py-3 custom-scrollbar">
|
||||
{missions.length === 0 ? (
|
||||
<div className="p-4 text-center text-xs text-[var(--ui-text-muted)] bg-white/5 rounded-lg border border-dashed border-white/10">
|
||||
No active missions (epics) found.
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
missions.map(m => {
|
||||
const isSelected = swarmId === m.id;
|
||||
const hasChildren = m.dependencies.filter(d => d.type === 'parent').length;
|
||||
const progress = hasChildren > 0 ? 30 : 0;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
onClick={() => {
|
||||
setView('swarm');
|
||||
setSwarmId(m.id);
|
||||
}}
|
||||
className={cn(
|
||||
'flex flex-col items-start p-3 min-h-[60px] rounded-xl transition-all w-full focus:outline-none focus:ring-2 focus:ring-[var(--ui-accent-info)] text-left mb-2 shadow-[0_18px_28px_-22px_rgba(0,0,0,0.96)]',
|
||||
isSelected
|
||||
? 'bg-[#183149] border border-[#2c4e73] ring-1 ring-[#7dd3fc]/20'
|
||||
: 'bg-[#111f2b] border border-transparent hover:bg-[#152736]'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full mb-1">
|
||||
<span className="font-semibold text-[13px] text-[var(--ui-text-primary)] line-clamp-1">{m.title}</span>
|
||||
<span className={cn(
|
||||
'text-[9px] uppercase font-bold px-1.5 py-0.5 rounded',
|
||||
m.status === 'in_progress' ? 'bg-[var(--ui-accent-warning)]/20 text-[var(--ui-accent-warning)]' : 'bg-white/10 text-[var(--ui-text-muted)]'
|
||||
)}>
|
||||
{m.status.replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-[var(--ui-text-muted)] font-mono mb-2">{m.id}</span>
|
||||
|
||||
<div className="w-full bg-[#0a111a] h-1.5 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="bg-[var(--ui-accent-info)] h-full rounded-full transition-all duration-500 ease-out"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className="border-t border-[var(--ui-border-soft)] px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-full bg-[linear-gradient(135deg,#9cb6bf,#f1dcc6)]" />
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[var(--ui-text-primary)]">Swarm Commander</p>
|
||||
<p className="text-xs text-[var(--ui-text-muted)]">Operations</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { SwarmLiveDag } from './swarm-live-dag';
|
||||
import { ConvoyStepper } from './convoy-stepper';
|
||||
import { Network, Blocks, FileCode2, Info } from 'lucide-react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
export function SwarmWorkspace({ selectedMissionId }: { selectedMissionId?: string }) {
|
||||
const [activeTab, setActiveTab] = useState<'operations' | 'archetypes' | 'templates'>('operations');
|
||||
|
|
@ -9,39 +13,114 @@ export function SwarmWorkspace({ selectedMissionId }: { selectedMissionId?: stri
|
|||
switch (activeTab) {
|
||||
case 'operations':
|
||||
return selectedMissionId
|
||||
? <div>Ops Details for {selectedMissionId}</div>
|
||||
: <div className="p-8 text-center text-muted-foreground">Select a mission from the left panel.</div>;
|
||||
? (
|
||||
<div className="flex flex-col h-full gap-4 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<ConvoyStepper activePhase="execution" />
|
||||
<div className="flex-1 min-h-0 bg-[#0f1824]/50 rounded-xl border border-[var(--ui-border-soft)] p-2 shadow-inner">
|
||||
<SwarmLiveDag epicId={selectedMissionId} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center space-y-4 animate-in fade-in duration-700">
|
||||
<div className="p-4 bg-[var(--ui-accent-info)]/10 rounded-full">
|
||||
<Info className="w-8 h-8 text-[var(--ui-accent-info)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--ui-text-primary)]">No Mission Selected</h3>
|
||||
<p className="text-sm text-[var(--ui-text-muted)] max-w-sm mx-auto mt-1">
|
||||
Select an active mission from the left panel to view live DAG telemetry and convoy operations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'archetypes':
|
||||
return <div>Archetypes UI</div>;
|
||||
return (
|
||||
<div className="p-6 bg-[#0f1824]/30 rounded-xl border border-[var(--ui-border-soft)] h-full animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<h3 className="text-xl font-bold text-[var(--ui-text-primary)] mb-2">Agent Archetypes</h3>
|
||||
<p className="text-[var(--ui-text-muted)] text-sm mb-6">Manage the base roles and system prompts available to your swarms.</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{/* Placeholder Cards */}
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="bg-[#111f2b] p-4 rounded-lg border border-[var(--ui-border-soft)] hover:border-[var(--ui-accent-info)]/50 transition-colors">
|
||||
<div className="h-10 w-10 rounded-lg bg-[var(--ui-accent-info)]/20 mb-3" />
|
||||
<div className="h-4 w-24 bg-white/10 rounded mb-2" />
|
||||
<div className="h-3 w-3/4 bg-white/5 rounded" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'templates':
|
||||
return <div>Templates UI</div>;
|
||||
return (
|
||||
<div className="p-6 bg-[#0f1824]/30 rounded-xl border border-[var(--ui-border-soft)] h-full animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<h3 className="text-xl font-bold text-[var(--ui-text-primary)] mb-2">Swarm Templates</h3>
|
||||
<p className="text-[var(--ui-text-muted)] text-sm mb-6">Define predefined teams and formulas for rapid mission deployment.</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{[1, 2].map(i => (
|
||||
<div key={i} className="bg-[#111f2b] p-5 rounded-lg border border-[var(--ui-border-soft)] flex items-center gap-4 hover:border-amber-500/50 transition-colors">
|
||||
<div className="h-12 w-12 rounded-full bg-amber-500/20" />
|
||||
<div>
|
||||
<div className="h-4 w-32 bg-white/10 rounded mb-2" />
|
||||
<div className="h-3 w-48 bg-white/5 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full">
|
||||
{/* Tab Navigation - Min 44px for mobile targets */}
|
||||
<div className="flex border-b min-h-[44px]">
|
||||
{['operations', 'archetypes', 'templates'].map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab as any)}
|
||||
className={`px-4 py-2 capitalize min-w-[80px] min-h-[44px] ${activeTab === tab
|
||||
? 'border-b-2 border-primary font-bold'
|
||||
: 'text-muted-foreground hover:bg-accent/50'
|
||||
} focus:outline-none focus:ring-2`}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col h-full w-full bg-[radial-gradient(ellipse_at_top,#142336_0%,#090d14_100%)]">
|
||||
<header className="px-6 py-5 border-b border-[var(--ui-border-soft)] bg-black/20 backdrop-blur-md sticky top-0 z-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-[var(--ui-text-primary)] flex items-center gap-2">
|
||||
<Network className="w-6 h-6 text-[var(--ui-accent-info)]" />
|
||||
Swarm Command
|
||||
</h1>
|
||||
<p className="text-sm text-[var(--ui-text-muted)] mt-1">Orchestrate agents, define archetypes, and monitor live DAG telemetry.</p>
|
||||
</div>
|
||||
|
||||
{/* Premium Custom Tabs */}
|
||||
<div className="flex bg-[#0f1824] p-1 rounded-lg border border-[var(--ui-border-soft)] shadow-inner">
|
||||
{[
|
||||
{ id: 'operations', label: 'Operations', icon: Network },
|
||||
{ id: 'archetypes', label: 'Archetypes', icon: Blocks },
|
||||
{ id: 'templates', label: 'Templates', icon: FileCode2 }
|
||||
].map(tab => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as any)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 rounded-md transition-all text-sm font-medium",
|
||||
isActive
|
||||
? "bg-[#183149] text-[var(--ui-text-primary)] shadow-sm ring-1 ring-[#7dd3fc]/20"
|
||||
: "text-[var(--ui-text-muted)] hover:text-[var(--ui-text-primary)] hover:bg-white/5"
|
||||
)}
|
||||
>
|
||||
<Icon className={cn("w-4 h-4", isActive ? "text-[var(--ui-accent-info)]" : "")} />
|
||||
{tab.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 overflow-auto p-4 content-visibility-auto">
|
||||
{renderTabContent()}
|
||||
</div>
|
||||
<main className="flex-1 overflow-auto p-6 custom-scrollbar">
|
||||
<div className="h-full max-w-7xl mx-auto">
|
||||
{renderTabContent()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue