fix: prevent Live Feed sidebar from sliding in on page navigation (#181)
Only apply the slide-in-right animation when the user actively re-expands the panel, not on initial mount. Also remove dead duplicate files src/live-feed.tsx and src/page.tsx.
This commit is contained in:
parent
6f2f0e74af
commit
9d39e51f56
|
|
@ -7,6 +7,7 @@ export function LiveFeed() {
|
|||
const { logs, sessions, activities, connection, dashboardMode, toggleLiveFeed } = useMissionControl()
|
||||
const isLocal = dashboardMode === 'local'
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
const [hasCollapsed, setHasCollapsed] = useState(false)
|
||||
|
||||
// Combine logs, activities, and (in local mode) session events into a unified feed
|
||||
const sessionItems = isLocal
|
||||
|
|
@ -70,7 +71,7 @@ export function LiveFeed() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="w-72 h-full bg-card border-l border-border flex flex-col shrink-0 slide-in-right">
|
||||
<div className={`w-72 h-full bg-card border-l border-border flex flex-col shrink-0${hasCollapsed ? ' slide-in-right' : ''}`}>
|
||||
{/* Header */}
|
||||
<div className="h-10 px-3 flex items-center justify-between border-b border-border shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -80,7 +81,7 @@ export function LiveFeed() {
|
|||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button
|
||||
onClick={() => setExpanded(false)}
|
||||
onClick={() => { setExpanded(false); setHasCollapsed(true) }}
|
||||
className="w-6 h-6 rounded text-muted-foreground hover:text-foreground hover:bg-secondary transition-smooth flex items-center justify-center"
|
||||
title="Collapse feed"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
'use client'
|
||||
|
||||
import { useMissionControl } from '@/store'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function LiveFeed() {
|
||||
const { logs, sessions, activities, connection, toggleLiveFeed } = useMissionControl()
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
|
||||
// Combine logs and activities into a unified feed
|
||||
const feedItems = [
|
||||
...logs.slice(0, 30).map(log => ({
|
||||
id: log.id,
|
||||
type: 'log' as const,
|
||||
level: log.level,
|
||||
message: log.message,
|
||||
source: log.source,
|
||||
timestamp: log.timestamp,
|
||||
})),
|
||||
...activities.slice(0, 20).map(act => ({
|
||||
id: `act-${act.id}`,
|
||||
type: 'activity' as const,
|
||||
level: 'info' as const,
|
||||
message: act.description,
|
||||
source: act.actor,
|
||||
timestamp: act.created_at * 1000,
|
||||
})),
|
||||
].sort((a, b) => b.timestamp - a.timestamp).slice(0, 40)
|
||||
|
||||
if (!expanded) {
|
||||
return (
|
||||
<div className="w-10 bg-card border-l border-border flex flex-col items-center py-3 shrink-0">
|
||||
<button
|
||||
onClick={() => setExpanded(true)}
|
||||
className="w-8 h-8 rounded-md text-muted-foreground hover:text-foreground hover:bg-secondary transition-smooth flex items-center justify-center"
|
||||
title="Show live feed"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M10 3l-5 5 5 5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
{/* Mini indicators */}
|
||||
<div className="mt-4 flex flex-col gap-2 items-center">
|
||||
{feedItems.slice(0, 5).map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`w-1.5 h-1.5 rounded-full ${
|
||||
item.level === 'error' ? 'bg-red-500' :
|
||||
item.level === 'warn' ? 'bg-amber-500' :
|
||||
'bg-blue-500/40'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-72 h-full bg-card border-l border-border flex flex-col shrink-0 slide-in-right">
|
||||
{/* Header */}
|
||||
<div className="h-10 px-3 flex items-center justify-between border-b border-border shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-green-500 pulse-dot" />
|
||||
<span className="text-xs font-semibold text-foreground">Live Feed</span>
|
||||
<span className="text-2xs text-muted-foreground font-mono-tight">{feedItems.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button
|
||||
onClick={() => setExpanded(false)}
|
||||
className="w-6 h-6 rounded text-muted-foreground hover:text-foreground hover:bg-secondary transition-smooth flex items-center justify-center"
|
||||
title="Collapse feed"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M6 3l5 5-5 5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleLiveFeed}
|
||||
className="w-6 h-6 rounded text-muted-foreground hover:text-foreground hover:bg-secondary transition-smooth flex items-center justify-center"
|
||||
title="Close feed"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M4 4l8 8M12 4l-8 8" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feed items */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{feedItems.length === 0 ? (
|
||||
<div className="px-3 py-8 text-center text-xs text-muted-foreground">
|
||||
No activity yet
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border/50">
|
||||
{feedItems.map((item) => (
|
||||
<FeedItem key={item.id} item={item} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Active sessions mini-list */}
|
||||
<div className="border-t border-border px-3 py-2 shrink-0">
|
||||
<div className="text-2xs font-medium text-muted-foreground mb-1.5">Active Sessions</div>
|
||||
<div className="space-y-1">
|
||||
{sessions.filter(s => s.active).slice(0, 4).map(session => (
|
||||
<div key={session.id} className="flex items-center gap-1.5 text-2xs">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-green-500" />
|
||||
<span className="text-foreground truncate flex-1 font-mono-tight">{session.key || session.id}</span>
|
||||
<span className="text-muted-foreground">{session.model?.split('/').pop()?.slice(0, 8)}</span>
|
||||
</div>
|
||||
))}
|
||||
{sessions.filter(s => s.active).length === 0 && (
|
||||
<div className="text-2xs text-muted-foreground">No active sessions</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FeedItem({ item }: { item: { id: string; type: string; level: string; message: string; source: string; timestamp: number } }) {
|
||||
const levelIndicator = item.level === 'error'
|
||||
? 'bg-red-500'
|
||||
: item.level === 'warn'
|
||||
? 'bg-amber-500'
|
||||
: item.level === 'debug'
|
||||
? 'bg-gray-500'
|
||||
: 'bg-blue-500/50'
|
||||
|
||||
const timeStr = formatRelativeTime(item.timestamp)
|
||||
|
||||
return (
|
||||
<div className="px-3 py-2 hover:bg-secondary/50 transition-smooth group">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className={`w-1.5 h-1.5 rounded-full mt-1.5 shrink-0 ${levelIndicator}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs text-foreground/90 leading-relaxed break-words">
|
||||
{item.message.length > 120 ? item.message.slice(0, 120) + '...' : item.message}
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
<span className="text-2xs text-muted-foreground font-mono-tight">{item.source}</span>
|
||||
<span className="text-2xs text-muted-foreground/50">·</span>
|
||||
<span className="text-2xs text-muted-foreground">{timeStr}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatRelativeTime(ts: number): string {
|
||||
const diff = Date.now() - ts
|
||||
if (diff < 60_000) return 'now'
|
||||
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m`
|
||||
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h`
|
||||
return `${Math.floor(diff / 86_400_000)}d`
|
||||
}
|
||||
166
src/page.tsx
166
src/page.tsx
|
|
@ -1,166 +0,0 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { NavRail } from '@/components/layout/nav-rail'
|
||||
import { HeaderBar } from '@/components/layout/header-bar'
|
||||
import { LiveFeed } from '@/components/layout/live-feed'
|
||||
import { Dashboard } from '@/components/dashboard/dashboard'
|
||||
import { AgentSpawnPanel } from '@/components/panels/agent-spawn-panel'
|
||||
import { LogViewerPanel } from '@/components/panels/log-viewer-panel'
|
||||
import { CronManagementPanel } from '@/components/panels/cron-management-panel'
|
||||
import { MemoryBrowserPanel } from '@/components/panels/memory-browser-panel'
|
||||
import { TokenDashboardPanel } from '@/components/panels/token-dashboard-panel'
|
||||
import { SessionDetailsPanel } from '@/components/panels/session-details-panel'
|
||||
import { TaskBoardPanel } from '@/components/panels/task-board-panel'
|
||||
import { ActivityFeedPanel } from '@/components/panels/activity-feed-panel'
|
||||
import { AgentSquadPanelPhase3 } from '@/components/panels/agent-squad-panel-phase3'
|
||||
import { StandupPanel } from '@/components/panels/standup-panel'
|
||||
import { OrchestrationBar } from '@/components/panels/orchestration-bar'
|
||||
import { NotificationsPanel } from '@/components/panels/notifications-panel'
|
||||
import { UserManagementPanel } from '@/components/panels/user-management-panel'
|
||||
import { AuditTrailPanel } from '@/components/panels/audit-trail-panel'
|
||||
import { AgentHistoryPanel } from '@/components/panels/agent-history-panel'
|
||||
import { WebhookPanel } from '@/components/panels/webhook-panel'
|
||||
import { SettingsPanel } from '@/components/panels/settings-panel'
|
||||
import { GatewayConfigPanel } from '@/components/panels/gateway-config-panel'
|
||||
import { IntegrationsPanel } from '@/components/panels/integrations-panel'
|
||||
import { AlertRulesPanel } from '@/components/panels/alert-rules-panel'
|
||||
import { MultiGatewayPanel } from '@/components/panels/multi-gateway-panel'
|
||||
import { ChatPanel } from '@/components/chat/chat-panel'
|
||||
import { useWebSocket } from '@/lib/websocket'
|
||||
import { useServerEvents } from '@/lib/use-server-events'
|
||||
import { useMissionControl } from '@/store'
|
||||
|
||||
export default function Home() {
|
||||
const { connect } = useWebSocket()
|
||||
const { activeTab, setCurrentUser, liveFeedOpen, toggleLiveFeed } = useMissionControl()
|
||||
|
||||
// Connect to SSE for real-time local DB events (tasks, agents, chat, etc.)
|
||||
useServerEvents()
|
||||
const [isClient, setIsClient] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true)
|
||||
|
||||
// Fetch current user
|
||||
fetch('/api/auth/me')
|
||||
.then(res => res.ok ? res.json() : null)
|
||||
.then(data => { if (data?.user) setCurrentUser(data.user) })
|
||||
.catch(() => {})
|
||||
|
||||
// Auto-connect to gateway on mount
|
||||
const wsToken = process.env.NEXT_PUBLIC_GATEWAY_TOKEN || process.env.NEXT_PUBLIC_WS_TOKEN || ''
|
||||
const gatewayPort = process.env.NEXT_PUBLIC_GATEWAY_PORT || '18789'
|
||||
const gatewayHost = window.location.hostname
|
||||
const wsUrl = `ws://${gatewayHost}:${gatewayPort}`
|
||||
connect(wsUrl, wsToken)
|
||||
}, [connect, setCurrentUser])
|
||||
|
||||
if (!isClient) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-primary flex items-center justify-center">
|
||||
<span className="text-primary-foreground font-bold text-sm">MC</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
|
||||
<span className="text-sm text-muted-foreground">Loading Mission Control...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background overflow-hidden">
|
||||
{/* Left: Icon rail navigation (hidden on mobile, shown as bottom bar instead) */}
|
||||
<NavRail />
|
||||
|
||||
{/* Center: Header + Content */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<HeaderBar />
|
||||
<main className="flex-1 overflow-auto pb-16 md:pb-0">
|
||||
<ContentRouter tab={activeTab} />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Right: Live feed (hidden on mobile) */}
|
||||
{liveFeedOpen && (
|
||||
<div className="hidden lg:flex h-full">
|
||||
<LiveFeed />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Floating button to reopen LiveFeed when closed */}
|
||||
{!liveFeedOpen && (
|
||||
<button
|
||||
onClick={toggleLiveFeed}
|
||||
className="hidden lg:flex fixed right-0 top-1/2 -translate-y-1/2 z-30 w-6 h-12 items-center justify-center bg-card border border-r-0 border-border rounded-l-md text-muted-foreground hover:text-foreground hover:bg-secondary transition-all duration-200"
|
||||
title="Show live feed"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M10 3l-5 5 5 5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Chat panel overlay */}
|
||||
<ChatPanel />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ContentRouter({ tab }: { tab: string }) {
|
||||
switch (tab) {
|
||||
case 'overview':
|
||||
return <Dashboard />
|
||||
case 'tasks':
|
||||
return <TaskBoardPanel />
|
||||
case 'agents':
|
||||
return (
|
||||
<>
|
||||
<OrchestrationBar />
|
||||
<AgentSquadPanelPhase3 />
|
||||
</>
|
||||
)
|
||||
case 'activity':
|
||||
return <ActivityFeedPanel />
|
||||
case 'notifications':
|
||||
return <NotificationsPanel />
|
||||
case 'standup':
|
||||
return <StandupPanel />
|
||||
case 'spawn':
|
||||
return <AgentSpawnPanel />
|
||||
case 'sessions':
|
||||
return <SessionDetailsPanel />
|
||||
case 'logs':
|
||||
return <LogViewerPanel />
|
||||
case 'cron':
|
||||
return <CronManagementPanel />
|
||||
case 'memory':
|
||||
return <MemoryBrowserPanel />
|
||||
case 'tokens':
|
||||
return <TokenDashboardPanel />
|
||||
case 'users':
|
||||
return <UserManagementPanel />
|
||||
case 'history':
|
||||
return <AgentHistoryPanel />
|
||||
case 'audit':
|
||||
return <AuditTrailPanel />
|
||||
case 'webhooks':
|
||||
return <WebhookPanel />
|
||||
case 'alerts':
|
||||
return <AlertRulesPanel />
|
||||
case 'gateways':
|
||||
return <MultiGatewayPanel />
|
||||
case 'gateway-config':
|
||||
return <GatewayConfigPanel />
|
||||
case 'integrations':
|
||||
return <IntegrationsPanel />
|
||||
case 'settings':
|
||||
return <SettingsPanel />
|
||||
default:
|
||||
return <Dashboard />
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue